dotcloud/docker

View on GitHub
distribution/manifest.go

Summary

Maintainability
B
4 hrs
Test Coverage
package distribution

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "os"
    "strings"

    "github.com/containerd/containerd/content"
    "github.com/containerd/containerd/remotes"
    cerrdefs "github.com/containerd/errdefs"
    "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")
}