From 09f904525b2e8ffc986b1a49d8afeeed4034cf7e Mon Sep 17 00:00:00 2001 From: Istvan Kispal Date: Sat, 10 Nov 2018 16:49:26 +0100 Subject: [PATCH] Add ManifestList support --- Gopkg.lock | 20 +-- registry/manifest.go | 87 +++++++++- .../manifest/manifestlist/manifestlist.go | 155 ++++++++++++++++++ 3 files changed, 239 insertions(+), 23 deletions(-) create mode 100644 vendor/github.com/docker/distribution/manifest/manifestlist/manifestlist.go diff --git a/Gopkg.lock b/Gopkg.lock index b7b4d020..6b636aeb 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,57 +2,43 @@ [[projects]] - digest = "1:aea2b6612160c862bd882c4f0f9f4b9c507dd90f8d3dbfa69348f7bef303c5ed" name = "github.com/docker/distribution" packages = [ ".", "digestset", "manifest", + "manifest/manifestlist", "manifest/schema1", "manifest/schema2", - "reference", + "reference" ] - pruneopts = "NUT" revision = "7484e51bf6af0d3b1a849644cdaced3cfcf13617" [[projects]] branch = "master" - digest = "1:ce43438a8204a4259b4461153a392bc3e504bef7e4785a8192344f002c7bd935" name = "github.com/docker/libtrust" packages = ["."] - pruneopts = "NUT" revision = "aabc10ec26b754e797f9028f4589c5b7bd90dc20" [[projects]] - digest = "1:e0cc8395ea893c898ff5eb0850f4d9851c1f57c78c232304a026379a47a552d0" name = "github.com/opencontainers/go-digest" packages = ["."] - pruneopts = "NUT" revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf" version = "v1.0.0-rc1" [[projects]] - digest = "1:d92e31b7ac0c1ec5f66aeea583928edf750918f7a95088c73a40f3758fbec3bc" name = "github.com/sirupsen/logrus" packages = ["."] - pruneopts = "NUT" revision = "3ec0642a7fb6488f65b06f9040adc67e3990296a" [[projects]] - digest = "1:4a6460f20f2f12c5252f55a1c372139f2445289eb8ede2adc966bde45125d07d" name = "golang.org/x/sys" packages = ["unix"] - pruneopts = "NUT" revision = "8dbc5d05d6edcc104950cc299a1ce6641235bc86" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - input-imports = [ - "github.com/docker/distribution", - "github.com/docker/distribution/manifest/schema1", - "github.com/docker/distribution/manifest/schema2", - "github.com/opencontainers/go-digest", - ] + inputs-digest = "c8f73ce1939360cc1c654ea1c6c2c9aaa70c84ab7637cf862a8c43bcd1f94c37" solver-name = "gps-cdcl" solver-version = 1 diff --git a/registry/manifest.go b/registry/manifest.go index 5f04d23b..69751a51 100644 --- a/registry/manifest.go +++ b/registry/manifest.go @@ -2,15 +2,19 @@ package registry import ( "bytes" + "encoding/json" + "fmt" "io/ioutil" "net/http" "github.com/docker/distribution" + "github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" digest "github.com/opencontainers/go-digest" ) +// Manifest returns with the schema1 manifest addressed by repository/reference func (registry *Registry) Manifest(repository, reference string) (*schema1.SignedManifest, error) { url := registry.url("/v2/%s/manifests/%s", repository, reference) registry.Logf("registry.manifest.get url=%s repository=%s reference=%s", url, repository, reference) @@ -41,6 +45,40 @@ func (registry *Registry) Manifest(repository, reference string) (*schema1.Signe return signedManifest, nil } +// ManifestList returns with the ManifestList addressed by repository/reference +func (registry *Registry) ManifestList(repository, reference string) (*manifestlist.DeserializedManifestList, error) { + url := registry.url("/v2/%s/manifests/%s", repository, reference) + registry.Logf("registry.manifestlist.get url=%s repository=%s reference=%s", url, repository, reference) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", manifestlist.MediaTypeManifestList) + resp, err := registry.Client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ml := new(manifestlist.DeserializedManifestList) + err = json.NewDecoder(resp.Body).Decode(ml) + if err != nil { + return nil, err + } + + if ml.MediaType != manifestlist.MediaTypeManifestList { + err = fmt.Errorf("mediaType in manifest list should be '%s' not '%s'", + manifestlist.MediaTypeManifestList, ml.MediaType) + return nil, err + } + return ml, nil +} + +// ManifestV2 returns with the schema2 manifest addressed by repository/reference +// If reference is an image digest (sha256:...) that is the hash of a manifestlist, +// not a manifest, then the method will return the first manifest with amd64 architecture, +// or in the absence thereof, the first manifest in the list. (Rationale: the Docker +// Image Digests returned by `docker images --digests` often refer to manifestlists) func (registry *Registry) ManifestV2(repository, reference string) (*schema2.DeserializedManifest, error) { url := registry.url("/v2/%s/manifests/%s", repository, reference) registry.Logf("registry.manifest.get url=%s repository=%s reference=%s", url, repository, reference) @@ -55,19 +93,56 @@ func (registry *Registry) ManifestV2(repository, reference string) (*schema2.Des if err != nil { return nil, err } - defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } - deserialized := &schema2.DeserializedManifest{} - err = deserialized.UnmarshalJSON(body) - if err != nil { - return nil, err + actualMediaType := resp.Header.Get("Content-Type") + switch actualMediaType { + case schema2.MediaTypeManifest: + deserialized := &schema2.DeserializedManifest{} + err = deserialized.UnmarshalJSON(body) + if err != nil { + return nil, err + } + return deserialized, nil + + case manifestlist.MediaTypeManifestList: + // if `reference` is an image digest, a manifest list may be received even though a schema2 manifest was requested + // (since the image digest is the hash of the manifest list, not the manifest) + // unwrap the referred manifest in this case + ml := new(manifestlist.DeserializedManifestList) + err := ml.UnmarshalJSON(body) + if err != nil { + return nil, err + } + + if ml.MediaType != manifestlist.MediaTypeManifestList { + err = fmt.Errorf("mediaType in manifest list should be '%s' not '%s'", + manifestlist.MediaTypeManifestList, ml.MediaType) + return nil, err + } + if len(ml.Manifests) == 0 { + return nil, fmt.Errorf("empty manifest list was receceived: repository=%s reference=%s", repository, reference) + } + + // use the amd64 manifest by default + // TODO: query current platform architecture, OS and Variant and use those as selection criteria + for _, m := range ml.Manifests { + if m.Platform.Architecture == "amd64" { + // address the manifest explicitly with its digest + return registry.ManifestV2(repository, m.Digest.String()) + } + } + // fallback: use the first manifest in the list + // NOTE: emptiness of the list was checked above + return registry.ManifestV2(repository, ml.Manifests[0].Digest.String()) + + default: + return nil, fmt.Errorf("unexpected manifest schema was received from registry: mediatype should be %s, not %s (registry may not support schema2 manifests)", schema2.MediaTypeManifest, actualMediaType) } - return deserialized, nil } func (registry *Registry) ManifestDigest(repository, reference string) (digest.Digest, error) { diff --git a/vendor/github.com/docker/distribution/manifest/manifestlist/manifestlist.go b/vendor/github.com/docker/distribution/manifest/manifestlist/manifestlist.go new file mode 100644 index 00000000..3aa0662d --- /dev/null +++ b/vendor/github.com/docker/distribution/manifest/manifestlist/manifestlist.go @@ -0,0 +1,155 @@ +package manifestlist + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/docker/distribution" + "github.com/docker/distribution/manifest" + "github.com/opencontainers/go-digest" +) + +// MediaTypeManifestList specifies the mediaType for manifest lists. +const 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, +} + +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 + } + + 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)) + } +} + +// 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) { + m := ManifestList{ + Versioned: SchemaVersion, + } + + m.Manifests = make([]ManifestDescriptor, len(descriptors), 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), 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) { + return m.MediaType, m.canonical, nil +}