Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API support for managing and tracking artifact manifests in image indexes #1833

Merged
merged 8 commits into from
Feb 5, 2024
29 changes: 29 additions & 0 deletions internal/deepcopy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package internal

import (
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)

// DeepCopyDescriptor copies a Descriptor, deeply copying its contents
func DeepCopyDescriptor(original *v1.Descriptor) *v1.Descriptor {
tmp := *original
if original.URLs != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(slices.Clone(nil) == nil, so this check is not strictly necessary. Also elsewhere.

OTOH that behavior is intentional per a comment in the implementation, but not documented.)

tmp.URLs = slices.Clone(original.URLs)
}
if original.Annotations != nil {
tmp.Annotations = maps.Clone(original.Annotations)
}
if original.Data != nil {
tmp.Data = slices.Clone(original.Data)
}
if original.Platform != nil {
tmpPlatform := *original.Platform
if original.Platform.OSFeatures != nil {
tmpPlatform.OSFeatures = slices.Clone(original.Platform.OSFeatures)
}
tmp.Platform = &tmpPlatform
}
return &tmp
}
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
30 changes: 30 additions & 0 deletions libimage/manifest_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package libimage
import (
"context"
"errors"
"path/filepath"
"testing"

"github.com/containers/common/pkg/config"
Expand Down Expand Up @@ -147,3 +148,32 @@ func TestCreateAndRemoveManifestList(t *testing.T) {
require.True(t, rmReports[0].Removed)
require.Equal(t, []string{"localhost/manifestlist:latest"}, rmReports[0].Untagged)
}

// TestAddArtifacts ensures that we don't fail to add artifact manifests to a
// manifest list, even (or especially) when their config blobs aren't valid OCI
// or Docker config blobs.
func TestAddArtifacts(t *testing.T) {
listName := "manifestlist"
runtime := testNewRuntime(t)
ctx := context.Background()

list, err := runtime.CreateManifestList(listName)
require.NoError(t, err)
require.NotNil(t, list)

manifestListOpts := &ManifestListAddOptions{All: true}
absPath, err := filepath.Abs(filepath.Join("..", "pkg", "manifests", "testdata", "artifacts", "blobs-only"))
require.NoError(t, err)
_, err = list.Add(ctx, "oci:"+absPath, manifestListOpts)
require.NoError(t, err)

absPath, err = filepath.Abs(filepath.Join("..", "pkg", "manifests", "testdata", "artifacts", "config-only"))
require.NoError(t, err)
_, err = list.Add(ctx, "oci:"+absPath, manifestListOpts)
require.NoError(t, err)

absPath, err = filepath.Abs(filepath.Join("..", "pkg", "manifests", "testdata", "artifacts", "no-blobs"))
require.NoError(t, err)
_, err = list.Add(ctx, "oci:"+absPath, manifestListOpts)
require.NoError(t, err)
}
Loading
Loading