From 1ad13e9459e384cd43c5ceddd9f5c254f8930df9 Mon Sep 17 00:00:00 2001 From: Nalin Dahyabhai Date: Mon, 22 Jan 2024 11:35:00 -0500 Subject: [PATCH] libimage.ManifestList.Inspect(): show artifact types and file lists When listing instances in an image index, show their artifact types and the names of any files that they're tracking. Signed-off-by: Nalin Dahyabhai --- libimage/define/manifests.go | 22 +++++---- libimage/manifest_list.go | 55 +++++++++++++++++++++- libimage/manifests/manifests.go | 36 +++++++++++++- libimage/manifests/manifests_test.go | 70 ++++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 12 deletions(-) diff --git a/libimage/define/manifests.go b/libimage/define/manifests.go index 1e02984b2..c59a58f70 100644 --- a/libimage/define/manifests.go +++ b/libimage/define/manifests.go @@ -4,24 +4,28 @@ import ( "github.com/containers/image/v5/manifest" ) -// ManifestListDescriptor references a platform-specific manifest. -// Contains exclusive field like `annotations` which is only present in -// OCI spec and not in docker image spec. +// ManifestListDescriptor describes a manifest that is mentioned in an +// image index or manifest list. +// Contains a subset of the fields which are present in both the OCI spec and +// the Docker spec, along with some which are unique to one or the other. type ManifestListDescriptor struct { manifest.Schema2Descriptor - Platform manifest.Schema2PlatformSpec `json:"platform"` - // Annotations contains arbitrary metadata for the image index. - Annotations map[string]string `json:"annotations,omitempty"` + Platform manifest.Schema2PlatformSpec `json:"platform,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + ArtifactType string `json:"artifactType,omitempty"` + Data []byte `json:"data,omitempty"` + Files []string `json:"files,omitempty"` } // ManifestListData is a list of platform-specific manifests, specifically used to // generate output struct for `podman manifest inspect`. Reason for maintaining and -// having this type is to ensure we can have a common type which contains exclusive +// having this type is to ensure we can have a single type which contains exclusive // fields from both Docker manifest format and OCI manifest format. type ManifestListData struct { SchemaVersion int `json:"schemaVersion"` MediaType string `json:"mediaType"` + ArtifactType string `json:"artifactType,omitempty"` Manifests []ManifestListDescriptor `json:"manifests"` - // Annotations contains arbitrary metadata for the image index. - Annotations map[string]string `json:"annotations,omitempty"` + Subject *ManifestListDescriptor `json:"subject,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` } diff --git a/libimage/manifest_list.go b/libimage/manifest_list.go index 8f4d6877f..4b581764e 100644 --- a/libimage/manifest_list.go +++ b/libimage/manifest_list.go @@ -18,6 +18,7 @@ import ( "github.com/containers/storage" structcopier "github.com/jinzhu/copier" "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) // NOTE: the abstractions and APIs here are a first step to further merge @@ -221,17 +222,67 @@ func (i *Image) IsManifestList(ctx context.Context) (bool, error) { // Inspect returns a dockerized version of the manifest list. func (m *ManifestList) Inspect() (*define.ManifestListData, error) { inspectList := define.ManifestListData{} + // Copy the fields from the Docker-format version of the list. dockerFormat := m.list.Docker() err := structcopier.Copy(&inspectList, &dockerFormat) if err != nil { return &inspectList, err } - // Get missing annotation field from OCIv1 Spec - // and populate inspect data. + // Get OCI-specific fields from the OCIv1-format version of the list + // and copy them to the inspect data. ociFormat := m.list.OCIv1() + inspectList.ArtifactType = ociFormat.ArtifactType inspectList.Annotations = ociFormat.Annotations for i, manifest := range ociFormat.Manifests { inspectList.Manifests[i].Annotations = manifest.Annotations + inspectList.Manifests[i].ArtifactType = manifest.ArtifactType + inspectList.Manifests[i].URLs = append([]string{}, manifest.URLs...) + inspectList.Manifests[i].Data = manifest.Data + inspectList.Manifests[i].Files, err = m.list.Files(manifest.Digest) + if err != nil { + return &inspectList, err + } + } + if ociFormat.Subject != nil { + platform := ociFormat.Subject.Platform + if platform == nil { + platform = &imgspecv1.Platform{} + } + inspectList.Subject = &define.ManifestListDescriptor{ + Platform: manifest.Schema2PlatformSpec{ + OS: platform.OS, + Architecture: platform.Architecture, + OSVersion: platform.OSVersion, + Variant: platform.Variant, + OSFeatures: append([]string{}, platform.OSFeatures...), + }, + Schema2Descriptor: manifest.Schema2Descriptor{ + MediaType: ociFormat.Subject.MediaType, + Digest: ociFormat.Subject.Digest, + Size: ociFormat.Subject.Size, + URLs: ociFormat.Subject.URLs, + }, + Annotations: ociFormat.Subject.Annotations, + ArtifactType: ociFormat.Subject.ArtifactType, + Data: ociFormat.Subject.Data, + } + } + // Set MediaType to mirror the value we'd use when saving the list + // using defaults, instead of forcing it to one or the other by + // using the value from one version or the other that we explicitly + // requested above. + serialized, err := m.list.Serialize("") + if err != nil { + return &inspectList, err + } + var typed struct { + MediaType string `json:"mediaType,omitempty"` + } + if err := json.Unmarshal(serialized, &typed); err != nil { + return &inspectList, err + } + if typed.MediaType != "" { + inspectList.MediaType = typed.MediaType } return &inspectList, nil } diff --git a/libimage/manifests/manifests.go b/libimage/manifests/manifests.go index cbec93b8e..b03dbb2f8 100644 --- a/libimage/manifests/manifests.go +++ b/libimage/manifests/manifests.go @@ -81,6 +81,8 @@ type List interface { Push(ctx context.Context, dest types.ImageReference, options PushOptions) (reference.Canonical, digest.Digest, error) Add(ctx context.Context, sys *types.SystemContext, ref types.ImageReference, all bool) (digest.Digest, error) AddArtifact(ctx context.Context, sys *types.SystemContext, options AddArtifactOptions, files ...string) (digest.Digest, error) + InstanceByFile(file string) (digest.Digest, error) + Files(instanceDigest digest.Digest) ([]string, error) } // PushOptions includes various settings which are needed for pushing the @@ -218,6 +220,38 @@ func (l *list) SaveToImage(store storage.Store, imageID string, names []string, return img.ID, nil } +// Files returns the list of files associated with a particular artifact +// instance in the image index, primarily for display purposes. +func (l *list) Files(instanceDigest digest.Digest) ([]string, error) { + filesList, ok := l.artifacts.Files[instanceDigest] + if ok { + return append([]string{}, filesList...), nil + } + return nil, nil +} + +// instanceByFile returns the instanceDigest of the first manifest in the index +// which refers to the named file. The name will be passed to filepath.Abs() +// before searching for an instance which references it. +func (l *list) InstanceByFile(file string) (digest.Digest, error) { + if parsedDigest, err := digest.Parse(file); err == nil { + // nice try, but that's already a digest! + return parsedDigest, nil + } + abs, err := filepath.Abs(file) + if err != nil { + return "", err + } + for instanceDigest, files := range l.artifacts.Files { + for _, file := range files { + if file == abs { + return instanceDigest, nil + } + } + } + return "", os.ErrNotExist +} + // Reference returns an image reference for the composite image being built // in the list, or an error if the list has never been saved to a local image. func (l *list) Reference(store storage.Store, multiple cp.ImageListSelection, instances []digest.Digest) (types.ImageReference, error) { @@ -255,7 +289,7 @@ func (l *list) Reference(store storage.Store, multiple cp.ImageListSelection, in if err != nil { return nil, fmt.Errorf("locating per-image directory for %s: %w", img.ID, err) } - tmp, err := os.MkdirTemp(imgDirectory, pushArtifactsSubdirectory) + tmp, err := os.MkdirTemp(imgDirectory, pushingArtifactsSubdirectory) if err != nil { return nil, err } diff --git a/libimage/manifests/manifests_test.go b/libimage/manifests/manifests_test.go index ca689714c..d98867d82 100644 --- a/libimage/manifests/manifests_test.go +++ b/libimage/manifests/manifests_test.go @@ -644,3 +644,73 @@ func TestPushManifest(t *testing.T) { _, _, err = list.Push(ctx, destRef, options) assert.NoError(t, err, "list.Push(with ForceCompressionFormat: true)") } + +func TestInstanceByImageAndFiles(t *testing.T) { + if unshare.IsRootless() { + t.Skip("Test can only run as root") + } + ctx := context.Background() + + dir := t.TempDir() + storeOptions := storage.StoreOptions{ + GraphRoot: filepath.Join(dir, "root"), + RunRoot: filepath.Join(dir, "runroot"), + GraphDriverName: "vfs", + } + store, err := storage.GetStore(storeOptions) + assert.NoError(t, err, "error opening store") + if store == nil { + return + } + defer func() { + if _, err := store.Shutdown(true); err != nil { + assert.NoError(t, err, "error closing store") + } + }() + + cconfig := filepath.Join("..", "testdata", "containers.conf") + absCconfig, err := filepath.Abs(cconfig) + assert.NoError(t, err) + gzipped := filepath.Join("..", "testdata", "oci-name-only.tar.gz") + absGzipped, err := filepath.Abs(gzipped) + assert.NoError(t, err) + pngfile := filepath.Join("..", "..", "logos", "containers.png") + absPngfile, err := filepath.Abs(pngfile) + assert.NoError(t, err) + + list := Create() + options := AddArtifactOptions{} + firstInstanceDigest, err := list.AddArtifact(ctx, sys, options, cconfig, gzipped) + assert.NoError(t, err) + secondInstanceDigest, err := list.AddArtifact(ctx, sys, options, pngfile) + assert.NoError(t, err) + + candidate, err := list.InstanceByFile(cconfig) + assert.NoError(t, err) + assert.Equal(t, firstInstanceDigest, candidate) + candidate, err = list.InstanceByFile(gzipped) + assert.NoError(t, err) + assert.Equal(t, firstInstanceDigest, candidate) + + firstFiles, err := list.Files(firstInstanceDigest) + assert.NoError(t, err) + assert.ElementsMatch(t, []string{absCconfig, absGzipped}, firstFiles) + + candidate, err = list.InstanceByFile(pngfile) + assert.NoError(t, err) + assert.Equal(t, secondInstanceDigest, candidate) + + secondFiles, err := list.Files(secondInstanceDigest) + assert.NoError(t, err) + assert.ElementsMatch(t, []string{absPngfile}, secondFiles) + + _, err = list.InstanceByFile("ha ha, fooled you") + assert.Error(t, err) + assert.ErrorIs(t, err, os.ErrNotExist) + + otherDigest, err := digest.Parse(otherListDigest) + assert.NoError(t, err) + noFiles, err := list.Files(otherDigest) + assert.NoError(t, err) + assert.ElementsMatch(t, []string{}, noFiles) +}