diff --git a/pkg/manifests/errors.go b/pkg/manifests/errors.go new file mode 100644 index 00000000000..8398d7efcfe --- /dev/null +++ b/pkg/manifests/errors.go @@ -0,0 +1,16 @@ +package manifests + +import ( + "errors" +) + +var ( + // ErrDigestNotFound is returned when we look for an image instance + // with a particular digest in a list or index, and fail to find it. + ErrDigestNotFound = errors.New("no image instance matching the specified digest was found in the list or index") + // ErrManifestTypeNotSupported is returned when we attempt to parse a + // manifest with a known MIME type as a list or index, or when we attempt + // to serialize a list or index to a manifest with a MIME type that we + // don't know how to encode. + ErrManifestTypeNotSupported = errors.New("manifest type not supported") +) diff --git a/pkg/manifests/manifests.go b/pkg/manifests/manifests.go new file mode 100644 index 00000000000..ea9495ee73a --- /dev/null +++ b/pkg/manifests/manifests.go @@ -0,0 +1,493 @@ +package manifests + +import ( + "encoding/json" + "os" + + "github.com/containers/image/v5/manifest" + digest "github.com/opencontainers/go-digest" + imgspec "github.com/opencontainers/image-spec/specs-go" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" +) + +// List is a generic interface for manipulating a manifest list or an image +// index. +type List interface { + AddInstance(manifestDigest digest.Digest, manifestSize int64, manifestType, os, architecture, osVersion string, osFeatures []string, variant string, features []string, annotations []string) error + Remove(instanceDigest digest.Digest) error + + SetURLs(instanceDigest digest.Digest, urls []string) error + URLs(instanceDigest digest.Digest) ([]string, error) + + SetAnnotations(instanceDigest *digest.Digest, annotations map[string]string) error + Annotations(instanceDigest *digest.Digest) (map[string]string, error) + + SetOS(instanceDigest digest.Digest, os string) error + OS(instanceDigest digest.Digest) (string, error) + + SetArchitecture(instanceDigest digest.Digest, arch string) error + Architecture(instanceDigest digest.Digest) (string, error) + + SetOSVersion(instanceDigest digest.Digest, osVersion string) error + OSVersion(instanceDigest digest.Digest) (string, error) + + SetVariant(instanceDigest digest.Digest, variant string) error + Variant(instanceDigest digest.Digest) (string, error) + + SetFeatures(instanceDigest digest.Digest, features []string) error + Features(instanceDigest digest.Digest) ([]string, error) + + SetOSFeatures(instanceDigest digest.Digest, osFeatures []string) error + OSFeatures(instanceDigest digest.Digest) ([]string, error) + + Serialize(mimeType string) ([]byte, error) + Instances() []digest.Digest + OCIv1() *v1.Index + Docker() *manifest.Schema2List + + findDocker(instanceDigest digest.Digest) (*manifest.Schema2ManifestDescriptor, error) + findOCIv1(instanceDigest digest.Digest) (*v1.Descriptor, error) +} + +type list struct { + docker manifest.Schema2List + oci v1.Index +} + +// OCIv1 returns the list as a Docker schema 2 list. The returned structure should NOT be modified. +func (l *list) Docker() *manifest.Schema2List { + return &l.docker +} + +// OCIv1 returns the list as an OCI image index. The returned structure should NOT be modified. +func (l *list) OCIv1() *v1.Index { + return &l.oci +} + +// Create creates a new list. +func Create() List { + return &list{ + docker: manifest.Schema2List{ + SchemaVersion: 2, + MediaType: manifest.DockerV2ListMediaType, + }, + oci: v1.Index{ + Versioned: imgspec.Versioned{SchemaVersion: 2}, + }, + } +} + +// AddInstance adds an entry for the specified manifest digest, with assorted +// additional information specified in parameters, to the list or index. +func (l *list) AddInstance(manifestDigest digest.Digest, manifestSize int64, manifestType, osName, architecture, osVersion string, osFeatures []string, variant string, features []string, annotations []string) error { + if err := l.Remove(manifestDigest); err != nil && !os.IsNotExist(errors.Cause(err)) { + return err + } + + schema2platform := manifest.Schema2PlatformSpec{ + Architecture: architecture, + OS: osName, + OSVersion: osVersion, + OSFeatures: osFeatures, + Variant: variant, + Features: features, + } + l.docker.Manifests = append(l.docker.Manifests, manifest.Schema2ManifestDescriptor{ + Schema2Descriptor: manifest.Schema2Descriptor{ + MediaType: manifestType, + Size: manifestSize, + Digest: manifestDigest, + }, + Platform: schema2platform, + }) + + ociv1platform := v1.Platform{ + Architecture: architecture, + OS: osName, + OSVersion: osVersion, + OSFeatures: osFeatures, + Variant: variant, + } + l.oci.Manifests = append(l.oci.Manifests, v1.Descriptor{ + MediaType: manifestType, + Size: manifestSize, + Digest: manifestDigest, + Platform: &ociv1platform, + }) + + return nil +} + +// Remove filters out any instances in the list which match the specified digest. +func (l *list) Remove(instanceDigest digest.Digest) error { + err := errors.Wrapf(os.ErrNotExist, "no instance matching digest %q found in manifest list", instanceDigest) + newDockerManifests := make([]manifest.Schema2ManifestDescriptor, 0, len(l.docker.Manifests)) + for i := range l.docker.Manifests { + if l.docker.Manifests[i].Digest != instanceDigest { + newDockerManifests = append(newDockerManifests, l.docker.Manifests[i]) + } else { + err = nil + } + } + l.docker.Manifests = newDockerManifests + newOCIv1Manifests := make([]v1.Descriptor, 0, len(l.oci.Manifests)) + for i := range l.oci.Manifests { + if l.oci.Manifests[i].Digest != instanceDigest { + newOCIv1Manifests = append(newOCIv1Manifests, l.oci.Manifests[i]) + } else { + err = nil + } + } + l.oci.Manifests = newOCIv1Manifests + return err +} + +func (l *list) findDocker(instanceDigest digest.Digest) (*manifest.Schema2ManifestDescriptor, error) { + for i := range l.docker.Manifests { + if l.docker.Manifests[i].Digest == instanceDigest { + return &l.docker.Manifests[i], nil + } + } + return nil, errors.Wrapf(ErrDigestNotFound, "no Docker manifest matching digest %q was found in list", instanceDigest.String()) +} + +func (l *list) findOCIv1(instanceDigest digest.Digest) (*v1.Descriptor, error) { + for i := range l.oci.Manifests { + if l.oci.Manifests[i].Digest == instanceDigest { + return &l.oci.Manifests[i], nil + } + } + return nil, errors.Wrapf(ErrDigestNotFound, "no OCI manifest matching digest %q was found in list", instanceDigest.String()) +} + +// SetURLs sets the URLs where the manifest might also be found. +func (l *list) SetURLs(instanceDigest digest.Digest, urls []string) error { + oci, err := l.findOCIv1(instanceDigest) + if err != nil { + return err + } + docker, err := l.findDocker(instanceDigest) + if err != nil { + return err + } + oci.URLs = append([]string{}, urls...) + docker.URLs = append([]string{}, urls...) + return nil +} + +// URLs retrieves the locations from which this object might possibly be downloaded. +func (l *list) URLs(instanceDigest digest.Digest) ([]string, error) { + oci, err := l.findOCIv1(instanceDigest) + if err != nil { + return nil, err + } + return append([]string{}, oci.URLs...), nil +} + +// SetAnnotations sets annotations on the image index, or on a specific manifest. +// The field is specific to the OCI image index format, and is not present in Docker manifest lists. +func (l *list) SetAnnotations(instanceDigest *digest.Digest, annotations map[string]string) error { + a := &l.oci.Annotations + if instanceDigest != nil { + oci, err := l.findOCIv1(*instanceDigest) + if err != nil { + return err + } + a = &oci.Annotations + } + (*a) = make(map[string]string) + for k, v := range annotations { + (*a)[k] = v + } + return nil +} + +// Annotations retrieves the annotations which have been set on the image index, or on one instance. +// The field is specific to the OCI image index format, and is not present in Docker manifest lists. +func (l *list) Annotations(instanceDigest *digest.Digest) (map[string]string, error) { + a := l.oci.Annotations + if instanceDigest != nil { + oci, err := l.findOCIv1(*instanceDigest) + if err != nil { + return nil, err + } + a = oci.Annotations + } + annotations := make(map[string]string) + for k, v := range a { + annotations[k] = v + } + return annotations, nil +} + +// SetOS sets the OS field in the platform information associated with the instance with the specified digest. +func (l *list) SetOS(instanceDigest digest.Digest, os string) error { + docker, err := l.findDocker(instanceDigest) + if err != nil { + return err + } + oci, err := l.findOCIv1(instanceDigest) + if err != nil { + return err + } + docker.Platform.OS = os + oci.Platform.OS = os + return nil +} + +// OS retrieves the OS field in the platform information associated with the instance with the specified digest. +func (l *list) OS(instanceDigest digest.Digest) (string, error) { + oci, err := l.findOCIv1(instanceDigest) + if err != nil { + return "", err + } + return oci.Platform.OS, nil +} + +// SetArchitecture sets the Architecture field in the platform information associated with the instance with the specified digest. +func (l *list) SetArchitecture(instanceDigest digest.Digest, arch string) error { + docker, err := l.findDocker(instanceDigest) + if err != nil { + return err + } + oci, err := l.findOCIv1(instanceDigest) + if err != nil { + return err + } + docker.Platform.Architecture = arch + oci.Platform.Architecture = arch + return nil +} + +// Architecture retrieves the Architecture field in the platform information associated with the instance with the specified digest. +func (l *list) Architecture(instanceDigest digest.Digest) (string, error) { + oci, err := l.findOCIv1(instanceDigest) + if err != nil { + return "", err + } + return oci.Platform.Architecture, nil +} + +// SetOSVersion sets the OSVersion field in the platform information associated with the instance with the specified digest. +func (l *list) SetOSVersion(instanceDigest digest.Digest, osVersion string) error { + docker, err := l.findDocker(instanceDigest) + if err != nil { + return err + } + oci, err := l.findOCIv1(instanceDigest) + if err != nil { + return err + } + docker.Platform.OSVersion = osVersion + oci.Platform.OSVersion = osVersion + return nil +} + +// OSVersion retrieves the OSVersion field in the platform information associated with the instance with the specified digest. +func (l *list) OSVersion(instanceDigest digest.Digest) (string, error) { + oci, err := l.findOCIv1(instanceDigest) + if err != nil { + return "", err + } + return oci.Platform.OSVersion, nil +} + +// SetVariant sets the Variant field in the platform information associated with the instance with the specified digest. +func (l *list) SetVariant(instanceDigest digest.Digest, variant string) error { + docker, err := l.findDocker(instanceDigest) + if err != nil { + return err + } + oci, err := l.findOCIv1(instanceDigest) + if err != nil { + return err + } + docker.Platform.Variant = variant + oci.Platform.Variant = variant + return nil +} + +// Variant retrieves the Variant field in the platform information associated with the instance with the specified digest. +func (l *list) Variant(instanceDigest digest.Digest) (string, error) { + oci, err := l.findOCIv1(instanceDigest) + if err != nil { + return "", err + } + return oci.Platform.Variant, nil +} + +// SetFeatures sets the features list in the platform information associated with the instance with the specified digest. +// The field is specific to the Docker manifest list format, and is not present in OCI's image indexes. +func (l *list) SetFeatures(instanceDigest digest.Digest, features []string) error { + docker, err := l.findDocker(instanceDigest) + if err != nil { + return err + } + docker.Platform.Features = append([]string{}, features...) + // no OCI equivalent + return nil +} + +// Features retrieves the features list from the platform information associated with the instance with the specified digest. +// The field is specific to the Docker manifest list format, and is not present in OCI's image indexes. +func (l *list) Features(instanceDigest digest.Digest) ([]string, error) { + docker, err := l.findDocker(instanceDigest) + if err != nil { + return nil, err + } + return append([]string{}, docker.Platform.Features...), nil +} + +// SetOSFeatures sets the OS features list in the platform information associated with the instance with the specified digest. +func (l *list) SetOSFeatures(instanceDigest digest.Digest, osFeatures []string) error { + docker, err := l.findDocker(instanceDigest) + if err != nil { + return err + } + oci, err := l.findOCIv1(instanceDigest) + if err != nil { + return err + } + docker.Platform.OSFeatures = append([]string{}, osFeatures...) + oci.Platform.OSFeatures = append([]string{}, osFeatures...) + return nil +} + +// OSFeatures retrieves the OS features list from the platform information associated with the instance with the specified digest. +func (l *list) OSFeatures(instanceDigest digest.Digest) ([]string, error) { + oci, err := l.findOCIv1(instanceDigest) + if err != nil { + return nil, err + } + return append([]string{}, oci.Platform.OSFeatures...), nil +} + +// FromBlob builds a list from an encoded manifest list or image index. +func FromBlob(manifestBytes []byte) (List, error) { + manifestType := manifest.GuessMIMEType(manifestBytes) + list := &list{ + docker: manifest.Schema2List{ + SchemaVersion: 2, + MediaType: manifest.DockerV2ListMediaType, + }, + oci: v1.Index{ + Versioned: imgspec.Versioned{SchemaVersion: 2}, + }, + } + switch manifestType { + default: + return nil, errors.Wrapf(ErrManifestTypeNotSupported, "unable to load manifest list: unsupported format %q", manifestType) + case manifest.DockerV2ListMediaType: + if err := json.Unmarshal(manifestBytes, &list.docker); err != nil { + return nil, errors.Wrapf(err, "unable to parse Docker manifest list from image") + } + for _, m := range list.docker.Manifests { + list.oci.Manifests = append(list.oci.Manifests, v1.Descriptor{ + MediaType: m.Schema2Descriptor.MediaType, + Size: m.Schema2Descriptor.Size, + Digest: m.Schema2Descriptor.Digest, + Platform: &v1.Platform{ + Architecture: m.Platform.Architecture, + OS: m.Platform.OS, + OSVersion: m.Platform.OSVersion, + OSFeatures: m.Platform.OSFeatures, + Variant: m.Platform.Variant, + }, + }) + } + case v1.MediaTypeImageIndex: + if err := json.Unmarshal(manifestBytes, &list.oci); err != nil { + return nil, errors.Wrapf(err, "unable to parse OCIv1 manifest list") + } + for _, m := range list.oci.Manifests { + platform := m.Platform + if platform == nil { + platform = &v1.Platform{} + } + list.docker.Manifests = append(list.docker.Manifests, manifest.Schema2ManifestDescriptor{ + Schema2Descriptor: manifest.Schema2Descriptor{ + MediaType: m.MediaType, + Size: m.Size, + Digest: m.Digest, + }, + Platform: manifest.Schema2PlatformSpec{ + Architecture: platform.Architecture, + OS: platform.OS, + OSVersion: platform.OSVersion, + OSFeatures: platform.OSFeatures, + Variant: platform.Variant, + }, + }) + } + } + return list, nil +} + +func (l *list) preferOCI() bool { + // If we have any data that's only in the OCI format, use that. + for _, m := range l.oci.Manifests { + if len(m.URLs) > 0 { + return true + } + if len(m.Annotations) > 0 { + return true + } + } + // If we have any data that's only in the Docker format, use that. + for _, m := range l.docker.Manifests { + if len(m.Platform.Features) > 0 { + return false + } + } + // If we have no manifests, remember that the Docker format is + // explicitly typed, so use that. Otherwise, default to using the OCI + // format. + return len(l.docker.Manifests) != 0 +} + +// Serialize encodes the list using the specified format, or by selecting one +// which it thinks is appropriate. +func (l *list) Serialize(mimeType string) ([]byte, error) { + var manifestBytes []byte + switch mimeType { + case "": + if l.preferOCI() { + manifest, err := json.Marshal(&l.oci) + if err != nil { + return nil, errors.Wrapf(err, "error marshalling OCI image index") + } + manifestBytes = manifest + } else { + manifest, err := json.Marshal(&l.docker) + if err != nil { + return nil, errors.Wrapf(err, "error marshalling Docker manifest list") + } + manifestBytes = manifest + } + case v1.MediaTypeImageIndex: + manifest, err := json.Marshal(&l.oci) + if err != nil { + return nil, errors.Wrapf(err, "error marshalling OCI image index") + } + manifestBytes = manifest + case manifest.DockerV2ListMediaType: + manifest, err := json.Marshal(&l.docker) + if err != nil { + return nil, errors.Wrapf(err, "error marshalling Docker manifest list") + } + manifestBytes = manifest + default: + return nil, errors.Wrapf(ErrManifestTypeNotSupported, "serializing list to type %q not implemented", mimeType) + } + return manifestBytes, nil +} + +// Instances returns the list of image instances mentioned in this list. +func (l *list) Instances() []digest.Digest { + instances := make([]digest.Digest, 0, len(l.oci.Manifests)) + for _, instance := range l.oci.Manifests { + instances = append(instances, instance.Digest) + } + return instances +} diff --git a/pkg/manifests/manifests_test.go b/pkg/manifests/manifests_test.go new file mode 100644 index 00000000000..1f160c384e1 --- /dev/null +++ b/pkg/manifests/manifests_test.go @@ -0,0 +1,369 @@ +package manifests + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/containers/image/v5/manifest" + "github.com/containers/storage/pkg/reexec" + digest "github.com/opencontainers/go-digest" + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +const ( + expectedInstance = digest.Digest("sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b") + ociFixture = "testdata/fedora.index.json" + dockerFixture = "testdata/fedora.list.json" +) + +var ( + _ List = &list{} +) + +func TestMain(m *testing.M) { + if reexec.Init() { + return + } + os.Exit(m.Run()) +} + +func TestCreate(t *testing.T) { + list := Create() + if list == nil { + t.Fatalf("error creating an empty list") + } +} + +func TestFromBlob(t *testing.T) { + for _, version := range []string{ + ociFixture, + dockerFixture, + } { + bytes, err := ioutil.ReadFile(version) + if err != nil { + t.Fatalf("error loading %s: %v", version, err) + } + list, err := FromBlob(bytes) + if err != nil { + t.Fatalf("error parsing %s: %v", version, err) + } + if len(list.Docker().Manifests) != len(list.OCIv1().Manifests) { + t.Fatalf("%s: expected the same number of manifests, but %d != %d", version, len(list.Docker().Manifests), len(list.OCIv1().Manifests)) + } + for i := range list.Docker().Manifests { + d := list.Docker().Manifests[i] + o := list.OCIv1().Manifests[i] + if d.Platform.OS != o.Platform.OS { + t.Fatalf("%s: expected the same OS", version) + } + if d.Platform.Architecture != o.Platform.Architecture { + t.Fatalf("%s: expected the same Architecture", version) + } + } + } +} + +func TestAddInstance(t *testing.T) { + manifestBytes, err := ioutil.ReadFile("testdata/fedora-minimal.schema2.json") + if err != nil { + t.Fatalf("error loading testdata/fedora-minimal.schema2.json: %v", err) + } + manifestType := manifest.GuessMIMEType(manifestBytes) + manifestDigest, err := manifest.Digest(manifestBytes) + if err != nil { + t.Fatalf("error digesting testdata/fedora-minimal.schema2.json: %v", err) + } + for _, version := range []string{ + ociFixture, + dockerFixture, + } { + bytes, err := ioutil.ReadFile(version) + if err != nil { + t.Fatalf("error loading %s: %v", version, err) + } + list, err := FromBlob(bytes) + if err != nil { + t.Fatalf("error parsing %s: %v", version, err) + } + if err = list.AddInstance(manifestDigest, int64(len(manifestBytes)), manifestType, "linux", "amd64", "", nil, "", nil, nil); err != nil { + t.Fatalf("adding an instance failed in %s: %v", version, err) + } + if d, err := list.findDocker(manifestDigest); d == nil || err != nil { + t.Fatalf("adding an instance failed in %s: %v", version, err) + } + if o, err := list.findOCIv1(manifestDigest); o == nil || err != nil { + t.Fatalf("adding an instance failed in %s: %v", version, err) + } + } +} + +func TestRemove(t *testing.T) { + bytes, err := ioutil.ReadFile(ociFixture) + if err != nil { + t.Fatalf("error loading blob: %v", err) + } + list, err := FromBlob(bytes) + if err != nil { + t.Fatalf("error parsing blob: %v", err) + } + before := len(list.OCIv1().Manifests) + instanceDigest := expectedInstance + if d, err := list.findDocker(instanceDigest); d == nil || err != nil { + t.Fatalf("finding expected instance failed: %v", err) + } + if o, err := list.findOCIv1(instanceDigest); o == nil || err != nil { + t.Fatalf("finding expected instance failed: %v", err) + } + err = list.Remove(instanceDigest) + if err != nil { + t.Fatalf("error parsing blob: %v", err) + } + after := len(list.Docker().Manifests) + if after != before-1 { + t.Fatalf("removing instance should have succeeded") + } + if d, err := list.findDocker(instanceDigest); d != nil || err == nil { + t.Fatalf("finding instance should have failed") + } + if o, err := list.findOCIv1(instanceDigest); o != nil || err == nil { + t.Fatalf("finding instance should have failed") + } +} + +func testString(t *testing.T, values []string, set func(List, digest.Digest, string) error, get func(List, digest.Digest) (string, error)) { + bytes, err := ioutil.ReadFile(ociFixture) + if err != nil { + t.Fatalf("error loading blob: %v", err) + } + list, err := FromBlob(bytes) + if err != nil { + t.Fatalf("error parsing blob: %v", err) + } + for _, testString := range values { + if err = set(list, expectedInstance, testString); err != nil { + t.Fatalf("error setting %q: %v", testString, err) + } + b, err := list.Serialize("") + if err != nil { + t.Fatalf("error serializing list: %v", err) + } + list, err := FromBlob(b) + if err != nil { + t.Fatalf("error parsing list: %v", err) + } + value, err := get(list, expectedInstance) + if err != nil { + t.Fatalf("error retrieving value %q: %v", testString, err) + } + if value != testString { + t.Fatalf("expected value %q, got %q: %v", value, testString, err) + } + } +} + +func testStringSlice(t *testing.T, values [][]string, set func(List, digest.Digest, []string) error, get func(List, digest.Digest) ([]string, error)) { + bytes, err := ioutil.ReadFile(ociFixture) + if err != nil { + t.Fatalf("error loading blob: %v", err) + } + list, err := FromBlob(bytes) + if err != nil { + t.Fatalf("error parsing blob: %v", err) + } + for _, testSlice := range values { + if err = set(list, expectedInstance, testSlice); err != nil { + t.Fatalf("error setting %v: %v", testSlice, err) + } + b, err := list.Serialize("") + if err != nil { + t.Fatalf("error serializing list: %v", err) + } + list, err := FromBlob(b) + if err != nil { + t.Fatalf("error parsing list: %v", err) + } + values, err := get(list, expectedInstance) + if err != nil { + t.Fatalf("error retrieving value %v: %v", testSlice, err) + } + if !reflect.DeepEqual(values, testSlice) { + t.Fatalf("expected values %v, got %v: %v", testSlice, values, err) + } + } +} + +func testMap(t *testing.T, values []map[string]string, set func(List, *digest.Digest, map[string]string) error, get func(List, *digest.Digest) (map[string]string, error)) { + bytes, err := ioutil.ReadFile(ociFixture) + if err != nil { + t.Fatalf("error loading blob: %v", err) + } + list, err := FromBlob(bytes) + if err != nil { + t.Fatalf("error parsing blob: %v", err) + } + instance := expectedInstance + for _, instanceDigest := range []*digest.Digest{nil, &instance} { + for _, testMap := range values { + if err = set(list, instanceDigest, testMap); err != nil { + t.Fatalf("error setting %v: %v", testMap, err) + } + b, err := list.Serialize("") + if err != nil { + t.Fatalf("error serializing list: %v", err) + } + list, err := FromBlob(b) + if err != nil { + t.Fatalf("error parsing list: %v", err) + } + values, err := get(list, instanceDigest) + if err != nil { + t.Fatalf("error retrieving value %v: %v", testMap, err) + } + if len(values) != len(testMap) { + t.Fatalf("expected %d map entries, got %d", len(testMap), len(values)) + } + for k, v := range testMap { + if values[k] != v { + t.Fatalf("expected map value %q=%q, got %q", k, v, values[k]) + } + } + } + } +} + +func TestAnnotations(t *testing.T) { + testMap(t, + []map[string]string{{"A": "B", "C": "D"}, {"E": "F", "G": "H"}}, + func(l List, i *digest.Digest, m map[string]string) error { + return l.SetAnnotations(i, m) + }, + func(l List, i *digest.Digest) (map[string]string, error) { + return l.Annotations(i) + }, + ) +} + +func TestArchitecture(t *testing.T) { + testString(t, + []string{"abacus", "sliderule"}, + func(l List, i digest.Digest, s string) error { + return l.SetArchitecture(i, s) + }, + func(l List, i digest.Digest) (string, error) { + return l.Architecture(i) + }, + ) +} + +func TestFeatures(t *testing.T) { + testStringSlice(t, + [][]string{{"chrome", "hubcaps"}, {"climate", "control"}}, + func(l List, i digest.Digest, s []string) error { + return l.SetFeatures(i, s) + }, + func(l List, i digest.Digest) ([]string, error) { + return l.Features(i) + }, + ) +} + +func TestOS(t *testing.T) { + testString(t, + []string{"linux", "darwin"}, + func(l List, i digest.Digest, s string) error { + return l.SetOS(i, s) + }, + func(l List, i digest.Digest) (string, error) { + return l.OS(i) + }, + ) +} + +func TestOSFeatures(t *testing.T) { + testStringSlice(t, + [][]string{{"ipv6", "containers"}, {"nested", "virtualization"}}, + func(l List, i digest.Digest, s []string) error { + return l.SetOSFeatures(i, s) + }, + func(l List, i digest.Digest) ([]string, error) { + return l.OSFeatures(i) + }, + ) +} + +func TestOSVersion(t *testing.T) { + testString(t, + []string{"el7", "el8"}, + func(l List, i digest.Digest, s string) error { + return l.SetOSVersion(i, s) + }, + func(l List, i digest.Digest) (string, error) { + return l.OSVersion(i) + }, + ) +} + +func TestURLs(t *testing.T) { + testStringSlice(t, + [][]string{{"https://example.com", "https://example.net"}, {"http://example.com", "http://example.net"}}, + func(l List, i digest.Digest, s []string) error { + return l.SetURLs(i, s) + }, + func(l List, i digest.Digest) ([]string, error) { + return l.URLs(i) + }, + ) +} + +func TestVariant(t *testing.T) { + testString(t, + []string{"workstation", "cloud", "server"}, + func(l List, i digest.Digest, s string) error { + return l.SetVariant(i, s) + }, + func(l List, i digest.Digest) (string, error) { + return l.Variant(i) + }, + ) +} + +func TestSerialize(t *testing.T) { + for _, version := range []string{ + ociFixture, + dockerFixture, + } { + bytes, err := ioutil.ReadFile(version) + if err != nil { + t.Fatalf("error loading %s: %v", version, err) + } + list, err := FromBlob(bytes) + if err != nil { + t.Fatalf("error parsing %s: %v", version, err) + } + for _, mimeType := range []string{"", v1.MediaTypeImageIndex, manifest.DockerV2ListMediaType} { + b, err := list.Serialize(mimeType) + if err != nil { + t.Fatalf("error serializing %s with type %q: %v", version, mimeType, err) + } + l, err := FromBlob(b) + if err != nil { + t.Fatalf("error parsing %s re-encoded as %q: %v\n%s", version, mimeType, err, string(b)) + } + if !reflect.DeepEqual(list.Docker().Manifests, l.Docker().Manifests) { + t.Fatalf("re-encoded %s as %q was different\n%#v\n%#v", version, mimeType, list, l) + } + for i := range list.OCIv1().Manifests { + manifest := list.OCIv1().Manifests[i] + m := l.OCIv1().Manifests[i] + if manifest.Digest != m.Digest || + manifest.MediaType != m.MediaType || + manifest.Size != m.Size || + !reflect.DeepEqual(list.OCIv1().Manifests[i].Platform, l.OCIv1().Manifests[i].Platform) { + t.Fatalf("re-encoded %s OCI %d as %q was different\n%#v\n%#v", version, i, mimeType, list, l) + } + } + } + } +} diff --git a/pkg/manifests/testdata/fedora-minimal.schema2.json b/pkg/manifests/testdata/fedora-minimal.schema2.json new file mode 100644 index 00000000000..4d294837f7a --- /dev/null +++ b/pkg/manifests/testdata/fedora-minimal.schema2.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1316, + "digest": "sha256:847a6054047619b8908f61e0211e3480ab20ce4b6cf17b03db081322ade301d3" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 42163967, + "digest": "sha256:8fad33c002fa130aceec4fd0cadc8bad0d7561667ab24aee75dea825be933de0" + } + ] +} diff --git a/pkg/manifests/testdata/fedora.index.json b/pkg/manifests/testdata/fedora.index.json new file mode 100644 index 00000000000..503f80d0404 --- /dev/null +++ b/pkg/manifests/testdata/fedora.index.json @@ -0,0 +1,45 @@ +{ + "manifests": [ + { + "digest": "sha256:f81f09918379d5442d20dff82a298f29698197035e737f76e511d5af422cabd7", + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "platform": { + "architecture": "amd64", + "os": "linux" + }, + "size": 529 + }, + { + "digest": "sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "platform": { + "architecture": "arm64", + "os": "linux", + "variant": "v8" + }, + "size": 529 + }, + { + "digest": "sha256:68b26da78d8790df143479ec2e3174c57cedb1c2e84ce1b2675d942d6848f2da", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "platform": { + "architecture": "ppc64le", + "os": "linux" + }, + "size": 529 + }, + { + "digest": "sha256:15352d97781ffdf357bf3459c037be3efac4133dc9070c2dce7eca7c05c3e736", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "platform": { + "architecture": "s390x", + "os": "linux" + }, + "size": 529 + } + ], + "schemaVersion": 2, + "annotations": { + "foo": "bar" + } +} diff --git a/pkg/manifests/testdata/fedora.list.json b/pkg/manifests/testdata/fedora.list.json new file mode 100644 index 00000000000..11898ba3754 --- /dev/null +++ b/pkg/manifests/testdata/fedora.list.json @@ -0,0 +1,43 @@ +{ + "manifests": [ + { + "digest": "sha256:f81f09918379d5442d20dff82a298f29698197035e737f76e511d5af422cabd7", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "platform": { + "architecture": "amd64", + "os": "linux" + }, + "size": 529 + }, + { + "digest": "sha256:c829b1810d2dbb456e74a695fd3847530c8319e5a95dca623e9f1b1b89020d8b", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "platform": { + "architecture": "arm64", + "os": "linux", + "variant": "v8" + }, + "size": 529 + }, + { + "digest": "sha256:68b26da78d8790df143479ec2e3174c57cedb1c2e84ce1b2675d942d6848f2da", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "platform": { + "architecture": "ppc64le", + "os": "linux" + }, + "size": 529 + }, + { + "digest": "sha256:15352d97781ffdf357bf3459c037be3efac4133dc9070c2dce7eca7c05c3e736", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "platform": { + "architecture": "s390x", + "os": "linux" + }, + "size": 529 + } + ], + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "schemaVersion": 2 +} diff --git a/pkg/manifests/testdata/fedora.schema2.json b/pkg/manifests/testdata/fedora.schema2.json new file mode 100644 index 00000000000..43cd2bb3f8d --- /dev/null +++ b/pkg/manifests/testdata/fedora.schema2.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 2037, + "digest": "sha256:e9ed59d2baf72308f3a811ebc49ff3f4e0175abf40bf636bea0160759c637999" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 69532283, + "digest": "sha256:5a915a173fbc36dc8e1410afdd9de2b08f71efb226f8eb1ebcdc00a1acbced62" + } + ] +}