Merge pull request #410 from thaJeztah/19.03_backport_fix_buildkit_prunegc_filter_config
[19.03 backport] daemon/config: fix filter type in BuildKit GC config
diff --git a/contrib/dockerd-rootless.sh b/contrib/dockerd-rootless.sh
index 214162f..b14a0dd 100755
--- a/contrib/dockerd-rootless.sh
+++ b/contrib/dockerd-rootless.sh
@@ -39,6 +39,9 @@
: "${DOCKERD_ROOTLESS_ROOTLESSKIT_NET:=}"
: "${DOCKERD_ROOTLESS_ROOTLESSKIT_MTU:=}"
+# if slirp4netns v0.4.0+ is installed, slirp4netns is hardened using sandbox (mount namespace) and seccomp
+: "${DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX:=auto}"
+: "${DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP:=auto}"
net=$DOCKERD_ROOTLESS_ROOTLESSKIT_NET
mtu=$DOCKERD_ROOTLESS_ROOTLESSKIT_MTU
if [ -z $net ]; then
@@ -77,6 +80,8 @@
# * /run: copy-up is required so that we can create /run/docker (hardcoded for plugins) in our namespace
exec $rootlesskit \
--net=$net --mtu=$mtu \
+ --slirp4netns-sandbox=$DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX \
+ --slirp4netns-seccomp=$DOCKERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP \
--disable-host-loopback --port-driver=builtin \
--copy-up=/etc --copy-up=/run \
$DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS \
diff --git a/daemon/monitor.go b/daemon/monitor.go
index 2f47497..c294742 100644
--- a/daemon/monitor.go
+++ b/daemon/monitor.go
@@ -2,8 +2,6 @@
import (
"context"
- "errors"
- "fmt"
"runtime"
"strconv"
"time"
@@ -12,6 +10,7 @@
"github.com/docker/docker/container"
libcontainerdtypes "github.com/docker/docker/libcontainerd/types"
"github.com/docker/docker/restartmanager"
+ "github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@@ -29,8 +28,8 @@
// ProcessEvent is called by libcontainerd whenever an event occurs
func (daemon *Daemon) ProcessEvent(id string, e libcontainerdtypes.EventType, ei libcontainerdtypes.EventInfo) error {
c, err := daemon.GetContainer(id)
- if c == nil || err != nil {
- return fmt.Errorf("no such container: %s", id)
+ if err != nil {
+ return errors.Wrapf(err, "could not find container %s", id)
}
switch e {
diff --git a/distribution/oci.go b/distribution/oci.go
deleted file mode 100644
index 92a8561..0000000
--- a/distribution/oci.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package distribution
-
-import (
- "fmt"
-
- "github.com/docker/distribution"
- "github.com/docker/distribution/manifest/schema2"
- digest "github.com/opencontainers/go-digest"
- ocispec "github.com/opencontainers/image-spec/specs-go/v1"
-)
-
-func init() {
- // TODO: Remove this registration if distribution is included with OCI support
-
- ocischemaFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
- m := new(schema2.DeserializedManifest)
- 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: ocispec.MediaTypeImageManifest}, err
- }
- err := distribution.RegisterManifestSchema(ocispec.MediaTypeImageManifest, ocischemaFunc)
- if err != nil {
- panic(fmt.Sprintf("Unable to register manifest: %s", err))
- }
-}
diff --git a/distribution/pull_v2.go b/distribution/pull_v2.go
index 77c5f64..3307458 100644
--- a/distribution/pull_v2.go
+++ b/distribution/pull_v2.go
@@ -14,6 +14,7 @@
"github.com/containerd/containerd/platforms"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist"
+ "github.com/docker/distribution/manifest/ocischema"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference"
@@ -410,6 +411,11 @@
if err != nil {
return false, err
}
+ case *ocischema.DeserializedManifest:
+ id, manifestDigest, err = p.pullOCI(ctx, ref, v, platform)
+ if err != nil {
+ return false, err
+ }
case *manifestlist.DeserializedManifestList:
id, manifestDigest, err = p.pullManifestList(ctx, ref, v, platform)
if err != nil {
@@ -557,24 +563,18 @@
return imageID, manifestDigest, nil
}
-func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *schema2.DeserializedManifest, platform *specs.Platform) (id digest.Digest, manifestDigest digest.Digest, err error) {
- manifestDigest, err = schema2ManifestDigest(ref, mfst)
- if err != nil {
- return "", "", err
- }
-
- target := mfst.Target()
+func (p *v2Puller) pullSchema2Layers(ctx context.Context, target distribution.Descriptor, layers []distribution.Descriptor, platform *specs.Platform) (id digest.Digest, err error) {
if _, err := p.config.ImageStore.Get(target.Digest); err == nil {
// If the image already exists locally, no need to pull
// anything.
- return target.Digest, manifestDigest, nil
+ return target.Digest, nil
}
var descriptors []xfer.DownloadDescriptor
// Note that the order of this loop is in the direction of bottom-most
// to top-most, so that the downloads slice gets ordered correctly.
- for _, d := range mfst.Layers {
+ for _, d := range layers {
layerDescriptor := &v2LayerDescriptor{
digest: d.Digest,
repo: p.repo,
@@ -629,23 +629,23 @@
if runtime.GOOS == "windows" {
configJSON, configRootFS, configPlatform, err = receiveConfig(p.config.ImageStore, configChan, configErrChan)
if err != nil {
- return "", "", err
+ return "", err
}
if configRootFS == nil {
- return "", "", errRootFSInvalid
+ return "", errRootFSInvalid
}
if err := checkImageCompatibility(configPlatform.OS, configPlatform.OSVersion); err != nil {
- return "", "", err
+ return "", err
}
if len(descriptors) != len(configRootFS.DiffIDs) {
- return "", "", errRootFSMismatch
+ return "", errRootFSMismatch
}
if platform == nil {
// Early bath if the requested OS doesn't match that of the configuration.
// This avoids doing the download, only to potentially fail later.
if !system.IsOSSupported(configPlatform.OS) {
- return "", "", fmt.Errorf("cannot download image with operating system %q when requesting %q", configPlatform.OS, layerStoreOS)
+ return "", fmt.Errorf("cannot download image with operating system %q when requesting %q", configPlatform.OS, layerStoreOS)
}
layerStoreOS = configPlatform.OS
}
@@ -692,14 +692,14 @@
case <-downloadsDone:
case <-layerErrChan:
}
- return "", "", err
+ return "", err
}
}
select {
case <-downloadsDone:
case err = <-layerErrChan:
- return "", "", err
+ return "", err
}
if release != nil {
@@ -711,22 +711,40 @@
// Otherwise the image config could be referencing layers that aren't
// included in the manifest.
if len(downloadedRootFS.DiffIDs) != len(configRootFS.DiffIDs) {
- return "", "", errRootFSMismatch
+ return "", errRootFSMismatch
}
for i := range downloadedRootFS.DiffIDs {
if downloadedRootFS.DiffIDs[i] != configRootFS.DiffIDs[i] {
- return "", "", errRootFSMismatch
+ return "", errRootFSMismatch
}
}
}
imageID, err := p.config.ImageStore.Put(configJSON)
if err != nil {
- return "", "", err
+ return "", err
}
- return imageID, manifestDigest, nil
+ return imageID, nil
+}
+
+func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *schema2.DeserializedManifest, platform *specs.Platform) (id digest.Digest, manifestDigest digest.Digest, err error) {
+ manifestDigest, err = schema2ManifestDigest(ref, mfst)
+ if err != nil {
+ return "", "", err
+ }
+ id, err = p.pullSchema2Layers(ctx, mfst.Target(), mfst.Layers, platform)
+ return id, manifestDigest, err
+}
+
+func (p *v2Puller) pullOCI(ctx context.Context, ref reference.Named, mfst *ocischema.DeserializedManifest, platform *specs.Platform) (id digest.Digest, manifestDigest digest.Digest, err error) {
+ manifestDigest, err = schema2ManifestDigest(ref, mfst)
+ if err != nil {
+ return "", "", err
+ }
+ id, err = p.pullSchema2Layers(ctx, mfst.Target(), mfst.Layers, platform)
+ return id, manifestDigest, err
}
func receiveConfig(s ImageConfigStore, configChan <-chan []byte, errChan <-chan error) ([]byte, *image.RootFS, *specs.Platform, error) {
@@ -811,6 +829,12 @@
if err != nil {
return "", "", err
}
+ case *ocischema.DeserializedManifest:
+ platform := toOCIPlatform(manifestMatches[0].Platform)
+ id, _, err = p.pullOCI(ctx, manifestRef, v, &platform)
+ if err != nil {
+ return "", "", err
+ }
default:
return "", "", errors.New("unsupported manifest format")
}
diff --git a/docs/rootless.md b/docs/rootless.md
index 7efdd86..698f581 100644
--- a/docs/rootless.md
+++ b/docs/rootless.md
@@ -20,43 +20,107 @@
penguin:231072:65536
```
-
### Distribution-specific hint
-#### Debian (excluding Ubuntu)
-* `sudo sh -c "echo 1 > /proc/sys/kernel/unprivileged_userns_clone"` is required
+Using Ubuntu kernel is recommended.
+
+#### Ubuntu
+* No preparation is needed.
+* `overlay2` is enabled by default ([Ubuntu-specific kernel patch](https://kernel.ubuntu.com/git/ubuntu/ubuntu-bionic.git/commit/fs/overlayfs?id=3b7da90f28fe1ed4b79ef2d994c81efbc58f1144)).
+* Known to work on Ubuntu 16.04 and 18.04.
+
+#### Debian GNU/Linux
+* Add `kernel.unprivileged_userns_clone=1` to `/etc/sysctl.conf` (or `/etc/sysctl.d`) and run `sudo sysctl -p`
+* To use `overlay2` storage driver (recommended), run `sudo modprobe overlay permit_mounts_in_userns=1` ([Debian-specific kernel patch, introduced in Debian 10](https://salsa.debian.org/kernel-team/linux/blob/283390e7feb21b47779b48e0c8eb0cc409d2c815/debian/patches/debian/overlayfs-permit-mounts-in-userns.patch)). Put the configuration to `/etc/modprobe.d` for persistence.
+* Known to work on Debian 9 and 10. `overlay2` is only supported since Debian 10 and needs `modprobe` configuration described above.
#### Arch Linux
-* `sudo sh -c "echo 1 > /proc/sys/kernel/unprivileged_userns_clone"` is required
+* Add `kernel.unprivileged_userns_clone=1` to `/etc/sysctl.conf` (or `/etc/sysctl.d`) and run `sudo sysctl -p`
#### openSUSE
* `sudo modprobe ip_tables iptable_mangle iptable_nat iptable_filter` is required. (This is likely to be required on other distros as well)
+* Known to work on openSUSE 15.
+
+#### Fedora 31 and later
+* Run `sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=0"` and reboot.
+
+#### Fedora 30
+* No preparation is needed
+
+#### RHEL/CentOS 8
+* No preparation is needed
#### RHEL/CentOS 7
-* `sudo sh -c "echo 28633 > /proc/sys/user/max_user_namespaces"` is required
-* [COPR package `vbatts/shadow-utils-newxidmap`](https://copr.fedorainfracloud.org/coprs/vbatts/shadow-utils-newxidmap/) needs to be installed
+* Add `user.max_user_namespaces=28633` to `/etc/sysctl.conf` (or `/etc/sysctl.d`) and run `sudo sysctl -p`
+* `systemctl --user` does not work by default. Run the daemon directly without systemd: `dockerd-rootless.sh --experimental --storage-driver vfs`
+* Known to work on RHEL/CentOS 7.7. Older releases require extra configuration steps.
+* RHEL/CentOS 7.6 and older releases require [COPR package `vbatts/shadow-utils-newxidmap`](https://copr.fedorainfracloud.org/coprs/vbatts/shadow-utils-newxidmap/) to be installed.
+* RHEL/CentOS 7.5 and older releases require running `sudo grubby --update-kernel=ALL --args="user_namespace.enable=1"` and reboot.
-## Restrictions
+## Known limitations
-* Only `vfs` graphdriver is supported. However, on [Ubuntu](http://kernel.ubuntu.com/git/ubuntu/ubuntu-artful.git/commit/fs/overlayfs?h=Ubuntu-4.13.0-25.29&id=0a414bdc3d01f3b61ed86cfe3ce8b63a9240eba7) and a few distros, `overlay2` and `overlay` are also supported.
+* Only `vfs` graphdriver is supported. However, on Ubuntu and Debian 10, `overlay2` and `overlay` are also supported.
* Following features are not supported:
* Cgroups (including `docker top`, which depends on the cgroups device controller)
* Apparmor
* Checkpoint
* Overlay network
* Exposing SCTP ports
-* To expose a TCP/UDP port, the host port number needs to be set to >= 1024.
+* To use `ping` command, see [Routing ping packets](#routing-ping-packets)
+* To expose privileged TCP/UDP ports (< 1024), see [Exposing privileged ports](#exposing-privileged-ports)
+
+## Install
+
+The installation script is available at https://get.docker.com/rootless .
+
+```console
+$ curl -fsSL https://get.docker.com/rootless | sh
+```
+
+Make sure to run the script as a non-root user.
+
+The script will show the environment variables that are needed to be set:
+
+```console
+$ curl -fsSL https://get.docker.com/rootless | sh
+...
+# Docker binaries are installed in /home/penguin/bin
+# WARN: dockerd is not in your current PATH or pointing to /home/penguin/bin/dockerd
+# Make sure the following environment variables are set (or add them to ~/.bashrc):
+
+export PATH=/home/penguin/bin:$PATH
+export PATH=$PATH:/sbin
+export DOCKER_HOST=unix:///run/user/1001/docker.sock
+
+#
+# To control docker service run:
+# systemctl --user (start|stop|restart) docker
+#
+```
+
+To install the binaries manually without using the installer, extract `docker-rootless-extras-<version>.tar.gz` along with `docker-<version>.tar.gz`: https://download.docker.com/linux/static/stable/x86_64/
## Usage
### Daemon
-You need to run `dockerd-rootless.sh` instead of `dockerd`.
-
+Use `systemctl --user` to manage the lifecycle of the daemon:
```console
-$ dockerd-rootless.sh --experimental
+$ systemctl --user start docker
```
-As Rootless mode is experimental per se, currently you always need to run `dockerd-rootless.sh` with `--experimental`.
+
+To launch the daemon on system startup, enable systemd lingering:
+```console
+$ sudo loginctl enable-linger $(whoami)
+```
+
+To run the daemon directly without systemd, you need to run `dockerd-rootless.sh` instead of `dockerd`:
+```console
+$ dockerd-rootless.sh --experimental --storage-driver vfs
+```
+
+As Rootless mode is experimental, currently you always need to run `dockerd-rootless.sh` with `--experimental`.
+You also need `--storage-driver vfs` unless using Ubuntu or Debian 10 kernel.
Remarks:
* The socket path is set to `$XDG_RUNTIME_DIR/docker.sock` by default. `$XDG_RUNTIME_DIR` is typically set to `/run/user/$UID`.
@@ -69,12 +133,24 @@
### Client
-You can just use the upstream Docker client but you need to set the socket path explicitly.
+You need to set the socket path explicitly.
```console
-$ docker -H unix://$XDG_RUNTIME_DIR/docker.sock run -d nginx
+$ export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
+$ docker run -d nginx
```
+### Rootless Docker in Docker
+
+To run Rootless Docker inside "rootful" Docker, use `docker:<version>-dind-rootless` image instead of `docker:<version>-dind` image.
+
+```console
+$ docker run -d --name dind-rootless --privileged docker:19.03-dind-rootless --experimental
+```
+
+`docker:<version>-dind-rootless` image runs as a non-root user (UID 1000).
+However, `--privileged` is required for disabling seccomp, AppArmor, and mount masks.
+
### Expose Docker API socket via TCP
To expose the Docker API socket via TCP, you need to launch `dockerd-rootless.sh` with `DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS="-p 0.0.0.0:2376:2376/tcp"`.
@@ -88,12 +164,23 @@
### Routing ping packets
-To route ping packets, you need to set up `net.ipv4.ping_group_range` properly as the root.
+Add `net.ipv4.ping_group_range = 0 2147483647` to `/etc/sysctl.conf` (or `/etc/sysctl.d`) and run `sudo sysctl -p`.
+
+### Exposing privileged ports
+
+To expose privileged ports (< 1024), set `CAP_NET_BIND_SERVICE` on `rootlesskit` binary.
```console
-$ sudo sh -c "echo 0 2147483647 > /proc/sys/net/ipv4/ping_group_range"
+$ sudo setcap cap_net_bind_service=ep $HOME/bin/rootlesskit
```
+Or add `net.ipv4.ip_unprivileged_port_start=0` to `/etc/sysctl.conf` (or `/etc/sysctl.d`) and run `sudo sysctl -p`.
+
+### Limiting resources
+
+Currently rootless mode ignores cgroup-related `docker run` flags such as `--cpus` and `memory`.
+However, traditional `ulimit` and [`cpulimit`](https://github.com/opsengine/cpulimit) can be still used, though it works in process-granularity rather than container-granularity.
+
### Changing network stack
`dockerd-rootless.sh` uses [slirp4netns](https://github.com/rootless-containers/slirp4netns) (if installed) or [VPNKit](https://github.com/moby/vpnkit) as the network stack by default.
diff --git a/hack/dockerfile/install/rootlesskit.installer b/hack/dockerfile/install/rootlesskit.installer
index 964207e..45dae93 100755
--- a/hack/dockerfile/install/rootlesskit.installer
+++ b/hack/dockerfile/install/rootlesskit.installer
@@ -1,7 +1,7 @@
#!/bin/sh
-# v0.6.0
-ROOTLESSKIT_COMMIT=2fcff6ceae968a1d895e6205e5154b107247356f
+# v0.7.0
+ROOTLESSKIT_COMMIT=791ac8cb209a107505cd1ca5ddf23a49913e176c
install_rootlesskit() {
case "$1" in
diff --git a/vendor/github.com/docker/distribution/manifest/ocischema/builder.go b/vendor/github.com/docker/distribution/manifest/ocischema/builder.go
new file mode 100644
index 0000000..d90453b
--- /dev/null
+++ b/vendor/github.com/docker/distribution/manifest/ocischema/builder.go
@@ -0,0 +1,107 @@
+package ocischema
+
+import (
+ "context"
+ "errors"
+
+ "github.com/docker/distribution"
+ "github.com/docker/distribution/manifest"
+ "github.com/opencontainers/go-digest"
+ "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+// Builder is a type for constructing manifests.
+type Builder struct {
+ // bs is a BlobService used to publish the configuration blob.
+ bs distribution.BlobService
+
+ // configJSON references
+ configJSON []byte
+
+ // layers is a list of layer descriptors that gets built by successive
+ // calls to AppendReference.
+ layers []distribution.Descriptor
+
+ // Annotations contains arbitrary metadata relating to the targeted content.
+ annotations map[string]string
+
+ // For testing purposes
+ mediaType string
+}
+
+// NewManifestBuilder is used to build new manifests for the current schema
+// version. It takes a BlobService so it can publish the configuration blob
+// as part of the Build process, and annotations.
+func NewManifestBuilder(bs distribution.BlobService, configJSON []byte, annotations map[string]string) distribution.ManifestBuilder {
+ mb := &Builder{
+ bs: bs,
+ configJSON: make([]byte, len(configJSON)),
+ annotations: annotations,
+ mediaType: v1.MediaTypeImageManifest,
+ }
+ copy(mb.configJSON, configJSON)
+
+ return mb
+}
+
+// SetMediaType assigns the passed mediatype or error if the mediatype is not a
+// valid media type for oci image manifests currently: "" or "application/vnd.oci.image.manifest.v1+json"
+func (mb *Builder) SetMediaType(mediaType string) error {
+ if mediaType != "" && mediaType != v1.MediaTypeImageManifest {
+ return errors.New("invalid media type for OCI image manifest")
+ }
+
+ mb.mediaType = mediaType
+ return nil
+}
+
+// Build produces a final manifest from the given references.
+func (mb *Builder) Build(ctx context.Context) (distribution.Manifest, error) {
+ m := Manifest{
+ Versioned: manifest.Versioned{
+ SchemaVersion: 2,
+ MediaType: mb.mediaType,
+ },
+ Layers: make([]distribution.Descriptor, len(mb.layers)),
+ Annotations: mb.annotations,
+ }
+ copy(m.Layers, mb.layers)
+
+ configDigest := digest.FromBytes(mb.configJSON)
+
+ var err error
+ m.Config, err = mb.bs.Stat(ctx, configDigest)
+ switch err {
+ case nil:
+ // Override MediaType, since Put always replaces the specified media
+ // type with application/octet-stream in the descriptor it returns.
+ m.Config.MediaType = v1.MediaTypeImageConfig
+ return FromStruct(m)
+ case distribution.ErrBlobUnknown:
+ // nop
+ default:
+ return nil, err
+ }
+
+ // Add config to the blob store
+ m.Config, err = mb.bs.Put(ctx, v1.MediaTypeImageConfig, mb.configJSON)
+ // Override MediaType, since Put always replaces the specified media
+ // type with application/octet-stream in the descriptor it returns.
+ m.Config.MediaType = v1.MediaTypeImageConfig
+ if err != nil {
+ return nil, err
+ }
+
+ return FromStruct(m)
+}
+
+// AppendReference adds a reference to the current ManifestBuilder.
+func (mb *Builder) AppendReference(d distribution.Describable) error {
+ mb.layers = append(mb.layers, d.Descriptor())
+ return nil
+}
+
+// References returns the current references added to this builder.
+func (mb *Builder) References() []distribution.Descriptor {
+ return mb.layers
+}
diff --git a/vendor/github.com/docker/distribution/manifest/ocischema/manifest.go b/vendor/github.com/docker/distribution/manifest/ocischema/manifest.go
new file mode 100644
index 0000000..b8c4bab
--- /dev/null
+++ b/vendor/github.com/docker/distribution/manifest/ocischema/manifest.go
@@ -0,0 +1,124 @@
+package ocischema
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/docker/distribution"
+ "github.com/docker/distribution/manifest"
+ "github.com/opencontainers/go-digest"
+ "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+var (
+ // SchemaVersion provides a pre-initialized version structure for this
+ // packages version of the manifest.
+ SchemaVersion = manifest.Versioned{
+ SchemaVersion: 2, // historical value here.. does not pertain to OCI or docker version
+ MediaType: v1.MediaTypeImageManifest,
+ }
+)
+
+func init() {
+ ocischemaFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
+ m := new(DeserializedManifest)
+ 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: v1.MediaTypeImageManifest}, err
+ }
+ err := distribution.RegisterManifestSchema(v1.MediaTypeImageManifest, ocischemaFunc)
+ if err != nil {
+ panic(fmt.Sprintf("Unable to register manifest: %s", err))
+ }
+}
+
+// Manifest defines a ocischema manifest.
+type Manifest struct {
+ manifest.Versioned
+
+ // Config references the image configuration as a blob.
+ Config distribution.Descriptor `json:"config"`
+
+ // Layers lists descriptors for the layers referenced by the
+ // configuration.
+ Layers []distribution.Descriptor `json:"layers"`
+
+ // Annotations contains arbitrary metadata for the image manifest.
+ Annotations map[string]string `json:"annotations,omitempty"`
+}
+
+// References returns the descriptors of this manifests references.
+func (m Manifest) References() []distribution.Descriptor {
+ references := make([]distribution.Descriptor, 0, 1+len(m.Layers))
+ references = append(references, m.Config)
+ references = append(references, m.Layers...)
+ return references
+}
+
+// Target returns the target of this manifest.
+func (m Manifest) Target() distribution.Descriptor {
+ return m.Config
+}
+
+// DeserializedManifest wraps Manifest with a copy of the original JSON.
+// It satisfies the distribution.Manifest interface.
+type DeserializedManifest struct {
+ Manifest
+
+ // canonical is the canonical byte representation of the Manifest.
+ canonical []byte
+}
+
+// FromStruct takes a Manifest structure, marshals it to JSON, and returns a
+// DeserializedManifest which contains the manifest and its JSON representation.
+func FromStruct(m Manifest) (*DeserializedManifest, error) {
+ var deserialized DeserializedManifest
+ deserialized.Manifest = m
+
+ var err error
+ deserialized.canonical, err = json.MarshalIndent(&m, "", " ")
+ return &deserialized, err
+}
+
+// UnmarshalJSON populates a new Manifest struct from JSON data.
+func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
+ m.canonical = make([]byte, len(b))
+ // store manifest in canonical
+ copy(m.canonical, b)
+
+ // Unmarshal canonical JSON into Manifest object
+ var manifest Manifest
+ if err := json.Unmarshal(m.canonical, &manifest); err != nil {
+ return err
+ }
+
+ if manifest.MediaType != "" && manifest.MediaType != v1.MediaTypeImageManifest {
+ return fmt.Errorf("if present, mediaType in manifest should be '%s' not '%s'",
+ v1.MediaTypeImageManifest, manifest.MediaType)
+ }
+
+ m.Manifest = manifest
+
+ return nil
+}
+
+// MarshalJSON returns the contents of canonical. If canonical is empty,
+// marshals the inner contents.
+func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
+ if len(m.canonical) > 0 {
+ return m.canonical, nil
+ }
+
+ return nil, errors.New("JSON representation not initialized in DeserializedManifest")
+}
+
+// Payload returns the raw content of the manifest. The contents can be used to
+// calculate the content identifier.
+func (m DeserializedManifest) Payload() (string, []byte, error) {
+ return v1.MediaTypeImageManifest, m.canonical, nil
+}