mirror of https://github.com/containers/image.git
manifest: introduce internal/manifest.ListInternal and freeze manifest.List
Flips dependency of internal and public manifest.List API and introduces private manifest.List API which will be extended in future for new features. [NO NEW TESTS NEEDED] [NO TESTS NEEDED] Signed-off-by: Aditya R <arajan@redhat.com>
This commit is contained in:
parent
b5205c6e22
commit
8a1955ba80
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/containers/image/v5/internal/image"
|
||||
"github.com/containers/image/v5/internal/imagedestination"
|
||||
"github.com/containers/image/v5/internal/imagesource"
|
||||
internalManifest "github.com/containers/image/v5/internal/manifest"
|
||||
"github.com/containers/image/v5/internal/pkg/platform"
|
||||
"github.com/containers/image/v5/internal/private"
|
||||
"github.com/containers/image/v5/internal/set"
|
||||
|
|
@ -317,7 +318,7 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef,
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("reading manifest for %s: %w", transports.ImageName(srcRef), err)
|
||||
}
|
||||
manifestList, err := manifest.ListFromBlob(mfest, manifestType)
|
||||
manifestList, err := internalManifest.ListFromBlob(mfest, manifestType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing primary manifest as list for %s: %w", transports.ImageName(srcRef), err)
|
||||
}
|
||||
|
|
@ -417,11 +418,11 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("reading manifest list: %w", err)
|
||||
}
|
||||
originalList, err := manifest.ListFromBlob(manifestList, manifestType)
|
||||
originalList, err := internalManifest.ListFromBlob(manifestList, manifestType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing manifest list %q: %w", string(manifestList), err)
|
||||
}
|
||||
updatedList := originalList.Clone()
|
||||
updatedList := originalList.CloneInternal()
|
||||
|
||||
sigs, err := c.sourceSignatures(ctx, unparsedToplevel, options,
|
||||
"Getting image list signatures",
|
||||
|
|
@ -525,7 +526,7 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur
|
|||
c.Printf("Writing manifest list to image destination\n")
|
||||
var errs []string
|
||||
for _, thisListType := range append([]string{selectedListType}, otherManifestMIMETypeCandidates...) {
|
||||
attemptedList := updatedList
|
||||
var attemptedList internalManifest.ListPublic = updatedList
|
||||
|
||||
logrus.Debugf("Trying to use manifest list type %s…", thisListType)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import (
|
|||
|
||||
"github.com/containers/image/v5/internal/imagesource/impl"
|
||||
"github.com/containers/image/v5/internal/imagesource/stubs"
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
"github.com/containers/image/v5/internal/private"
|
||||
"github.com/containers/image/v5/internal/signature"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
"github.com/containers/image/v5/types"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
"github.com/containers/image/v5/types"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// AllowedManifestFields is a bit mask of “essential” manifest fields that ValidateUnambiguousManifestFormat
|
||||
// can expect to be present.
|
||||
type AllowedManifestFields int
|
||||
|
||||
const (
|
||||
AllowedFieldConfig AllowedManifestFields = 1 << iota
|
||||
AllowedFieldFSLayers
|
||||
AllowedFieldHistory
|
||||
AllowedFieldLayers
|
||||
AllowedFieldManifests
|
||||
AllowedFieldFirstUnusedBit // Keep this at the end!
|
||||
)
|
||||
|
||||
// ValidateUnambiguousManifestFormat rejects manifests (incl. multi-arch) that look like more than
|
||||
// one kind we currently recognize, i.e. if they contain any of the known “essential” format fields
|
||||
// other than the ones the caller specifically allows.
|
||||
// expectedMIMEType is used only for diagnostics.
|
||||
// NOTE: The caller should do the non-heuristic validations (e.g. check for any specified format
|
||||
// identification/version, or other “magic numbers”) before calling this, to cleanly reject unambiguous
|
||||
// data that just isn’t what was expected, as opposed to actually ambiguous data.
|
||||
func ValidateUnambiguousManifestFormat(manifest []byte, expectedMIMEType string,
|
||||
allowed AllowedManifestFields) error {
|
||||
if allowed >= AllowedFieldFirstUnusedBit {
|
||||
return fmt.Errorf("internal error: invalid allowedManifestFields value %#v", allowed)
|
||||
}
|
||||
// Use a private type to decode, not just a map[string]any, because we want
|
||||
// to also reject case-insensitive matches (which would be used by Go when really decoding
|
||||
// the manifest).
|
||||
// (It is expected that as manifest formats are added or extended over time, more fields will be added
|
||||
// here.)
|
||||
detectedFields := struct {
|
||||
Config any `json:"config"`
|
||||
FSLayers any `json:"fsLayers"`
|
||||
History any `json:"history"`
|
||||
Layers any `json:"layers"`
|
||||
Manifests any `json:"manifests"`
|
||||
}{}
|
||||
if err := json.Unmarshal(manifest, &detectedFields); err != nil {
|
||||
// The caller was supposed to already validate version numbers, so this should not happen;
|
||||
// let’s not bother with making this error “nice”.
|
||||
return err
|
||||
}
|
||||
unexpected := []string{}
|
||||
// Sadly this isn’t easy to automate in Go, without reflection. So, copy&paste.
|
||||
if detectedFields.Config != nil && (allowed&AllowedFieldConfig) == 0 {
|
||||
unexpected = append(unexpected, "config")
|
||||
}
|
||||
if detectedFields.FSLayers != nil && (allowed&AllowedFieldFSLayers) == 0 {
|
||||
unexpected = append(unexpected, "fsLayers")
|
||||
}
|
||||
if detectedFields.History != nil && (allowed&AllowedFieldHistory) == 0 {
|
||||
unexpected = append(unexpected, "history")
|
||||
}
|
||||
if detectedFields.Layers != nil && (allowed&AllowedFieldLayers) == 0 {
|
||||
unexpected = append(unexpected, "layers")
|
||||
}
|
||||
if detectedFields.Manifests != nil && (allowed&AllowedFieldManifests) == 0 {
|
||||
unexpected = append(unexpected, "manifests")
|
||||
}
|
||||
if len(unexpected) != 0 {
|
||||
return fmt.Errorf(`rejecting ambiguous manifest, unexpected fields %#v in supposedly %s`,
|
||||
unexpected, expectedMIMEType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateUnambiguousManifestFormat(t *testing.T) {
|
||||
const allAllowedFields = AllowedFieldFirstUnusedBit - 1
|
||||
const mt = "text/plain" // Just some MIME type that shows up in error messages
|
||||
|
||||
type test struct {
|
||||
manifest string
|
||||
allowed AllowedManifestFields
|
||||
}
|
||||
|
||||
// Smoke tests: Success
|
||||
for _, c := range []test{
|
||||
{"{}", allAllowedFields},
|
||||
{"{}", 0},
|
||||
} {
|
||||
err := ValidateUnambiguousManifestFormat([]byte(c.manifest), mt, c.allowed)
|
||||
assert.NoError(t, err, c)
|
||||
}
|
||||
// Smoke tests: Failure
|
||||
for _, c := range []test{
|
||||
{"{}", AllowedFieldFirstUnusedBit}, // Invalid "allowed"
|
||||
{"@", allAllowedFields}, // Invalid JSON
|
||||
} {
|
||||
err := ValidateUnambiguousManifestFormat([]byte(c.manifest), mt, c.allowed)
|
||||
assert.Error(t, err, c)
|
||||
}
|
||||
|
||||
fields := map[AllowedManifestFields]string{
|
||||
AllowedFieldConfig: "config",
|
||||
AllowedFieldFSLayers: "fsLayers",
|
||||
AllowedFieldHistory: "history",
|
||||
AllowedFieldLayers: "layers",
|
||||
AllowedFieldManifests: "manifests",
|
||||
}
|
||||
// Ensure this test covers all defined AllowedManifestFields values
|
||||
allFields := AllowedManifestFields(0)
|
||||
for k := range fields {
|
||||
allFields |= k
|
||||
}
|
||||
assert.Equal(t, allAllowedFields, allFields)
|
||||
|
||||
// Every single field is allowed by its bit, and rejected by any other bit
|
||||
for bit, fieldName := range fields {
|
||||
json := []byte(fmt.Sprintf(`{"%s":[]}`, fieldName))
|
||||
err := ValidateUnambiguousManifestFormat(json, mt, bit)
|
||||
assert.NoError(t, err, fieldName)
|
||||
err = ValidateUnambiguousManifestFormat(json, mt, allAllowedFields^bit)
|
||||
assert.Error(t, err, fieldName)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that parser() rejects all of the provided manifest fixtures.
|
||||
// Intended to help test manifest parsers' detection of schema mismatches.
|
||||
func testManifestFixturesAreRejected(t *testing.T, parser func([]byte) error, fixtures []string) {
|
||||
for _, fixture := range fixtures {
|
||||
manifest, err := os.ReadFile(filepath.Join("testdata", fixture))
|
||||
require.NoError(t, err, fixture)
|
||||
err = parser(manifest)
|
||||
assert.Error(t, err, fixture)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that parser() rejects validManifest with an added top-level field with any of the provided field names.
|
||||
// Intended to help test callers of validateUnambiguousManifestFormat.
|
||||
func testValidManifestWithExtraFieldsIsRejected(t *testing.T, parser func([]byte) error,
|
||||
validManifest []byte, fields []string) {
|
||||
for _, field := range fields {
|
||||
// end (the final '}') is not always at len(validManifest)-1 because the manifest can end with
|
||||
// white space.
|
||||
end := bytes.LastIndexByte(validManifest, '}')
|
||||
require.NotEqual(t, end, -1)
|
||||
updatedManifest := []byte(string(validManifest[:end]) +
|
||||
fmt.Sprintf(`,"%s":[]}`, field))
|
||||
err := parser(updatedManifest)
|
||||
// Make sure it is the error from validateUnambiguousManifestFormat, not something that
|
||||
// went wrong with creating updatedManifest.
|
||||
assert.ErrorContains(t, err, "rejecting ambiguous manifest", field)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
// Schema2Descriptor is a “descriptor” in docker/distribution schema 2.
|
||||
//
|
||||
// This is publicly visible as c/image/manifest.Schema2Descriptor.
|
||||
type Schema2Descriptor struct {
|
||||
MediaType string `json:"mediaType"`
|
||||
Size int64 `json:"size"`
|
||||
Digest digest.Digest `json:"digest"`
|
||||
URLs []string `json:"urls,omitempty"`
|
||||
}
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
platform "github.com/containers/image/v5/internal/pkg/platform"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Schema2PlatformSpec describes the platform which a particular manifest is
|
||||
// specialized for.
|
||||
// This is publicly visible as c/image/manifest.Schema2PlatformSpec.
|
||||
type Schema2PlatformSpec struct {
|
||||
Architecture string `json:"architecture"`
|
||||
OS string `json:"os"`
|
||||
OSVersion string `json:"os.version,omitempty"`
|
||||
OSFeatures []string `json:"os.features,omitempty"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
Features []string `json:"features,omitempty"` // removed in OCI
|
||||
}
|
||||
|
||||
// Schema2ManifestDescriptor references a platform-specific manifest.
|
||||
// This is publicly visible as c/image/manifest.Schema2ManifestDescriptor.
|
||||
type Schema2ManifestDescriptor struct {
|
||||
Schema2Descriptor
|
||||
Platform Schema2PlatformSpec `json:"platform"`
|
||||
}
|
||||
|
||||
// Schema2ListPublic is a list of platform-specific manifests.
|
||||
// This is publicly visible as c/image/manifest.Schema2List.
|
||||
// Internal users should usually use Schema2List instead.
|
||||
type Schema2ListPublic struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
MediaType string `json:"mediaType"`
|
||||
Manifests []Schema2ManifestDescriptor `json:"manifests"`
|
||||
}
|
||||
|
||||
// MIMEType returns the MIME type of this particular manifest list.
|
||||
func (list *Schema2ListPublic) MIMEType() string {
|
||||
return list.MediaType
|
||||
}
|
||||
|
||||
// Instances returns a slice of digests of the manifests that this list knows of.
|
||||
func (list *Schema2ListPublic) Instances() []digest.Digest {
|
||||
results := make([]digest.Digest, len(list.Manifests))
|
||||
for i, m := range list.Manifests {
|
||||
results[i] = m.Digest
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Instance returns the ListUpdate of a particular instance in the list.
|
||||
func (list *Schema2ListPublic) Instance(instanceDigest digest.Digest) (ListUpdate, error) {
|
||||
for _, manifest := range list.Manifests {
|
||||
if manifest.Digest == instanceDigest {
|
||||
return ListUpdate{
|
||||
Digest: manifest.Digest,
|
||||
Size: manifest.Size,
|
||||
MediaType: manifest.MediaType,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return ListUpdate{}, fmt.Errorf("unable to find instance %s passed to Schema2List.Instances", instanceDigest)
|
||||
}
|
||||
|
||||
// UpdateInstances updates the sizes, digests, and media types of the manifests
|
||||
// which the list catalogs.
|
||||
func (list *Schema2ListPublic) UpdateInstances(updates []ListUpdate) error {
|
||||
if len(updates) != len(list.Manifests) {
|
||||
return fmt.Errorf("incorrect number of update entries passed to Schema2List.UpdateInstances: expected %d, got %d", len(list.Manifests), len(updates))
|
||||
}
|
||||
for i := range updates {
|
||||
if err := updates[i].Digest.Validate(); err != nil {
|
||||
return fmt.Errorf("update %d of %d passed to Schema2List.UpdateInstances contained an invalid digest: %w", i+1, len(updates), err)
|
||||
}
|
||||
list.Manifests[i].Digest = updates[i].Digest
|
||||
if updates[i].Size < 0 {
|
||||
return fmt.Errorf("update %d of %d passed to Schema2List.UpdateInstances had an invalid size (%d)", i+1, len(updates), updates[i].Size)
|
||||
}
|
||||
list.Manifests[i].Size = updates[i].Size
|
||||
if updates[i].MediaType == "" {
|
||||
return fmt.Errorf("update %d of %d passed to Schema2List.UpdateInstances had no media type (was %q)", i+1, len(updates), list.Manifests[i].MediaType)
|
||||
}
|
||||
list.Manifests[i].MediaType = updates[i].MediaType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChooseInstance parses blob as a schema2 manifest list, and returns the digest
|
||||
// of the image which is appropriate for the current environment.
|
||||
func (list *Schema2ListPublic) ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) {
|
||||
wantedPlatforms, err := platform.WantedPlatforms(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting platform information %#v: %w", ctx, err)
|
||||
}
|
||||
for _, wantedPlatform := range wantedPlatforms {
|
||||
for _, d := range list.Manifests {
|
||||
imagePlatform := imgspecv1.Platform{
|
||||
Architecture: d.Platform.Architecture,
|
||||
OS: d.Platform.OS,
|
||||
OSVersion: d.Platform.OSVersion,
|
||||
OSFeatures: slices.Clone(d.Platform.OSFeatures),
|
||||
Variant: d.Platform.Variant,
|
||||
}
|
||||
if platform.MatchesPlatform(imagePlatform, wantedPlatform) {
|
||||
return d.Digest, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no image found in manifest list for architecture %s, variant %q, OS %s", wantedPlatforms[0].Architecture, wantedPlatforms[0].Variant, wantedPlatforms[0].OS)
|
||||
}
|
||||
|
||||
// Serialize returns the list in a blob format.
|
||||
// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made!
|
||||
func (list *Schema2ListPublic) Serialize() ([]byte, error) {
|
||||
buf, err := json.Marshal(list)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling Schema2List %#v: %w", list, err)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// Schema2ListPublicFromComponents creates a Schema2 manifest list instance from the
|
||||
// supplied data.
|
||||
// This is publicly visible as c/image/manifest.Schema2ListFromComponents.
|
||||
func Schema2ListPublicFromComponents(components []Schema2ManifestDescriptor) *Schema2ListPublic {
|
||||
list := Schema2ListPublic{
|
||||
SchemaVersion: 2,
|
||||
MediaType: DockerV2ListMediaType,
|
||||
Manifests: make([]Schema2ManifestDescriptor, len(components)),
|
||||
}
|
||||
for i, component := range components {
|
||||
m := Schema2ManifestDescriptor{
|
||||
Schema2Descriptor{
|
||||
MediaType: component.MediaType,
|
||||
Size: component.Size,
|
||||
Digest: component.Digest,
|
||||
URLs: slices.Clone(component.URLs),
|
||||
},
|
||||
Schema2PlatformSpec{
|
||||
Architecture: component.Platform.Architecture,
|
||||
OS: component.Platform.OS,
|
||||
OSVersion: component.Platform.OSVersion,
|
||||
OSFeatures: slices.Clone(component.Platform.OSFeatures),
|
||||
Variant: component.Platform.Variant,
|
||||
Features: slices.Clone(component.Platform.Features),
|
||||
},
|
||||
}
|
||||
list.Manifests[i] = m
|
||||
}
|
||||
return &list
|
||||
}
|
||||
|
||||
// Schema2ListPublicClone creates a deep copy of the passed-in list.
|
||||
// This is publicly visible as c/image/manifest.Schema2ListClone.
|
||||
func Schema2ListPublicClone(list *Schema2ListPublic) *Schema2ListPublic {
|
||||
return Schema2ListPublicFromComponents(list.Manifests)
|
||||
}
|
||||
|
||||
// ToOCI1Index returns the list encoded as an OCI1 index.
|
||||
func (list *Schema2ListPublic) ToOCI1Index() (*OCI1IndexPublic, error) {
|
||||
components := make([]imgspecv1.Descriptor, 0, len(list.Manifests))
|
||||
for _, manifest := range list.Manifests {
|
||||
converted := imgspecv1.Descriptor{
|
||||
MediaType: manifest.MediaType,
|
||||
Size: manifest.Size,
|
||||
Digest: manifest.Digest,
|
||||
URLs: slices.Clone(manifest.URLs),
|
||||
Platform: &imgspecv1.Platform{
|
||||
OS: manifest.Platform.OS,
|
||||
Architecture: manifest.Platform.Architecture,
|
||||
OSFeatures: slices.Clone(manifest.Platform.OSFeatures),
|
||||
OSVersion: manifest.Platform.OSVersion,
|
||||
Variant: manifest.Platform.Variant,
|
||||
},
|
||||
}
|
||||
components = append(components, converted)
|
||||
}
|
||||
oci := OCI1IndexPublicFromComponents(components, nil)
|
||||
return oci, nil
|
||||
}
|
||||
|
||||
// ToSchema2List returns the list encoded as a Schema2 list.
|
||||
func (list *Schema2ListPublic) ToSchema2List() (*Schema2ListPublic, error) {
|
||||
return Schema2ListPublicClone(list), nil
|
||||
}
|
||||
|
||||
// Schema2ListPublicFromManifest creates a Schema2 manifest list instance from marshalled
|
||||
// JSON, presumably generated by encoding a Schema2 manifest list.
|
||||
// This is publicly visible as c/image/manifest.Schema2ListFromManifest.
|
||||
func Schema2ListPublicFromManifest(manifest []byte) (*Schema2ListPublic, error) {
|
||||
list := Schema2ListPublic{
|
||||
Manifests: []Schema2ManifestDescriptor{},
|
||||
}
|
||||
if err := json.Unmarshal(manifest, &list); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling Schema2List %q: %w", string(manifest), err)
|
||||
}
|
||||
if err := ValidateUnambiguousManifestFormat(manifest, DockerV2ListMediaType,
|
||||
AllowedFieldManifests); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &list, nil
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of this list and its contents.
|
||||
func (list *Schema2ListPublic) Clone() ListPublic {
|
||||
return Schema2ListPublicClone(list)
|
||||
}
|
||||
|
||||
// ConvertToMIMEType converts the passed-in manifest list to a manifest
|
||||
// list of the specified type.
|
||||
func (list *Schema2ListPublic) ConvertToMIMEType(manifestMIMEType string) (ListPublic, error) {
|
||||
switch normalized := NormalizedMIMEType(manifestMIMEType); normalized {
|
||||
case DockerV2ListMediaType:
|
||||
return list.Clone(), nil
|
||||
case imgspecv1.MediaTypeImageIndex:
|
||||
return list.ToOCI1Index()
|
||||
case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType:
|
||||
return nil, fmt.Errorf("Can not convert manifest list to MIME type %q, which is not a list type", manifestMIMEType)
|
||||
default:
|
||||
// Note that this may not be reachable, NormalizedMIMEType has a default for unknown values.
|
||||
return nil, fmt.Errorf("Unimplemented manifest list MIME type %s", manifestMIMEType)
|
||||
}
|
||||
}
|
||||
|
||||
// Schema2List is a list of platform-specific manifests.
|
||||
type Schema2List struct {
|
||||
Schema2ListPublic
|
||||
}
|
||||
|
||||
func schema2ListFromPublic(public *Schema2ListPublic) *Schema2List {
|
||||
return &Schema2List{*public}
|
||||
}
|
||||
|
||||
func (index *Schema2List) CloneInternal() List {
|
||||
return schema2ListFromPublic(Schema2ListPublicClone(&index.Schema2ListPublic))
|
||||
}
|
||||
|
||||
func (index *Schema2List) Clone() ListPublic {
|
||||
return index.CloneInternal()
|
||||
}
|
||||
|
||||
// Schema2ListFromManifest creates a Schema2 manifest list instance from marshalled
|
||||
// JSON, presumably generated by encoding a Schema2 manifest list.
|
||||
func Schema2ListFromManifest(manifest []byte) (*Schema2List, error) {
|
||||
public, err := Schema2ListPublicFromManifest(manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return schema2ListFromPublic(public), nil
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSchema2ListPublicFromManifest(t *testing.T) {
|
||||
validManifest, err := os.ReadFile(filepath.Join("testdata", "v2list.manifest.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
parser := func(m []byte) error {
|
||||
_, err := Schema2ListPublicFromManifest(m)
|
||||
return err
|
||||
}
|
||||
// Schema mismatch is rejected
|
||||
testManifestFixturesAreRejected(t, parser, []string{
|
||||
"schema2-to-schema1-by-docker.json",
|
||||
"v2s2.manifest.json",
|
||||
"ociv1.manifest.json",
|
||||
// Not "ociv1.image.index.json" yet, without validating mediaType the two are too similar to tell the difference.
|
||||
})
|
||||
// Extra fields are rejected
|
||||
testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"})
|
||||
}
|
||||
|
||||
func TestSchema2ListFromManifest(t *testing.T) {
|
||||
validManifest, err := os.ReadFile(filepath.Join("testdata", "v2list.manifest.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
parser := func(m []byte) error {
|
||||
_, err := Schema2ListFromManifest(m)
|
||||
return err
|
||||
}
|
||||
// Schema mismatch is rejected
|
||||
testManifestFixturesAreRejected(t, parser, []string{
|
||||
"schema2-to-schema1-by-docker.json",
|
||||
"v2s2.manifest.json",
|
||||
"ociv1.manifest.json",
|
||||
// Not "ociv1.image.index.json" yet, without validating mediaType the two are too similar to tell the difference.
|
||||
})
|
||||
// Extra fields are rejected
|
||||
testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"})
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containers/image/v5/types"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// ListPublic is a subset of List which is a part of the public API;
|
||||
// so no methods can be added, removed or changed.
|
||||
//
|
||||
// Internal users should usually use List instead.
|
||||
type ListPublic interface {
|
||||
// MIMEType returns the MIME type of this particular manifest list.
|
||||
MIMEType() string
|
||||
|
||||
// Instances returns a list of the manifests that this list knows of, other than its own.
|
||||
Instances() []digest.Digest
|
||||
|
||||
// Update information about the list's instances. The length of the passed-in slice must
|
||||
// match the length of the list of instances which the list already contains, and every field
|
||||
// must be specified.
|
||||
UpdateInstances([]ListUpdate) error
|
||||
|
||||
// Instance returns the size and MIME type of a particular instance in the list.
|
||||
Instance(digest.Digest) (ListUpdate, error)
|
||||
|
||||
// ChooseInstance selects which manifest is most appropriate for the platform described by the
|
||||
// SystemContext, or for the current platform if the SystemContext doesn't specify any details.
|
||||
ChooseInstance(ctx *types.SystemContext) (digest.Digest, error)
|
||||
|
||||
// Serialize returns the list in a blob format.
|
||||
// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded
|
||||
// from, even if no modifications were made!
|
||||
Serialize() ([]byte, error)
|
||||
|
||||
// ConvertToMIMEType returns the list rebuilt to the specified MIME type, or an error.
|
||||
ConvertToMIMEType(mimeType string) (ListPublic, error)
|
||||
|
||||
// Clone returns a deep copy of this list and its contents.
|
||||
Clone() ListPublic
|
||||
}
|
||||
|
||||
// List is an interface for parsing, modifying lists of image manifests.
|
||||
// Callers can either use this abstract interface without understanding the details of the formats,
|
||||
// or instantiate a specific implementation (e.g. manifest.OCI1Index) and access the public members
|
||||
// directly.
|
||||
type List interface {
|
||||
ListPublic
|
||||
// CloneInternal returns a deep copy of this list and its contents.
|
||||
CloneInternal() List
|
||||
}
|
||||
|
||||
// ListUpdate includes the fields which a List's UpdateInstances() method will modify.
|
||||
// This is publicly visible as c/image/manifest.ListUpdate.
|
||||
type ListUpdate struct {
|
||||
Digest digest.Digest
|
||||
Size int64
|
||||
MediaType string
|
||||
}
|
||||
|
||||
// ListPublicFromBlob parses a list of manifests.
|
||||
// This is publicly visible as c/image/manifest.ListFromBlob.
|
||||
func ListPublicFromBlob(manifest []byte, manifestMIMEType string) (ListPublic, error) {
|
||||
list, err := ListFromBlob(manifest, manifestMIMEType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// ListFromBlob parses a list of manifests.
|
||||
func ListFromBlob(manifest []byte, manifestMIMEType string) (List, error) {
|
||||
normalized := NormalizedMIMEType(manifestMIMEType)
|
||||
switch normalized {
|
||||
case DockerV2ListMediaType:
|
||||
return Schema2ListFromManifest(manifest)
|
||||
case imgspecv1.MediaTypeImageIndex:
|
||||
return OCI1IndexFromManifest(manifest)
|
||||
case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType:
|
||||
return nil, fmt.Errorf("Treating single images as manifest lists is not implemented")
|
||||
}
|
||||
return nil, fmt.Errorf("Unimplemented manifest list MIME type %s (normalized as %s)", manifestMIMEType, normalized)
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func pare(m List) {
|
||||
if impl, ok := m.(*OCI1Index); ok {
|
||||
impl.Annotations = nil
|
||||
}
|
||||
if impl, ok := m.(*Schema2List); ok {
|
||||
for i := range impl.Manifests {
|
||||
impl.Manifests[i].Platform.Features = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLists(t *testing.T) {
|
||||
cases := []struct {
|
||||
path string
|
||||
mimeType string
|
||||
}{
|
||||
{"ociv1.image.index.json", imgspecv1.MediaTypeImageIndex},
|
||||
{"v2list.manifest.json", DockerV2ListMediaType},
|
||||
}
|
||||
for _, c := range cases {
|
||||
manifest, err := os.ReadFile(filepath.Join("testdata", c.path))
|
||||
require.NoError(t, err, "error reading file %q", filepath.Join("testdata", c.path))
|
||||
assert.Equal(t, GuessMIMEType(manifest), c.mimeType)
|
||||
|
||||
// c/image/manifest.TestParseLists verifies that FromBlob refuses to parse the manifest list
|
||||
|
||||
m, err := ListFromBlob(manifest, c.mimeType)
|
||||
require.NoError(t, err, "manifest list %q should parse as list types", c.path)
|
||||
assert.Equal(t, m.MIMEType(), c.mimeType, "manifest %q is not of the expected MIME type", c.path)
|
||||
|
||||
clone := m.Clone()
|
||||
assert.Equal(t, clone, m, "manifest %q is missing some fields after being cloned", c.path)
|
||||
|
||||
pare(m)
|
||||
|
||||
index, err := m.ConvertToMIMEType(imgspecv1.MediaTypeImageIndex)
|
||||
require.NoError(t, err, "error converting %q to an OCI1Index", c.path)
|
||||
|
||||
list, err := m.ConvertToMIMEType(DockerV2ListMediaType)
|
||||
require.NoError(t, err, "error converting %q to an Schema2List", c.path)
|
||||
|
||||
index2, err := list.ConvertToMIMEType(imgspecv1.MediaTypeImageIndex)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, index, index2, "index %q lost data in conversion", c.path)
|
||||
|
||||
list2, err := index.ConvertToMIMEType(DockerV2ListMediaType)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, list, list2, "list %q lost data in conversion", c.path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChooseInstance(t *testing.T) {
|
||||
type expectedMatch struct {
|
||||
arch, variant string
|
||||
instanceDigest digest.Digest
|
||||
}
|
||||
for _, manifestList := range []struct {
|
||||
listFile string
|
||||
matchedInstances []expectedMatch
|
||||
unmatchedInstances []string
|
||||
}{
|
||||
{
|
||||
listFile: "schema2list.json",
|
||||
matchedInstances: []expectedMatch{
|
||||
{"amd64", "", "sha256:030fcb92e1487b18c974784dcc110a93147c9fc402188370fbfd17efabffc6af"},
|
||||
{"s390x", "", "sha256:e5aa1b0a24620228b75382997a0977f609b3ca3a95533dafdef84c74cc8df642"},
|
||||
{"arm", "v7", "sha256:b5dbad4bdb4444d919294afe49a095c23e86782f98cdf0aa286198ddb814b50b"},
|
||||
{"arm64", "", "sha256:dc472a59fb006797aa2a6bfb54cc9c57959bb0a6d11fadaa608df8c16dea39cf"},
|
||||
},
|
||||
unmatchedInstances: []string{
|
||||
"unmatched",
|
||||
},
|
||||
},
|
||||
{ // Focus on ARM variant field testing
|
||||
listFile: "schema2list-variants.json",
|
||||
matchedInstances: []expectedMatch{
|
||||
{"amd64", "", "sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610"},
|
||||
{"arm", "v7", "sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39"},
|
||||
{"arm", "v6", "sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39"},
|
||||
{"arm", "v5", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53"},
|
||||
{"arm", "", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53"},
|
||||
{"arm", "unrecognized-present", "sha256:bcf9771c0b505e68c65440474179592ffdfa98790eb54ffbf129969c5e429990"},
|
||||
{"arm", "unrecognized-not-present", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53"},
|
||||
},
|
||||
unmatchedInstances: []string{
|
||||
"unmatched",
|
||||
},
|
||||
},
|
||||
{
|
||||
listFile: "oci1index.json",
|
||||
matchedInstances: []expectedMatch{
|
||||
{"amd64", "", "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270"},
|
||||
{"ppc64le", "", "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"},
|
||||
},
|
||||
unmatchedInstances: []string{
|
||||
"unmatched",
|
||||
},
|
||||
},
|
||||
} {
|
||||
rawManifest, err := os.ReadFile(filepath.Join("..", "..", "internal", "image", "fixtures", manifestList.listFile))
|
||||
require.NoError(t, err)
|
||||
list, err := ListPublicFromBlob(rawManifest, GuessMIMEType(rawManifest))
|
||||
require.NoError(t, err)
|
||||
// Match found
|
||||
for _, match := range manifestList.matchedInstances {
|
||||
testName := fmt.Sprintf("%s %q+%q", manifestList.listFile, match.arch, match.variant)
|
||||
digest, err := list.ChooseInstance(&types.SystemContext{
|
||||
ArchitectureChoice: match.arch,
|
||||
VariantChoice: match.variant,
|
||||
OSChoice: "linux",
|
||||
})
|
||||
require.NoError(t, err, testName)
|
||||
assert.Equal(t, match.instanceDigest, digest, testName)
|
||||
}
|
||||
// Not found
|
||||
for _, arch := range manifestList.unmatchedInstances {
|
||||
_, err := list.ChooseInstance(&types.SystemContext{
|
||||
ArchitectureChoice: arch,
|
||||
OSChoice: "linux",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/containers/libtrust"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// FIXME: Should we just use docker/distribution and docker/docker implementations directly?
|
||||
|
||||
// FIXME(runcom, mitr): should we have a mediatype pkg??
|
||||
const (
|
||||
// DockerV2Schema1MediaType MIME type represents Docker manifest schema 1
|
||||
DockerV2Schema1MediaType = "application/vnd.docker.distribution.manifest.v1+json"
|
||||
// DockerV2Schema1MediaType MIME type represents Docker manifest schema 1 with a JWS signature
|
||||
DockerV2Schema1SignedMediaType = "application/vnd.docker.distribution.manifest.v1+prettyjws"
|
||||
// DockerV2Schema2MediaType MIME type represents Docker manifest schema 2
|
||||
DockerV2Schema2MediaType = "application/vnd.docker.distribution.manifest.v2+json"
|
||||
// DockerV2Schema2ConfigMediaType is the MIME type used for schema 2 config blobs.
|
||||
DockerV2Schema2ConfigMediaType = "application/vnd.docker.container.image.v1+json"
|
||||
// DockerV2Schema2LayerMediaType is the MIME type used for schema 2 layers.
|
||||
DockerV2Schema2LayerMediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip"
|
||||
// DockerV2SchemaLayerMediaTypeUncompressed is the mediaType used for uncompressed layers.
|
||||
DockerV2SchemaLayerMediaTypeUncompressed = "application/vnd.docker.image.rootfs.diff.tar"
|
||||
// DockerV2ListMediaType MIME type represents Docker manifest schema 2 list
|
||||
DockerV2ListMediaType = "application/vnd.docker.distribution.manifest.list.v2+json"
|
||||
// DockerV2Schema2ForeignLayerMediaType is the MIME type used for schema 2 foreign layers.
|
||||
DockerV2Schema2ForeignLayerMediaType = "application/vnd.docker.image.rootfs.foreign.diff.tar"
|
||||
// DockerV2Schema2ForeignLayerMediaType is the MIME type used for gzipped schema 2 foreign layers.
|
||||
DockerV2Schema2ForeignLayerMediaTypeGzip = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
|
||||
)
|
||||
|
||||
// GuessMIMEType guesses MIME type of a manifest and returns it _if it is recognized_, or "" if unknown or unrecognized.
|
||||
// FIXME? We should, in general, prefer out-of-band MIME type instead of blindly parsing the manifest,
|
||||
// but we may not have such metadata available (e.g. when the manifest is a local file).
|
||||
// This is publicly visible as c/image/manifest.GuessMIMEType.
|
||||
func GuessMIMEType(manifest []byte) string {
|
||||
// A subset of manifest fields; the rest is silently ignored by json.Unmarshal.
|
||||
// Also docker/distribution/manifest.Versioned.
|
||||
meta := struct {
|
||||
MediaType string `json:"mediaType"`
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
Signatures any `json:"signatures"`
|
||||
}{}
|
||||
if err := json.Unmarshal(manifest, &meta); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch meta.MediaType {
|
||||
case DockerV2Schema2MediaType, DockerV2ListMediaType,
|
||||
imgspecv1.MediaTypeImageManifest, imgspecv1.MediaTypeImageIndex: // A recognized type.
|
||||
return meta.MediaType
|
||||
}
|
||||
// this is the only way the function can return DockerV2Schema1MediaType, and recognizing that is essential for stripping the JWS signatures = computing the correct manifest digest.
|
||||
switch meta.SchemaVersion {
|
||||
case 1:
|
||||
if meta.Signatures != nil {
|
||||
return DockerV2Schema1SignedMediaType
|
||||
}
|
||||
return DockerV2Schema1MediaType
|
||||
case 2:
|
||||
// Best effort to understand if this is an OCI image since mediaType
|
||||
// wasn't in the manifest for OCI image-spec < 1.0.2.
|
||||
// For docker v2s2 meta.MediaType should have been set. But given the data, this is our best guess.
|
||||
ociMan := struct {
|
||||
Config struct {
|
||||
MediaType string `json:"mediaType"`
|
||||
} `json:"config"`
|
||||
}{}
|
||||
if err := json.Unmarshal(manifest, &ociMan); err != nil {
|
||||
return ""
|
||||
}
|
||||
switch ociMan.Config.MediaType {
|
||||
case imgspecv1.MediaTypeImageConfig:
|
||||
return imgspecv1.MediaTypeImageManifest
|
||||
case DockerV2Schema2ConfigMediaType:
|
||||
// This case should not happen since a Docker image
|
||||
// must declare a top-level media type and
|
||||
// `meta.MediaType` has already been checked.
|
||||
return DockerV2Schema2MediaType
|
||||
}
|
||||
// Maybe an image index or an OCI artifact.
|
||||
ociIndex := struct {
|
||||
Manifests []imgspecv1.Descriptor `json:"manifests"`
|
||||
}{}
|
||||
if err := json.Unmarshal(manifest, &ociIndex); err != nil {
|
||||
return ""
|
||||
}
|
||||
if len(ociIndex.Manifests) != 0 {
|
||||
if ociMan.Config.MediaType == "" {
|
||||
return imgspecv1.MediaTypeImageIndex
|
||||
}
|
||||
// FIXME: this is mixing media types of manifests and configs.
|
||||
return ociMan.Config.MediaType
|
||||
}
|
||||
// It's most likely an OCI artifact with a custom config media
|
||||
// type which is not (and cannot) be covered by the media-type
|
||||
// checks cabove.
|
||||
return imgspecv1.MediaTypeImageManifest
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Digest returns the a digest of a docker manifest, with any necessary implied transformations like stripping v1s1 signatures.
|
||||
// This is publicly visible as c/image/manifest.Digest.
|
||||
func Digest(manifest []byte) (digest.Digest, error) {
|
||||
if GuessMIMEType(manifest) == DockerV2Schema1SignedMediaType {
|
||||
sig, err := libtrust.ParsePrettySignature(manifest, "signatures")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
manifest, err = sig.Payload()
|
||||
if err != nil {
|
||||
// Coverage: This should never happen, libtrust's Payload() can fail only if joseBase64UrlDecode() fails, on a string
|
||||
// that libtrust itself has josebase64UrlEncode()d
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return digest.FromBytes(manifest), nil
|
||||
}
|
||||
|
||||
// MatchesDigest returns true iff the manifest matches expectedDigest.
|
||||
// Error may be set if this returns false.
|
||||
// Note that this is not doing ConstantTimeCompare; by the time we get here, the cryptographic signature must already have been verified,
|
||||
// or we are not using a cryptographic channel and the attacker can modify the digest along with the manifest blob.
|
||||
// This is publicly visible as c/image/manifest.MatchesDigest.
|
||||
func MatchesDigest(manifest []byte, expectedDigest digest.Digest) (bool, error) {
|
||||
// This should eventually support various digest types.
|
||||
actualDigest, err := Digest(manifest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return expectedDigest == actualDigest, nil
|
||||
}
|
||||
|
||||
// NormalizedMIMEType returns the effective MIME type of a manifest MIME type returned by a server,
|
||||
// centralizing various workarounds.
|
||||
// This is publicly visible as c/image/manifest.NormalizedMIMEType.
|
||||
func NormalizedMIMEType(input string) string {
|
||||
switch input {
|
||||
// "application/json" is a valid v2s1 value per https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md .
|
||||
// This works for now, when nothing else seems to return "application/json"; if that were not true, the mapping/detection might
|
||||
// need to happen within the ImageSource.
|
||||
case "application/json":
|
||||
return DockerV2Schema1SignedMediaType
|
||||
case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType,
|
||||
imgspecv1.MediaTypeImageManifest,
|
||||
imgspecv1.MediaTypeImageIndex,
|
||||
DockerV2Schema2MediaType,
|
||||
DockerV2ListMediaType:
|
||||
return input
|
||||
default:
|
||||
// If it's not a recognized manifest media type, or we have failed determining the type, we'll try one last time
|
||||
// to deserialize using v2s1 as per https://github.com/docker/distribution/blob/master/manifests.go#L108
|
||||
// and https://github.com/docker/distribution/blob/master/manifest/schema1/manifest.go#L50
|
||||
//
|
||||
// Crane registries can also return "text/plain", or pretty much anything else depending on a file extension “recognized” in the tag.
|
||||
// This makes no real sense, but it happens
|
||||
// because requests for manifests are
|
||||
// redirected to a content distribution
|
||||
// network which is configured that way. See https://bugzilla.redhat.com/show_bug.cgi?id=1389442
|
||||
return DockerV2Schema1SignedMediaType
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
digestSha256EmptyTar = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
)
|
||||
|
||||
func TestGuessMIMEType(t *testing.T) {
|
||||
cases := []struct {
|
||||
path string
|
||||
mimeType string
|
||||
}{
|
||||
{"v2s2.manifest.json", DockerV2Schema2MediaType},
|
||||
{"v2list.manifest.json", DockerV2ListMediaType},
|
||||
{"v2s1.manifest.json", DockerV2Schema1SignedMediaType},
|
||||
{"v2s1-unsigned.manifest.json", DockerV2Schema1MediaType},
|
||||
{"v2s1-invalid-signatures.manifest.json", DockerV2Schema1SignedMediaType},
|
||||
{"v2s2nomime.manifest.json", DockerV2Schema2MediaType}, // It is unclear whether this one is legal, but we should guess v2s2 if anything at all.
|
||||
{"unknown-version.manifest.json", ""},
|
||||
{"non-json.manifest.json", ""}, // Not a manifest (nor JSON) at all
|
||||
{"ociv1.manifest.json", imgspecv1.MediaTypeImageManifest},
|
||||
{"ociv1.artifact.json", imgspecv1.MediaTypeImageManifest},
|
||||
{"ociv1.image.index.json", imgspecv1.MediaTypeImageIndex},
|
||||
{"ociv1nomime.manifest.json", imgspecv1.MediaTypeImageManifest},
|
||||
{"ociv1nomime.artifact.json", imgspecv1.MediaTypeImageManifest},
|
||||
{"ociv1nomime.image.index.json", imgspecv1.MediaTypeImageIndex},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
manifest, err := os.ReadFile(filepath.Join("testdata", c.path))
|
||||
require.NoError(t, err)
|
||||
mimeType := GuessMIMEType(manifest)
|
||||
assert.Equal(t, c.mimeType, mimeType, c.path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigest(t *testing.T) {
|
||||
cases := []struct {
|
||||
path string
|
||||
expectedDigest digest.Digest
|
||||
}{
|
||||
{"v2s2.manifest.json", TestDockerV2S2ManifestDigest},
|
||||
{"v2s1.manifest.json", TestDockerV2S1ManifestDigest},
|
||||
{"v2s1-unsigned.manifest.json", TestDockerV2S1UnsignedManifestDigest},
|
||||
}
|
||||
for _, c := range cases {
|
||||
manifest, err := os.ReadFile(filepath.Join("testdata", c.path))
|
||||
require.NoError(t, err)
|
||||
actualDigest, err := Digest(manifest)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.expectedDigest, actualDigest)
|
||||
}
|
||||
|
||||
manifest, err := os.ReadFile("testdata/v2s1-invalid-signatures.manifest.json")
|
||||
require.NoError(t, err)
|
||||
_, err = Digest(manifest)
|
||||
assert.Error(t, err)
|
||||
|
||||
actualDigest, err := Digest([]byte{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, digest.Digest(digestSha256EmptyTar), actualDigest)
|
||||
}
|
||||
|
||||
func TestMatchesDigest(t *testing.T) {
|
||||
cases := []struct {
|
||||
path string
|
||||
expectedDigest digest.Digest
|
||||
result bool
|
||||
}{
|
||||
// Success
|
||||
{"v2s2.manifest.json", TestDockerV2S2ManifestDigest, true},
|
||||
{"v2s1.manifest.json", TestDockerV2S1ManifestDigest, true},
|
||||
// No match (switched s1/s2)
|
||||
{"v2s2.manifest.json", TestDockerV2S1ManifestDigest, false},
|
||||
{"v2s1.manifest.json", TestDockerV2S2ManifestDigest, false},
|
||||
// Unrecognized algorithm
|
||||
{"v2s2.manifest.json", digest.Digest("md5:2872f31c5c1f62a694fbd20c1e85257c"), false},
|
||||
// Mangled format
|
||||
{"v2s2.manifest.json", digest.Digest(TestDockerV2S2ManifestDigest.String() + "abc"), false},
|
||||
{"v2s2.manifest.json", digest.Digest(TestDockerV2S2ManifestDigest.String()[:20]), false},
|
||||
{"v2s2.manifest.json", digest.Digest(""), false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
manifest, err := os.ReadFile(filepath.Join("testdata", c.path))
|
||||
require.NoError(t, err)
|
||||
res, err := MatchesDigest(manifest, c.expectedDigest)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.result, res)
|
||||
}
|
||||
|
||||
manifest, err := os.ReadFile("testdata/v2s1-invalid-signatures.manifest.json")
|
||||
require.NoError(t, err)
|
||||
// Even a correct SHA256 hash is rejected if we can't strip the JSON signature.
|
||||
res, err := MatchesDigest(manifest, digest.FromBytes(manifest))
|
||||
assert.False(t, res)
|
||||
assert.Error(t, err)
|
||||
|
||||
res, err = MatchesDigest([]byte{}, digest.Digest(digestSha256EmptyTar))
|
||||
assert.True(t, res)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNormalizedMIMEType(t *testing.T) {
|
||||
for _, c := range []string{ // Valid MIME types, normalized to themselves
|
||||
DockerV2Schema1MediaType,
|
||||
DockerV2Schema1SignedMediaType,
|
||||
DockerV2Schema2MediaType,
|
||||
DockerV2ListMediaType,
|
||||
imgspecv1.MediaTypeImageManifest,
|
||||
imgspecv1.MediaTypeImageIndex,
|
||||
} {
|
||||
res := NormalizedMIMEType(c)
|
||||
assert.Equal(t, c, res, c)
|
||||
}
|
||||
for _, c := range []string{
|
||||
"application/json",
|
||||
"text/plain",
|
||||
"not at all a valid MIME type",
|
||||
"",
|
||||
} {
|
||||
res := NormalizedMIMEType(c)
|
||||
assert.Equal(t, DockerV2Schema1SignedMediaType, res, c)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
platform "github.com/containers/image/v5/internal/pkg/platform"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
imgspec "github.com/opencontainers/image-spec/specs-go"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// OCI1IndexPublic is just an alias for the OCI index type, but one which we can
|
||||
// provide methods for.
|
||||
// This is publicly visible as c/image/manifest.OCI1Index
|
||||
// Internal users should usually use OCI1Index instead.
|
||||
type OCI1IndexPublic struct {
|
||||
imgspecv1.Index
|
||||
}
|
||||
|
||||
// MIMEType returns the MIME type of this particular manifest index.
|
||||
func (index *OCI1IndexPublic) MIMEType() string {
|
||||
return imgspecv1.MediaTypeImageIndex
|
||||
}
|
||||
|
||||
// Instances returns a slice of digests of the manifests that this index knows of.
|
||||
func (index *OCI1IndexPublic) Instances() []digest.Digest {
|
||||
results := make([]digest.Digest, len(index.Manifests))
|
||||
for i, m := range index.Manifests {
|
||||
results[i] = m.Digest
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Instance returns the ListUpdate of a particular instance in the index.
|
||||
func (index *OCI1IndexPublic) Instance(instanceDigest digest.Digest) (ListUpdate, error) {
|
||||
for _, manifest := range index.Manifests {
|
||||
if manifest.Digest == instanceDigest {
|
||||
return ListUpdate{
|
||||
Digest: manifest.Digest,
|
||||
Size: manifest.Size,
|
||||
MediaType: manifest.MediaType,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return ListUpdate{}, fmt.Errorf("unable to find instance %s in OCI1Index", instanceDigest)
|
||||
}
|
||||
|
||||
// UpdateInstances updates the sizes, digests, and media types of the manifests
|
||||
// which the list catalogs.
|
||||
func (index *OCI1IndexPublic) UpdateInstances(updates []ListUpdate) error {
|
||||
if len(updates) != len(index.Manifests) {
|
||||
return fmt.Errorf("incorrect number of update entries passed to OCI1Index.UpdateInstances: expected %d, got %d", len(index.Manifests), len(updates))
|
||||
}
|
||||
for i := range updates {
|
||||
if err := updates[i].Digest.Validate(); err != nil {
|
||||
return fmt.Errorf("update %d of %d passed to OCI1Index.UpdateInstances contained an invalid digest: %w", i+1, len(updates), err)
|
||||
}
|
||||
index.Manifests[i].Digest = updates[i].Digest
|
||||
if updates[i].Size < 0 {
|
||||
return fmt.Errorf("update %d of %d passed to OCI1Index.UpdateInstances had an invalid size (%d)", i+1, len(updates), updates[i].Size)
|
||||
}
|
||||
index.Manifests[i].Size = updates[i].Size
|
||||
if updates[i].MediaType == "" {
|
||||
return fmt.Errorf("update %d of %d passed to OCI1Index.UpdateInstances had no media type (was %q)", i+1, len(updates), index.Manifests[i].MediaType)
|
||||
}
|
||||
index.Manifests[i].MediaType = updates[i].MediaType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChooseInstance parses blob as an oci v1 manifest index, and returns the digest
|
||||
// of the image which is appropriate for the current environment.
|
||||
func (index *OCI1IndexPublic) ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) {
|
||||
wantedPlatforms, err := platform.WantedPlatforms(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting platform information %#v: %w", ctx, err)
|
||||
}
|
||||
for _, wantedPlatform := range wantedPlatforms {
|
||||
for _, d := range index.Manifests {
|
||||
if d.Platform == nil {
|
||||
continue
|
||||
}
|
||||
imagePlatform := imgspecv1.Platform{
|
||||
Architecture: d.Platform.Architecture,
|
||||
OS: d.Platform.OS,
|
||||
OSVersion: d.Platform.OSVersion,
|
||||
OSFeatures: slices.Clone(d.Platform.OSFeatures),
|
||||
Variant: d.Platform.Variant,
|
||||
}
|
||||
if platform.MatchesPlatform(imagePlatform, wantedPlatform) {
|
||||
return d.Digest, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range index.Manifests {
|
||||
if d.Platform == nil {
|
||||
return d.Digest, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no image found in image index for architecture %s, variant %q, OS %s", wantedPlatforms[0].Architecture, wantedPlatforms[0].Variant, wantedPlatforms[0].OS)
|
||||
}
|
||||
|
||||
// Serialize returns the index in a blob format.
|
||||
// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made!
|
||||
func (index *OCI1IndexPublic) Serialize() ([]byte, error) {
|
||||
buf, err := json.Marshal(index)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling OCI1Index %#v: %w", index, err)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// OCI1IndexPublicFromComponents creates an OCI1 image index instance from the
|
||||
// supplied data.
|
||||
// This is publicly visible as c/image/manifest.OCI1IndexFromComponents.
|
||||
func OCI1IndexPublicFromComponents(components []imgspecv1.Descriptor, annotations map[string]string) *OCI1IndexPublic {
|
||||
index := OCI1IndexPublic{
|
||||
imgspecv1.Index{
|
||||
Versioned: imgspec.Versioned{SchemaVersion: 2},
|
||||
MediaType: imgspecv1.MediaTypeImageIndex,
|
||||
Manifests: make([]imgspecv1.Descriptor, len(components)),
|
||||
Annotations: maps.Clone(annotations),
|
||||
},
|
||||
}
|
||||
for i, component := range components {
|
||||
var platform *imgspecv1.Platform
|
||||
if component.Platform != nil {
|
||||
platform = &imgspecv1.Platform{
|
||||
Architecture: component.Platform.Architecture,
|
||||
OS: component.Platform.OS,
|
||||
OSVersion: component.Platform.OSVersion,
|
||||
OSFeatures: slices.Clone(component.Platform.OSFeatures),
|
||||
Variant: component.Platform.Variant,
|
||||
}
|
||||
}
|
||||
m := imgspecv1.Descriptor{
|
||||
MediaType: component.MediaType,
|
||||
Size: component.Size,
|
||||
Digest: component.Digest,
|
||||
URLs: slices.Clone(component.URLs),
|
||||
Annotations: maps.Clone(component.Annotations),
|
||||
Platform: platform,
|
||||
}
|
||||
index.Manifests[i] = m
|
||||
}
|
||||
return &index
|
||||
}
|
||||
|
||||
// OCI1IndexPublicClone creates a deep copy of the passed-in index.
|
||||
// This is publicly visible as c/image/manifest.OCI1IndexClone.
|
||||
func OCI1IndexPublicClone(index *OCI1IndexPublic) *OCI1IndexPublic {
|
||||
return OCI1IndexPublicFromComponents(index.Manifests, index.Annotations)
|
||||
}
|
||||
|
||||
// ToOCI1Index returns the index encoded as an OCI1 index.
|
||||
func (index *OCI1IndexPublic) ToOCI1Index() (*OCI1IndexPublic, error) {
|
||||
return OCI1IndexPublicClone(index), nil
|
||||
}
|
||||
|
||||
// ToSchema2List returns the index encoded as a Schema2 list.
|
||||
func (index *OCI1IndexPublic) ToSchema2List() (*Schema2ListPublic, error) {
|
||||
components := make([]Schema2ManifestDescriptor, 0, len(index.Manifests))
|
||||
for _, manifest := range index.Manifests {
|
||||
platform := manifest.Platform
|
||||
if platform == nil {
|
||||
platform = &imgspecv1.Platform{
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
}
|
||||
}
|
||||
converted := Schema2ManifestDescriptor{
|
||||
Schema2Descriptor{
|
||||
MediaType: manifest.MediaType,
|
||||
Size: manifest.Size,
|
||||
Digest: manifest.Digest,
|
||||
URLs: slices.Clone(manifest.URLs),
|
||||
},
|
||||
Schema2PlatformSpec{
|
||||
OS: platform.OS,
|
||||
Architecture: platform.Architecture,
|
||||
OSFeatures: slices.Clone(platform.OSFeatures),
|
||||
OSVersion: platform.OSVersion,
|
||||
Variant: platform.Variant,
|
||||
},
|
||||
}
|
||||
components = append(components, converted)
|
||||
}
|
||||
s2 := Schema2ListPublicFromComponents(components)
|
||||
return s2, nil
|
||||
}
|
||||
|
||||
// OCI1IndexPublicFromManifest creates an OCI1 manifest index instance from marshalled
|
||||
// JSON, presumably generated by encoding a OCI1 manifest index.
|
||||
// This is publicly visible as c/image/manifest.OCI1IndexFromManifest.
|
||||
func OCI1IndexPublicFromManifest(manifest []byte) (*OCI1IndexPublic, error) {
|
||||
index := OCI1IndexPublic{
|
||||
Index: imgspecv1.Index{
|
||||
Versioned: imgspec.Versioned{SchemaVersion: 2},
|
||||
MediaType: imgspecv1.MediaTypeImageIndex,
|
||||
Manifests: []imgspecv1.Descriptor{},
|
||||
Annotations: make(map[string]string),
|
||||
},
|
||||
}
|
||||
if err := json.Unmarshal(manifest, &index); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling OCI1Index %q: %w", string(manifest), err)
|
||||
}
|
||||
if err := ValidateUnambiguousManifestFormat(manifest, imgspecv1.MediaTypeImageIndex,
|
||||
AllowedFieldManifests); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &index, nil
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of this list and its contents.
|
||||
func (index *OCI1IndexPublic) Clone() ListPublic {
|
||||
return OCI1IndexPublicClone(index)
|
||||
}
|
||||
|
||||
// ConvertToMIMEType converts the passed-in image index to a manifest list of
|
||||
// the specified type.
|
||||
func (index *OCI1IndexPublic) ConvertToMIMEType(manifestMIMEType string) (ListPublic, error) {
|
||||
switch normalized := NormalizedMIMEType(manifestMIMEType); normalized {
|
||||
case DockerV2ListMediaType:
|
||||
return index.ToSchema2List()
|
||||
case imgspecv1.MediaTypeImageIndex:
|
||||
return index.Clone(), nil
|
||||
case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType:
|
||||
return nil, fmt.Errorf("Can not convert image index to MIME type %q, which is not a list type", manifestMIMEType)
|
||||
default:
|
||||
// Note that this may not be reachable, NormalizedMIMEType has a default for unknown values.
|
||||
return nil, fmt.Errorf("Unimplemented manifest MIME type %s", manifestMIMEType)
|
||||
}
|
||||
}
|
||||
|
||||
type OCI1Index struct {
|
||||
OCI1IndexPublic
|
||||
}
|
||||
|
||||
func oci1IndexFromPublic(public *OCI1IndexPublic) *OCI1Index {
|
||||
return &OCI1Index{*public}
|
||||
}
|
||||
|
||||
func (index *OCI1Index) CloneInternal() List {
|
||||
return oci1IndexFromPublic(OCI1IndexPublicClone(&index.OCI1IndexPublic))
|
||||
}
|
||||
|
||||
func (index *OCI1Index) Clone() ListPublic {
|
||||
return index.CloneInternal()
|
||||
}
|
||||
|
||||
// OCI1IndexFromManifest creates a OCI1 manifest list instance from marshalled
|
||||
// JSON, presumably generated by encoding a OCI1 manifest list.
|
||||
func OCI1IndexFromManifest(manifest []byte) (*OCI1Index, error) {
|
||||
public, err := OCI1IndexPublicFromManifest(manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return oci1IndexFromPublic(public), nil
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOCI1IndexPublicFromManifest(t *testing.T) {
|
||||
validManifest, err := os.ReadFile(filepath.Join("testdata", "ociv1.image.index.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
parser := func(m []byte) error {
|
||||
_, err := OCI1IndexPublicFromManifest(m)
|
||||
return err
|
||||
}
|
||||
// Schema mismatch is rejected
|
||||
testManifestFixturesAreRejected(t, parser, []string{
|
||||
"schema2-to-schema1-by-docker.json",
|
||||
"v2s2.manifest.json",
|
||||
// Not "v2list.manifest.json" yet, without mediaType the two are too similar to tell the difference.
|
||||
"ociv1.manifest.json",
|
||||
})
|
||||
// Extra fields are rejected
|
||||
testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"})
|
||||
}
|
||||
|
||||
func TestOCI1IndexFromManifest(t *testing.T) {
|
||||
validManifest, err := os.ReadFile(filepath.Join("testdata", "ociv1.image.index.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
parser := func(m []byte) error {
|
||||
_, err := OCI1IndexFromManifest(m)
|
||||
return err
|
||||
}
|
||||
// Schema mismatch is rejected
|
||||
testManifestFixturesAreRejected(t, parser, []string{
|
||||
"schema2-to-schema1-by-docker.json",
|
||||
"v2s2.manifest.json",
|
||||
// Not "v2list.manifest.json" yet, without mediaType the two are too similar to tell the difference.
|
||||
"ociv1.manifest.json",
|
||||
})
|
||||
// Extra fields are rejected
|
||||
testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"})
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.custom.artifact.config.v1+json",
|
||||
"digest": "",
|
||||
"size": 0
|
||||
},
|
||||
"layers": null
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"size": 7143,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
|
||||
"platform": {
|
||||
"architecture": "ppc64le",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"size": 7682,
|
||||
"digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"os.features": [
|
||||
"sse4"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"com.example.key1": "value1",
|
||||
"com.example.key2": "value2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"size": 7023,
|
||||
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 32654,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 16724,
|
||||
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 73109,
|
||||
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"com.example.key1": "value1",
|
||||
"com.example.key2": "value2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.custom.artifact.config.v1+json",
|
||||
"digest": "",
|
||||
"size": 0
|
||||
},
|
||||
"layers": null
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"size": 7143,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
|
||||
"platform": {
|
||||
"architecture": "ppc64le",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"size": 7682,
|
||||
"digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"os.features": [
|
||||
"sse4"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"com.example.key1": "value1",
|
||||
"com.example.key2": "value2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"size": 7023,
|
||||
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 32654,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 16724,
|
||||
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 73109,
|
||||
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"com.example.key1": "value1",
|
||||
"com.example.key2": "value2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../../internal/image/fixtures/schema2-to-schema1-by-docker.json
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"schemaVersion": 99999,
|
||||
"name": "mitr/noversion-nonsense",
|
||||
"tag": "latest"
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
|
||||
"size": 2094,
|
||||
"digest": "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783",
|
||||
"platform": {
|
||||
"architecture": "ppc64le",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
|
||||
"size": 1922,
|
||||
"digest": "sha256:ae1b0e06e8ade3a11267564a26e750585ba2259c0ecab59ab165ad1af41d1bdd",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"features": [
|
||||
"sse"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
|
||||
"size": 2084,
|
||||
"digest": "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2",
|
||||
"platform": {
|
||||
"architecture": "s390x",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
|
||||
"size": 2084,
|
||||
"digest": "sha256:07ebe243465ef4a667b78154ae6c3ea46fdb1582936aac3ac899ea311a701b40",
|
||||
"platform": {
|
||||
"architecture": "arm",
|
||||
"os": "linux",
|
||||
"variant": "armv7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
|
||||
"size": 2090,
|
||||
"digest": "sha256:fb2fc0707b86dafa9959fe3d29e66af8787aee4d9a23581714be65db4265ad8a",
|
||||
"platform": {
|
||||
"architecture": "arm64",
|
||||
"os": "linux",
|
||||
"variant": "armv8"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"name": "mitr/busybox",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
],
|
||||
"history": [
|
||||
],
|
||||
"signatures": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"name": "mitr/busybox",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
}
|
||||
],
|
||||
"history": [
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"name": "mitr/busybox",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
}
|
||||
],
|
||||
"history": [
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
}
|
||||
],
|
||||
"signatures": [
|
||||
{
|
||||
"header": {
|
||||
"jwk": {
|
||||
"crv": "P-256",
|
||||
"kid": "OZ45:U3IG:TDOI:PMBD:NGP2:LDIW:II2U:PSBI:MMCZ:YZUP:TUUO:XPZT",
|
||||
"kty": "EC",
|
||||
"x": "ReC5c0J9tgXSdUL4_xzEt5RsD8kFt2wWSgJcpAcOQx8",
|
||||
"y": "3sBGEqQ3ZMeqPKwQBAadN2toOUEASha18xa0WwsDF-M"
|
||||
},
|
||||
"alg": "ES256"
|
||||
},
|
||||
"signature": "dV1paJ3Ck1Ph4FcEhg_frjqxdlGdI6-ywRamk6CvMOcaOEUdCWCpCPQeBQpD2N6tGjkoG1BbstkFNflllfenCw",
|
||||
"protected": "eyJmb3JtYXRMZW5ndGgiOjU0NzgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNi0wNC0xOFQyMDo1NDo0MloifQ"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 7023,
|
||||
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 32654,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 16724,
|
||||
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 73109,
|
||||
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 7023,
|
||||
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
|
||||
},
|
||||
"layers": [
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package manifest
|
||||
|
||||
import "github.com/opencontainers/go-digest"
|
||||
|
||||
const (
|
||||
// TestV2S2ManifestDigest is the Docker manifest digest of "v2s2.manifest.json"
|
||||
TestDockerV2S2ManifestDigest = digest.Digest("sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55")
|
||||
// TestV2S1ManifestDigest is the Docker manifest digest of "v2s1.manifest.json"
|
||||
TestDockerV2S1ManifestDigest = digest.Digest("sha256:7364fea9d84ee548ab67d4c46c6006289800c98de3fbf8c0a97138dfcc23f000")
|
||||
// TestV2S1UnsignedManifestDigest is the Docker manifest digest of "v2s1unsigned.manifest.json"
|
||||
TestDockerV2S1UnsignedManifestDigest = digest.Digest("sha256:7364fea9d84ee548ab67d4c46c6006289800c98de3fbf8c0a97138dfcc23f000")
|
||||
)
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
compressiontypes "github.com/containers/image/v5/pkg/compression/types"
|
||||
|
|
@ -9,72 +8,6 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// allowedManifestFields is a bit mask of “essential” manifest fields that validateUnambiguousManifestFormat
|
||||
// can expect to be present.
|
||||
type allowedManifestFields int
|
||||
|
||||
const (
|
||||
allowedFieldConfig allowedManifestFields = 1 << iota
|
||||
allowedFieldFSLayers
|
||||
allowedFieldHistory
|
||||
allowedFieldLayers
|
||||
allowedFieldManifests
|
||||
allowedFieldFirstUnusedBit // Keep this at the end!
|
||||
)
|
||||
|
||||
// validateUnambiguousManifestFormat rejects manifests (incl. multi-arch) that look like more than
|
||||
// one kind we currently recognize, i.e. if they contain any of the known “essential” format fields
|
||||
// other than the ones the caller specifically allows.
|
||||
// expectedMIMEType is used only for diagnostics.
|
||||
// NOTE: The caller should do the non-heuristic validations (e.g. check for any specified format
|
||||
// identification/version, or other “magic numbers”) before calling this, to cleanly reject unambiguous
|
||||
// data that just isn’t what was expected, as opposed to actually ambiguous data.
|
||||
func validateUnambiguousManifestFormat(manifest []byte, expectedMIMEType string,
|
||||
allowed allowedManifestFields) error {
|
||||
if allowed >= allowedFieldFirstUnusedBit {
|
||||
return fmt.Errorf("internal error: invalid allowedManifestFields value %#v", allowed)
|
||||
}
|
||||
// Use a private type to decode, not just a map[string]any, because we want
|
||||
// to also reject case-insensitive matches (which would be used by Go when really decoding
|
||||
// the manifest).
|
||||
// (It is expected that as manifest formats are added or extended over time, more fields will be added
|
||||
// here.)
|
||||
detectedFields := struct {
|
||||
Config any `json:"config"`
|
||||
FSLayers any `json:"fsLayers"`
|
||||
History any `json:"history"`
|
||||
Layers any `json:"layers"`
|
||||
Manifests any `json:"manifests"`
|
||||
}{}
|
||||
if err := json.Unmarshal(manifest, &detectedFields); err != nil {
|
||||
// The caller was supposed to already validate version numbers, so this should not happen;
|
||||
// let’s not bother with making this error “nice”.
|
||||
return err
|
||||
}
|
||||
unexpected := []string{}
|
||||
// Sadly this isn’t easy to automate in Go, without reflection. So, copy&paste.
|
||||
if detectedFields.Config != nil && (allowed&allowedFieldConfig) == 0 {
|
||||
unexpected = append(unexpected, "config")
|
||||
}
|
||||
if detectedFields.FSLayers != nil && (allowed&allowedFieldFSLayers) == 0 {
|
||||
unexpected = append(unexpected, "fsLayers")
|
||||
}
|
||||
if detectedFields.History != nil && (allowed&allowedFieldHistory) == 0 {
|
||||
unexpected = append(unexpected, "history")
|
||||
}
|
||||
if detectedFields.Layers != nil && (allowed&allowedFieldLayers) == 0 {
|
||||
unexpected = append(unexpected, "layers")
|
||||
}
|
||||
if detectedFields.Manifests != nil && (allowed&allowedFieldManifests) == 0 {
|
||||
unexpected = append(unexpected, "manifests")
|
||||
}
|
||||
if len(unexpected) != 0 {
|
||||
return fmt.Errorf(`rejecting ambiguous manifest, unexpected fields %#v in supposedly %s`,
|
||||
unexpected, expectedMIMEType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// layerInfosToStrings converts a list of layer infos, presumably obtained from a Manifest.LayerInfos()
|
||||
// method call, into a format suitable for inclusion in a types.ImageInspectInfo structure.
|
||||
func layerInfosToStrings(infos []LayerInfo) []string {
|
||||
|
|
|
|||
|
|
@ -15,56 +15,6 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateUnambiguousManifestFormat(t *testing.T) {
|
||||
const allAllowedFields = allowedFieldFirstUnusedBit - 1
|
||||
const mt = "text/plain" // Just some MIME type that shows up in error messages
|
||||
|
||||
type test struct {
|
||||
manifest string
|
||||
allowed allowedManifestFields
|
||||
}
|
||||
|
||||
// Smoke tests: Success
|
||||
for _, c := range []test{
|
||||
{"{}", allAllowedFields},
|
||||
{"{}", 0},
|
||||
} {
|
||||
err := validateUnambiguousManifestFormat([]byte(c.manifest), mt, c.allowed)
|
||||
assert.NoError(t, err, c)
|
||||
}
|
||||
// Smoke tests: Failure
|
||||
for _, c := range []test{
|
||||
{"{}", allowedFieldFirstUnusedBit}, // Invalid "allowed"
|
||||
{"@", allAllowedFields}, // Invalid JSON
|
||||
} {
|
||||
err := validateUnambiguousManifestFormat([]byte(c.manifest), mt, c.allowed)
|
||||
assert.Error(t, err, c)
|
||||
}
|
||||
|
||||
fields := map[allowedManifestFields]string{
|
||||
allowedFieldConfig: "config",
|
||||
allowedFieldFSLayers: "fsLayers",
|
||||
allowedFieldHistory: "history",
|
||||
allowedFieldLayers: "layers",
|
||||
allowedFieldManifests: "manifests",
|
||||
}
|
||||
// Ensure this test covers all defined allowedManifestFields values
|
||||
allFields := allowedManifestFields(0)
|
||||
for k := range fields {
|
||||
allFields |= k
|
||||
}
|
||||
assert.Equal(t, allAllowedFields, allFields)
|
||||
|
||||
// Every single field is allowed by its bit, and rejected by any other bit
|
||||
for bit, fieldName := range fields {
|
||||
json := []byte(fmt.Sprintf(`{"%s":[]}`, fieldName))
|
||||
err := validateUnambiguousManifestFormat(json, mt, bit)
|
||||
assert.NoError(t, err, fieldName)
|
||||
err = validateUnambiguousManifestFormat(json, mt, allAllowedFields^bit)
|
||||
assert.Error(t, err, fieldName)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that parser() rejects all of the provided manifest fixtures.
|
||||
// Intended to help test manifest parsers' detection of schema mismatches.
|
||||
func testManifestFixturesAreRejected(t *testing.T, parser func([]byte) error, fixtures []string) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/containers/image/v5/docker/reference"
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
"github.com/containers/image/v5/internal/set"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/storage/pkg/regexp"
|
||||
|
|
@ -55,16 +56,16 @@ type Schema1V1Compatibility struct {
|
|||
// Schema1FromManifest creates a Schema1 manifest instance from a manifest blob.
|
||||
// (NOTE: The instance is not necessary a literal representation of the original blob,
|
||||
// layers with duplicate IDs are eliminated.)
|
||||
func Schema1FromManifest(manifest []byte) (*Schema1, error) {
|
||||
func Schema1FromManifest(manifestBlob []byte) (*Schema1, error) {
|
||||
s1 := Schema1{}
|
||||
if err := json.Unmarshal(manifest, &s1); err != nil {
|
||||
if err := json.Unmarshal(manifestBlob, &s1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s1.SchemaVersion != 1 {
|
||||
return nil, fmt.Errorf("unsupported schema version %d", s1.SchemaVersion)
|
||||
}
|
||||
if err := validateUnambiguousManifestFormat(manifest, DockerV2Schema1SignedMediaType,
|
||||
allowedFieldFSLayers|allowedFieldHistory); err != nil {
|
||||
if err := manifest.ValidateUnambiguousManifestFormat(manifestBlob, DockerV2Schema1SignedMediaType,
|
||||
manifest.AllowedFieldFSLayers|manifest.AllowedFieldHistory); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s1.initialize(); err != nil {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
compressiontypes "github.com/containers/image/v5/pkg/compression/types"
|
||||
"github.com/containers/image/v5/pkg/strslice"
|
||||
"github.com/containers/image/v5/types"
|
||||
|
|
@ -12,12 +13,7 @@ import (
|
|||
)
|
||||
|
||||
// Schema2Descriptor is a “descriptor” in docker/distribution schema 2.
|
||||
type Schema2Descriptor struct {
|
||||
MediaType string `json:"mediaType"`
|
||||
Size int64 `json:"size"`
|
||||
Digest digest.Digest `json:"digest"`
|
||||
URLs []string `json:"urls,omitempty"`
|
||||
}
|
||||
type Schema2Descriptor = manifest.Schema2Descriptor
|
||||
|
||||
// BlobInfoFromSchema2Descriptor returns a types.BlobInfo based on the input schema 2 descriptor.
|
||||
func BlobInfoFromSchema2Descriptor(desc Schema2Descriptor) types.BlobInfo {
|
||||
|
|
@ -159,13 +155,13 @@ type Schema2Image struct {
|
|||
}
|
||||
|
||||
// Schema2FromManifest creates a Schema2 manifest instance from a manifest blob.
|
||||
func Schema2FromManifest(manifest []byte) (*Schema2, error) {
|
||||
func Schema2FromManifest(manifestBlob []byte) (*Schema2, error) {
|
||||
s2 := Schema2{}
|
||||
if err := json.Unmarshal(manifest, &s2); err != nil {
|
||||
if err := json.Unmarshal(manifestBlob, &s2); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateUnambiguousManifestFormat(manifest, DockerV2Schema2MediaType,
|
||||
allowedFieldConfig|allowedFieldLayers); err != nil {
|
||||
if err := manifest.ValidateUnambiguousManifestFormat(manifestBlob, DockerV2Schema2MediaType,
|
||||
manifest.AllowedFieldConfig|manifest.AllowedFieldLayers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check manifest's and layers' media types.
|
||||
|
|
|
|||
|
|
@ -1,221 +1,32 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
platform "github.com/containers/image/v5/internal/pkg/platform"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"golang.org/x/exp/slices"
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
)
|
||||
|
||||
// Schema2PlatformSpec describes the platform which a particular manifest is
|
||||
// specialized for.
|
||||
type Schema2PlatformSpec struct {
|
||||
Architecture string `json:"architecture"`
|
||||
OS string `json:"os"`
|
||||
OSVersion string `json:"os.version,omitempty"`
|
||||
OSFeatures []string `json:"os.features,omitempty"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
Features []string `json:"features,omitempty"` // removed in OCI
|
||||
}
|
||||
type Schema2PlatformSpec = manifest.Schema2PlatformSpec
|
||||
|
||||
// Schema2ManifestDescriptor references a platform-specific manifest.
|
||||
type Schema2ManifestDescriptor struct {
|
||||
Schema2Descriptor
|
||||
Platform Schema2PlatformSpec `json:"platform"`
|
||||
}
|
||||
type Schema2ManifestDescriptor = manifest.Schema2ManifestDescriptor
|
||||
|
||||
// Schema2List is a list of platform-specific manifests.
|
||||
type Schema2List struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
MediaType string `json:"mediaType"`
|
||||
Manifests []Schema2ManifestDescriptor `json:"manifests"`
|
||||
}
|
||||
|
||||
// MIMEType returns the MIME type of this particular manifest list.
|
||||
func (list *Schema2List) MIMEType() string {
|
||||
return list.MediaType
|
||||
}
|
||||
|
||||
// Instances returns a slice of digests of the manifests that this list knows of.
|
||||
func (list *Schema2List) Instances() []digest.Digest {
|
||||
results := make([]digest.Digest, len(list.Manifests))
|
||||
for i, m := range list.Manifests {
|
||||
results[i] = m.Digest
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Instance returns the ListUpdate of a particular instance in the list.
|
||||
func (list *Schema2List) Instance(instanceDigest digest.Digest) (ListUpdate, error) {
|
||||
for _, manifest := range list.Manifests {
|
||||
if manifest.Digest == instanceDigest {
|
||||
return ListUpdate{
|
||||
Digest: manifest.Digest,
|
||||
Size: manifest.Size,
|
||||
MediaType: manifest.MediaType,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return ListUpdate{}, fmt.Errorf("unable to find instance %s passed to Schema2List.Instances", instanceDigest)
|
||||
}
|
||||
|
||||
// UpdateInstances updates the sizes, digests, and media types of the manifests
|
||||
// which the list catalogs.
|
||||
func (list *Schema2List) UpdateInstances(updates []ListUpdate) error {
|
||||
if len(updates) != len(list.Manifests) {
|
||||
return fmt.Errorf("incorrect number of update entries passed to Schema2List.UpdateInstances: expected %d, got %d", len(list.Manifests), len(updates))
|
||||
}
|
||||
for i := range updates {
|
||||
if err := updates[i].Digest.Validate(); err != nil {
|
||||
return fmt.Errorf("update %d of %d passed to Schema2List.UpdateInstances contained an invalid digest: %w", i+1, len(updates), err)
|
||||
}
|
||||
list.Manifests[i].Digest = updates[i].Digest
|
||||
if updates[i].Size < 0 {
|
||||
return fmt.Errorf("update %d of %d passed to Schema2List.UpdateInstances had an invalid size (%d)", i+1, len(updates), updates[i].Size)
|
||||
}
|
||||
list.Manifests[i].Size = updates[i].Size
|
||||
if updates[i].MediaType == "" {
|
||||
return fmt.Errorf("update %d of %d passed to Schema2List.UpdateInstances had no media type (was %q)", i+1, len(updates), list.Manifests[i].MediaType)
|
||||
}
|
||||
list.Manifests[i].MediaType = updates[i].MediaType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChooseInstance parses blob as a schema2 manifest list, and returns the digest
|
||||
// of the image which is appropriate for the current environment.
|
||||
func (list *Schema2List) ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) {
|
||||
wantedPlatforms, err := platform.WantedPlatforms(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting platform information %#v: %w", ctx, err)
|
||||
}
|
||||
for _, wantedPlatform := range wantedPlatforms {
|
||||
for _, d := range list.Manifests {
|
||||
imagePlatform := imgspecv1.Platform{
|
||||
Architecture: d.Platform.Architecture,
|
||||
OS: d.Platform.OS,
|
||||
OSVersion: d.Platform.OSVersion,
|
||||
OSFeatures: slices.Clone(d.Platform.OSFeatures),
|
||||
Variant: d.Platform.Variant,
|
||||
}
|
||||
if platform.MatchesPlatform(imagePlatform, wantedPlatform) {
|
||||
return d.Digest, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no image found in manifest list for architecture %s, variant %q, OS %s", wantedPlatforms[0].Architecture, wantedPlatforms[0].Variant, wantedPlatforms[0].OS)
|
||||
}
|
||||
|
||||
// Serialize returns the list in a blob format.
|
||||
// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made!
|
||||
func (list *Schema2List) Serialize() ([]byte, error) {
|
||||
buf, err := json.Marshal(list)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling Schema2List %#v: %w", list, err)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
type Schema2List = manifest.Schema2ListPublic
|
||||
|
||||
// Schema2ListFromComponents creates a Schema2 manifest list instance from the
|
||||
// supplied data.
|
||||
func Schema2ListFromComponents(components []Schema2ManifestDescriptor) *Schema2List {
|
||||
list := Schema2List{
|
||||
SchemaVersion: 2,
|
||||
MediaType: DockerV2ListMediaType,
|
||||
Manifests: make([]Schema2ManifestDescriptor, len(components)),
|
||||
}
|
||||
for i, component := range components {
|
||||
m := Schema2ManifestDescriptor{
|
||||
Schema2Descriptor{
|
||||
MediaType: component.MediaType,
|
||||
Size: component.Size,
|
||||
Digest: component.Digest,
|
||||
URLs: slices.Clone(component.URLs),
|
||||
},
|
||||
Schema2PlatformSpec{
|
||||
Architecture: component.Platform.Architecture,
|
||||
OS: component.Platform.OS,
|
||||
OSVersion: component.Platform.OSVersion,
|
||||
OSFeatures: slices.Clone(component.Platform.OSFeatures),
|
||||
Variant: component.Platform.Variant,
|
||||
Features: slices.Clone(component.Platform.Features),
|
||||
},
|
||||
}
|
||||
list.Manifests[i] = m
|
||||
}
|
||||
return &list
|
||||
return manifest.Schema2ListPublicFromComponents(components)
|
||||
}
|
||||
|
||||
// Schema2ListClone creates a deep copy of the passed-in list.
|
||||
func Schema2ListClone(list *Schema2List) *Schema2List {
|
||||
return Schema2ListFromComponents(list.Manifests)
|
||||
}
|
||||
|
||||
// ToOCI1Index returns the list encoded as an OCI1 index.
|
||||
func (list *Schema2List) ToOCI1Index() (*OCI1Index, error) {
|
||||
components := make([]imgspecv1.Descriptor, 0, len(list.Manifests))
|
||||
for _, manifest := range list.Manifests {
|
||||
converted := imgspecv1.Descriptor{
|
||||
MediaType: manifest.MediaType,
|
||||
Size: manifest.Size,
|
||||
Digest: manifest.Digest,
|
||||
URLs: slices.Clone(manifest.URLs),
|
||||
Platform: &imgspecv1.Platform{
|
||||
OS: manifest.Platform.OS,
|
||||
Architecture: manifest.Platform.Architecture,
|
||||
OSFeatures: slices.Clone(manifest.Platform.OSFeatures),
|
||||
OSVersion: manifest.Platform.OSVersion,
|
||||
Variant: manifest.Platform.Variant,
|
||||
},
|
||||
}
|
||||
components = append(components, converted)
|
||||
}
|
||||
oci := OCI1IndexFromComponents(components, nil)
|
||||
return oci, nil
|
||||
}
|
||||
|
||||
// ToSchema2List returns the list encoded as a Schema2 list.
|
||||
func (list *Schema2List) ToSchema2List() (*Schema2List, error) {
|
||||
return Schema2ListClone(list), nil
|
||||
return manifest.Schema2ListPublicClone(list)
|
||||
}
|
||||
|
||||
// Schema2ListFromManifest creates a Schema2 manifest list instance from marshalled
|
||||
// JSON, presumably generated by encoding a Schema2 manifest list.
|
||||
func Schema2ListFromManifest(manifest []byte) (*Schema2List, error) {
|
||||
list := Schema2List{
|
||||
Manifests: []Schema2ManifestDescriptor{},
|
||||
}
|
||||
if err := json.Unmarshal(manifest, &list); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling Schema2List %q: %w", string(manifest), err)
|
||||
}
|
||||
if err := validateUnambiguousManifestFormat(manifest, DockerV2ListMediaType,
|
||||
allowedFieldManifests); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &list, nil
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of this list and its contents.
|
||||
func (list *Schema2List) Clone() List {
|
||||
return Schema2ListClone(list)
|
||||
}
|
||||
|
||||
// ConvertToMIMEType converts the passed-in manifest list to a manifest
|
||||
// list of the specified type.
|
||||
func (list *Schema2List) ConvertToMIMEType(manifestMIMEType string) (List, error) {
|
||||
switch normalized := NormalizedMIMEType(manifestMIMEType); normalized {
|
||||
case DockerV2ListMediaType:
|
||||
return list.Clone(), nil
|
||||
case imgspecv1.MediaTypeImageIndex:
|
||||
return list.ToOCI1Index()
|
||||
case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType:
|
||||
return nil, fmt.Errorf("Can not convert manifest list to MIME type %q, which is not a list type", manifestMIMEType)
|
||||
default:
|
||||
// Note that this may not be reachable, NormalizedMIMEType has a default for unknown values.
|
||||
return nil, fmt.Errorf("Unimplemented manifest list MIME type %s", manifestMIMEType)
|
||||
}
|
||||
func Schema2ListFromManifest(manifestBlob []byte) (*Schema2List, error) {
|
||||
return manifest.Schema2ListPublicFromManifest(manifestBlob)
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/non-json.manifest.json
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.custom.artifact.config.v1+json",
|
||||
"digest": "",
|
||||
"size": 0
|
||||
},
|
||||
"layers": null
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/ociv1.artifact.json
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"size": 7143,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
|
||||
"platform": {
|
||||
"architecture": "ppc64le",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"size": 7682,
|
||||
"digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"os.features": [
|
||||
"sse4"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"com.example.key1": "value1",
|
||||
"com.example.key2": "value2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/ociv1.image.index.json
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"size": 7023,
|
||||
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 32654,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 16724,
|
||||
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 73109,
|
||||
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"com.example.key1": "value1",
|
||||
"com.example.key2": "value2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/ociv1.manifest.json
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.custom.artifact.config.v1+json",
|
||||
"digest": "",
|
||||
"size": 0
|
||||
},
|
||||
"layers": null
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/ociv1nomime.artifact.json
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"size": 7143,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
|
||||
"platform": {
|
||||
"architecture": "ppc64le",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"size": 7682,
|
||||
"digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"os.features": [
|
||||
"sse4"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"com.example.key1": "value1",
|
||||
"com.example.key2": "value2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/ociv1nomime.image.index.json
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"size": 7023,
|
||||
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 32654,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 16724,
|
||||
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 73109,
|
||||
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"com.example.key1": "value1",
|
||||
"com.example.key2": "value2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/ociv1nomime.manifest.json
|
||||
|
|
@ -1 +1 @@
|
|||
../../internal/image/fixtures/schema2-to-schema1-by-docker.json
|
||||
../../internal/manifest/testdata/schema2-to-schema1-by-docker.json
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 99999,
|
||||
"name": "mitr/noversion-nonsense",
|
||||
"tag": "latest"
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/unknown-version.manifest.json
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
|
||||
"size": 2094,
|
||||
"digest": "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783",
|
||||
"platform": {
|
||||
"architecture": "ppc64le",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
|
||||
"size": 1922,
|
||||
"digest": "sha256:ae1b0e06e8ade3a11267564a26e750585ba2259c0ecab59ab165ad1af41d1bdd",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"features": [
|
||||
"sse"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
|
||||
"size": 2084,
|
||||
"digest": "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2",
|
||||
"platform": {
|
||||
"architecture": "s390x",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
|
||||
"size": 2084,
|
||||
"digest": "sha256:07ebe243465ef4a667b78154ae6c3ea46fdb1582936aac3ac899ea311a701b40",
|
||||
"platform": {
|
||||
"architecture": "arm",
|
||||
"os": "linux",
|
||||
"variant": "armv7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
|
||||
"size": 2090,
|
||||
"digest": "sha256:fb2fc0707b86dafa9959fe3d29e66af8787aee4d9a23581714be65db4265ad8a",
|
||||
"platform": {
|
||||
"architecture": "arm64",
|
||||
"os": "linux",
|
||||
"variant": "armv8"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/v2list.manifest.json
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"name": "mitr/busybox",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
],
|
||||
"history": [
|
||||
],
|
||||
"signatures": 1
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/v2s1-invalid-signatures.manifest.json
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"name": "mitr/busybox",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
}
|
||||
],
|
||||
"history": [
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/v2s1-unsigned.manifest.json
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"name": "mitr/busybox",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
}
|
||||
],
|
||||
"history": [
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
}
|
||||
],
|
||||
"signatures": [
|
||||
{
|
||||
"header": {
|
||||
"jwk": {
|
||||
"crv": "P-256",
|
||||
"kid": "OZ45:U3IG:TDOI:PMBD:NGP2:LDIW:II2U:PSBI:MMCZ:YZUP:TUUO:XPZT",
|
||||
"kty": "EC",
|
||||
"x": "ReC5c0J9tgXSdUL4_xzEt5RsD8kFt2wWSgJcpAcOQx8",
|
||||
"y": "3sBGEqQ3ZMeqPKwQBAadN2toOUEASha18xa0WwsDF-M"
|
||||
},
|
||||
"alg": "ES256"
|
||||
},
|
||||
"signature": "dV1paJ3Ck1Ph4FcEhg_frjqxdlGdI6-ywRamk6CvMOcaOEUdCWCpCPQeBQpD2N6tGjkoG1BbstkFNflllfenCw",
|
||||
"protected": "eyJmb3JtYXRMZW5ndGgiOjU0NzgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNi0wNC0xOFQyMDo1NDo0MloifQ"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/v2s1.manifest.json
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 7023,
|
||||
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 32654,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 16724,
|
||||
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 73109,
|
||||
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/v2s2.manifest.json
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 7023,
|
||||
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
|
||||
},
|
||||
"layers": [
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../internal/manifest/testdata/v2s2nomime.manifest.json
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containers/image/v5/types"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
|
|
@ -21,56 +18,14 @@ var (
|
|||
// Callers can either use this abstract interface without understanding the details of the formats,
|
||||
// or instantiate a specific implementation (e.g. manifest.OCI1Index) and access the public members
|
||||
// directly.
|
||||
type List interface {
|
||||
// MIMEType returns the MIME type of this particular manifest list.
|
||||
MIMEType() string
|
||||
|
||||
// Instances returns a list of the manifests that this list knows of, other than its own.
|
||||
Instances() []digest.Digest
|
||||
|
||||
// Update information about the list's instances. The length of the passed-in slice must
|
||||
// match the length of the list of instances which the list already contains, and every field
|
||||
// must be specified.
|
||||
UpdateInstances([]ListUpdate) error
|
||||
|
||||
// Instance returns the size and MIME type of a particular instance in the list.
|
||||
Instance(digest.Digest) (ListUpdate, error)
|
||||
|
||||
// ChooseInstance selects which manifest is most appropriate for the platform described by the
|
||||
// SystemContext, or for the current platform if the SystemContext doesn't specify any details.
|
||||
ChooseInstance(ctx *types.SystemContext) (digest.Digest, error)
|
||||
|
||||
// Serialize returns the list in a blob format.
|
||||
// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded
|
||||
// from, even if no modifications were made!
|
||||
Serialize() ([]byte, error)
|
||||
|
||||
// ConvertToMIMEType returns the list rebuilt to the specified MIME type, or an error.
|
||||
ConvertToMIMEType(mimeType string) (List, error)
|
||||
|
||||
// Clone returns a deep copy of this list and its contents.
|
||||
Clone() List
|
||||
}
|
||||
type List = manifest.ListPublic
|
||||
|
||||
// ListUpdate includes the fields which a List's UpdateInstances() method will modify.
|
||||
type ListUpdate struct {
|
||||
Digest digest.Digest
|
||||
Size int64
|
||||
MediaType string
|
||||
}
|
||||
type ListUpdate = manifest.ListUpdate
|
||||
|
||||
// ListFromBlob parses a list of manifests.
|
||||
func ListFromBlob(manifest []byte, manifestMIMEType string) (List, error) {
|
||||
normalized := NormalizedMIMEType(manifestMIMEType)
|
||||
switch normalized {
|
||||
case DockerV2ListMediaType:
|
||||
return Schema2ListFromManifest(manifest)
|
||||
case imgspecv1.MediaTypeImageIndex:
|
||||
return OCI1IndexFromManifest(manifest)
|
||||
case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType:
|
||||
return nil, fmt.Errorf("Treating single images as manifest lists is not implemented")
|
||||
}
|
||||
return nil, fmt.Errorf("Unimplemented manifest list MIME type %s (normalized as %s)", manifestMIMEType, normalized)
|
||||
func ListFromBlob(manifestBlob []byte, manifestMIMEType string) (List, error) {
|
||||
return manifest.ListPublicFromBlob(manifestBlob, manifestMIMEType)
|
||||
}
|
||||
|
||||
// ConvertListToMIMEType converts the passed-in manifest list to a manifest
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
"github.com/containers/image/v5/types"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
|
@ -14,10 +15,10 @@ import (
|
|||
)
|
||||
|
||||
func pare(m List) {
|
||||
if impl, ok := m.(*OCI1Index); ok {
|
||||
if impl, ok := m.(*manifest.OCI1Index); ok {
|
||||
impl.Annotations = nil
|
||||
}
|
||||
if impl, ok := m.(*Schema2List); ok {
|
||||
if impl, ok := m.(*manifest.Schema2List); ok {
|
||||
for i := range impl.Manifests {
|
||||
impl.Manifests[i].Platform.Features = nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
internalManifest "github.com/containers/image/v5/internal/manifest"
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/libtrust"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
|
|
@ -16,28 +15,28 @@ import (
|
|||
// FIXME(runcom, mitr): should we have a mediatype pkg??
|
||||
const (
|
||||
// DockerV2Schema1MediaType MIME type represents Docker manifest schema 1
|
||||
DockerV2Schema1MediaType = "application/vnd.docker.distribution.manifest.v1+json"
|
||||
DockerV2Schema1MediaType = manifest.DockerV2Schema1MediaType
|
||||
// DockerV2Schema1MediaType MIME type represents Docker manifest schema 1 with a JWS signature
|
||||
DockerV2Schema1SignedMediaType = "application/vnd.docker.distribution.manifest.v1+prettyjws"
|
||||
DockerV2Schema1SignedMediaType = manifest.DockerV2Schema1SignedMediaType
|
||||
// DockerV2Schema2MediaType MIME type represents Docker manifest schema 2
|
||||
DockerV2Schema2MediaType = "application/vnd.docker.distribution.manifest.v2+json"
|
||||
DockerV2Schema2MediaType = manifest.DockerV2Schema2MediaType
|
||||
// DockerV2Schema2ConfigMediaType is the MIME type used for schema 2 config blobs.
|
||||
DockerV2Schema2ConfigMediaType = "application/vnd.docker.container.image.v1+json"
|
||||
DockerV2Schema2ConfigMediaType = manifest.DockerV2Schema2ConfigMediaType
|
||||
// DockerV2Schema2LayerMediaType is the MIME type used for schema 2 layers.
|
||||
DockerV2Schema2LayerMediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip"
|
||||
DockerV2Schema2LayerMediaType = manifest.DockerV2Schema2LayerMediaType
|
||||
// DockerV2SchemaLayerMediaTypeUncompressed is the mediaType used for uncompressed layers.
|
||||
DockerV2SchemaLayerMediaTypeUncompressed = "application/vnd.docker.image.rootfs.diff.tar"
|
||||
DockerV2SchemaLayerMediaTypeUncompressed = manifest.DockerV2SchemaLayerMediaTypeUncompressed
|
||||
// DockerV2ListMediaType MIME type represents Docker manifest schema 2 list
|
||||
DockerV2ListMediaType = "application/vnd.docker.distribution.manifest.list.v2+json"
|
||||
DockerV2ListMediaType = manifest.DockerV2ListMediaType
|
||||
// DockerV2Schema2ForeignLayerMediaType is the MIME type used for schema 2 foreign layers.
|
||||
DockerV2Schema2ForeignLayerMediaType = "application/vnd.docker.image.rootfs.foreign.diff.tar"
|
||||
DockerV2Schema2ForeignLayerMediaType = manifest.DockerV2Schema2ForeignLayerMediaType
|
||||
// DockerV2Schema2ForeignLayerMediaType is the MIME type used for gzipped schema 2 foreign layers.
|
||||
DockerV2Schema2ForeignLayerMediaTypeGzip = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
|
||||
DockerV2Schema2ForeignLayerMediaTypeGzip = manifest.DockerV2Schema2ForeignLayerMediaTypeGzip
|
||||
)
|
||||
|
||||
// NonImageArtifactError (detected via errors.As) is used when asking for an image-specific operation
|
||||
// on an object which is not a “container image” in the standard sense (e.g. an OCI artifact)
|
||||
type NonImageArtifactError = internalManifest.NonImageArtifactError
|
||||
type NonImageArtifactError = manifest.NonImageArtifactError
|
||||
|
||||
// SupportedSchema2MediaType checks if the specified string is a supported Docker v2s2 media type.
|
||||
func SupportedSchema2MediaType(m string) error {
|
||||
|
|
@ -102,102 +101,21 @@ type LayerInfo struct {
|
|||
// GuessMIMEType guesses MIME type of a manifest and returns it _if it is recognized_, or "" if unknown or unrecognized.
|
||||
// FIXME? We should, in general, prefer out-of-band MIME type instead of blindly parsing the manifest,
|
||||
// but we may not have such metadata available (e.g. when the manifest is a local file).
|
||||
func GuessMIMEType(manifest []byte) string {
|
||||
// A subset of manifest fields; the rest is silently ignored by json.Unmarshal.
|
||||
// Also docker/distribution/manifest.Versioned.
|
||||
meta := struct {
|
||||
MediaType string `json:"mediaType"`
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
Signatures any `json:"signatures"`
|
||||
}{}
|
||||
if err := json.Unmarshal(manifest, &meta); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch meta.MediaType {
|
||||
case DockerV2Schema2MediaType, DockerV2ListMediaType,
|
||||
imgspecv1.MediaTypeImageManifest, imgspecv1.MediaTypeImageIndex: // A recognized type.
|
||||
return meta.MediaType
|
||||
}
|
||||
// this is the only way the function can return DockerV2Schema1MediaType, and recognizing that is essential for stripping the JWS signatures = computing the correct manifest digest.
|
||||
switch meta.SchemaVersion {
|
||||
case 1:
|
||||
if meta.Signatures != nil {
|
||||
return DockerV2Schema1SignedMediaType
|
||||
}
|
||||
return DockerV2Schema1MediaType
|
||||
case 2:
|
||||
// Best effort to understand if this is an OCI image since mediaType
|
||||
// wasn't in the manifest for OCI image-spec < 1.0.2.
|
||||
// For docker v2s2 meta.MediaType should have been set. But given the data, this is our best guess.
|
||||
ociMan := struct {
|
||||
Config struct {
|
||||
MediaType string `json:"mediaType"`
|
||||
} `json:"config"`
|
||||
}{}
|
||||
if err := json.Unmarshal(manifest, &ociMan); err != nil {
|
||||
return ""
|
||||
}
|
||||
switch ociMan.Config.MediaType {
|
||||
case imgspecv1.MediaTypeImageConfig:
|
||||
return imgspecv1.MediaTypeImageManifest
|
||||
case DockerV2Schema2ConfigMediaType:
|
||||
// This case should not happen since a Docker image
|
||||
// must declare a top-level media type and
|
||||
// `meta.MediaType` has already been checked.
|
||||
return DockerV2Schema2MediaType
|
||||
}
|
||||
// Maybe an image index or an OCI artifact.
|
||||
ociIndex := struct {
|
||||
Manifests []imgspecv1.Descriptor `json:"manifests"`
|
||||
}{}
|
||||
if err := json.Unmarshal(manifest, &ociIndex); err != nil {
|
||||
return ""
|
||||
}
|
||||
if len(ociIndex.Manifests) != 0 {
|
||||
if ociMan.Config.MediaType == "" {
|
||||
return imgspecv1.MediaTypeImageIndex
|
||||
}
|
||||
// FIXME: this is mixing media types of manifests and configs.
|
||||
return ociMan.Config.MediaType
|
||||
}
|
||||
// It's most likely an OCI artifact with a custom config media
|
||||
// type which is not (and cannot) be covered by the media-type
|
||||
// checks cabove.
|
||||
return imgspecv1.MediaTypeImageManifest
|
||||
}
|
||||
return ""
|
||||
func GuessMIMEType(manifestBlob []byte) string {
|
||||
return manifest.GuessMIMEType(manifestBlob)
|
||||
}
|
||||
|
||||
// Digest returns the a digest of a docker manifest, with any necessary implied transformations like stripping v1s1 signatures.
|
||||
func Digest(manifest []byte) (digest.Digest, error) {
|
||||
if GuessMIMEType(manifest) == DockerV2Schema1SignedMediaType {
|
||||
sig, err := libtrust.ParsePrettySignature(manifest, "signatures")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
manifest, err = sig.Payload()
|
||||
if err != nil {
|
||||
// Coverage: This should never happen, libtrust's Payload() can fail only if joseBase64UrlDecode() fails, on a string
|
||||
// that libtrust itself has josebase64UrlEncode()d
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return digest.FromBytes(manifest), nil
|
||||
func Digest(manifestBlob []byte) (digest.Digest, error) {
|
||||
return manifest.Digest(manifestBlob)
|
||||
}
|
||||
|
||||
// MatchesDigest returns true iff the manifest matches expectedDigest.
|
||||
// Error may be set if this returns false.
|
||||
// Note that this is not doing ConstantTimeCompare; by the time we get here, the cryptographic signature must already have been verified,
|
||||
// or we are not using a cryptographic channel and the attacker can modify the digest along with the manifest blob.
|
||||
func MatchesDigest(manifest []byte, expectedDigest digest.Digest) (bool, error) {
|
||||
// This should eventually support various digest types.
|
||||
actualDigest, err := Digest(manifest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return expectedDigest == actualDigest, nil
|
||||
func MatchesDigest(manifestBlob []byte, expectedDigest digest.Digest) (bool, error) {
|
||||
return manifest.MatchesDigest(manifestBlob, expectedDigest)
|
||||
}
|
||||
|
||||
// AddDummyV2S1Signature adds an JWS signature with a temporary key (i.e. useless) to a v2s1 manifest.
|
||||
|
|
@ -231,30 +149,7 @@ func MIMETypeSupportsEncryption(mimeType string) bool {
|
|||
// NormalizedMIMEType returns the effective MIME type of a manifest MIME type returned by a server,
|
||||
// centralizing various workarounds.
|
||||
func NormalizedMIMEType(input string) string {
|
||||
switch input {
|
||||
// "application/json" is a valid v2s1 value per https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md .
|
||||
// This works for now, when nothing else seems to return "application/json"; if that were not true, the mapping/detection might
|
||||
// need to happen within the ImageSource.
|
||||
case "application/json":
|
||||
return DockerV2Schema1SignedMediaType
|
||||
case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType,
|
||||
imgspecv1.MediaTypeImageManifest,
|
||||
imgspecv1.MediaTypeImageIndex,
|
||||
DockerV2Schema2MediaType,
|
||||
DockerV2ListMediaType:
|
||||
return input
|
||||
default:
|
||||
// If it's not a recognized manifest media type, or we have failed determining the type, we'll try one last time
|
||||
// to deserialize using v2s1 as per https://github.com/docker/distribution/blob/master/manifests.go#L108
|
||||
// and https://github.com/docker/distribution/blob/master/manifest/schema1/manifest.go#L50
|
||||
//
|
||||
// Crane registries can also return "text/plain", or pretty much anything else depending on a file extension “recognized” in the tag.
|
||||
// This makes no real sense, but it happens
|
||||
// because requests for manifests are
|
||||
// redirected to a content distribution
|
||||
// network which is configured that way. See https://bugzilla.redhat.com/show_bug.cgi?id=1389442
|
||||
return DockerV2Schema1SignedMediaType
|
||||
}
|
||||
return manifest.NormalizedMIMEType(input)
|
||||
}
|
||||
|
||||
// FromBlob returns a Manifest instance for the specified manifest blob and the corresponding MIME type
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
internalManifest "github.com/containers/image/v5/internal/manifest"
|
||||
compressiontypes "github.com/containers/image/v5/pkg/compression/types"
|
||||
"github.com/containers/image/v5/types"
|
||||
|
|
@ -50,13 +51,13 @@ func SupportedOCI1MediaType(m string) error {
|
|||
}
|
||||
|
||||
// OCI1FromManifest creates an OCI1 manifest instance from a manifest blob.
|
||||
func OCI1FromManifest(manifest []byte) (*OCI1, error) {
|
||||
func OCI1FromManifest(manifestBlob []byte) (*OCI1, error) {
|
||||
oci1 := OCI1{}
|
||||
if err := json.Unmarshal(manifest, &oci1); err != nil {
|
||||
if err := json.Unmarshal(manifestBlob, &oci1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateUnambiguousManifestFormat(manifest, imgspecv1.MediaTypeImageIndex,
|
||||
allowedFieldConfig|allowedFieldLayers); err != nil {
|
||||
if err := manifest.ValidateUnambiguousManifestFormat(manifestBlob, imgspecv1.MediaTypeImageIndex,
|
||||
manifest.AllowedFieldConfig|manifest.AllowedFieldLayers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &oci1, nil
|
||||
|
|
|
|||
|
|
@ -1,234 +1,27 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
platform "github.com/containers/image/v5/internal/pkg/platform"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
imgspec "github.com/opencontainers/image-spec/specs-go"
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// OCI1Index is just an alias for the OCI index type, but one which we can
|
||||
// provide methods for.
|
||||
type OCI1Index struct {
|
||||
imgspecv1.Index
|
||||
}
|
||||
|
||||
// MIMEType returns the MIME type of this particular manifest index.
|
||||
func (index *OCI1Index) MIMEType() string {
|
||||
return imgspecv1.MediaTypeImageIndex
|
||||
}
|
||||
|
||||
// Instances returns a slice of digests of the manifests that this index knows of.
|
||||
func (index *OCI1Index) Instances() []digest.Digest {
|
||||
results := make([]digest.Digest, len(index.Manifests))
|
||||
for i, m := range index.Manifests {
|
||||
results[i] = m.Digest
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Instance returns the ListUpdate of a particular instance in the index.
|
||||
func (index *OCI1Index) Instance(instanceDigest digest.Digest) (ListUpdate, error) {
|
||||
for _, manifest := range index.Manifests {
|
||||
if manifest.Digest == instanceDigest {
|
||||
return ListUpdate{
|
||||
Digest: manifest.Digest,
|
||||
Size: manifest.Size,
|
||||
MediaType: manifest.MediaType,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return ListUpdate{}, fmt.Errorf("unable to find instance %s in OCI1Index", instanceDigest)
|
||||
}
|
||||
|
||||
// UpdateInstances updates the sizes, digests, and media types of the manifests
|
||||
// which the list catalogs.
|
||||
func (index *OCI1Index) UpdateInstances(updates []ListUpdate) error {
|
||||
if len(updates) != len(index.Manifests) {
|
||||
return fmt.Errorf("incorrect number of update entries passed to OCI1Index.UpdateInstances: expected %d, got %d", len(index.Manifests), len(updates))
|
||||
}
|
||||
for i := range updates {
|
||||
if err := updates[i].Digest.Validate(); err != nil {
|
||||
return fmt.Errorf("update %d of %d passed to OCI1Index.UpdateInstances contained an invalid digest: %w", i+1, len(updates), err)
|
||||
}
|
||||
index.Manifests[i].Digest = updates[i].Digest
|
||||
if updates[i].Size < 0 {
|
||||
return fmt.Errorf("update %d of %d passed to OCI1Index.UpdateInstances had an invalid size (%d)", i+1, len(updates), updates[i].Size)
|
||||
}
|
||||
index.Manifests[i].Size = updates[i].Size
|
||||
if updates[i].MediaType == "" {
|
||||
return fmt.Errorf("update %d of %d passed to OCI1Index.UpdateInstances had no media type (was %q)", i+1, len(updates), index.Manifests[i].MediaType)
|
||||
}
|
||||
index.Manifests[i].MediaType = updates[i].MediaType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChooseInstance parses blob as an oci v1 manifest index, and returns the digest
|
||||
// of the image which is appropriate for the current environment.
|
||||
func (index *OCI1Index) ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) {
|
||||
wantedPlatforms, err := platform.WantedPlatforms(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting platform information %#v: %w", ctx, err)
|
||||
}
|
||||
for _, wantedPlatform := range wantedPlatforms {
|
||||
for _, d := range index.Manifests {
|
||||
if d.Platform == nil {
|
||||
continue
|
||||
}
|
||||
imagePlatform := imgspecv1.Platform{
|
||||
Architecture: d.Platform.Architecture,
|
||||
OS: d.Platform.OS,
|
||||
OSVersion: d.Platform.OSVersion,
|
||||
OSFeatures: slices.Clone(d.Platform.OSFeatures),
|
||||
Variant: d.Platform.Variant,
|
||||
}
|
||||
if platform.MatchesPlatform(imagePlatform, wantedPlatform) {
|
||||
return d.Digest, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range index.Manifests {
|
||||
if d.Platform == nil {
|
||||
return d.Digest, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no image found in image index for architecture %s, variant %q, OS %s", wantedPlatforms[0].Architecture, wantedPlatforms[0].Variant, wantedPlatforms[0].OS)
|
||||
}
|
||||
|
||||
// Serialize returns the index in a blob format.
|
||||
// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made!
|
||||
func (index *OCI1Index) Serialize() ([]byte, error) {
|
||||
buf, err := json.Marshal(index)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling OCI1Index %#v: %w", index, err)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
type OCI1Index = manifest.OCI1IndexPublic
|
||||
|
||||
// OCI1IndexFromComponents creates an OCI1 image index instance from the
|
||||
// supplied data.
|
||||
func OCI1IndexFromComponents(components []imgspecv1.Descriptor, annotations map[string]string) *OCI1Index {
|
||||
index := OCI1Index{
|
||||
imgspecv1.Index{
|
||||
Versioned: imgspec.Versioned{SchemaVersion: 2},
|
||||
MediaType: imgspecv1.MediaTypeImageIndex,
|
||||
Manifests: make([]imgspecv1.Descriptor, len(components)),
|
||||
Annotations: maps.Clone(annotations),
|
||||
},
|
||||
}
|
||||
for i, component := range components {
|
||||
var platform *imgspecv1.Platform
|
||||
if component.Platform != nil {
|
||||
platform = &imgspecv1.Platform{
|
||||
Architecture: component.Platform.Architecture,
|
||||
OS: component.Platform.OS,
|
||||
OSVersion: component.Platform.OSVersion,
|
||||
OSFeatures: slices.Clone(component.Platform.OSFeatures),
|
||||
Variant: component.Platform.Variant,
|
||||
}
|
||||
}
|
||||
m := imgspecv1.Descriptor{
|
||||
MediaType: component.MediaType,
|
||||
Size: component.Size,
|
||||
Digest: component.Digest,
|
||||
URLs: slices.Clone(component.URLs),
|
||||
Annotations: maps.Clone(component.Annotations),
|
||||
Platform: platform,
|
||||
}
|
||||
index.Manifests[i] = m
|
||||
}
|
||||
return &index
|
||||
return manifest.OCI1IndexPublicFromComponents(components, annotations)
|
||||
}
|
||||
|
||||
// OCI1IndexClone creates a deep copy of the passed-in index.
|
||||
func OCI1IndexClone(index *OCI1Index) *OCI1Index {
|
||||
return OCI1IndexFromComponents(index.Manifests, index.Annotations)
|
||||
}
|
||||
|
||||
// ToOCI1Index returns the index encoded as an OCI1 index.
|
||||
func (index *OCI1Index) ToOCI1Index() (*OCI1Index, error) {
|
||||
return OCI1IndexClone(index), nil
|
||||
}
|
||||
|
||||
// ToSchema2List returns the index encoded as a Schema2 list.
|
||||
func (index *OCI1Index) ToSchema2List() (*Schema2List, error) {
|
||||
components := make([]Schema2ManifestDescriptor, 0, len(index.Manifests))
|
||||
for _, manifest := range index.Manifests {
|
||||
platform := manifest.Platform
|
||||
if platform == nil {
|
||||
platform = &imgspecv1.Platform{
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
}
|
||||
}
|
||||
converted := Schema2ManifestDescriptor{
|
||||
Schema2Descriptor{
|
||||
MediaType: manifest.MediaType,
|
||||
Size: manifest.Size,
|
||||
Digest: manifest.Digest,
|
||||
URLs: slices.Clone(manifest.URLs),
|
||||
},
|
||||
Schema2PlatformSpec{
|
||||
OS: platform.OS,
|
||||
Architecture: platform.Architecture,
|
||||
OSFeatures: slices.Clone(platform.OSFeatures),
|
||||
OSVersion: platform.OSVersion,
|
||||
Variant: platform.Variant,
|
||||
},
|
||||
}
|
||||
components = append(components, converted)
|
||||
}
|
||||
s2 := Schema2ListFromComponents(components)
|
||||
return s2, nil
|
||||
return manifest.OCI1IndexPublicClone(index)
|
||||
}
|
||||
|
||||
// OCI1IndexFromManifest creates an OCI1 manifest index instance from marshalled
|
||||
// JSON, presumably generated by encoding a OCI1 manifest index.
|
||||
func OCI1IndexFromManifest(manifest []byte) (*OCI1Index, error) {
|
||||
index := OCI1Index{
|
||||
Index: imgspecv1.Index{
|
||||
Versioned: imgspec.Versioned{SchemaVersion: 2},
|
||||
MediaType: imgspecv1.MediaTypeImageIndex,
|
||||
Manifests: []imgspecv1.Descriptor{},
|
||||
Annotations: make(map[string]string),
|
||||
},
|
||||
}
|
||||
if err := json.Unmarshal(manifest, &index); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling OCI1Index %q: %w", string(manifest), err)
|
||||
}
|
||||
if err := validateUnambiguousManifestFormat(manifest, imgspecv1.MediaTypeImageIndex,
|
||||
allowedFieldManifests); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &index, nil
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of this list and its contents.
|
||||
func (index *OCI1Index) Clone() List {
|
||||
return OCI1IndexClone(index)
|
||||
}
|
||||
|
||||
// ConvertToMIMEType converts the passed-in image index to a manifest list of
|
||||
// the specified type.
|
||||
func (index *OCI1Index) ConvertToMIMEType(manifestMIMEType string) (List, error) {
|
||||
switch normalized := NormalizedMIMEType(manifestMIMEType); normalized {
|
||||
case DockerV2ListMediaType:
|
||||
return index.ToSchema2List()
|
||||
case imgspecv1.MediaTypeImageIndex:
|
||||
return index.Clone(), nil
|
||||
case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType:
|
||||
return nil, fmt.Errorf("Can not convert image index to MIME type %q, which is not a list type", manifestMIMEType)
|
||||
default:
|
||||
// Note that this may not be reachable, NormalizedMIMEType has a default for unknown values.
|
||||
return nil, fmt.Errorf("Unimplemented manifest MIME type %s", manifestMIMEType)
|
||||
}
|
||||
func OCI1IndexFromManifest(manifestBlob []byte) (*OCI1Index, error) {
|
||||
return manifest.OCI1IndexPublicFromManifest(manifestBlob)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import (
|
|||
|
||||
"github.com/containers/image/v5/internal/imagedestination/impl"
|
||||
"github.com/containers/image/v5/internal/imagedestination/stubs"
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
"github.com/containers/image/v5/internal/private"
|
||||
"github.com/containers/image/v5/internal/putblobdigest"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/types"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
imgspec "github.com/opencontainers/image-spec/specs-go"
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import (
|
|||
|
||||
"github.com/containers/image/v5/internal/imagesource/impl"
|
||||
"github.com/containers/image/v5/internal/imagesource/stubs"
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
"github.com/containers/image/v5/internal/private"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/pkg/tlsclientconfig"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import (
|
|||
"github.com/containers/image/v5/internal/image"
|
||||
"github.com/containers/image/v5/internal/imagesource"
|
||||
"github.com/containers/image/v5/internal/imagesource/impl"
|
||||
"github.com/containers/image/v5/internal/manifest"
|
||||
"github.com/containers/image/v5/internal/private"
|
||||
"github.com/containers/image/v5/internal/signature"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/pkg/compression"
|
||||
"github.com/containers/image/v5/transports"
|
||||
"github.com/containers/image/v5/types"
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
imanifest "github.com/containers/image/v5/internal/manifest"
|
||||
"github.com/containers/image/v5/internal/private"
|
||||
imanifest "github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/pkg/blobinfocache/memory"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/storage"
|
||||
|
|
|
|||
Loading…
Reference in New Issue