62b33a2604
This patch disables pulling legacy (schema1 and schema 2, version 1) images by default. A `DOCKER_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE` environment-variable is introduced to allow re-enabling this feature, aligning with the environment variable used in containerd 2.0 (`CONTAINERD_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE`). With this patch, attempts to pull a legacy image produces an error: With graphdrivers: docker pull docker:1.0 1.0: Pulling from library/docker [DEPRECATION NOTICE] Docker Image Format v1, and Docker Image manifest version 2, schema 1 support will be removed in an upcoming release. Suggest the author of docker.io/library/docker:1.0 to upgrade the image to the OCI Format, or Docker Image manifest v2, schema 2. More information at https://docs.docker.com/go/deprecated-image-specs/ With the containerd image store enabled, output is slightly different as it returns the error before printing the `1.0: pulling ...`: docker pull docker:1.0 Error response from daemon: [DEPRECATION NOTICE] Docker Image Format v1 and Docker Image manifest version 2, schema 1 support is disabled by default and will be removed in an upcoming release. Suggest the author of docker.io/library/docker:1.0 to upgrade the image to the OCI Format or Docker Image manifest v2, schema 2. More information at https://docs.docker.com/go/deprecated-image-specs/ Using the "distribution" endpoint to resolve the digest for an image also produces an error: curl -v --unix-socket /var/run/docker.sock http://foo/distribution/docker.io/library/docker:1.0/json * Trying /var/run/docker.sock:0... * Connected to foo (/var/run/docker.sock) port 80 (#0) > GET /distribution/docker.io/library/docker:1.0/json HTTP/1.1 > Host: foo > User-Agent: curl/7.88.1 > Accept: */* > < HTTP/1.1 400 Bad Request < Api-Version: 1.45 < Content-Type: application/json < Docker-Experimental: false < Ostype: linux < Server: Docker/dev (linux) < Date: Tue, 27 Feb 2024 16:09:42 GMT < Content-Length: 354 < {"message":"[DEPRECATION NOTICE] Docker Image Format v1, and Docker Image manifest version 2, schema 1 support will be removed in an upcoming release. Suggest the author of docker.io/library/docker:1.0 to upgrade the image to the OCI Format, or Docker Image manifest v2, schema 2. More information at https://docs.docker.com/go/deprecated-image-specs/"} * Connection #0 to host foo left intact Starting the daemon with the `DOCKER_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE` env-var set to a non-empty value allows pulling the image; docker pull docker:1.0 [DEPRECATION NOTICE] Docker Image Format v1 and Docker Image manifest version 2, schema 1 support is disabled by default and will be removed in an upcoming release. Suggest the author of docker.io/library/docker:1.0 to upgrade the image to the OCI Format or Docker Image manifest v2, schema 2. More information at https://docs.docker.com/go/deprecated-image-specs/ b0a0e6710d13: Already exists d193ad713811: Already exists ba7268c3149b: Already exists c862d82a67a2: Already exists Digest: sha256:5e7081837926c7a40e58881bbebc52044a95a62a2ea52fb240db3fc539212fe5 Status: Image is up to date for docker:1.0 docker.io/library/docker:1.0 Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
326 lines
11 KiB
Go
326 lines
11 KiB
Go
package distribution
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/containerd/containerd/content"
|
|
cerrdefs "github.com/containerd/containerd/errdefs"
|
|
"github.com/containerd/containerd/remotes"
|
|
"github.com/containerd/log"
|
|
"github.com/distribution/reference"
|
|
"github.com/docker/distribution"
|
|
"github.com/docker/distribution/manifest/manifestlist"
|
|
"github.com/docker/distribution/manifest/schema1"
|
|
"github.com/docker/distribution/manifest/schema2"
|
|
"github.com/docker/docker/registry"
|
|
"github.com/opencontainers/go-digest"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// labelDistributionSource describes the source blob comes from.
|
|
const labelDistributionSource = "containerd.io/distribution.source"
|
|
|
|
// This is used by manifestStore to pare down the requirements to implement a
|
|
// full distribution.ManifestService, since `Get` is all we use here.
|
|
type manifestGetter interface {
|
|
Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error)
|
|
Exists(ctx context.Context, dgst digest.Digest) (bool, error)
|
|
}
|
|
|
|
type manifestStore struct {
|
|
local ContentStore
|
|
remote manifestGetter
|
|
}
|
|
|
|
// ContentStore is the interface used to persist registry blobs
|
|
//
|
|
// Currently this is only used to persist manifests and manifest lists.
|
|
// It is exported because `distribution.Pull` takes one as an argument.
|
|
type ContentStore interface {
|
|
content.Ingester
|
|
content.Provider
|
|
Info(ctx context.Context, dgst digest.Digest) (content.Info, error)
|
|
Abort(ctx context.Context, ref string) error
|
|
Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error)
|
|
}
|
|
|
|
func makeDistributionSourceLabel(ref reference.Named) (string, string) {
|
|
domain := reference.Domain(ref)
|
|
if domain == "" {
|
|
domain = registry.DefaultNamespace
|
|
}
|
|
repo := reference.Path(ref)
|
|
|
|
return fmt.Sprintf("%s.%s", labelDistributionSource, domain), repo
|
|
}
|
|
|
|
// Taken from https://github.com/containerd/containerd/blob/e079e4a155c86f07bbd602fe6753ecacc78198c2/remotes/docker/handler.go#L84-L108
|
|
func appendDistributionSourceLabel(originLabel, repo string) string {
|
|
repos := []string{}
|
|
if originLabel != "" {
|
|
repos = strings.Split(originLabel, ",")
|
|
}
|
|
repos = append(repos, repo)
|
|
|
|
// use empty string to present duplicate items
|
|
for i := 1; i < len(repos); i++ {
|
|
tmp, j := repos[i], i-1
|
|
for ; j >= 0 && repos[j] >= tmp; j-- {
|
|
if repos[j] == tmp {
|
|
tmp = ""
|
|
}
|
|
repos[j+1] = repos[j]
|
|
}
|
|
repos[j+1] = tmp
|
|
}
|
|
|
|
i := 0
|
|
for ; i < len(repos) && repos[i] == ""; i++ {
|
|
}
|
|
|
|
return strings.Join(repos[i:], ",")
|
|
}
|
|
|
|
func hasDistributionSource(label, repo string) bool {
|
|
sources := strings.Split(label, ",")
|
|
for _, s := range sources {
|
|
if s == repo {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *manifestStore) getLocal(ctx context.Context, desc ocispec.Descriptor, ref reference.Named) (distribution.Manifest, error) {
|
|
ra, err := m.local.ReaderAt(ctx, desc)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error getting content store reader")
|
|
}
|
|
defer ra.Close()
|
|
|
|
distKey, distRepo := makeDistributionSourceLabel(ref)
|
|
info, err := m.local.Info(ctx, desc.Digest)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error getting content info")
|
|
}
|
|
|
|
if _, ok := ref.(reference.Canonical); ok {
|
|
// Since this is specified by digest...
|
|
// We know we have the content locally, we need to check if we've seen this content at the specified repository before.
|
|
// If we have, we can just return the manifest from the local content store.
|
|
// If we haven't, we need to check the remote repository to see if it has the content, otherwise we can end up returning
|
|
// a manifest that has never even existed in the remote before.
|
|
if !hasDistributionSource(info.Labels[distKey], distRepo) {
|
|
log.G(ctx).WithField("ref", ref).Debug("found manifest but no mataching source repo is listed, checking with remote")
|
|
exists, err := m.remote.Exists(ctx, desc.Digest)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error checking if remote exists")
|
|
}
|
|
|
|
if !exists {
|
|
return nil, errors.Wrapf(cerrdefs.ErrNotFound, "manifest %v not found", desc.Digest)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// Update the distribution sources since we now know the content exists in the remote.
|
|
if info.Labels == nil {
|
|
info.Labels = map[string]string{}
|
|
}
|
|
info.Labels[distKey] = appendDistributionSourceLabel(info.Labels[distKey], distRepo)
|
|
if _, err := m.local.Update(ctx, info, "labels."+distKey); err != nil {
|
|
log.G(ctx).WithError(err).WithField("ref", ref).Warn("Could not update content distribution source")
|
|
}
|
|
|
|
r := io.NewSectionReader(ra, 0, ra.Size())
|
|
data, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error reading manifest from content store")
|
|
}
|
|
|
|
manifest, _, err := distribution.UnmarshalManifest(desc.MediaType, data)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error unmarshaling manifest from content store")
|
|
}
|
|
|
|
return manifest, nil
|
|
}
|
|
|
|
func (m *manifestStore) getMediaType(ctx context.Context, desc ocispec.Descriptor) (string, error) {
|
|
ra, err := m.local.ReaderAt(ctx, desc)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "error getting reader to detect media type")
|
|
}
|
|
defer ra.Close()
|
|
|
|
mt, err := detectManifestMediaType(ra)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "error detecting media type")
|
|
}
|
|
return mt, nil
|
|
}
|
|
|
|
func (m *manifestStore) Get(ctx context.Context, desc ocispec.Descriptor, ref reference.Named) (distribution.Manifest, error) {
|
|
l := log.G(ctx)
|
|
|
|
if desc.MediaType == "" {
|
|
// When pulling by digest we will not have the media type on the
|
|
// descriptor since we have not made a request to the registry yet
|
|
//
|
|
// We already have the digest, so we only lookup locally... by digest.
|
|
//
|
|
// Let's try to detect the media type so we can have a good ref key
|
|
// here. We may not even have the content locally, and this is fine, but
|
|
// if we do we should determine that.
|
|
mt, err := m.getMediaType(ctx, desc)
|
|
if err != nil && !cerrdefs.IsNotFound(err) {
|
|
l.WithError(err).Warn("Error looking up media type of content")
|
|
}
|
|
desc.MediaType = mt
|
|
}
|
|
|
|
key := remotes.MakeRefKey(ctx, desc)
|
|
|
|
// Here we open a writer to the requested content. This both gives us a
|
|
// reference to write to if indeed we need to persist it and increments the
|
|
// ref count on the content.
|
|
w, err := m.local.Writer(ctx, content.WithDescriptor(desc), content.WithRef(key))
|
|
if err != nil {
|
|
if cerrdefs.IsAlreadyExists(err) {
|
|
var manifest distribution.Manifest
|
|
if manifest, err = m.getLocal(ctx, desc, ref); err == nil {
|
|
return manifest, nil
|
|
}
|
|
}
|
|
// always fallback to the remote if there is an error with the local store
|
|
}
|
|
if w != nil {
|
|
defer w.Close()
|
|
}
|
|
|
|
l.WithError(err).Debug("Fetching manifest from remote")
|
|
|
|
manifest, err := m.remote.Get(ctx, desc.Digest)
|
|
if err != nil {
|
|
if err := m.local.Abort(ctx, key); err != nil {
|
|
l.WithError(err).Warn("Error while attempting to abort content ingest")
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if w != nil {
|
|
// if `w` is nil here, something happened with the content store, so don't bother trying to persist.
|
|
if err := m.Put(ctx, manifest, desc, w, ref); err != nil {
|
|
if err := m.local.Abort(ctx, key); err != nil {
|
|
l.WithError(err).Warn("error aborting content ingest")
|
|
}
|
|
l.WithError(err).Warn("Error persisting manifest")
|
|
}
|
|
}
|
|
return manifest, nil
|
|
}
|
|
|
|
func (m *manifestStore) Put(ctx context.Context, manifest distribution.Manifest, desc ocispec.Descriptor, w content.Writer, ref reference.Named) error {
|
|
mt, payload, err := manifest.Payload()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
desc.Size = int64(len(payload))
|
|
desc.MediaType = mt
|
|
|
|
if _, err = w.Write(payload); err != nil {
|
|
return errors.Wrap(err, "error writing manifest to content store")
|
|
}
|
|
|
|
distKey, distSource := makeDistributionSourceLabel(ref)
|
|
if err := w.Commit(ctx, desc.Size, desc.Digest, content.WithLabels(map[string]string{
|
|
distKey: distSource,
|
|
})); err != nil {
|
|
return errors.Wrap(err, "error committing manifest to content store")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func detectManifestMediaType(ra content.ReaderAt) (string, error) {
|
|
dt := make([]byte, ra.Size())
|
|
if _, err := ra.ReadAt(dt, 0); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return detectManifestBlobMediaType(dt)
|
|
}
|
|
|
|
// This is used when the manifest store does not know the media type of a sha it
|
|
// was told to get. This would currently only happen when pulling by digest.
|
|
// The media type is needed so the blob can be unmarshalled properly.
|
|
func detectManifestBlobMediaType(dt []byte) (string, error) {
|
|
var mfst struct {
|
|
MediaType string `json:"mediaType"`
|
|
Manifests json.RawMessage `json:"manifests"` // oci index, manifest list
|
|
Config json.RawMessage `json:"config"` // schema2 Manifest
|
|
Layers json.RawMessage `json:"layers"` // schema2 Manifest
|
|
FSLayers json.RawMessage `json:"fsLayers"` // schema1 Manifest
|
|
}
|
|
|
|
if err := json.Unmarshal(dt, &mfst); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// We may have a media type specified in the json, in which case that should be used.
|
|
// Docker types should generally have a media type set.
|
|
// OCI (golang) types do not have a `mediaType` defined, and it is optional in the spec.
|
|
//
|
|
// `distribution.UnmarshalManifest`, which is used to unmarshal this for real, checks these media type values.
|
|
// If the specified media type does not match it will error, and in some cases (docker media types) it is required.
|
|
// So pretty much if we don't have a media type we can fall back to OCI.
|
|
// This does have a special fallback for schema1 manifests just because it is easy to detect.
|
|
switch mfst.MediaType {
|
|
case schema2.MediaTypeManifest, ocispec.MediaTypeImageManifest:
|
|
if mfst.Manifests != nil || mfst.FSLayers != nil {
|
|
return "", fmt.Errorf(`media-type: %q should not have "manifests" or "fsLayers"`, mfst.MediaType)
|
|
}
|
|
return mfst.MediaType, nil
|
|
case manifestlist.MediaTypeManifestList, ocispec.MediaTypeImageIndex:
|
|
if mfst.Config != nil || mfst.Layers != nil || mfst.FSLayers != nil {
|
|
return "", fmt.Errorf(`media-type: %q should not have "config", "layers", or "fsLayers"`, mfst.MediaType)
|
|
}
|
|
return mfst.MediaType, nil
|
|
case schema1.MediaTypeManifest:
|
|
if os.Getenv("DOCKER_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE") == "" {
|
|
err := DeprecatedSchema1ImageError(nil)
|
|
log.G(context.TODO()).Warn(err.Error())
|
|
return "", err
|
|
}
|
|
if mfst.Manifests != nil || mfst.Layers != nil {
|
|
return "", fmt.Errorf(`media-type: %q should not have "manifests" or "layers"`, mfst.MediaType)
|
|
}
|
|
return mfst.MediaType, nil
|
|
default:
|
|
if mfst.MediaType != "" {
|
|
return mfst.MediaType, nil
|
|
}
|
|
}
|
|
switch {
|
|
case mfst.FSLayers != nil && mfst.Manifests == nil && mfst.Layers == nil && mfst.Config == nil:
|
|
if os.Getenv("DOCKER_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE") == "" {
|
|
err := DeprecatedSchema1ImageError(nil)
|
|
log.G(context.TODO()).Warn(err.Error())
|
|
return "", err
|
|
}
|
|
return schema1.MediaTypeManifest, nil
|
|
case mfst.Config != nil && mfst.Manifests == nil && mfst.FSLayers == nil,
|
|
mfst.Layers != nil && mfst.Manifests == nil && mfst.FSLayers == nil:
|
|
return ocispec.MediaTypeImageManifest, nil
|
|
case mfst.Config == nil && mfst.Layers == nil && mfst.FSLayers == nil:
|
|
// fallback to index
|
|
return ocispec.MediaTypeImageIndex, nil
|
|
}
|
|
return "", errors.New("media-type: cannot determine")
|
|
}
|