Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions pkg/imgpkg/bundle/bundle_metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2026 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

package bundle

import (
"archive/tar"
"fmt"
"io"
"path/filepath"

regv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
"sigs.k8s.io/yaml"
)

// BundleAuthor contains author information from a bundle's metadata file.
type BundleAuthor struct {
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
}

// BundleWebsite contains a website from a bundle's metadata file.
type BundleWebsite struct {
URL string `json:"url,omitempty"`
}

// BundleMetadata contains the user-provided information in .imgpkg/bundle.yml.
type BundleMetadata struct {
Metadata map[string]string `json:"metadata,omitempty"`
Authors []BundleAuthor `json:"authors,omitempty"`
Websites []BundleWebsite `json:"websites,omitempty"`
}

// Metadata reads the metadata associated with the bundle.
func (o *Bundle) Metadata() (BundleMetadata, error) {
img, err := o.checkedImage()
if err != nil {
return BundleMetadata{}, err
}

return readBundleMetadata(img)
}

func readBundleMetadata(img regv1.Image) (BundleMetadata, error) {
metadata := BundleMetadata{}
layers, err := img.Layers()
if err != nil {
return metadata, err
}

if len(layers) != 1 {
return metadata, fmt.Errorf("Expected bundle to only have a single layer, got %d", len(layers))
}

layer := layers[0]
mediaType, err := layer.MediaType()
if err != nil {
return metadata, err
}

if mediaType != types.DockerLayer {
return metadata, fmt.Errorf("Expected layer to have docker layer media type, was %s", mediaType)
}

uncompressedReader, err := layer.Uncompressed()
if err != nil {
return metadata, fmt.Errorf("Could not read bundle image layer contents: %v", err)
}
defer uncompressedReader.Close()

tarReader := tar.NewReader(uncompressedReader)
for {
header, err := tarReader.Next()
if err != nil {
if err == io.EOF {
return metadata, nil
}
return metadata, fmt.Errorf("reading tar: %v", err)
}

if filepath.Dir(header.Name) == ImgpkgDir && filepath.Base(header.Name) == BundleMetadataFile {
break
}
}

contents, err := io.ReadAll(tarReader)
if err != nil {
return metadata, fmt.Errorf("Reading bundle.yml from layer: %s", err)
}

if err := yaml.Unmarshal(contents, &metadata); err != nil {
return metadata, fmt.Errorf("Unmarshalling bundle metadata: %s", err)
}

return metadata, nil
}
7 changes: 4 additions & 3 deletions pkg/imgpkg/bundle/contents.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import (
)

const (
ImgpkgDir = ".imgpkg"
BundlesDir = "bundles"
ImagesLockFile = "images.yml"
ImgpkgDir = ".imgpkg"
BundlesDir = "bundles"
BundleMetadataFile = "bundle.yml"
ImagesLockFile = "images.yml"
)

type Contents struct {
Expand Down
23 changes: 23 additions & 0 deletions pkg/imgpkg/v1/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ func (r *refWithDescription) describeBundleRec(visitedImgs map[string]refWithDes
if newBundle == nil {
return desc.bundle, fmt.Errorf("Internal inconsistency: bundle with ref '%s' could not be found in list of bundles", currentBundle.PrimaryLocation())
}
metadata, err := newBundle.Metadata()
if err != nil {
return desc.bundle, fmt.Errorf("Reading metadata for bundle '%s': %s", currentBundle.PrimaryLocation(), err)
}
desc.bundle.Metadata = newMetadata(metadata)

imagesRefs := newBundle.ImagesRefsWithErrors()
sort.Slice(imagesRefs, func(i, j int) bool {
Expand Down Expand Up @@ -230,6 +235,24 @@ func (r *refWithDescription) describeBundleRec(visitedImgs map[string]refWithDes
return desc.bundle, nil
}

func newMetadata(metadata bundle.BundleMetadata) Metadata {
authors := make([]Author, len(metadata.Authors))
for i, author := range metadata.Authors {
authors[i] = Author{Name: author.Name, Email: author.Email}
}

websites := make([]Website, len(metadata.Websites))
for i, website := range metadata.Websites {
websites[i] = Website{URL: website.URL}
}

return Metadata{
Metadata: metadata.Metadata,
Authors: authors,
Websites: websites,
}
}

func getImageLayersInfo(image string) ([]Layers, int64, error) {
layers := []Layers{}
parsedImgRef, err := regname.ParseReference(image, regname.WeakValidation)
Expand Down
41 changes: 41 additions & 0 deletions pkg/imgpkg/v1/describe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -369,6 +370,46 @@ func TestDescribeBundle(t *testing.T) {
})
}

func TestDescribeBundleMetadata(t *testing.T) {
logger := &helpers.Logger{LogLevel: helpers.LogDebug}
fakeRegBuilder := helpers.NewFakeRegistry(t, logger)
bundlePath := t.TempDir()
imgpkgPath := filepath.Join(bundlePath, ctlbundle.ImgpkgDir)
require.NoError(t, os.Mkdir(imgpkgPath, 0700))
require.NoError(t, os.WriteFile(filepath.Join(imgpkgPath, ctlbundle.BundleMetadataFile), []byte(`
apiVersion: imgpkg.carvel.dev/v1alpha1
kind: Bundle
metadata:
name: example
oslManifestURL: https://example.com/manifest
authors:
- name: Example Author
email: author@example.com
websites:
- url: https://example.com
`), 0600))
require.NoError(t, lockconfig.NewEmptyImagesLock().WriteToPath(filepath.Join(imgpkgPath, ctlbundle.ImagesLockFile)))

topBundle := fakeRegBuilder.WithBundleFromPath("repo/bundle-with-metadata", bundlePath)
fakeRegBuilder.Build()

description, err := v1.Describe(topBundle.RefDigest, v1.DescribeOpts{
Logger: logger,
Concurrency: 1,
}, registry.Opts{
EnvironFunc: os.Environ,
RetryCount: 3,
})
require.NoError(t, err)

assert.Equal(t, map[string]string{
"name": "example",
"oslManifestURL": "https://example.com/manifest",
}, description.Metadata.Metadata)
assert.Equal(t, []v1.Author{{Name: "Example Author", Email: "author@example.com"}}, description.Metadata.Authors)
assert.Equal(t, []v1.Website{{URL: "https://example.com"}}, description.Metadata.Websites)
}

type testImage struct {
testBundle
}
Expand Down