func (ms *manifests) Delete(ctx context.Context, dgst digest.Digest) error { ref, err := reference.WithDigest(ms.name, dgst) if err != nil { return err } u, err := ms.ub.BuildManifestURL(ref) if err != nil { return err } req, err := http.NewRequest("DELETE", u, nil) if err != nil { return err } resp, err := ms.client.Do(req) if err != nil { return err } defer resp.Body.Close() if SuccessStatus(resp.StatusCode) { return nil } return HandleErrorResponse(resp) }
// WithDigest combines the name from "name" and the digest from "digest" to form // a reference incorporating both the name and the digest. func WithDigest(name Named, digest digest.Digest) (Canonical, error) { r, err := distreference.WithDigest(name, digest) if err != nil { return nil, err } return &canonicalRef{namedRef{r}}, nil }
func (b *bridge) createManifestEvent(action string, repo reference.Named, sm distribution.Manifest) (*Event, error) { event := b.createEvent(action) event.Target.Repository = repo.Name() mt, p, err := sm.Payload() if err != nil { return nil, err } // Ensure we have the canonical manifest descriptor here _, desc, err := distribution.UnmarshalManifest(mt, p) if err != nil { return nil, err } event.Target.MediaType = mt event.Target.Length = desc.Size event.Target.Size = desc.Size event.Target.Digest = desc.Digest ref, err := reference.WithDigest(repo, event.Target.Digest) if err != nil { return nil, err } event.Target.URL, err = b.ub.BuildManifestURL(ref) if err != nil { return nil, err } return event, nil }
func (cli *DockerCli) trustedReference(ref reference.NamedTagged) (reference.Canonical, error) { repoInfo, err := registry.ParseRepositoryInfo(ref) if err != nil { return nil, err } // Resolve the Auth config relevant for this server authConfig := registry.ResolveAuthConfig(cli.configFile.AuthConfigs, repoInfo.Index) notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig) if err != nil { fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err) return nil, err } t, err := notaryRepo.GetTargetByName(ref.Tag()) if err != nil { return nil, err } r, err := convertTarget(*t) if err != nil { return nil, err } return reference.WithDigest(ref, r.digest) }
func (bs *blobStatter) Clear(ctx context.Context, dgst digest.Digest) error { ref, err := reference.WithDigest(bs.name, dgst) if err != nil { return err } blobURL, err := bs.ub.BuildBlobURL(ref) if err != nil { return err } req, err := http.NewRequest("DELETE", blobURL, nil) if err != nil { return err } resp, err := bs.client.Do(req) if err != nil { return err } defer resp.Body.Close() if SuccessStatus(resp.StatusCode) { return nil } return HandleErrorResponse(resp) }
func digestRef(ref reference.Named, digst string) (reference.Canonical, error) { rn, err := reference.ParseNamed(ref.Name()) if err != nil { return nil, err } d := digest.Digest(digst) return reference.WithDigest(rn, d) }
func (p *v2Puller) pullV2Repository(ref reference.Named) (err error) { var refs []reference.Named taggedName := p.repoInfo.LocalName if tagged, isTagged := ref.(reference.Tagged); isTagged { taggedName, err = reference.WithTag(p.repoInfo.LocalName, tagged.Tag()) if err != nil { return err } refs = []reference.Named{taggedName} } else if digested, isDigested := ref.(reference.Digested); isDigested { taggedName, err = reference.WithDigest(p.repoInfo.LocalName, digested.Digest()) if err != nil { return err } refs = []reference.Named{taggedName} } else { manSvc, err := p.repo.Manifests(context.Background()) if err != nil { return err } tags, err := manSvc.Tags() if err != nil { return err } // This probably becomes a lot nicer after the manifest // refactor... for _, tag := range tags { tagRef, err := reference.WithTag(p.repoInfo.LocalName, tag) if err != nil { return err } refs = append(refs, tagRef) } } var layersDownloaded bool for _, pullRef := range refs { // pulledNew is true if either new layers were downloaded OR if existing images were newly tagged // TODO(tiborvass): should we change the name of `layersDownload`? What about message in WriteStatus? pulledNew, err := p.pullV2Tag(p.config.OutStream, pullRef) if err != nil { return err } layersDownloaded = layersDownloaded || pulledNew } writeStatus(taggedName.String(), p.config.OutStream, p.sf, layersDownloaded) return nil }
// writeBlobCreatedHeaders writes the standard headers describing a newly // created blob. A 201 Created is written as well as the canonical URL and // blob digest. func (buh *blobUploadHandler) writeBlobCreatedHeaders(w http.ResponseWriter, desc distribution.Descriptor) error { ref, err := reference.WithDigest(buh.Repository.Named(), desc.Digest) if err != nil { return err } blobURL, err := buh.urlBuilder.BuildBlobURL(ref) if err != nil { return err } w.Header().Set("Location", blobURL) w.Header().Set("Content-Length", "0") w.Header().Set("Docker-Content-Digest", desc.Digest.String()) w.WriteHeader(http.StatusCreated) return nil }
func (bs *mockBlobDescriptorService) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { statter, ok := bs.BlobDescriptorService.(*linkedBlobStatter) if !ok { return distribution.Descriptor{}, fmt.Errorf("unexpected blob descriptor service: %T", bs.BlobDescriptorService) } name := statter.repository.Named() canonical, err := reference.WithDigest(name, dgst) if err != nil { return distribution.Descriptor{}, fmt.Errorf("failed to make canonical reference: %v", err) } bs.stats[canonical.String()]++ bs.t.Logf("calling Stat on %s", canonical.String()) return bs.BlobDescriptorService.Stat(ctx, dgst) }
// NormalizeLocalReference transforms a reference to use a normalized LocalName // for the name poriton. Passes through the reference without transformation on // error. func NormalizeLocalReference(ref reference.Named) reference.Named { localName := NormalizeLocalName(ref) if tagged, isTagged := ref.(reference.Tagged); isTagged { newRef, err := reference.WithTag(localName, tagged.Tag()) if err != nil { return ref } return newRef } else if digested, isDigested := ref.(reference.Digested); isDigested { newRef, err := reference.WithDigest(localName, digested.Digest()) if err != nil { return ref } return newRef } return localName }
func (pms proxyManifestStore) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { // At this point `dgst` was either specified explicitly, or returned by the // tagstore with the most recent association. var fromRemote bool manifest, err := pms.localManifests.Get(ctx, dgst, options...) if err != nil { if err := pms.authChallenger.tryEstablishChallenges(ctx); err != nil { return nil, err } manifest, err = pms.remoteManifests.Get(ctx, dgst, options...) if err != nil { return nil, err } fromRemote = true } _, payload, err := manifest.Payload() if err != nil { return nil, err } proxyMetrics.ManifestPush(uint64(len(payload))) if fromRemote { proxyMetrics.ManifestPull(uint64(len(payload))) _, err = pms.localManifests.Put(ctx, manifest) if err != nil { return nil, err } // Schedule the manifest blob for removal repoBlob, err := reference.WithDigest(pms.repositoryName, dgst) if err != nil { context.GetLogger(ctx).Errorf("Error creating reference: %s", err) return nil, err } pms.scheduler.AddManifest(repoBlob, repositoryTTL) // Ensure the manifest blob is cleaned up //pms.scheduler.AddBlob(blobRef, repositoryTTL) } return manifest, err }
func (pbs *proxyBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error { served, err := pbs.serveLocal(ctx, w, r, dgst) if err != nil { context.GetLogger(ctx).Errorf("Error serving blob from local storage: %s", err.Error()) return err } if served { return nil } if err := pbs.authChallenger.tryEstablishChallenges(ctx); err != nil { return err } mu.Lock() _, ok := inflight[dgst] if ok { mu.Unlock() _, err := pbs.copyContent(ctx, dgst, w) return err } inflight[dgst] = struct{}{} mu.Unlock() go func(dgst digest.Digest) { if err := pbs.storeLocal(ctx, dgst); err != nil { context.GetLogger(ctx).Errorf("Error committing to storage: %s", err.Error()) } blobRef, err := reference.WithDigest(pbs.repositoryName, dgst) if err != nil { context.GetLogger(ctx).Errorf("Error creating reference: %s", err) return } pbs.scheduler.AddBlob(blobRef, repositoryTTL) }(dgst) _, err = pbs.copyContent(ctx, dgst, w) if err != nil { return err } return nil }
// mountBlob attempts to mount a blob from another repository by its digest. If // successful, the blob is linked into the blob store and 201 Created is // returned with the canonical url of the blob. func (buh *blobUploadHandler) createBlobMountOption(fromRepo, mountDigest string) (distribution.BlobCreateOption, error) { dgst, err := digest.ParseDigest(mountDigest) if err != nil { return nil, err } ref, err := reference.ParseNamed(fromRepo) if err != nil { return nil, err } canonical, err := reference.WithDigest(ref, dgst) if err != nil { return nil, err } return storage.WithMountFrom(canonical), nil }
func (b *bridge) createBlobEvent(action string, repo reference.Named, desc distribution.Descriptor) (*Event, error) { event := b.createEvent(action) event.Target.Descriptor = desc event.Target.Length = desc.Size event.Target.Repository = repo.Name() ref, err := reference.WithDigest(repo, desc.Digest) if err != nil { return nil, err } event.Target.URL, err = b.ub.BuildBlobURL(ref) if err != nil { return nil, err } return event, nil }
func (bs *blobs) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) { ref, err := reference.WithDigest(bs.name, dgst) if err != nil { return nil, err } blobURL, err := bs.ub.BuildBlobURL(ref) if err != nil { return nil, err } return transport.NewHTTPReadSeeker(bs.client, blobURL, func(resp *http.Response) error { if resp.StatusCode == http.StatusNotFound { return distribution.ErrBlobUnknown } return HandleErrorResponse(resp) }), nil }
func checkCommonManifest(t *testing.T, action string, events ...Event) { checkCommon(t, events...) event := events[0] if event.Action != action { t.Fatalf("unexpected event action: %q != %q", event.Action, action) } repoRef, _ := reference.ParseNamed(repo) ref, _ := reference.WithDigest(repoRef, dgst) u, err := ub.BuildManifestURL(ref) if err != nil { t.Fatalf("error building expected url: %v", err) } if event.Target.URL != u { t.Fatalf("incorrect url passed: \n%q != \n%q", event.Target.URL, u) } }
func updateName(rn reference.Named, name string) (ref reference.Named, err error) { nr, err := reference.ParseNamed(name) if err != nil { return } //log.Printf("updateName: %#v %#v", rn, nr) switch r := rn.(type) { default: return nil, fmt.Errorf("Image name has neither tag nor digest (%T)", rn) case reference.Digested: ref, err = reference.WithDigest(nr, r.Digest()) case reference.Tagged: ref, err = reference.WithTag(nr, r.Tag()) case reference.Named: ref = nr } return }
func (r *registry) getBlob(ctx context.Context, name reference.Named, dgst digest.Digest) ([]byte, error) { ref, err := reference.WithDigest(name, dgst) if err != nil { return nil, err } blobURL, err := r.ub.BuildBlobURL(ref) if err != nil { return nil, err } reader := transport.NewHTTPReadSeeker(r.client, blobURL, func(resp *http.Response) error { if resp.StatusCode == http.StatusNotFound { return distribution.ErrBlobUnknown } return client.HandleErrorResponse(resp) }) defer reader.Close() return ioutil.ReadAll(reader) }
// imageWithDigestString takes an image such as name or name:tag // and returns the image pinned to a digest, such as name@sha256:34234... // Due to the difference between the docker/docker/reference, and the // docker/distribution/reference packages, we're parsing the image twice. // As the two packages converge, this function should be simplified. // TODO(nishanttotla): After the packages converge, the function must // convert distreference.Named -> distreference.Canonical, and the logic simplified. func (c *Cluster) imageWithDigestString(ctx context.Context, image string, authConfig *apitypes.AuthConfig) (string, error) { if _, err := digest.ParseDigest(image); err == nil { return "", errors.New("image reference is an image ID") } ref, err := distreference.ParseNamed(image) if err != nil { return "", err } // only query registry if not a canonical reference (i.e. with digest) if _, ok := ref.(distreference.Canonical); !ok { // create a docker/docker/reference Named object because GetRepository needs it dockerRef, err := reference.ParseNamed(image) if err != nil { return "", err } dockerRef = reference.WithDefaultTag(dockerRef) namedTaggedRef, ok := dockerRef.(reference.NamedTagged) if !ok { return "", errors.New("unable to cast image to NamedTagged reference object") } repo, _, err := c.config.Backend.GetRepository(ctx, namedTaggedRef, authConfig) if err != nil { return "", err } dscrptr, err := repo.Tags(ctx).Get(ctx, namedTaggedRef.Tag()) if err != nil { return "", err } namedDigestedRef, err := distreference.WithDigest(distreference.EnsureTagged(ref), dscrptr.Digest) if err != nil { return "", err } return namedDigestedRef.String(), nil } // reference already contains a digest, so just return it return ref.String(), nil }
func (ms *manifests) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { ref, err := reference.WithDigest(ms.name, dgst) if err != nil { return false, err } u, err := ms.ub.BuildManifestURL(ref) if err != nil { return false, err } resp, err := ms.client.Head(u) if err != nil { return false, err } if SuccessStatus(resp.StatusCode) { return true, nil } else if resp.StatusCode == http.StatusNotFound { return false, nil } return false, HandleErrorResponse(resp) }
func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { ref, err := reference.WithDigest(bs.name, dgst) if err != nil { return distribution.Descriptor{}, err } u, err := bs.ub.BuildBlobURL(ref) if err != nil { return distribution.Descriptor{}, err } resp, err := bs.client.Head(u) if err != nil { return distribution.Descriptor{}, err } defer resp.Body.Close() if SuccessStatus(resp.StatusCode) { lengthHeader := resp.Header.Get("Content-Length") if lengthHeader == "" { return distribution.Descriptor{}, fmt.Errorf("missing content-length header for request: %s", u) } length, err := strconv.ParseInt(lengthHeader, 10, 64) if err != nil { return distribution.Descriptor{}, fmt.Errorf("error parsing content-length: %v", err) } return distribution.Descriptor{ MediaType: resp.Header.Get("Content-Type"), Size: length, Digest: dgst, }, nil } else if resp.StatusCode == http.StatusNotFound { return distribution.Descriptor{}, distribution.ErrBlobUnknown } return distribution.Descriptor{}, HandleErrorResponse(resp) }
func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (distreference.Canonical, error) { repoInfo, err := registry.ParseRepositoryInfo(ref) if err != nil { return nil, err } authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") if err != nil { return nil, errors.Wrap(err, "error establishing connection to trust repository") } t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { return nil, trust.NotaryError(repoInfo.FullName(), err) } // Only get the tag if it's in the top level targets role or the releases delegation role // ignore it if it's in any other delegation roles if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { return nil, trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String())) } logrus.Debugf("retrieving target for %s role\n", t.Role) h, ok := t.Hashes["sha256"] if !ok { return nil, errors.New("no valid hash, expecting sha256") } dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h)) // Using distribution reference package to make sure that adding a // digest does not erase the tag. When the two reference packages // are unified, this will no longer be an issue. return distreference.WithDigest(ref, dgst) }
func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.Output) (distribution.Descriptor, error) { if fs, ok := pd.layer.(distribution.Describable); ok { if d := fs.Descriptor(); len(d.URLs) > 0 { progress.Update(progressOutput, pd.ID(), "Skipped foreign layer") return d, nil } } diffID := pd.DiffID() pd.pushState.Lock() if descriptor, ok := pd.pushState.remoteLayers[diffID]; ok { // it is already known that the push is not needed and // therefore doing a stat is unnecessary pd.pushState.Unlock() progress.Update(progressOutput, pd.ID(), "Layer already exists") return descriptor, nil } pd.pushState.Unlock() // Do we have any metadata associated with this layer's DiffID? v2Metadata, err := pd.v2MetadataService.GetMetadata(diffID) if err == nil { descriptor, exists, err := layerAlreadyExists(ctx, v2Metadata, pd.repoInfo, pd.repo, pd.pushState) if err != nil { progress.Update(progressOutput, pd.ID(), "Image push failed") return distribution.Descriptor{}, retryOnError(err) } if exists { progress.Update(progressOutput, pd.ID(), "Layer already exists") pd.pushState.Lock() pd.pushState.remoteLayers[diffID] = descriptor pd.pushState.Unlock() return descriptor, nil } } logrus.Debugf("Pushing layer: %s", diffID) // if digest was empty or not saved, or if blob does not exist on the remote repository, // then push the blob. bs := pd.repo.Blobs(ctx) var layerUpload distribution.BlobWriter mountAttemptsRemaining := 3 // Attempt to find another repository in the same registry to mount the layer // from to avoid an unnecessary upload. // Note: metadata is stored from oldest to newest, so we iterate through this // slice in reverse to maximize our chances of the blob still existing in the // remote repository. for i := len(v2Metadata) - 1; i >= 0 && mountAttemptsRemaining > 0; i-- { mountFrom := v2Metadata[i] sourceRepo, err := reference.ParseNamed(mountFrom.SourceRepository) if err != nil { continue } if pd.repoInfo.Hostname() != sourceRepo.Hostname() { // don't mount blobs from another registry continue } namedRef, err := reference.WithName(mountFrom.SourceRepository) if err != nil { continue } // TODO (brianbland): We need to construct a reference where the Name is // only the full remote name, so clean this up when distribution has a // richer reference package remoteRef, err := distreference.WithName(namedRef.RemoteName()) if err != nil { continue } canonicalRef, err := distreference.WithDigest(remoteRef, mountFrom.Digest) if err != nil { continue } logrus.Debugf("attempting to mount layer %s (%s) from %s", diffID, mountFrom.Digest, sourceRepo.FullName()) layerUpload, err = bs.Create(ctx, client.WithMountFrom(canonicalRef)) switch err := err.(type) { case distribution.ErrBlobMounted: progress.Updatef(progressOutput, pd.ID(), "Mounted from %s", err.From.Name()) err.Descriptor.MediaType = schema2.MediaTypeLayer pd.pushState.Lock() pd.pushState.confirmedV2 = true pd.pushState.remoteLayers[diffID] = err.Descriptor pd.pushState.Unlock() // Cache mapping from this layer's DiffID to the blobsum if err := pd.v2MetadataService.Add(diffID, metadata.V2Metadata{Digest: mountFrom.Digest, SourceRepository: pd.repoInfo.FullName()}); err != nil { return distribution.Descriptor{}, xfer.DoNotRetry{Err: err} } return err.Descriptor, nil case nil: // blob upload session created successfully, so begin the upload mountAttemptsRemaining = 0 default: // unable to mount layer from this repository, so this source mapping is no longer valid logrus.Debugf("unassociating layer %s (%s) with %s", diffID, mountFrom.Digest, mountFrom.SourceRepository) pd.v2MetadataService.Remove(mountFrom) mountAttemptsRemaining-- } } if layerUpload == nil { layerUpload, err = bs.Create(ctx) if err != nil { return distribution.Descriptor{}, retryOnError(err) } } defer layerUpload.Close() arch, err := pd.layer.TarStream() if err != nil { return distribution.Descriptor{}, xfer.DoNotRetry{Err: err} } // don't care if this fails; best effort size, _ := pd.layer.DiffSize() reader := progress.NewProgressReader(ioutils.NewCancelReadCloser(ctx, arch), progressOutput, size, pd.ID(), "Pushing") compressedReader, compressionDone := compress(reader) defer func() { reader.Close() <-compressionDone }() digester := digest.Canonical.New() tee := io.TeeReader(compressedReader, digester.Hash()) nn, err := layerUpload.ReadFrom(tee) compressedReader.Close() if err != nil { return distribution.Descriptor{}, retryOnError(err) } pushDigest := digester.Digest() if _, err := layerUpload.Commit(ctx, distribution.Descriptor{Digest: pushDigest}); err != nil { return distribution.Descriptor{}, retryOnError(err) } logrus.Debugf("uploaded layer %s (%s), %d bytes", diffID, pushDigest, nn) progress.Update(progressOutput, pd.ID(), "Pushed") // Cache mapping from this layer's DiffID to the blobsum if err := pd.v2MetadataService.Add(diffID, metadata.V2Metadata{Digest: pushDigest, SourceRepository: pd.repoInfo.FullName()}); err != nil { return distribution.Descriptor{}, xfer.DoNotRetry{Err: err} } pd.pushState.Lock() // If Commit succeeded, that's an indication that the remote registry // speaks the v2 protocol. pd.pushState.confirmedV2 = true descriptor := distribution.Descriptor{ Digest: pushDigest, MediaType: schema2.MediaTypeLayer, Size: nn, } pd.pushState.remoteLayers[diffID] = descriptor pd.pushState.Unlock() return descriptor, nil }
// PutImageManifest validates and stores an image in the registry. func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) { ctxu.GetLogger(imh).Debug("PutImageManifest") manifests, err := imh.Repository.Manifests(imh) if err != nil { imh.Errors = append(imh.Errors, err) return } var jsonBuf bytes.Buffer if err := copyFullPayload(w, r, &jsonBuf, imh, "image manifest PUT", &imh.Errors); err != nil { // copyFullPayload reports the error if necessary return } mediaType := r.Header.Get("Content-Type") manifest, desc, err := distribution.UnmarshalManifest(mediaType, jsonBuf.Bytes()) if err != nil { imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) return } if imh.Digest != "" { if desc.Digest != imh.Digest { ctxu.GetLogger(imh).Errorf("payload digest does match: %q != %q", desc.Digest, imh.Digest) imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid) return } } else if imh.Tag != "" { imh.Digest = desc.Digest } else { imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail("no tag or digest specified")) return } var options []distribution.ManifestServiceOption if imh.Tag != "" { options = append(options, distribution.WithTag(imh.Tag)) } _, err = manifests.Put(imh, manifest, options...) if err != nil { // TODO(stevvooe): These error handling switches really need to be // handled by an app global mapper. if err == distribution.ErrUnsupported { imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported) return } if err == distribution.ErrAccessDenied { imh.Errors = append(imh.Errors, errcode.ErrorCodeDenied) return } switch err := err.(type) { case distribution.ErrManifestVerification: for _, verificationError := range err { switch verificationError := verificationError.(type) { case distribution.ErrManifestBlobUnknown: imh.Errors = append(imh.Errors, v2.ErrorCodeManifestBlobUnknown.WithDetail(verificationError.Digest)) case distribution.ErrManifestNameInvalid: imh.Errors = append(imh.Errors, v2.ErrorCodeNameInvalid.WithDetail(err)) case distribution.ErrManifestUnverified: imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnverified) default: if verificationError == digest.ErrDigestInvalidFormat { imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid) } else { imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown, verificationError) } } } default: imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) } return } // Tag this manifest if imh.Tag != "" { tags := imh.Repository.Tags(imh) err = tags.Tag(imh, imh.Tag, desc) if err != nil { imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } } // Construct a canonical url for the uploaded manifest. ref, err := reference.WithDigest(imh.Repository.Named(), imh.Digest) if err != nil { imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } location, err := imh.urlBuilder.BuildManifestURL(ref) if err != nil { // NOTE(stevvooe): Given the behavior above, this absurdly unlikely to // happen. We'll log the error here but proceed as if it worked. Worst // case, we set an empty location header. ctxu.GetLogger(imh).Errorf("error building manifest url from digest: %v", err) } w.Header().Set("Location", location) w.Header().Set("Docker-Content-Digest", imh.Digest.String()) w.WriteHeader(http.StatusCreated) }
func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.Output) (distribution.Descriptor, error) { if fs, ok := pd.layer.(distribution.Describable); ok { if d := fs.Descriptor(); len(d.URLs) > 0 { progress.Update(progressOutput, pd.ID(), "Skipped foreign layer") return d, nil } } diffID := pd.DiffID() pd.pushState.Lock() if descriptor, ok := pd.pushState.remoteLayers[diffID]; ok { // it is already known that the push is not needed and // therefore doing a stat is unnecessary pd.pushState.Unlock() progress.Update(progressOutput, pd.ID(), "Layer already exists") return descriptor, nil } pd.pushState.Unlock() maxMountAttempts, maxExistenceChecks, checkOtherRepositories := getMaxMountAndExistenceCheckAttempts(pd.layer) // Do we have any metadata associated with this layer's DiffID? v2Metadata, err := pd.v2MetadataService.GetMetadata(diffID) if err == nil { // check for blob existence in the target repository if we have a mapping with it descriptor, exists, err := pd.layerAlreadyExists(ctx, progressOutput, diffID, false, 1, v2Metadata) if exists || err != nil { return descriptor, err } } // if digest was empty or not saved, or if blob does not exist on the remote repository, // then push the blob. bs := pd.repo.Blobs(ctx) var layerUpload distribution.BlobWriter // Attempt to find another repository in the same registry to mount the layer from to avoid an unnecessary upload candidates := getRepositoryMountCandidates(pd.repoInfo, pd.hmacKey, maxMountAttempts, v2Metadata) for _, mountCandidate := range candidates { logrus.Debugf("attempting to mount layer %s (%s) from %s", diffID, mountCandidate.Digest, mountCandidate.SourceRepository) createOpts := []distribution.BlobCreateOption{} if len(mountCandidate.SourceRepository) > 0 { namedRef, err := reference.WithName(mountCandidate.SourceRepository) if err != nil { logrus.Errorf("failed to parse source repository reference %v: %v", namedRef.String(), err) pd.v2MetadataService.Remove(mountCandidate) continue } // TODO (brianbland): We need to construct a reference where the Name is // only the full remote name, so clean this up when distribution has a // richer reference package remoteRef, err := distreference.WithName(namedRef.RemoteName()) if err != nil { logrus.Errorf("failed to make remote reference out of %q: %v", namedRef.RemoteName(), namedRef.RemoteName()) continue } canonicalRef, err := distreference.WithDigest(distreference.TrimNamed(remoteRef), mountCandidate.Digest) if err != nil { logrus.Errorf("failed to make canonical reference: %v", err) continue } createOpts = append(createOpts, client.WithMountFrom(canonicalRef)) } // send the layer lu, err := bs.Create(ctx, createOpts...) switch err := err.(type) { case nil: // noop case distribution.ErrBlobMounted: progress.Updatef(progressOutput, pd.ID(), "Mounted from %s", err.From.Name()) err.Descriptor.MediaType = schema2.MediaTypeLayer pd.pushState.Lock() pd.pushState.confirmedV2 = true pd.pushState.remoteLayers[diffID] = err.Descriptor pd.pushState.Unlock() // Cache mapping from this layer's DiffID to the blobsum if err := pd.v2MetadataService.TagAndAdd(diffID, pd.hmacKey, metadata.V2Metadata{ Digest: err.Descriptor.Digest, SourceRepository: pd.repoInfo.FullName(), }); err != nil { return distribution.Descriptor{}, xfer.DoNotRetry{Err: err} } return err.Descriptor, nil default: logrus.Infof("failed to mount layer %s (%s) from %s: %v", diffID, mountCandidate.Digest, mountCandidate.SourceRepository, err) } if len(mountCandidate.SourceRepository) > 0 && (metadata.CheckV2MetadataHMAC(&mountCandidate, pd.hmacKey) || len(mountCandidate.HMAC) == 0) { cause := "blob mount failure" if err != nil { cause = fmt.Sprintf("an error: %v", err.Error()) } logrus.Debugf("removing association between layer %s and %s due to %s", mountCandidate.Digest, mountCandidate.SourceRepository, cause) pd.v2MetadataService.Remove(mountCandidate) } if lu != nil { // cancel previous upload cancelLayerUpload(ctx, mountCandidate.Digest, layerUpload) layerUpload = lu } } if maxExistenceChecks-len(pd.checkedDigests) > 0 { // do additional layer existence checks with other known digests if any descriptor, exists, err := pd.layerAlreadyExists(ctx, progressOutput, diffID, checkOtherRepositories, maxExistenceChecks-len(pd.checkedDigests), v2Metadata) if exists || err != nil { return descriptor, err } } logrus.Debugf("Pushing layer: %s", diffID) if layerUpload == nil { layerUpload, err = bs.Create(ctx) if err != nil { return distribution.Descriptor{}, retryOnError(err) } } defer layerUpload.Close() // upload the blob desc, err := pd.uploadUsingSession(ctx, progressOutput, diffID, layerUpload) if err != nil { return desc, err } return desc, nil }
func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.Output) error { diffID := pd.DiffID() pd.pushState.Lock() if _, ok := pd.pushState.remoteLayers[diffID]; ok { // it is already known that the push is not needed and // therefore doing a stat is unnecessary pd.pushState.Unlock() progress.Update(progressOutput, pd.ID(), "Layer already exists") return nil } pd.pushState.Unlock() // Do we have any metadata associated with this layer's DiffID? v2Metadata, err := pd.v2MetadataService.GetMetadata(diffID) if err == nil { descriptor, exists, err := layerAlreadyExists(ctx, v2Metadata, pd.repoInfo, pd.repo, pd.pushState) if err != nil { progress.Update(progressOutput, pd.ID(), "Image push failed") return retryOnError(err) } if exists { progress.Update(progressOutput, pd.ID(), "Layer already exists") pd.pushState.Lock() pd.pushState.remoteLayers[diffID] = descriptor pd.pushState.Unlock() return nil } } logrus.Debugf("Pushing layer: %s", diffID) // if digest was empty or not saved, or if blob does not exist on the remote repository, // then push the blob. bs := pd.repo.Blobs(ctx) var mountFrom metadata.V2Metadata // Attempt to find another repository in the same registry to mount the layer from to avoid an unnecessary upload for _, metadata := range v2Metadata { sourceRepo, err := reference.ParseNamed(metadata.SourceRepository) if err != nil { continue } if pd.repoInfo.Hostname() == sourceRepo.Hostname() { logrus.Debugf("attempting to mount layer %s (%s) from %s", diffID, metadata.Digest, sourceRepo.FullName()) mountFrom = metadata break } } var createOpts []distribution.BlobCreateOption if mountFrom.SourceRepository != "" { namedRef, err := reference.WithName(mountFrom.SourceRepository) if err != nil { return err } // TODO (brianbland): We need to construct a reference where the Name is // only the full remote name, so clean this up when distribution has a // richer reference package remoteRef, err := distreference.WithName(namedRef.RemoteName()) if err != nil { return err } canonicalRef, err := distreference.WithDigest(remoteRef, mountFrom.Digest) if err != nil { return err } createOpts = append(createOpts, client.WithMountFrom(canonicalRef)) } // Send the layer layerUpload, err := bs.Create(ctx, createOpts...) switch err := err.(type) { case distribution.ErrBlobMounted: progress.Updatef(progressOutput, pd.ID(), "Mounted from %s", err.From.Name()) err.Descriptor.MediaType = schema2.MediaTypeLayer pd.pushState.Lock() pd.pushState.confirmedV2 = true pd.pushState.remoteLayers[diffID] = err.Descriptor pd.pushState.Unlock() // Cache mapping from this layer's DiffID to the blobsum if err := pd.v2MetadataService.Add(diffID, metadata.V2Metadata{Digest: mountFrom.Digest, SourceRepository: pd.repoInfo.FullName()}); err != nil { return xfer.DoNotRetry{Err: err} } return nil } if mountFrom.SourceRepository != "" { // unable to mount layer from this repository, so this source mapping is no longer valid logrus.Debugf("unassociating layer %s (%s) with %s", diffID, mountFrom.Digest, mountFrom.SourceRepository) pd.v2MetadataService.Remove(mountFrom) } if err != nil { return retryOnError(err) } defer layerUpload.Close() arch, err := pd.layer.TarStream() if err != nil { return xfer.DoNotRetry{Err: err} } // don't care if this fails; best effort size, _ := pd.layer.DiffSize() reader := progress.NewProgressReader(ioutils.NewCancelReadCloser(ctx, arch), progressOutput, size, pd.ID(), "Pushing") compressedReader, compressionDone := compress(reader) defer func() { reader.Close() <-compressionDone }() digester := digest.Canonical.New() tee := io.TeeReader(compressedReader, digester.Hash()) nn, err := layerUpload.ReadFrom(tee) compressedReader.Close() if err != nil { return retryOnError(err) } pushDigest := digester.Digest() if _, err := layerUpload.Commit(ctx, distribution.Descriptor{Digest: pushDigest}); err != nil { return retryOnError(err) } logrus.Debugf("uploaded layer %s (%s), %d bytes", diffID, pushDigest, nn) progress.Update(progressOutput, pd.ID(), "Pushed") // Cache mapping from this layer's DiffID to the blobsum if err := pd.v2MetadataService.Add(diffID, metadata.V2Metadata{Digest: pushDigest, SourceRepository: pd.repoInfo.FullName()}); err != nil { return xfer.DoNotRetry{Err: err} } pd.pushState.Lock() // If Commit succeded, that's an indication that the remote registry // speaks the v2 protocol. pd.pushState.confirmedV2 = true pd.pushState.remoteLayers[diffID] = distribution.Descriptor{ Digest: pushDigest, MediaType: schema2.MediaTypeLayer, Size: nn, } pd.pushState.Unlock() return nil }
// Put puts a manifest. A tag can be specified using an options parameter which uses some shared state to hold the // tag name in order to build the correct upload URL. func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { ref := ms.name var tagged bool for _, option := range options { if opt, ok := option.(distribution.WithTagOption); ok { var err error ref, err = reference.WithTag(ref, opt.Tag) if err != nil { return "", err } tagged = true } else { err := option.Apply(ms) if err != nil { return "", err } } } mediaType, p, err := m.Payload() if err != nil { return "", err } if !tagged { // generate a canonical digest and Put by digest _, d, err := distribution.UnmarshalManifest(mediaType, p) if err != nil { return "", err } ref, err = reference.WithDigest(ref, d.Digest) if err != nil { return "", err } } manifestURL, err := ms.ub.BuildManifestURL(ref) if err != nil { return "", err } putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(p)) if err != nil { return "", err } putRequest.Header.Set("Content-Type", mediaType) resp, err := ms.client.Do(putRequest) if err != nil { return "", err } defer resp.Body.Close() if SuccessStatus(resp.StatusCode) { dgstHeader := resp.Header.Get("Docker-Content-Digest") dgst, err := digest.ParseDigest(dgstHeader) if err != nil { return "", err } return dgst, nil } return "", HandleErrorResponse(resp) }
func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { var ( digestOrTag string ref reference.Named err error contentDgst *digest.Digest ) for _, option := range options { if opt, ok := option.(distribution.WithTagOption); ok { digestOrTag = opt.Tag ref, err = reference.WithTag(ms.name, opt.Tag) if err != nil { return nil, err } } else if opt, ok := option.(contentDigestOption); ok { contentDgst = opt.digest } else { err := option.Apply(ms) if err != nil { return nil, err } } } if digestOrTag == "" { digestOrTag = dgst.String() ref, err = reference.WithDigest(ms.name, dgst) if err != nil { return nil, err } } u, err := ms.ub.BuildManifestURL(ref) if err != nil { return nil, err } req, err := http.NewRequest("GET", u, nil) if err != nil { return nil, err } for _, t := range distribution.ManifestMediaTypes() { req.Header.Add("Accept", t) } if _, ok := ms.etags[digestOrTag]; ok { req.Header.Set("If-None-Match", ms.etags[digestOrTag]) } resp, err := ms.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode == http.StatusNotModified { return nil, distribution.ErrManifestNotModified } else if SuccessStatus(resp.StatusCode) { if contentDgst != nil { dgst, err := digest.ParseDigest(resp.Header.Get("Docker-Content-Digest")) if err == nil { *contentDgst = dgst } } mt := resp.Header.Get("Content-Type") body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } m, _, err := distribution.UnmarshalManifest(mt, body) if err != nil { return nil, err } return m, nil } return nil, HandleErrorResponse(resp) }
func TestLinkedBlobStoreCreateWithMountFrom(t *testing.T) { fooRepoName, _ := reference.ParseNamed("nm/foo") fooEnv := newManifestStoreTestEnv(t, fooRepoName, "thetag") ctx := context.Background() // Build up some test layers and add them to the manifest, saving the // readseekers for upload later. testLayers := map[digest.Digest]io.ReadSeeker{} for i := 0; i < 2; i++ { rs, ds, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("unexpected error generating test layer file") } dgst := digest.Digest(ds) testLayers[digest.Digest(dgst)] = rs } // upload the layers to foo/bar for dgst, rs := range testLayers { wr, err := fooEnv.repository.Blobs(fooEnv.ctx).Create(fooEnv.ctx) if err != nil { t.Fatalf("unexpected error creating test upload: %v", err) } if _, err := io.Copy(wr, rs); err != nil { t.Fatalf("unexpected error copying to upload: %v", err) } if _, err := wr.Commit(fooEnv.ctx, distribution.Descriptor{Digest: dgst}); err != nil { t.Fatalf("unexpected error finishing upload: %v", err) } } // create another repository nm/bar barRepoName, _ := reference.ParseNamed("nm/bar") barRepo, err := fooEnv.registry.Repository(ctx, barRepoName) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } // cross-repo mount the test layers into a nm/bar for dgst := range testLayers { fooCanonical, _ := reference.WithDigest(fooRepoName, dgst) option := WithMountFrom(fooCanonical) // ensure we can instrospect it createOpts := distribution.CreateOptions{} if err := option.Apply(&createOpts); err != nil { t.Fatalf("failed to apply MountFrom option: %v", err) } if !createOpts.Mount.ShouldMount || createOpts.Mount.From.String() != fooCanonical.String() { t.Fatalf("unexpected create options: %#+v", createOpts.Mount) } _, err := barRepo.Blobs(ctx).Create(ctx, WithMountFrom(fooCanonical)) if err == nil { t.Fatalf("unexpected non-error while mounting from %q: %v", fooRepoName.String(), err) } if _, ok := err.(distribution.ErrBlobMounted); !ok { t.Fatalf("expected ErrMountFrom error, not %T: %v", err, err) } } }
func TestBlobMount(t *testing.T) { dgst, content := newRandomBlob(1024) var m testutil.RequestResponseMap repo, _ := reference.ParseNamed("test.example.com/uploadrepo") sourceRepo, _ := reference.ParseNamed("test.example.com/sourcerepo") canonicalRef, _ := reference.WithDigest(sourceRepo, dgst) m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "POST", Route: "/v2/" + repo.Name() + "/blobs/uploads/", QueryParams: map[string][]string{"from": {sourceRepo.Name()}, "mount": {dgst.String()}}, }, Response: testutil.Response{ StatusCode: http.StatusCreated, Headers: http.Header(map[string][]string{ "Content-Length": {"0"}, "Location": {"/v2/" + repo.Name() + "/blobs/" + dgst.String()}, "Docker-Content-Digest": {dgst.String()}, }), }, }) m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "HEAD", Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), }, Response: testutil.Response{ StatusCode: http.StatusOK, Headers: http.Header(map[string][]string{ "Content-Length": {fmt.Sprint(len(content))}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, }), }, }) e, c := testServer(m) defer c() ctx := context.Background() r, err := NewRepository(ctx, repo, e, nil) if err != nil { t.Fatal(err) } l := r.Blobs(ctx) bw, err := l.Create(ctx, WithMountFrom(canonicalRef)) if bw != nil { t.Fatalf("Expected blob writer to be nil, was %v", bw) } if ebm, ok := err.(distribution.ErrBlobMounted); ok { if ebm.From.Digest() != dgst { t.Fatalf("Unexpected digest: %s, expected %s", ebm.From.Digest(), dgst) } if ebm.From.Name() != sourceRepo.Name() { t.Fatalf("Unexpected from: %s, expected %s", ebm.From.Name(), sourceRepo) } } else { t.Fatalf("Unexpected error: %v, expected an ErrBlobMounted", err) } }