2015-12-17 01:26:13 +00:00
|
|
|
package manifestlist
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
|
2020-08-24 11:18:39 +00:00
|
|
|
"github.com/distribution/distribution/v3"
|
|
|
|
"github.com/distribution/distribution/v3/manifest"
|
2016-12-17 00:28:34 +00:00
|
|
|
"github.com/opencontainers/go-digest"
|
2023-04-30 16:16:51 +00:00
|
|
|
"github.com/opencontainers/image-spec/specs-go"
|
2019-10-09 12:02:21 +00:00
|
|
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
2015-12-17 01:26:13 +00:00
|
|
|
)
|
|
|
|
|
2016-11-17 18:28:05 +00:00
|
|
|
const (
|
|
|
|
// MediaTypeManifestList specifies the mediaType for manifest lists.
|
|
|
|
MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
|
|
|
|
)
|
2015-12-17 01:26:13 +00:00
|
|
|
|
|
|
|
// SchemaVersion provides a pre-initialized version structure for this
|
|
|
|
// packages version of the manifest.
|
2023-04-30 16:16:51 +00:00
|
|
|
//
|
|
|
|
// Deprecated: use [specs.Versioned] and set MediaType on the manifest
|
|
|
|
// to [MediaTypeManifestList].
|
|
|
|
//
|
|
|
|
//nolint:staticcheck // ignore SA1019: manifest.Versioned is deprecated:
|
2015-12-17 01:26:13 +00:00
|
|
|
var SchemaVersion = manifest.Versioned{
|
|
|
|
SchemaVersion: 2,
|
2016-01-06 22:15:14 +00:00
|
|
|
MediaType: MediaTypeManifestList,
|
2015-12-17 01:26:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
2024-07-16 10:02:28 +00:00
|
|
|
if err := distribution.RegisterManifestSchema(MediaTypeManifestList, unmarshalManifestList); err != nil {
|
|
|
|
panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
|
|
|
}
|
|
|
|
}
|
2018-03-14 20:55:45 +00:00
|
|
|
|
2024-07-16 10:02:28 +00:00
|
|
|
func unmarshalManifestList(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
|
|
|
|
m := &DeserializedManifestList{}
|
|
|
|
if err := m.UnmarshalJSON(b); err != nil {
|
|
|
|
return nil, distribution.Descriptor{}, err
|
2015-12-17 01:26:13 +00:00
|
|
|
}
|
2024-07-16 10:02:28 +00:00
|
|
|
|
|
|
|
if m.MediaType != MediaTypeManifestList {
|
|
|
|
return nil, distribution.Descriptor{}, fmt.Errorf("mediaType in manifest list should be '%s' not '%s'", MediaTypeManifestList, m.MediaType)
|
2015-12-17 01:26:13 +00:00
|
|
|
}
|
2024-07-16 10:02:28 +00:00
|
|
|
|
|
|
|
return m, distribution.Descriptor{
|
|
|
|
Digest: digest.FromBytes(b),
|
|
|
|
Size: int64(len(b)),
|
|
|
|
MediaType: MediaTypeManifestList,
|
|
|
|
}, nil
|
2015-12-17 01:26:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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"`
|
|
|
|
|
2016-03-16 02:26:02 +00:00
|
|
|
// 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"`
|
|
|
|
|
2015-12-17 01:26:13 +00:00
|
|
|
// 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"`
|
|
|
|
|
2016-03-16 02:26:02 +00:00
|
|
|
// Features is an optional field specifying an array of strings, each
|
2015-12-17 01:26:13 +00:00
|
|
|
// 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 {
|
2023-04-30 16:16:51 +00:00
|
|
|
specs.Versioned
|
|
|
|
|
|
|
|
// MediaType is the media type of this schema.
|
|
|
|
MediaType string `json:"mediaType,omitempty"`
|
2015-12-17 01:26:13 +00:00
|
|
|
|
2022-06-27 23:43:41 +00:00
|
|
|
// Manifests references a list of manifests
|
2015-12-17 01:26:13 +00:00
|
|
|
Manifests []ManifestDescriptor `json:"manifests"`
|
|
|
|
}
|
|
|
|
|
2017-02-27 06:06:18 +00:00
|
|
|
// References returns the distribution descriptors for the referenced image
|
2015-12-17 01:26:13 +00:00
|
|
|
// 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
|
2019-04-29 13:47:52 +00:00
|
|
|
dependencies[i].Platform = &v1.Platform{
|
|
|
|
Architecture: m.Manifests[i].Platform.Architecture,
|
|
|
|
OS: m.Manifests[i].Platform.OS,
|
|
|
|
OSVersion: m.Manifests[i].Platform.OSVersion,
|
|
|
|
OSFeatures: m.Manifests[i].Platform.OSFeatures,
|
|
|
|
Variant: m.Manifests[i].Platform.Variant,
|
|
|
|
}
|
2015-12-17 01:26:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2023-03-31 10:35:30 +00:00
|
|
|
return fromDescriptorsWithMediaType(descriptors, MediaTypeManifestList)
|
2018-03-14 20:55:45 +00:00
|
|
|
}
|
|
|
|
|
2023-03-31 10:35:30 +00:00
|
|
|
// 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) {
|
2018-03-14 20:55:45 +00:00
|
|
|
m := ManifestList{
|
2023-04-30 16:16:51 +00:00
|
|
|
Versioned: specs.Versioned{SchemaVersion: 2},
|
|
|
|
MediaType: mediaType,
|
2015-12-17 01:26:13 +00:00
|
|
|
}
|
|
|
|
|
2019-02-05 00:01:04 +00:00
|
|
|
m.Manifests = make([]ManifestDescriptor, len(descriptors))
|
2015-12-17 01:26:13 +00:00
|
|
|
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 {
|
2019-02-05 00:01:04 +00:00
|
|
|
m.canonical = make([]byte, len(b))
|
2015-12-17 01:26:13 +00:00
|
|
|
// 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) {
|
2023-04-30 16:16:51 +00:00
|
|
|
var mediaType string
|
|
|
|
if m.MediaType == "" {
|
|
|
|
mediaType = v1.MediaTypeImageIndex
|
|
|
|
} else {
|
|
|
|
mediaType = m.MediaType
|
|
|
|
}
|
|
|
|
|
|
|
|
return mediaType, m.canonical, nil
|
2015-12-17 01:26:13 +00:00
|
|
|
}
|
2021-10-21 22:09:39 +00:00
|
|
|
|
2023-03-31 10:35:30 +00:00
|
|
|
// validateManifestList returns an error if the byte slice is invalid JSON or if it
|
2021-10-21 22:09:39 +00:00
|
|
|
// contains fields that belong to a manifest
|
2023-03-31 10:35:30 +00:00
|
|
|
func validateManifestList(b []byte) error {
|
|
|
|
var doc struct {
|
|
|
|
Config interface{} `json:"config,omitempty"`
|
|
|
|
Layers interface{} `json:"layers,omitempty"`
|
|
|
|
}
|
2021-10-21 22:09:39 +00:00
|
|
|
if err := json.Unmarshal(b, &doc); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if doc.Config != nil || doc.Layers != nil {
|
2023-03-31 10:35:30 +00:00
|
|
|
return errors.New("manifestlist: expected list but found manifest")
|
2021-10-21 22:09:39 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|