package manifestlist import ( "encoding/json" "errors" "fmt" "github.com/docker/distribution" "github.com/docker/distribution/manifest" "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" ) const ( // MediaTypeManifestList specifies the mediaType for manifest lists. MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" ) // SchemaVersion provides a pre-initialized version structure for this // packages version of the manifest. var SchemaVersion = manifest.Versioned{ SchemaVersion: 2, MediaType: MediaTypeManifestList, } // OCISchemaVersion provides a pre-initialized version structure for this // packages OCIschema version of the manifest. var OCISchemaVersion = manifest.Versioned{ SchemaVersion: 2, MediaType: v1.MediaTypeImageIndex, } func init() { manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { m := new(DeserializedManifestList) err := m.UnmarshalJSON(b) if err != nil { return nil, distribution.Descriptor{}, err } if m.MediaType != MediaTypeManifestList { err = fmt.Errorf("mediaType in manifest list should be '%s' not '%s'", MediaTypeManifestList, m.MediaType) return nil, distribution.Descriptor{}, err } dgst := digest.FromBytes(b) return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifestList}, err } err := distribution.RegisterManifestSchema(MediaTypeManifestList, manifestListFunc) if err != nil { panic(fmt.Sprintf("Unable to register manifest: %s", err)) } imageIndexFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { if err := validateIndex(b); err != nil { return nil, distribution.Descriptor{}, err } m := new(DeserializedManifestList) err := m.UnmarshalJSON(b) if err != nil { return nil, distribution.Descriptor{}, err } if m.MediaType != "" && m.MediaType != v1.MediaTypeImageIndex { err = fmt.Errorf("if present, mediaType in image index should be '%s' not '%s'", v1.MediaTypeImageIndex, m.MediaType) return nil, distribution.Descriptor{}, err } dgst := digest.FromBytes(b) return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: v1.MediaTypeImageIndex}, err } err = distribution.RegisterManifestSchema(v1.MediaTypeImageIndex, imageIndexFunc) if err != nil { panic(fmt.Sprintf("Unable to register OCI Image Index: %s", err)) } } // PlatformSpec specifies a platform where a particular image manifest is // applicable. type PlatformSpec struct { // Architecture field specifies the CPU architecture, for example // `amd64` or `ppc64`. Architecture string `json:"architecture"` // OS specifies the operating system, for example `linux` or `windows`. OS string `json:"os"` // OSVersion is an optional field specifying the operating system // version, for example `10.0.10586`. OSVersion string `json:"os.version,omitempty"` // OSFeatures is an optional field specifying an array of strings, // each listing a required OS feature (for example on Windows `win32k`). OSFeatures []string `json:"os.features,omitempty"` // Variant is an optional field specifying a variant of the CPU, for // example `ppc64le` to specify a little-endian version of a PowerPC CPU. Variant string `json:"variant,omitempty"` // Features is an optional field specifying an array of strings, each // listing a required CPU feature (for example `sse4` or `aes`). Features []string `json:"features,omitempty"` } // A ManifestDescriptor references a platform-specific manifest. type ManifestDescriptor struct { distribution.Descriptor // Platform specifies which platform the manifest pointed to by the // descriptor runs on. Platform PlatformSpec `json:"platform"` } // ManifestList references manifests for various platforms. type ManifestList struct { manifest.Versioned // Config references the image configuration as a blob. Manifests []ManifestDescriptor `json:"manifests"` } // References returns the distribution descriptors for the referenced image // manifests. func (m ManifestList) References() []distribution.Descriptor { dependencies := make([]distribution.Descriptor, len(m.Manifests)) for i := range m.Manifests { dependencies[i] = m.Manifests[i].Descriptor } return dependencies } // DeserializedManifestList wraps ManifestList with a copy of the original // JSON. type DeserializedManifestList struct { ManifestList // canonical is the canonical byte representation of the Manifest. canonical []byte } // FromDescriptors takes a slice of descriptors, and returns a // DeserializedManifestList which contains the resulting manifest list // and its JSON representation. func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) { var mediaType string if len(descriptors) > 0 && descriptors[0].Descriptor.MediaType == v1.MediaTypeImageManifest { mediaType = v1.MediaTypeImageIndex } else { mediaType = MediaTypeManifestList } return FromDescriptorsWithMediaType(descriptors, mediaType) } // FromDescriptorsWithMediaType is for testing purposes, it's useful to be able to specify the media type explicitly func FromDescriptorsWithMediaType(descriptors []ManifestDescriptor, mediaType string) (*DeserializedManifestList, error) { m := ManifestList{ Versioned: manifest.Versioned{ SchemaVersion: 2, MediaType: mediaType, }, } m.Manifests = make([]ManifestDescriptor, len(descriptors)) copy(m.Manifests, descriptors) deserialized := DeserializedManifestList{ ManifestList: m, } var err error deserialized.canonical, err = json.MarshalIndent(&m, "", " ") return &deserialized, err } // UnmarshalJSON populates a new ManifestList struct from JSON data. func (m *DeserializedManifestList) UnmarshalJSON(b []byte) error { m.canonical = make([]byte, len(b)) // store manifest list in canonical copy(m.canonical, b) // Unmarshal canonical JSON into ManifestList object var manifestList ManifestList if err := json.Unmarshal(m.canonical, &manifestList); err != nil { return err } m.ManifestList = manifestList return nil } // MarshalJSON returns the contents of canonical. If canonical is empty, // marshals the inner contents. func (m *DeserializedManifestList) MarshalJSON() ([]byte, error) { if len(m.canonical) > 0 { return m.canonical, nil } return nil, errors.New("JSON representation not initialized in DeserializedManifestList") } // Payload returns the raw content of the manifest list. The contents can be // used to calculate the content identifier. func (m DeserializedManifestList) Payload() (string, []byte, error) { var mediaType string if m.MediaType == "" { mediaType = v1.MediaTypeImageIndex } else { mediaType = m.MediaType } return mediaType, m.canonical, nil } // unknownDocument represents a manifest, manifest list, or index that has not // yet been validated type unknownDocument struct { Config interface{} `json:"config,omitempty"` Layers interface{} `json:"layers,omitempty"` } // validateIndex returns an error if the byte slice is invalid JSON or if it // contains fields that belong to a manifest func validateIndex(b []byte) error { var doc unknownDocument if err := json.Unmarshal(b, &doc); err != nil { return err } if doc.Config != nil || doc.Layers != nil { return errors.New("index: expected index but found manifest") } return nil }