Skip to content

Commit

Permalink
libimage.ManifestList.Inspect(): show artifact types and file lists
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
nalind committed Feb 2, 2024
1 parent 139b159 commit e12a405
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 11 deletions.
22 changes: 13 additions & 9 deletions libimage/define/manifests.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
62 changes: 60 additions & 2 deletions libimage/manifest_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/containers/storage"
structcopier "github.com/jinzhu/copier"
"github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/exp/slices"
)

// NOTE: the abstractions and APIs here are a first step to further merge
Expand Down Expand Up @@ -221,17 +223,73 @@ 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
if manifest.URLs != nil {
inspectList.Manifests[i].URLs = slices.Clone(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{}
}
var osFeatures []string
if platform.OSFeatures != nil {
osFeatures = slices.Clone(platform.OSFeatures)
}
inspectList.Subject = &define.ManifestListDescriptor{
Platform: manifest.Schema2PlatformSpec{
OS: platform.OS,
Architecture: platform.Architecture,
OSVersion: platform.OSVersion,
Variant: platform.Variant,
OSFeatures: 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
}
Expand Down
34 changes: 34 additions & 0 deletions libimage/manifests/manifests.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,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
Expand Down Expand Up @@ -228,6 +230,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 slices.Clone(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) {
Expand Down
70 changes: 70 additions & 0 deletions libimage/manifests/manifests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -703,3 +703,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)
}

0 comments on commit e12a405

Please sign in to comment.