package container import ( "context" "errors" "fmt" "io" "log" "runtime" "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/distribution/reference" "github.com/genuinetools/reg/registry" "github.com/genuinetools/reg/repoutils" digest "github.com/opencontainers/go-digest" ) func pull(ctx context.Context, dst distribution.Namespace, name reference.Named, tag string) error { // Get the auth config. auth, err := repoutils.GetAuthConfig("", "", reference.Domain(name)) if err != nil { return err } // TODO: add flag to flip switch for turning off SSL verification // Create a new registry client. src, err := registry.New(auth, registry.Opt{}) if err != nil { return fmt.Errorf("creating new registry api client failed: %v", err) } fmt.Println("pulling", name.String()) imgPath := reference.Path(name) // Get the manifest. manifest, err := src.Manifest(imgPath, tag) if err != nil { return fmt.Errorf("getting manifest for '%s:%s' failed: %v", imgPath, tag, err) } switch v := manifest.(type) { case *schema1.SignedManifest: return pullV1() case *schema2.DeserializedManifest: return pullV2(ctx, dst, src, v, name, imgPath, tag) case *manifestlist.DeserializedManifestList: return pullManifestList(ctx, dst, src, v, name, imgPath, tag) } return errors.New("unsupported manifest format") } func pullV1() error { return errors.New("schema1 manifest not supported") } func pullV2(ctx context.Context, dst distribution.Namespace, src *registry.Registry, manifest *schema2.DeserializedManifest, name reference.Named, imgPath, tag string) error { dstRepo, err := dst.Repository(ctx, name) if err != nil { return fmt.Errorf("creating the destination repository failed: %v", err) } dstBlobStore := dstRepo.Blobs(ctx) for _, ref := range manifest.References() { // TODO: make a progress bar fmt.Printf("pulling layer %s\n", ref.Digest.String()) blob, err := src.DownloadLayer(imgPath, ref.Digest) if err != nil { return fmt.Errorf("getting remote blob %q failed failed: %v", ref.Digest.String(), err) } upload, err := dstBlobStore.Create(ctx) if err != nil { return fmt.Errorf("creating the local blob writer failed: %v", err) } if _, err := io.Copy(upload, blob); err != nil { return fmt.Errorf("writing to the local blob failed: %v", err) } if _, err := upload.Commit(ctx, ref); err != nil { return fmt.Errorf("commiting %q locally failed: %v", ref.Digest.String(), err) } upload.Close() } // Create the manifest service locally. dms, err := dstRepo.Manifests(ctx) if err != nil { return fmt.Errorf("creating manifest service locally failed: %v", err) } // Put the manifest locally. manDst, err := dms.Put(ctx, manifest, distribution.WithTag(tag)) if err != nil { return fmt.Errorf("putting the manifest with tag %q locally failed: %v", tag, err) } // TODO: find a better way to get the manifest descriptor locally. // Get the manifest descriptor. mf, err := dms.Get(ctx, manDst) if err != nil { return fmt.Errorf("getting the manifest with digest %q locally failed: %v", manDst.String(), err) } mediatype, pl, err := mf.Payload() if err != nil { return fmt.Errorf("payload failed: %v", err) } _, desc, err := distribution.UnmarshalManifest(mediatype, pl) if err != nil { return fmt.Errorf("umarshal failed: %v", err) } // Put the tag locally. if err := dstRepo.Tags(ctx).Tag(ctx, tag, desc); err != nil { return fmt.Errorf("establishing a relationship between the tag %q and digest %q locally failed: %v", tag, manDst.String(), err) } return nil } func pullManifestList(ctx context.Context, dst distribution.Namespace, src *registry.Registry, mfstList *manifestlist.DeserializedManifestList, name reference.Named, imgPath, tag string) error { if _, err := schema2ManifestDigest(name, mfstList); err != nil { return err } log.Printf("%s resolved to a manifestList object with %d entries; looking for a %s/%s match", name, len(mfstList.Manifests), runtime.GOOS, runtime.GOARCH) manifestMatches := filterManifests(mfstList.Manifests, runtime.GOOS) if len(manifestMatches) == 0 { return fmt.Errorf("no matching manifest for %s/%s in the manifest list entries", runtime.GOOS, runtime.GOARCH) } if len(manifestMatches) > 1 { log.Printf("found multiple matches in manifest list, choosing best match %s", manifestMatches[0].Digest.String()) } manifestDigest := manifestMatches[0].Digest // Get the manifest. manifest, err := src.Manifest(imgPath, manifestDigest.String()) if err != nil { return fmt.Errorf("getting manifest for %s@%s failed: %v", imgPath, manifestDigest.String(), err) } switch v := manifest.(type) { case *schema1.SignedManifest: return pullV1() case *schema2.DeserializedManifest: return pullV2(ctx, dst, src, v, name, imgPath, tag) } return errors.New("unsupported manifest format") } // schema2ManifestDigest computes the manifest digest, and, if pulling by // digest, ensures that it matches the requested digest. func schema2ManifestDigest(ref reference.Named, mfst distribution.Manifest) (digest.Digest, error) { _, canonical, err := mfst.Payload() if err != nil { return "", err } // If pull by digest, then verify the manifest digest. if digested, isDigested := ref.(reference.Canonical); isDigested { verifier := digested.Digest().Verifier() if _, err := verifier.Write(canonical); err != nil { return "", err } if !verifier.Verified() { return "", fmt.Errorf("manifest verification failed for digest %s", digested.Digest()) } return digested.Digest(), nil } return digest.FromBytes(canonical), nil } func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []manifestlist.ManifestDescriptor { var matches []manifestlist.ManifestDescriptor for _, manifestDescriptor := range manifests { if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == os { matches = append(matches, manifestDescriptor) log.Printf("found match for %s/%s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.MediaType, manifestDescriptor.Digest.String()) } } return matches }