daemon/c8d: Implement save and load
Upstreams: 
- https://github.com/rumpl/moby/pull/33
- https://github.com/rumpl/moby/pull/112

**- What I did**
This makes the `docker save` and `docker load` work with the containerd image store. The archive is both OCI and Docker compatible.

Saved archive will only contain content which is available locally.  In case the saved image is a multi-platform manifest list, the behavior depends on the local availability of the content. This is to be reconsidered when we have the `--platform` option in the CLI.

- If all manifests and their contents, referenced by the manifest list are present, then the manifest-list is exported directly and the ID will be the same..
- If only one platform manifest is present, only that manifest is exported (the image id will change and will be the id of platform-specific manifest, instead of the full manifest list).
- If multiple, but not all, platform manifests are available, a new manifest list will be created which will be a subset of the original manifest list.


**- How I did it**

**- How to verify it**
<details>

<summary>Save</summary>

```bash
$ docker pull alpine
Using default tag: latest
f271e74b17ce: Download complete
41d876d4e443: Download complete
04eeaa5f8c35: Download complete
a9eaa45ef418: Download complete
docker.io/library/alpine:latest
$ docker save alpine >alpine-c8d.tar
$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
alpine       latest    f271e74b17ce   29 seconds ago   3.26MB $ docker save alpine >alpine-c8d.^Cr
$ tar  -f alpine-c8d.tar --to-stdout -x manifest.json | jq
[
  {
    "Config": "blobs/sha256/04eeaa5f8c35b8206a15c12425498c37ecd2181e0ef202bea24a2382e4dd240f",
    "RepoTags": [
      "alpine:latest"
    ],
    "Layers": [
      "blobs/sha256/a9eaa45ef418e883481a13c7d84fa9904f2ec56789c52a87ba5a9e6483f2b74f"
    ]
  }
]
$ tar  -f alpine-c8d.tar --to-stdout -x index.json | jq
{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "digest": "sha256:41d876d4e44348d1c27445fdb0e64592e0eb926d4dbbcf09a3526dee7e628329",
      "size": 528,
      "annotations": {
        "io.containerd.image.name": "docker.io/library/alpine:latest",
        "org.opencontainers.image.ref.name": "latest"
      },
      "platform": {
        "architecture": "arm64",
        "os": "linux",
        "variant": "v8"
      }
    }
  ]
}
$ docker pull --platform linux/amd64 alpine
Using default tag: latest
93d5a28ff72d: Download complete
f271e74b17ce: Exists
042a816809aa: Download complete
8921db27df28: Downloading [===============================>                   ]  2.097MB/3.371MB
docker.io/library/alpine:latest
$ docker save alpine >alpine2-c8d.tar
$ tar  -f alpine2-c8d.tar --to-stdout -x index.json | jq
{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.index.v1+json",
      "digest": "sha256:f67bf9932060788baef3f3681cac3396127820a1b8768c49b7ea9c6aff366fff",
      "size": 526,
      "annotations": {
        "io.containerd.image.name": "docker.io/library/alpine:latest",
        "org.opencontainers.image.ref.name": "latest"
      }
    }
  ]
}
$ tar  -f alpine2-c8d.tar --to-stdout -x manifest.json | jq
[
  {
    "Config": "blobs/sha256/04eeaa5f8c35b8206a15c12425498c37ecd2181e0ef202bea24a2382e4dd240f",
    "RepoTags": [
      "alpine:latest"
    ],
    "Layers": [
      "blobs/sha256/a9eaa45ef418e883481a13c7d84fa9904f2ec56789c52a87ba5a9e6483f2b74f"
    ]
  }
]
$ tar  -f alpine2-c8d.tar --to-stdout -x blobs/sha256/f67bf9932060788baef3f3681cac3396127820a1b8768c49b7ea9c6aff366fff | jq
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "digest": "sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0",
      "size": 528,
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "digest": "sha256:41d876d4e44348d1c27445fdb0e64592e0eb926d4dbbcf09a3526dee7e628329",
      "size": 528,
      "platform": {
        "architecture": "arm64",
        "os": "linux",
        "variant": "v8"
      }
    }
  ]
}


$ docker pull --platform linux/386 alpine
Using default tag: latest
f271e74b17ce: Exists
14d4381342be: Download complete
f106dae6f363: Download complete
40e5b0b2e2bd: Download complete
docker.io/library/alpine:latest
$ docker pull --platform linux/arm/v6 alpine
Using default tag: latest
f271e74b17ce: Exists
01a4cdaebc9c: Download complete
c1aae4382455: Download complete
0269c10e600f: Download complete
docker.io/library/alpine:latest
$ docker pull --platform linux/arm/v7 alpine
Using default tag: latest
f271e74b17ce: Exists
1c34b3cb760a: Download complete
7cd16f17a19f: Download complete
c527615e4ffa: Download complete
docker.io/library/alpine:latest
$ docker pull --platform linux/ppc64le alpine
Using default tag: latest
f271e74b17ce: Exists
145b24ad7f65: Download complete
d641b7bad7e9: Download complete
f45bfda3aa14: Downloading [==============================================>    ]  3.146MB/3.385MB
docker.io/library/alpine:latest
$ docker pull --platform linux/s390x alpine
Using default tag: latest
f271e74b17ce: Exists
176bc6c6e935: Download complete
8604c24e0b81: Download complete
ae982806674c: Download complete
docker.io/library/alpine:latest
$ docker save alpine >alpine-full-c8d.tar
$ tar  -f alpine-full-c8d.tar --to-stdout -x index.json | jq
{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
      "digest": "sha256:f271e74b17ced29b915d351685fd4644785c6d1559dd1f2d4189a5e851ef753a",
      "size": 1638,
      "annotations": {
        "io.containerd.image.name": "docker.io/library/alpine:latest",
        "org.opencontainers.image.ref.name": "latest"
      }
    }
  ]
}
```


</details>




<details>
<summary>Load</summary>

```bash

$ docker load <alpine-c8d.tar
$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
alpine       latest    41d876d4e443   2 seconds ago   3.26MB
$ ctr --address /run/docker/containerd/containerd.sock -n moby content ls
DIGEST                                                                  SIZE    AGE             LABELS
sha256:04eeaa5f8c35b8206a15c12425498c37ecd2181e0ef202bea24a2382e4dd240f 1.485kB 14 seconds      containerd.io/gc.ref.snapshot.overlayfs=sha256:0a24540c1e5a47e5a8116c43151cb45c985f7a65afe5d4d65127d9c6fea2ffcf
sha256:41d876d4e44348d1c27445fdb0e64592e0eb926d4dbbcf09a3526dee7e628329 528B    14 seconds      containerd.io/gc.ref.content.config=sha256:04eeaa5f8c35b8206a15c12425498c37ecd2181e0ef202bea24a2382e4dd240f,containerd.io/gc.ref.content.l.0=sha256:a9eaa45ef418e883481a13c7d84fa9904f2ec56789c52a87ba5a9e6483f2b74f
sha256:a9eaa45ef418e883481a13c7d84fa9904f2ec56789c52a87ba5a9e6483f2b74f 3.259MB 14 seconds      containerd.io/uncompressed=sha256:0a24540c1e5a47e5a8116c43151cb45c985f7a65afe5d4d65127d9c6fea2ffcf
$ ctr --address /run/docker/containerd/containerd.sock -n moby content get sha256:41d876d4e44348d1c27445fdb0e64592e0eb926d4dbbcf09a3526dee7e628329 | jq
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "size": 1485,
    "digest": "sha256:04eeaa5f8c35b8206a15c12425498c37ecd2181e0ef202bea24a2382e4dd240f"
  },
  "layers": [
    {
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 3259241,
      "digest": "sha256:a9eaa45ef418e883481a13c7d84fa9904f2ec56789c52a87ba5a9e6483f2b74f"
    }
  ]
}



$ docker load <alpine2-c8d.tar
$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
alpine       latest    f67bf9932060   46 seconds ago   3.26MB
$ ctr --address /run/docker/containerd/containerd.sock -n moby content ls
DIGEST                                                                  SIZE    AGE             LABELS
sha256:042a816809aac8d0f7d7cacac7965782ee2ecac3f21bcf9f24b1de1a7387b769 1.472kB 10 seconds      containerd.io/gc.ref.snapshot.overlayfs=sha256:8e012198eea15b2554b07014081c85fec4967a1b9cc4b65bd9a4bce3ae1c0c88
sha256:04eeaa5f8c35b8206a15c12425498c37ecd2181e0ef202bea24a2382e4dd240f 1.485kB 52 seconds      containerd.io/gc.ref.snapshot.overlayfs=sha256:0a24540c1e5a47e5a8116c43151cb45c985f7a65afe5d4d65127d9c6fea2ffcf
sha256:41d876d4e44348d1c27445fdb0e64592e0eb926d4dbbcf09a3526dee7e628329 528B    52 seconds      containerd.io/gc.ref.content.l.0=sha256:a9eaa45ef418e883481a13c7d84fa9904f2ec56789c52a87ba5a9e6483f2b74f,containerd.io/gc.ref.content.config=sha256:04eeaa5f8c35b8206a15c12425498c37ecd2181e0ef202bea24a2382e4dd240f
sha256:8921db27df2831fa6eaa85321205a2470c669b855f3ec95d5a3c2b46de0442c9 3.371MB 10 seconds      containerd.io/uncompressed=sha256:8e012198eea15b2554b07014081c85fec4967a1b9cc4b65bd9a4bce3ae1c0c88
sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0 528B    10 seconds      containerd.io/gc.ref.content.config=sha256:042a816809aac8d0f7d7cacac7965782ee2ecac3f21bcf9f24b1de1a7387b769,containerd.io/gc.ref.content.l.0=sha256:8921db27df2831fa6eaa85321205a2470c669b855f3ec95d5a3c2b46de0442c9
sha256:a9eaa45ef418e883481a13c7d84fa9904f2ec56789c52a87ba5a9e6483f2b74f 3.259MB 52 seconds      containerd.io/uncompressed=sha256:0a24540c1e5a47e5a8116c43151cb45c985f7a65afe5d4d65127d9c6fea2ffcf
sha256:f67bf9932060788baef3f3681cac3396127820a1b8768c49b7ea9c6aff366fff 526B    10 seconds      containerd.io/gc.ref.content.m.0=sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0,containerd.io/gc.ref.content.m.1=sha256:41d876d4e44348d1c27445fdb0e64592e0eb926d4dbbcf09a3526dee7e628329
$ ctrr^C
$ ctr --address /run/docker/containerd/containerd.sock -n moby content get sha256:f67bf9932060788baef3f3681cac3396127820a1b8768c49b7ea9c6aff366fff | jq
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "digest": "sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0",
      "size": 528,
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "digest": "sha256:41d876d4e44348d1c27445fdb0e64592e0eb926d4dbbcf09a3526dee7e628329",
      "size": 528,
      "platform": {
        "architecture": "arm64",
        "os": "linux",
        "variant": "v8"
      }
    }
  ]
}
$ docker load <alpine-full-c8d.tar
$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
alpine       latest    f271e74b17ce   21 minutes ago   3.26MB
$ ctr --address /run/docker/containerd/containerd.sock -n moby content ls
DIGEST                                                                  SIZE    AGE             LABELS
sha256:01a4cdaebc9c6af607753cc538c507d0867897cdf9a1caa70bbab2eb1506c964 528B    10 seconds      containerd.io/gc.ref.content.config=sha256:c1aae4382455d858fc7dc11997cb34c5aae2da6a768619c16b2a7c70b3c12b43,containerd.io/gc.ref.content.l.0=sha256:0269c10e600f3a375f36ddabdbd264ce9503a455f0d0969ce8a00f24eaecc032
sha256:0269c10e600f3a375f36ddabdbd264ce9503a455f0d0969ce8a00f24eaecc032 3.107MB 10 seconds      containerd.io/uncompressed=sha256:86ddd1858dda5de0cc1e3cc31a93fddc627a28136b08db8cda7460de39f657a3
sha256:042a816809aac8d0f7d7cacac7965782ee2ecac3f21bcf9f24b1de1a7387b769 1.472kB 21 minutes      containerd.io/gc.ref.snapshot.overlayfs=sha256:8e012198eea15b2554b07014081c85fec4967a1b9cc4b65bd9a4bce3ae1c0c88
sha256:04eeaa5f8c35b8206a15c12425498c37ecd2181e0ef202bea24a2382e4dd240f 1.485kB 21 minutes      containerd.io/gc.ref.snapshot.overlayfs=sha256:0a24540c1e5a47e5a8116c43151cb45c985f7a65afe5d4d65127d9c6fea2ffcf
sha256:145b24ad7f65c5368b1cae556cec31da472a135d429b0082a611616579f2752d 528B    10 seconds      containerd.io/gc.ref.content.config=sha256:d641b7bad7e90e03bc539f8365c90a8aa5bc3bfd275d88fe51aa2fecec097c30,containerd.io/gc.ref.content.l.0=sha256:f45bfda3aa14e255d9eb4c9a108eb3d8c6721946b4aa2e5808e5092242344a1c
sha256:14d4381342be1cab4adc3d5288a279a8a2fc665907363e1dce510bec52f29af3 528B    10 seconds      containerd.io/gc.ref.content.config=sha256:f106dae6f3630d726e4097289b182175aa475b850a042b785bc088381f3d641d,containerd.io/gc.ref.content.l.0=sha256:40e5b0b2e2bde18974628cadecd8a2f190f45f06c32846c16885d69b2908bf68
sha256:176bc6c6e93528f4b729fae1f8dbd70b73861264dba3a3f64c49c92e1f42a5aa 528B    10 seconds      containerd.io/gc.ref.content.config=sha256:8604c24e0b81c89dda8fd64e3609796b450567e92f1e348ef2dd73fab4d83fb6,containerd.io/gc.ref.content.l.0=sha256:ae982806674c51a962c0fdd6e19f464ebd673df529c5cfb7c1d049e0b618d384
sha256:1c34b3cb760a98c23361d919357b99fa497074576c898e7289425d45ef67b46a 528B    10 seconds      containerd.io/gc.ref.content.config=sha256:7cd16f17a19f36043f7337dc35fd160da1326bc99c7338a451c37ead222f54d0,containerd.io/gc.ref.content.l.0=sha256:c527615e4ffa2d5b9b777fd469b3b5ba7c1b1e9201c065be2c43569de48a3754
sha256:40e5b0b2e2bde18974628cadecd8a2f190f45f06c32846c16885d69b2908bf68 3.408MB 10 seconds      containerd.io/uncompressed=sha256:253f1efcceb575705106468bdc1c875a7c9b2a734d4bcca3b1335946656d7acb
sha256:41d876d4e44348d1c27445fdb0e64592e0eb926d4dbbcf09a3526dee7e628329 528B    21 minutes      containerd.io/gc.ref.content.config=sha256:04eeaa5f8c35b8206a15c12425498c37ecd2181e0ef202bea24a2382e4dd240f,containerd.io/gc.ref.content.l.0=sha256:a9eaa45ef418e883481a13c7d84fa9904f2ec56789c52a87ba5a9e6483f2b74f
sha256:7cd16f17a19f36043f7337dc35fd160da1326bc99c7338a451c37ead222f54d0 1.484kB 10 seconds      containerd.io/gc.ref.snapshot.overlayfs=sha256:383269fd0d7d9a5f547ad63131df9c00938dbbb23170a498357bc9fb5cd0948a
sha256:8604c24e0b81c89dda8fd64e3609796b450567e92f1e348ef2dd73fab4d83fb6 1.47kB  10 seconds      containerd.io/gc.ref.snapshot.overlayfs=sha256:b5902734b682781f128d816229ad15ec369201ddec7177fc18f3a8dcfeb00697
sha256:8921db27df2831fa6eaa85321205a2470c669b855f3ec95d5a3c2b46de0442c9 3.371MB 21 minutes      containerd.io/uncompressed=sha256:8e012198eea15b2554b07014081c85fec4967a1b9cc4b65bd9a4bce3ae1c0c88
sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0 528B    21 minutes      containerd.io/gc.ref.content.l.0=sha256:8921db27df2831fa6eaa85321205a2470c669b855f3ec95d5a3c2b46de0442c9,containerd.io/gc.ref.content.config=sha256:042a816809aac8d0f7d7cacac7965782ee2ecac3f21bcf9f24b1de1a7387b769
sha256:a9eaa45ef418e883481a13c7d84fa9904f2ec56789c52a87ba5a9e6483f2b74f 3.259MB 21 minutes      containerd.io/uncompressed=sha256:0a24540c1e5a47e5a8116c43151cb45c985f7a65afe5d4d65127d9c6fea2ffcf
sha256:ae982806674c51a962c0fdd6e19f464ebd673df529c5cfb7c1d049e0b618d384 3.171MB 10 seconds      containerd.io/uncompressed=sha256:b5902734b682781f128d816229ad15ec369201ddec7177fc18f3a8dcfeb00697
sha256:c1aae4382455d858fc7dc11997cb34c5aae2da6a768619c16b2a7c70b3c12b43 1.485kB 10 seconds      containerd.io/gc.ref.snapshot.overlayfs=sha256:86ddd1858dda5de0cc1e3cc31a93fddc627a28136b08db8cda7460de39f657a3
sha256:c527615e4ffa2d5b9b777fd469b3b5ba7c1b1e9201c065be2c43569de48a3754 2.865MB 10 seconds      containerd.io/uncompressed=sha256:383269fd0d7d9a5f547ad63131df9c00938dbbb23170a498357bc9fb5cd0948a
sha256:d641b7bad7e90e03bc539f8365c90a8aa5bc3bfd275d88fe51aa2fecec097c30 1.474kB 10 seconds      containerd.io/gc.ref.snapshot.overlayfs=sha256:0e3395c9c4d69931dec5029f4c593bdabd1bf4cc1c06b7d568ee72ea29783132
sha256:f106dae6f3630d726e4097289b182175aa475b850a042b785bc088381f3d641d 1.47kB  10 seconds      containerd.io/gc.ref.snapshot.overlayfs=sha256:253f1efcceb575705106468bdc1c875a7c9b2a734d4bcca3b1335946656d7acb
sha256:f271e74b17ced29b915d351685fd4644785c6d1559dd1f2d4189a5e851ef753a 1.638kB 10 seconds      containerd.io/gc.ref.content.m.6=sha256:176bc6c6e93528f4b729fae1f8dbd70b73861264dba3a3f64c49c92e1f42a5aa,containerd.io/gc.ref.content.m.0=sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0,containerd.io/gc.ref.content.m.1=sha256:01a4cdaebc9c6af607753cc538c507d0867897cdf9a1caa70bbab2eb1506c964,containerd.io/gc.ref.content.m.2=sha256:1c34b3cb760a98c23361d919357b99fa497074576c898e7289425d45ef67b46a,containerd.io/gc.ref.content.m.3=sha256:41d876d4e44348d1c27445fdb0e64592e0eb926d4dbbcf09a3526dee7e628329,containerd.io/gc.ref.content.m.4=sha256:14d4381342be1cab4adc3d5288a279a8a2fc665907363e1dce510bec52f29af3,containerd.io/gc.ref.content.m.5=sha256:145b24ad7f65c5368b1cae556cec31da472a135d429b0082a611616579f2752d
sha256:f45bfda3aa14e255d9eb4c9a108eb3d8c6721946b4aa2e5808e5092242344a1c 3.385MB 10 seconds      containerd.io/uncompressed=sha256:0e3395c9c4d69931dec5029f4c593bdabd1bf4cc1c06b7d568ee72ea29783132
$ ctr --address /run/docker/containerd/containerd.sock -n moby content get sha256:f271e74b17ced29b915d351685fd4644785c6d1559dd1f2d4189a5e851ef753a | jq
{
  "manifests": [
    {
      "digest": "sha256:93d5a28ff72d288d69b5997b8ba47396d2cbb62a72b5d87cd3351094b5d578a0",
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      },
      "size": 528
    },
    {
      "digest": "sha256:01a4cdaebc9c6af607753cc538c507d0867897cdf9a1caa70bbab2eb1506c964",
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "arm",
        "os": "linux",
        "variant": "v6"
      },
      "size": 528
    },
    {
      "digest": "sha256:1c34b3cb760a98c23361d919357b99fa497074576c898e7289425d45ef67b46a",
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "arm",
        "os": "linux",
        "variant": "v7"
      },
      "size": 528
    },
    {
      "digest": "sha256:41d876d4e44348d1c27445fdb0e64592e0eb926d4dbbcf09a3526dee7e628329",
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "arm64",
        "os": "linux",
        "variant": "v8"
      },
      "size": 528
    },
    {
      "digest": "sha256:14d4381342be1cab4adc3d5288a279a8a2fc665907363e1dce510bec52f29af3",
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "386",
        "os": "linux"
      },
      "size": 528
    },
    {
      "digest": "sha256:145b24ad7f65c5368b1cae556cec31da472a135d429b0082a611616579f2752d",
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "ppc64le",
        "os": "linux"
      },
      "size": 528
    },
    {
      "digest": "sha256:176bc6c6e93528f4b729fae1f8dbd70b73861264dba3a3f64c49c92e1f42a5aa",
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "platform": {
        "architecture": "s390x",
        "os": "linux"
      },
      "size": 528
    }
  ],
  "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
  "schemaVersion": 2
}

```

</details>


**- Description for the changelog**

**- A picture of a cute animal (not mandatory but encouraged)**
![image](https://user-images.githubusercontent.com/5046555/212901105-6185f842-57d3-4134-a82a-20799d16d2c1.png)
daemon/containerd/image_exporter.go
package containerd

import (
	"context"
	"io"

	"github.com/containerd/containerd"
	"github.com/containerd/containerd/images/archive"
	"github.com/containerd/containerd/platforms"
	"github.com/docker/distribution/reference"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// ExportImage exports a list of images to the given output stream. The
// exported images are archived into a tar when written to the output
// stream. All images with the given tag and all versions containing
// the same tag are exported. names is the set of tags to export, and
// outStream is the writer which the images are written to.
//
// TODO(thaJeztah): produce JSON stream progress response and image events; see https://github.com/moby/moby/issues/43910
func (i *ImageService) ExportImage(ctx context.Context, names []string, outStream io.Writer) error {
	opts := []archive.ExportOpt{
		archive.WithPlatform(platforms.Ordered(platforms.DefaultSpec())),
		archive.WithSkipNonDistributableBlobs(),
	}
	is := i.client.ImageService()
	for _, imageRef := range names {
		named, err := reference.ParseDockerRef(imageRef)
		if err != nil {
			return err
		}
		opts = append(opts, archive.WithImage(is, named.String()))
	}
	return i.client.Export(ctx, outStream, opts...)
}

// LoadImage uploads a set of images into the repository. This is the
// complement of ExportImage.  The input stream is an uncompressed tar
// ball containing images and metadata.
//
// TODO(thaJeztah): produce JSON stream progress response and image events; see https://github.com/moby/moby/issues/43910
func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, outStream io.Writer, quiet bool) error {
	platform := platforms.All
	imgs, err := i.client.Import(ctx, inTar, containerd.WithImportPlatform(platform))

	if err != nil {
		// TODO(thaJeztah): remove this log or change to debug once we can; see https://github.com/moby/moby/pull/43822#discussion_r937502405
		logrus.WithError(err).Warn("failed to import image to containerd")
		return errors.Wrap(err, "failed to import image")
	}

	for _, img := range imgs {
		platformImg := containerd.NewImageWithPlatform(i.client, img, platform)

		unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
		if err != nil {
			// TODO(thaJeztah): remove this log or change to debug once we can; see https://github.com/moby/moby/pull/43822#discussion_r937502405
			logrus.WithError(err).WithField("image", img.Name).Debug("failed to check if image is unpacked")
			continue
		}

		if !unpacked {
			err := platformImg.Unpack(ctx, i.snapshotter)
			if err != nil {
				// TODO(thaJeztah): remove this log or change to debug once we can; see https://github.com/moby/moby/pull/43822#discussion_r937502405
				logrus.WithError(err).WithField("image", img.Name).Warn("failed to unpack image")
				return errors.Wrap(err, "failed to unpack image")
			}
		}
	}
	return nil
}

package containerd

import (
	"context"
	"fmt"
	"io"

	"github.com/containerd/containerd"
	cerrdefs "github.com/containerd/containerd/errdefs"
	containerdimages "github.com/containerd/containerd/images"
	"github.com/containerd/containerd/images/archive"
	cplatforms "github.com/containerd/containerd/platforms"
	"github.com/docker/distribution/reference"
	"github.com/docker/docker/errdefs"
	"github.com/docker/docker/pkg/platforms"
	"github.com/docker/docker/pkg/streamformatter"
	"github.com/opencontainers/image-spec/specs-go"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/sirupsen/logrus"
)

// ExportImage exports a list of images to the given output stream. The
// exported images are archived into a tar when written to the output
// stream. All images with the given tag and all versions containing
// the same tag are exported. names is the set of tags to export, and
// outStream is the writer which the images are written to.
//
// TODO(thaJeztah): produce JSON stream progress response and image events; see https://github.com/moby/moby/issues/43910
func (i *ImageService) ExportImage(ctx context.Context, names []string, outStream io.Writer) error {
	platform := platforms.AllPlatformsWithPreference(cplatforms.Default())
	opts := []archive.ExportOpt{
		archive.WithSkipNonDistributableBlobs(),

		// This makes the exported archive also include `manifest.json`
		// when the image is a manifest list. It is needed for backwards
		// compatibility with Docker image format.
		// The containerd will choose only one manifest for the `manifest.json`.
		// Our preference is to have it point to the default platform.
		// Example:
		//  Daemon is running on linux/arm64
		//  When we export linux/amd64 and linux/arm64, manifest.json will point to linux/arm64.
		//  When we export linux/amd64 only, manifest.json will point to linux/amd64.
		// Note: This is only applicable if importing this archive into non-containerd Docker.
		// Importing the same archive into containerd, will not restrict the platforms.
		archive.WithPlatform(platform),
	}

	ctx, release, err := i.client.WithLease(ctx)
	if err != nil {
		return errdefs.System(err)
	}
	defer release(ctx)

	for _, name := range names {
		target, err := i.resolveDescriptor(ctx, name)
		if err != nil {
			return err
		}

		// We may not have locally all the platforms that are specified in the index.
		// Export only those manifests that we have.
		// TODO(vvoland): Reconsider this when `--platform` is added.
		if containerdimages.IsIndexType(target.MediaType) {
			desc, err := i.getBestDescriptorForExport(ctx, target)
			if err != nil {
				return err
			}
			target = desc
		}

		if ref, err := reference.ParseNormalizedNamed(name); err == nil {
			ref = reference.TagNameOnly(ref)
			opts = append(opts, archive.WithManifest(target, ref.String()))

			logrus.WithFields(logrus.Fields{
				"target": target,
				"name":   ref.String(),
			}).Debug("export image")
		} else {
			opts = append(opts, archive.WithManifest(target))

			logrus.WithFields(logrus.Fields{
				"target": target,
			}).Debug("export image without name")
		}
	}

	return i.client.Export(ctx, outStream, opts...)
}

// LoadImage uploads a set of images into the repository. This is the
// complement of ExportImage.  The input stream is an uncompressed tar
// ball containing images and metadata.
//
// TODO(thaJeztah): produce JSON stream progress response and image events; see https://github.com/moby/moby/issues/43910
func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, outStream io.Writer, quiet bool) error {
	// TODO(vvoland): Allow user to pass platform
	platform := cplatforms.All
	imgs, err := i.client.Import(ctx, inTar, containerd.WithImportPlatform(platform))

	if err != nil {
		logrus.WithError(err).Debug("failed to import image to containerd")
		return errdefs.System(err)
	}

	store := i.client.ContentStore()
	progress := streamformatter.NewStdoutWriter(outStream)

	for _, img := range imgs {
		allPlatforms, err := containerdimages.Platforms(ctx, store, img.Target)
		if err != nil {
			logrus.WithError(err).WithField("image", img.Name).Debug("failed to get image platforms")
			return errdefs.Unknown(err)
		}

		name := img.Name
		if named, err := reference.ParseNormalizedNamed(img.Name); err == nil {
			name = reference.FamiliarName(named)
		}

		for _, platform := range allPlatforms {
			logger := logrus.WithFields(logrus.Fields{
				"platform": platform,
				"image":    name,
			})
			platformImg := containerd.NewImageWithPlatform(i.client, img, cplatforms.OnlyStrict(platform))

			unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
			if err != nil {
				logger.WithError(err).Debug("failed to check if image is unpacked")
				continue
			}

			if !unpacked {
				err = platformImg.Unpack(ctx, i.snapshotter)

				if err != nil {
					return errdefs.System(err)
				}
			}
			logger.WithField("alreadyUnpacked", unpacked).WithError(err).Debug("unpack")
		}

		fmt.Fprintf(progress, "Loaded image: %s\n", name)
	}
	return nil
}

// getBestDescriptorForExport returns a descriptor which only references content available locally.
// The returned descriptor can be:
// - The same index descriptor - if all content is available
// - Platform specific manifest - if only one manifest from the whole index is available
// - Reduced index descriptor - if not all, but more than one manifest is available
//
// The reduced index descriptor is stored in the content store and may be garbage collected.
// It's advised to pass a context with a lease that's long enough to cover usage of the blob.
func (i *ImageService) getBestDescriptorForExport(ctx context.Context, indexDesc ocispec.Descriptor) (ocispec.Descriptor, error) {
	none := ocispec.Descriptor{}

	if !containerdimages.IsIndexType(indexDesc.MediaType) {
		err := fmt.Errorf("index/manifest-list descriptor expected, got: %s", indexDesc.MediaType)
		return none, errdefs.InvalidParameter(err)
	}
	store := i.client.ContentStore()
	children, err := containerdimages.Children(ctx, store, indexDesc)
	if err != nil {
		if cerrdefs.IsNotFound(err) {
			return none, errdefs.NotFound(err)
		}
		return none, errdefs.System(err)
	}

	// Check which platform manifests have all their blobs available.
	hasMissingManifests := false
	var presentManifests []ocispec.Descriptor
	for _, mfst := range children {
		if containerdimages.IsManifestType(mfst.MediaType) {
			available, _, _, missing, err := containerdimages.Check(ctx, store, mfst, nil)
			if err != nil {
				hasMissingManifests = true
				logrus.WithField("manifest", mfst.Digest).Warn("failed to check manifest's blob availability, won't export")
				continue
			}

			if available && len(missing) == 0 {
				presentManifests = append(presentManifests, mfst)
				logrus.WithField("manifest", mfst.Digest).Debug("manifest content present, will export")
			} else {
				hasMissingManifests = true
				logrus.WithFields(logrus.Fields{
					"manifest": mfst.Digest,
					"missing":  missing,
				}).Debug("manifest is missing, won't export")
			}
		}
	}

	if !hasMissingManifests || len(children) == 0 {
		// If we have the full image, or it has no manifests, just export the original index.
		return indexDesc, nil
	} else if len(presentManifests) == 1 {
		// If only one platform is present, export that one manifest.
		return presentManifests[0], nil
	} else if len(presentManifests) == 0 {
		// Return error when none of the image's manifest is present.
		return none, errdefs.NotFound(fmt.Errorf("none of the manifests is fully present in the content store"))
	}

	// Create a new index which contains only the manifests we have in store.
	index := ocispec.Index{
		Versioned: specs.Versioned{
			SchemaVersion: 2,
		},
		MediaType:   ocispec.MediaTypeImageIndex,
		Manifests:   presentManifests,
		Annotations: indexDesc.Annotations,
	}

	reducedIndexDesc, err := storeJson(ctx, store, index.MediaType, index, nil)
	if err != nil {
		return none, err
	}

	return reducedIndexDesc, nil
}

This line made the comment confusing to me since containerd is only looking at manifest.json as a fallback (if that?).
Shouldn't this be `dockerd without containerd backed storage will choose only one manifest`?