diff --git a/Gopkg.lock b/Gopkg.lock index b3909a61..26a2391b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -781,7 +781,7 @@ version = "v3.0.0" [[projects]] - digest = "1:f47d5892e50c2937477813f188fba877953faa98e2846497f842211ae3b52670" + digest = "1:87bef79892c094091d2ce4e1d9244e79660baf2f839ab9f075f82ed19a006263" name = "github.com/pivotal/image-relocation" packages = [ "pkg/image", @@ -789,7 +789,8 @@ "pkg/registry", ] pruneopts = "NUT" - revision = "532dd0b42e7a50010d7868364309cd314a2bb376" + revision = "3c9c32bb3d97fc213476f1c9846a99ca35ecba5d" + version = "v0.1" [[projects]] digest = "1:14715f705ff5dfe0ffd6571d7d201dd8e921030f8070321a79380d8ca4ec1a24" diff --git a/Gopkg.toml b/Gopkg.toml index 3a2c96cc..7e432a0f 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -57,7 +57,7 @@ [[constraint]] name = "github.com/pivotal/image-relocation" - revision = "532dd0b42e7a50010d7868364309cd314a2bb376" + version = "v0.1" [[constraint]] name = "github.com/deislabs/cnab-go" diff --git a/cmd/duffle/export.go b/cmd/duffle/export.go index 101451a6..6e677253 100644 --- a/cmd/duffle/export.go +++ b/cmd/duffle/export.go @@ -5,6 +5,7 @@ import ( "io" "github.com/deislabs/duffle/pkg/duffle/home" + "github.com/deislabs/duffle/pkg/imagestore/construction" "github.com/deislabs/duffle/pkg/loader" "github.com/deislabs/duffle/pkg/packager" @@ -76,12 +77,12 @@ func (ex *exportCmd) run() error { } func (ex *exportCmd) Export(bundlefile string, l loader.BundleLoader) error { - is, err := packager.NewImageStore(ex.thin) + ctor, err := construction.NewConstructor(ex.thin) if err != nil { return err } - exp, err := packager.NewExporter(bundlefile, ex.dest, ex.home.Logs(), l, is) + exp, err := packager.NewExporter(bundlefile, ex.dest, ex.home.Logs(), l, ctor) if err != nil { return fmt.Errorf("Unable to set up exporter: %s", err) } @@ -89,7 +90,7 @@ func (ex *exportCmd) Export(bundlefile string, l loader.BundleLoader) error { return err } if ex.verbose { - fmt.Fprintf(ex.out, "Export logs: %s\n", exp.Logs) + fmt.Fprintf(ex.out, "Export logs: %s\n", exp.Logs()) } return nil } diff --git a/cmd/duffle/relocate.go b/cmd/duffle/relocate.go index cf0b83fd..4325361f 100644 --- a/cmd/duffle/relocate.go +++ b/cmd/duffle/relocate.go @@ -5,12 +5,19 @@ import ( "fmt" "io" "io/ioutil" + "os" + "path/filepath" "strconv" "strings" + "github.com/deislabs/duffle/pkg/imagestore" + "github.com/deislabs/duffle/pkg/imagestore/construction" + "github.com/deislabs/duffle/pkg/loader" + "github.com/deislabs/duffle/pkg/packager" + "github.com/deislabs/duffle/pkg/relocator" + "github.com/pivotal/image-relocation/pkg/image" "github.com/pivotal/image-relocation/pkg/pathmapping" - "github.com/pivotal/image-relocation/pkg/registry" "github.com/deislabs/cnab-go/bundle" @@ -51,8 +58,9 @@ type relocateCmd struct { out io.Writer // dependencies - mapping pathmapping.PathMapping - registryClient registry.Client + mapping pathmapping.PathMapping + imageStoreConstructor imagestore.Constructor + imageStore imagestore.Store } func newRelocateCmd(w io.Writer) *cobra.Command { @@ -81,7 +89,7 @@ duffle relocate path/to/bundle.json --relocation-mapping path/to/relmap.json --r relocate.home = home.Home(homePath()) relocate.mapping = pathmapping.FlattenRepoPathPreserveTagDigest - relocate.registryClient = registry.NewRegistryClient() + relocate.imageStoreConstructor = construction.NewLocatingConstructor() return relocate.run() }, @@ -98,92 +106,88 @@ duffle relocate path/to/bundle.json --relocation-mapping path/to/relmap.json --r } func (r *relocateCmd) run() error { - bun, err := r.setup() + relMap := make(map[string]string) + + rel, cleanup, err := r.setup() if err != nil { return err } + defer cleanup() - return r.relocate(bun) + if err := rel.Relocate(relMap); err != nil { + return err + } + + return r.writeRelocationMapping(relMap) } -func (r *relocateCmd) relocate(bun *bundle.Bundle) error { - relMap := make(map[string]string) - for i := range bun.InvocationImages { - ii := bun.InvocationImages[i] - modified, err := r.relocateImage(&ii.BaseImage, relMap) - if err != nil { - return err - } - if modified { - bun.InvocationImages[i] = ii - } +// The caller is responsible for running the returned cleanup function, which may delete the returned bundle. +func (r *relocateCmd) setup() (*relocator.Relocator, func(), error) { + nop := func() {} + dest := "" + bundleFile, err := resolveBundleFilePath(r.inputBundle, r.home.String(), r.bundleIsFile) + if err != nil { + return nil, nop, err } - for k := range bun.Images { - im := bun.Images[k] - modified, err := r.relocateImage(&im.BaseImage, relMap) + var bun *bundle.Bundle + + if strings.HasSuffix(bundleFile, ".tgz") { + var err error + bun, dest, err = unzipBundle(bundleFile) if err != nil { - return err + return nil, nop, err } - if modified { - bun.Images[k] = im + } else { + bun, err = loadBundle(bundleFile) + if err != nil { + return nil, nop, err } } - return r.writeRelocationMapping(relMap) -} - -func (r *relocateCmd) relocateImage(i *bundle.BaseImage, relMap map[string]string) (bool, error) { - if !isOCI(i.ImageType) && !isDocker(i.ImageType) { - return false, nil - } - // map the image name - n, err := image.NewName(i.Image) - if err != nil { - return false, err + if err = bun.Validate(); err != nil { + return nil, nop, err } - rn := r.mapping(r.repoPrefix, n) - // tag/push the image to its new repository - dig, err := r.registryClient.Copy(n, rn) + r.imageStore, err = r.imageStoreConstructor(imagestore.WithArchiveDir(dest)) if err != nil { - return false, err + return nil, nop, err } - if i.Digest != "" && dig.String() != i.Digest { - // should not happen - return false, fmt.Errorf("digest of image %s not preserved: old digest %s; new digest %s", i.Image, i.Digest, dig.String()) - } - - // update the relocation map - relMap[i.Image] = rn.String() - return true, nil -} + mapping := func(i image.Name) image.Name { + return pathmapping.FlattenRepoPathPreserveTagDigest(r.repoPrefix, i) + } -func isOCI(imageType string) bool { - return imageType == "" || imageType == "oci" -} + reloc, err := relocator.NewRelocator(bun, mapping, r.imageStore, r.out) + if err != nil { + return nil, nop, err + } -func isDocker(imageType string) bool { - return imageType == "docker" + return reloc, func() { os.RemoveAll(dest) }, nil } -func (r *relocateCmd) setup() (*bundle.Bundle, error) { - bundleFile, err := resolveBundleFilePath(r.inputBundle, r.home.String(), r.bundleIsFile) +func unzipBundle(bundleFile string) (*bundle.Bundle, string, error) { + source, err := filepath.Abs(bundleFile) if err != nil { - return nil, err + return nil, "", err } - bun, err := loadBundle(bundleFile) + dest, err := ioutil.TempDir("", "duffle-relocate-unzip") if err != nil { - return nil, err + return nil, "", err } - if err = bun.Validate(); err != nil { - return nil, err + l := loader.NewLoader() + imp, err := packager.NewImporter(source, dest, l, false) + if err != nil { + return nil, "", err + } + dest, bun, err := imp.Unzip() + if err != nil { + return nil, "", err } - return bun, nil + return bun, dest, nil } func (r *relocateCmd) writeRelocationMapping(relMap map[string]string) error { diff --git a/cmd/duffle/relocate_test.go b/cmd/duffle/relocate_test.go index 669456e3..00591cd1 100644 --- a/cmd/duffle/relocate_test.go +++ b/cmd/duffle/relocate_test.go @@ -2,46 +2,63 @@ package main import ( "encoding/json" - "fmt" + "errors" "io/ioutil" "os" "path" "path/filepath" "reflect" + "strings" "testing" "github.com/pivotal/image-relocation/pkg/image" - "github.com/pivotal/image-relocation/pkg/registry" "github.com/deislabs/duffle/pkg/duffle/home" + "github.com/deislabs/duffle/pkg/imagestore" + "github.com/deislabs/duffle/pkg/imagestore/imagestoremocks" ) const ( testRepositoryPrefix = "example.com/user" originalInvocationImageName = "technosophos/helloworld:0.1.0" - relocatedInvocationImageName = "example.com/user/technosophos/helloworld/relocated:0.1.0" - invocationImageDigest = "sha256:86959ecb500308ae523922eab84f2f94082f20b4e7bda84ce9be219f3f1b4e65" + relocatedInvocationImageName = "example.com/user/technosophos-helloworld-6731e0d41b7fd5a24e14c853af93bd81:0.1.0" originalImageNameA = "deislabs/duffle@sha256:4d41eeb38fb14266b7c0461ef1ef0b2f8c05f41cd544987a259a9d92cdad2540" - relocatedImageNameA = "example.com/user/deislabs/duffle/relocated@sha256:4d41eeb38fb14266b7c0461ef1ef0b2f8c05f41cd544987a259a9d92cdad2540" + relocatedImageNameA = "example.com/user/deislabs-duffle-50aa5cc4ebb040ac696a9753d1695298@sha256:4d41eeb38fb14266b7c0461ef1ef0b2f8c05f41cd544987a259a9d92cdad2540" imageDigestA = "sha256:4d41eeb38fb14266b7c0461ef1ef0b2f8c05f41cd544987a259a9d92cdad2540" - originalImageNameB = "deislabs/duffle:0.1.0-ralpha.5-englishrose" - relocatedImageNameB = "example.com/user/deislabs/duffle/relocated:0.1.0-ralpha.5-englishrose" - originalImageDigestB = "sha256:14d6134d892aeccb7e142557fe746ccd0a8f736a747c195ef04c9f3f0f0bbd49" - relocatedImageDigestB = "sha256:deadbeef892aeccb7e142557fe746ccd0a8f736a747c195ef04c9f3f0f0bbd49" + originalImageNameB = "deislabs/duffle:0.1.0-ralpha.5-englishrose" + relocatedImageNameB = "example.com/user/deislabs-duffle-50aa5cc4ebb040ac696a9753d1695298:0.1.0-ralpha.5-englishrose" + originalImageDigestB = "sha256:14d6134d892aeccb7e142557fe746ccd0a8f736a747c195ef04c9f3f0f0bbd49" ) -func TestRelocateFileToFilePreservingDigests(t *testing.T) { - relocateFileToFile(t, true, nil) +func TestRelocateFileToFileSupportedImageTypes(t *testing.T) { + relocateFileToFile(t, "testdata/relocate/bundle.json", nil, func(archiveDir string) { + if archiveDir != "" { + t.Fatalf("archiveDir was %q, expected %q", archiveDir, "") + } + }) } -func TestRelocateFileToFileMutatingDigests(t *testing.T) { - relocateFileToFile(t, false, fmt.Errorf("digest of image %s not preserved: old digest %s; new digest %s", originalImageNameB, originalImageDigestB, relocatedImageDigestB)) +func TestRelocateThickBundleToFileSupportedImageTypes(t *testing.T) { + relocateFileToFile(t, "testdata/relocate/testrelocate-0.1.tgz", nil, func(archiveDir string) { + expectedSuffix := "testrelocate-0.1" + if !strings.HasSuffix(archiveDir, expectedSuffix) { + t.Fatalf("archiveDir was %q, expected it to end with %q", archiveDir, expectedSuffix) + } + }) +} + +func TestRelocateFileToFileUnsupportedImageType(t *testing.T) { + relocateFileToFile(t, "testdata/relocate/bundle-with-unsupported-image-type.json", errors.New("cannot relocate image c with imageType c: only oci and docker image types are currently supported"), func(archiveDir string) { + if archiveDir != "" { + t.Fatalf("archiveDir was %q, expected %q", archiveDir, "") + } + }) } -func relocateFileToFile(t *testing.T, preserveDigest bool, expectedErr error) { +func relocateFileToFile(t *testing.T, bundle string, expectedErr error, archiveDirStub func(archiveDir string)) { duffleHome, err := ioutil.TempDir("", "dufflehome") if err != nil { @@ -57,8 +74,34 @@ func relocateFileToFile(t *testing.T, preserveDigest bool, expectedErr error) { relMapPath := filepath.Join(work, "relmap.json") + is := &imagestoremocks.MockStore{ + PushStub: func(dig image.Digest, src image.Name, dst image.Name) error { + type pair struct { + first string + second string + } + digests := map[string]pair{ + "docker.io/" + originalInvocationImageName: {"", relocatedInvocationImageName}, + "docker.io/" + originalImageNameA: {imageDigestA, relocatedImageNameA}, + "docker.io/" + originalImageNameB: {originalImageDigestB, relocatedImageNameB}, + } + exp, ok := digests[src.String()] + if !ok { + t.Fatalf("unexpected source image %v", src) + } + expectedDig, expectedDst := exp.first, exp.second + if dig.String() != expectedDig { + t.Fatalf("digest for source image %v was %s, expected %s", src, dig, expectedDig) + } + if dst.String() != expectedDst { + t.Fatalf("destination image for source image %v was %v, expected %s", src, dst, expectedDst) + } + return nil + }, + } + cmd := &relocateCmd{ - inputBundle: "testdata/relocate/bundle.json", + inputBundle: bundle, repoPrefix: testRepositoryPrefix, bundleIsFile: true, @@ -73,43 +116,9 @@ func relocateFileToFile(t *testing.T, preserveDigest bool, expectedErr error) { } return testMapping(originalImage, t) }, - registryClient: &mockRegClient{ - copyStub: func(source image.Name, target image.Name) (image.Digest, error) { - oiin, err := image.NewName(originalInvocationImageName) - if err != nil { - t.Fatal(err) - } - oinA, err := image.NewName(originalImageNameA) - if err != nil { - t.Fatal(err) - } - oinB, err := image.NewName(originalImageNameB) - if err != nil { - t.Fatal(err) - } - switch source { - case oiin: - if target.String() == relocatedInvocationImageName { - return image.NewDigest(invocationImageDigest) - } - case oinA: - if target.String() == relocatedImageNameA { - return image.NewDigest(imageDigestA) - } - case oinB: - if target.String() == relocatedImageNameB { - if preserveDigest { - return image.NewDigest(originalImageDigestB) - } - // check behaviour if digest is modified, even though this is not normally expected - return image.NewDigest(relocatedImageDigestB) - } - default: - t.Fatalf("unexpected source %v", source) - } - t.Fatalf("unexpected mapping from %v to %v", source, target) - return image.EmptyDigest, nil // unreachable - }, + imageStoreConstructor: func(option ...imagestore.Option) (store imagestore.Store, e error) { + archiveDirStub(imagestore.Create(option...).ArchiveDir) + return is, nil }, } @@ -166,19 +175,3 @@ func testMapping(originalImage image.Name, t *testing.T) image.Name { } return rn } - -type mockRegClient struct { - copyStub func(source image.Name, target image.Name) (image.Digest, error) - digestStub func(n image.Name) (image.Digest, error) - newLayoutStub func(path string) (registry.Layout, error) - readLayoutStub func(path string) (registry.Layout, error) -} - -func (r *mockRegClient) Digest(n image.Name) (image.Digest, error) { return r.digestStub(n) } -func (r *mockRegClient) Copy(src image.Name, tgt image.Name) (image.Digest, error) { - return r.copyStub(src, tgt) -} -func (r *mockRegClient) NewLayout(path string) (registry.Layout, error) { return r.newLayoutStub(path) } -func (r *mockRegClient) ReadLayout(path string) (registry.Layout, error) { - return r.readLayoutStub(path) -} diff --git a/cmd/duffle/testdata/relocate/bundle-with-unsupported-image-type.json b/cmd/duffle/testdata/relocate/bundle-with-unsupported-image-type.json new file mode 100644 index 00000000..0fd5385e --- /dev/null +++ b/cmd/duffle/testdata/relocate/bundle-with-unsupported-image-type.json @@ -0,0 +1,31 @@ +{ + "name": "testrelocate", + "version": "0.1", + "schemaVersion": "v1.0.0-WD", + "description": "a bundle with images", + "invocationImages": [ + { + "image": "technosophos/helloworld:0.1.0", + "imageType": "docker" + } + ], + "images": { + "a": { + "description": "digested oci", + "image": "deislabs/duffle@sha256:4d41eeb38fb14266b7c0461ef1ef0b2f8c05f41cd544987a259a9d92cdad2540", + "digest": "sha256:4d41eeb38fb14266b7c0461ef1ef0b2f8c05f41cd544987a259a9d92cdad2540", + "imageType": "oci" + }, + "b": { + "description": "tagged docker", + "image": "deislabs/duffle:0.1.0-ralpha.5-englishrose", + "digest": "sha256:14d6134d892aeccb7e142557fe746ccd0a8f736a747c195ef04c9f3f0f0bbd49", + "imageType": "docker" + }, + "c": { + "description": "neither oci nor docker", + "image": "c", + "imageType": "c" + } + } +} diff --git a/cmd/duffle/testdata/relocate/bundle.json b/cmd/duffle/testdata/relocate/bundle.json index 33c5557b..7dc69a14 100644 --- a/cmd/duffle/testdata/relocate/bundle.json +++ b/cmd/duffle/testdata/relocate/bundle.json @@ -21,11 +21,6 @@ "image": "deislabs/duffle:0.1.0-ralpha.5-englishrose", "digest": "sha256:14d6134d892aeccb7e142557fe746ccd0a8f736a747c195ef04c9f3f0f0bbd49", "imageType": "docker" - }, - "c": { - "description": "neither oci nor docker", - "image": "c", - "imageType": "c" } } } \ No newline at end of file diff --git a/cmd/duffle/testdata/relocate/testrelocate-0.1.tgz b/cmd/duffle/testdata/relocate/testrelocate-0.1.tgz new file mode 100644 index 00000000..0319ce75 Binary files /dev/null and b/cmd/duffle/testdata/relocate/testrelocate-0.1.tgz differ diff --git a/pkg/imagestore/construction/construction.go b/pkg/imagestore/construction/construction.go new file mode 100644 index 00000000..07cc881d --- /dev/null +++ b/pkg/imagestore/construction/construction.go @@ -0,0 +1,43 @@ +package construction + +import ( + "os" + "path/filepath" + + "github.com/deislabs/duffle/pkg/imagestore" + "github.com/deislabs/duffle/pkg/imagestore/ocilayout" + "github.com/deislabs/duffle/pkg/imagestore/remote" +) + +// NewConstructor creates an image store constructor which will, if necessary, create archive contents. +func NewConstructor(remoteRepos bool) (imagestore.Constructor, error) { + // infer the concrete type of the image store from the input parameters + if remoteRepos { + return remote.Create, nil + } + return ocilayout.Create, nil +} + +// NewLocatingConstructor creates an image store constructor which will, if necessary, find existing archive contents. +func NewLocatingConstructor() imagestore.Constructor { + return func(options ...imagestore.Option) (imagestore.Store, error) { + parms := imagestore.Create(options...) + if thin(parms.ArchiveDir) { + return remote.Create() + } + return ocilayout.LocateOciLayout(parms.ArchiveDir) + } +} + +func thin(archiveDir string) bool { + // If there is no archive directory, the bundle is thin + if archiveDir == "" { + return true + } + + // If there is an archive directory, the bundle is thin if and only if the archive directory has no artifacts/ + // subdirectory + layoutDir := filepath.Join(archiveDir, "artifacts") + _, err := os.Stat(layoutDir) + return os.IsNotExist(err) +} diff --git a/pkg/imagestore/imagestoremocks/store.go b/pkg/imagestore/imagestoremocks/store.go new file mode 100644 index 00000000..75bdd3d9 --- /dev/null +++ b/pkg/imagestore/imagestoremocks/store.go @@ -0,0 +1,16 @@ +package imagestoremocks + +import "github.com/pivotal/image-relocation/pkg/image" + +type MockStore struct { + AddStub func(im string) (string, error) + PushStub func(image.Digest, image.Name, image.Name) error +} + +func (i *MockStore) Add(im string) (string, error) { + return i.AddStub(im) +} + +func (i *MockStore) Push(dig image.Digest, src image.Name, dst image.Name) error { + return i.PushStub(dig, src, dst) +} diff --git a/pkg/imagestore/ocilayout/ocilayout.go b/pkg/imagestore/ocilayout/ocilayout.go new file mode 100644 index 00000000..3b065600 --- /dev/null +++ b/pkg/imagestore/ocilayout/ocilayout.go @@ -0,0 +1,79 @@ +package ocilayout + +import ( + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/pivotal/image-relocation/pkg/image" + "github.com/pivotal/image-relocation/pkg/registry" + + "github.com/deislabs/duffle/pkg/imagestore" +) + +// ociLayout is an image store which stores images as an OCI image layout. +type ociLayout struct { + layout registry.Layout + logs io.Writer +} + +func Create(options ...imagestore.Option) (imagestore.Store, error) { + parms := imagestore.Create(options...) + + layoutDir := filepath.Join(parms.ArchiveDir, "artifacts", "layout") + if err := os.MkdirAll(layoutDir, 0755); err != nil { + return nil, err + } + + layout, err := registry.NewRegistryClient().NewLayout(layoutDir) + if err != nil { + return nil, err + } + + return &ociLayout{ + layout: layout, + logs: parms.Logs, + }, nil +} + +func LocateOciLayout(archiveDir string) (imagestore.Store, error) { + layoutDir := filepath.Join(archiveDir, "artifacts", "layout") + if _, err := os.Stat(layoutDir); os.IsNotExist(err) { + return nil, err + } + layout, err := registry.NewRegistryClient().ReadLayout(layoutDir) + if err != nil { + return nil, err + } + + return &ociLayout{ + layout: layout, + logs: ioutil.Discard, + }, nil +} + +func (o *ociLayout) Add(im string) (string, error) { + n, err := image.NewName(im) + if err != nil { + return "", err + } + + dig, err := o.layout.Add(n) + if err != nil { + return "", err + } + + return dig.String(), nil +} + +func (o *ociLayout) Push(dig image.Digest, src image.Name, dst image.Name) error { + if dig == image.EmptyDigest { + var err error + dig, err = o.layout.Find(src) + if err != nil { + return err + } + } + return o.layout.Push(dig, dst) +} diff --git a/pkg/imagestore/remote/remote.go b/pkg/imagestore/remote/remote.go new file mode 100644 index 00000000..a5c8bc4a --- /dev/null +++ b/pkg/imagestore/remote/remote.go @@ -0,0 +1,37 @@ +package remote + +import ( + "fmt" + + "github.com/pivotal/image-relocation/pkg/image" + "github.com/pivotal/image-relocation/pkg/registry" + + "github.com/deislabs/duffle/pkg/imagestore" +) + +// remote is an image store which does not actually store images. It is used to represent thin bundles. +type remote struct { + registryClient registry.Client +} + +func Create(...imagestore.Option) (imagestore.Store, error) { + return &remote{ + registryClient: registry.NewRegistryClient(), + }, nil +} + +func (r *remote) Add(im string) (string, error) { + return "", nil +} + +func (r *remote) Push(d image.Digest, src image.Name, dst image.Name) error { + dig, _, err := r.registryClient.Copy(src, dst) + if err != nil { + return err + } + + if d != image.EmptyDigest && dig != d { + return fmt.Errorf("digest of image %s not preserved: old digest %s; new digest %s", src, d.String(), dig.String()) + } + return nil +} diff --git a/pkg/imagestore/store.go b/pkg/imagestore/store.go new file mode 100644 index 00000000..b6fd897c --- /dev/null +++ b/pkg/imagestore/store.go @@ -0,0 +1,60 @@ +package imagestore + +import ( + "io" + "io/ioutil" + + "github.com/pivotal/image-relocation/pkg/image" +) + +// Store is an abstract image store. +type Store interface { + // Add copies the image with the given name to the image store. + Add(img string) (contentDigest string, err error) + + // Push copies the image with the given digest from an image with the given name in the image store to a repository + // with the given name. + Push(dig image.Digest, src image.Name, dst image.Name) error +} + +// Constructor is a function which creates an images store based on parameters represented as options +type Constructor func(...Option) (Store, error) + +// Parameters is used to create image stores. +type Parameters struct { + ArchiveDir string + Logs io.Writer +} + +// Options is a function which returns updated parameters. +type Option func(Parameters) Parameters + +func Create(options ...Option) Parameters { + b := Parameters{ + Logs: ioutil.Discard, + } + for _, op := range options { + b = op(b) + } + return b +} + +// WithArchiveDir return an option to set the archive directory parameter. +func WithArchiveDir(archiveDir string) Option { + return func(b Parameters) Parameters { + return Parameters{ + ArchiveDir: archiveDir, + Logs: b.Logs, + } + } +} + +// WithArchiveDir return an option to set the logs parameter. +func WithLogs(logs io.Writer) Option { + return func(b Parameters) Parameters { + return Parameters{ + ArchiveDir: b.ArchiveDir, + Logs: logs, + } + } +} diff --git a/pkg/packager/export.go b/pkg/packager/export.go index 7ac666bb..dc818aaf 100644 --- a/pkg/packager/export.go +++ b/pkg/packager/export.go @@ -8,6 +8,8 @@ import ( "path/filepath" "time" + "github.com/deislabs/duffle/pkg/imagestore" + "github.com/deislabs/cnab-go/bundle" "github.com/docker/docker/pkg/archive" @@ -15,41 +17,30 @@ import ( ) type Exporter struct { - Source string - Destination string - ImageStore ImageStore - Logs string - Loader loader.BundleLoader + source string + destination string + imageStoreConstructor imagestore.Constructor + imageStore imagestore.Store + logs string + loader loader.BundleLoader } // NewExporter returns an *Exporter given information about where a bundle // lives, where the compressed bundle should be exported to, // and what form a bundle should be exported in (thin or thick/full). It also // sets up a docker client to work with images. -func NewExporter(source, dest, logsDir string, l loader.BundleLoader, is ImageStore) (*Exporter, error) { +func NewExporter(source, dest, logsDir string, l loader.BundleLoader, c imagestore.Constructor) (*Exporter, error) { logs := filepath.Join(logsDir, "export-"+time.Now().Format("20060102150405")) return &Exporter{ - Source: source, - Destination: dest, - ImageStore: is, - Logs: logs, - Loader: l, + source: source, + destination: dest, + imageStoreConstructor: c, + logs: logs, + loader: l, }, nil } -type ImageStore interface { - configure(archiveDir string, logs io.Writer) error - add(img string) (contentDigest string, err error) -} - -func NewImageStore(thin bool) (ImageStore, error) { - if thin { - return newNop(), nil - } - return newOciLayout(), nil -} - // Export prepares an artifacts directory containing all of the necessary // images, packages the bundle along with the artifacts in a gzipped tar // file, and saves that file to the file path specified as destination. @@ -57,21 +48,21 @@ func NewImageStore(thin bool) (ImageStore, error) { // exist func (ex *Exporter) Export() error { //prepare log file for this export - logsf, err := os.Create(ex.Logs) + logsf, err := os.Create(ex.logs) if err != nil { return err } defer logsf.Close() - fi, err := os.Stat(ex.Source) + fi, err := os.Stat(ex.source) if os.IsNotExist(err) { return err } if fi.IsDir() { - return fmt.Errorf("Bundle manifest %s is a directory, should be a file", ex.Source) + return fmt.Errorf("Bundle manifest %s is a directory, should be a file", ex.source) } - bun, err := ex.Loader.Load(ex.Source) + bun, err := ex.loader.Load(ex.source) if err != nil { return fmt.Errorf("Error loading bundle: %s", err) } @@ -85,7 +76,7 @@ func (ex *Exporter) Export() error { } defer os.RemoveAll(archiveDir) - from, err := os.Open(ex.Source) + from, err := os.Open(ex.source) if err != nil { return err } @@ -103,16 +94,18 @@ func (ex *Exporter) Export() error { return err } - if err := ex.ImageStore.configure(archiveDir, logsf); err != nil { + ex.imageStore, err = ex.imageStoreConstructor(imagestore.WithArchiveDir(archiveDir), imagestore.WithLogs(logsf)) + if err != nil { return fmt.Errorf("Error creating artifacts: %s", err) } + if err := ex.prepareArtifacts(bun); err != nil { return fmt.Errorf("Error preparing artifacts: %s", err) } dest := name + ".tgz" - if ex.Destination != "" { - dest = ex.Destination + if ex.destination != "" { + dest = ex.destination } writer, err := os.Create(dest) @@ -157,7 +150,7 @@ func (ex *Exporter) prepareArtifacts(bun *bundle.Bundle) error { // addImage pulls an image, adds it to the artifacts/ directory, and verifies its digest func (ex *Exporter) addImage(image bundle.BaseImage) error { - dig, err := ex.ImageStore.add(image.Image) + dig, err := ex.imageStore.Add(image.Image) if err != nil { return err } @@ -176,3 +169,7 @@ func checkDigest(image bundle.BaseImage, dig string) error { } return nil } + +func (ex *Exporter) Logs() string { + return ex.logs +} diff --git a/pkg/packager/export_test.go b/pkg/packager/export_test.go index 0766cb97..bd8f58bf 100644 --- a/pkg/packager/export_test.go +++ b/pkg/packager/export_test.go @@ -1,7 +1,6 @@ package packager import ( - "io" "io/ioutil" "os" "path/filepath" @@ -10,6 +9,9 @@ import ( "strings" "testing" + "github.com/deislabs/duffle/pkg/imagestore" + "github.com/deislabs/duffle/pkg/imagestore/imagestoremocks" + "github.com/deislabs/duffle/pkg/loader" ) @@ -28,37 +30,34 @@ func TestExport(t *testing.T) { os.RemoveAll(tempPWD) }() - configArchiveDir := "" imagesAdded := []string{} - is := &mockImageStore{ - configureStub: func(archiveDir string, logs io.Writer) error { - configArchiveDir = archiveDir - return nil - }, - addStub: func(im string) (string, error) { + is := &imagestoremocks.MockStore{ + AddStub: func(im string) (string, error) { imagesAdded = append(imagesAdded, im) return "", nil }, } ex := Exporter{ - Source: source, - ImageStore: is, - Logs: filepath.Join(tempDir, "export-logs"), - Loader: loader.NewLoader(), + source: source, + imageStoreConstructor: func(option ...imagestore.Option) (store imagestore.Store, e error) { + parms := imagestore.Create(option...) + const expectedPrefix = "examplebun-0.1.0" + configArchiveDirBase := filepath.Base(parms.ArchiveDir) + if !strings.HasPrefix(configArchiveDirBase, expectedPrefix) { + t.Errorf("expected archive ending in %s, got %s", expectedPrefix, configArchiveDirBase) + } + return is, nil + }, + logs: filepath.Join(tempDir, "export-logs"), + loader: loader.NewLoader(), } if err := ex.Export(); err != nil { t.Errorf("Expected no error, got error: %v", err) } - expectedPrefix := "examplebun-0.1.0" - configArchiveDirBase := filepath.Base(configArchiveDir) - if !strings.HasPrefix(configArchiveDirBase, expectedPrefix) { - t.Errorf("ImageStore.configure was passed an archive directory %s; expected prefix %s", configArchiveDirBase, expectedPrefix) - } - expectedImagesAdded := []string{"mock/examplebun:0.1.0", "mock/image-a:58326809e0p19b79054015bdd4e93e84b71ae1ta", "mock/image-b:88426103e0p19b38554015bd34e93e84b71de2fc"} sort.Strings(expectedImagesAdded) sort.Strings(imagesAdded) @@ -82,38 +81,35 @@ func TestExportCreatesFileProperly(t *testing.T) { } defer os.RemoveAll(tempDir) - configArchiveDir := "" imagesAdded := []string{} - is := &mockImageStore{ - configureStub: func(archiveDir string, logs io.Writer) error { - configArchiveDir = archiveDir - return nil - }, - addStub: func(im string) (string, error) { + is := &imagestoremocks.MockStore{ + AddStub: func(im string) (string, error) { imagesAdded = append(imagesAdded, im) return "", nil }, } ex := Exporter{ - Source: "testdata/examplebun/bundle.json", - Destination: filepath.Join(tempDir, "random-directory", "examplebun-whatev.tgz"), - ImageStore: is, - Logs: filepath.Join(tempDir, "export-logs"), - Loader: loader.NewLoader(), + source: "testdata/examplebun/bundle.json", + destination: filepath.Join(tempDir, "random-directory", "examplebun-whatev.tgz"), + imageStoreConstructor: func(option ...imagestore.Option) (store imagestore.Store, e error) { + parms := imagestore.Create(option...) + const expectedPrefix = "examplebun-0.1.0" + configArchiveDirBase := filepath.Base(parms.ArchiveDir) + if !strings.HasPrefix(configArchiveDirBase, expectedPrefix) { + t.Errorf("expected archive ending in %s, got %s", expectedPrefix, configArchiveDirBase) + } + return is, nil + }, + logs: filepath.Join(tempDir, "export-logs"), + loader: loader.NewLoader(), } if err := ex.Export(); err == nil { t.Error("Expected path does not exist error, got no error") } - expectedPrefix := "examplebun-0.1.0" - configArchiveDirBase := filepath.Base(configArchiveDir) - if !strings.HasPrefix(configArchiveDirBase, expectedPrefix) { - t.Errorf("ImageStore.configure was passed an archive directory %s; expected prefix %s", configArchiveDirBase, expectedPrefix) - } - expectedImagesAdded := []string{"mock/examplebun:0.1.0", "mock/image-a:58326809e0p19b79054015bdd4e93e84b71ae1ta", "mock/image-b:88426103e0p19b38554015bd34e93e84b71de2fc"} sort.Strings(expectedImagesAdded) sort.Strings(imagesAdded) @@ -153,11 +149,8 @@ func TestExportDigestMismatch(t *testing.T) { os.RemoveAll(tempPWD) }() - is := &mockImageStore{ - configureStub: func(archiveDir string, logs io.Writer) error { - return nil - }, - addStub: func(im string) (string, error) { + is := &imagestoremocks.MockStore{ + AddStub: func(im string) (string, error) { // return the same digest for all images, but only one of them has a digest in the bundle manifest so just // that one will fail verification return "sha256:222222228fb14266b7c0461ef1ef0b2f8c05f41cd544987a259a9d92cdad2540", nil @@ -165,10 +158,12 @@ func TestExportDigestMismatch(t *testing.T) { } ex := Exporter{ - Source: source, - ImageStore: is, - Logs: filepath.Join(tempDir, "export-logs"), - Loader: loader.NewLoader(), + source: source, + imageStoreConstructor: func(...imagestore.Option) (store imagestore.Store, e error) { + return is, nil + }, + logs: filepath.Join(tempDir, "export-logs"), + loader: loader.NewLoader(), } if err := ex.Export(); err.Error() != "Error preparing artifacts: content digest mismatch: image mock/image-a:"+ @@ -216,16 +211,3 @@ func setupExportTestEnvironment() (string, string, string, error) { return tempDir, tempPWD, pwd, nil } - -type mockImageStore struct { - configureStub func(archiveDir string, logs io.Writer) error - addStub func(im string) (string, error) -} - -func (i *mockImageStore) configure(archiveDir string, logs io.Writer) error { - return i.configureStub(archiveDir, logs) -} - -func (i *mockImageStore) add(im string) (string, error) { - return i.addStub(im) -} diff --git a/pkg/packager/import.go b/pkg/packager/import.go index 4bce992f..05707b5c 100644 --- a/pkg/packager/import.go +++ b/pkg/packager/import.go @@ -7,6 +7,8 @@ import ( "path/filepath" "strings" + "github.com/deislabs/cnab-go/bundle" + "github.com/docker/docker/pkg/archive" "github.com/deislabs/duffle/pkg/loader" @@ -41,15 +43,24 @@ func NewImporter(source, destination string, load loader.BundleLoader, verbose b // Import decompresses a bundle from Source (location of the compressed bundle) and properly places artifacts in the correct location(s) func (im *Importer) Import() error { + _, _, err := im.Unzip() + + // TODO: /~https://github.com/deislabs/duffle/issues/758 + + return err +} + +// Unzip decompresses a bundle from Source (location of the compressed bundle) and returns the path of the bundle and the bundle itself. +func (im *Importer) Unzip() (string, *bundle.Bundle, error) { baseDir := strings.TrimSuffix(filepath.Base(im.Source), ".tgz") dest := filepath.Join(im.Destination, baseDir) if err := os.MkdirAll(dest, 0755); err != nil { - return err + return "", nil, err } reader, err := os.Open(im.Source) if err != nil { - return err + return "", nil, err } defer reader.Close() @@ -61,7 +72,7 @@ func (im *Importer) Import() error { NoLchown: true, } if err := archive.Untar(reader, dest, tarOptions); err != nil { - return fmt.Errorf("untar failed: %s", err) + return "", nil, fmt.Errorf("untar failed: %s", err) } // We try to load a bundle.cnab file first, and fall back to a bundle.json @@ -70,16 +81,13 @@ func (im *Importer) Import() error { ext = "json" } - _, err = im.Loader.Load(filepath.Join(dest, "bundle."+ext)) + bun, err := im.Loader.Load(filepath.Join(dest, "bundle."+ext)) if err != nil { removeErr := os.RemoveAll(dest) if removeErr != nil { - return fmt.Errorf("failed to load and validate bundle.%s on import %s and failed to remove invalid bundle from filesystem %s", ext, err, removeErr) + return "", nil, fmt.Errorf("failed to load and validate bundle.%s on import %s and failed to remove invalid bundle from filesystem %s", ext, err, removeErr) } - return fmt.Errorf("failed to load and validate bundle.%s: %s", ext, err) + return "", nil, fmt.Errorf("failed to load and validate bundle.%s: %s", ext, err) } - - // TODO: /~https://github.com/deislabs/duffle/issues/758 - - return nil + return dest, bun, nil } diff --git a/pkg/packager/nop.go b/pkg/packager/nop.go deleted file mode 100644 index beb5c0b9..00000000 --- a/pkg/packager/nop.go +++ /dev/null @@ -1,20 +0,0 @@ -package packager - -import ( - "io" -) - -// nop is an ImageStore which does not store images. It is used to construct thin bundles. -type nop struct{} - -func newNop() nop { - return nop{} -} - -func (t nop) configure(archiveDir string, logs io.Writer) error { - return nil -} - -func (t nop) add(im string) (string, error) { - return "", nil -} diff --git a/pkg/packager/ocilayout.go b/pkg/packager/ocilayout.go deleted file mode 100644 index 85524049..00000000 --- a/pkg/packager/ocilayout.go +++ /dev/null @@ -1,54 +0,0 @@ -package packager - -import ( - "io" - "os" - "path/filepath" - - "github.com/pivotal/image-relocation/pkg/image" - "github.com/pivotal/image-relocation/pkg/registry" -) - -// ociLayout is an ImageStore which stores images as an OCI image layout. -type ociLayout struct { - registryClient registry.Client - - layout registry.Layout - logs io.Writer -} - -func newOciLayout() *ociLayout { - return &ociLayout{ - registryClient: registry.NewRegistryClient(), - } -} - -func (t *ociLayout) configure(archiveDir string, logs io.Writer) error { - layoutDir := filepath.Join(archiveDir, "artifacts", "layout") - if err := os.MkdirAll(layoutDir, 0755); err != nil { - return err - } - - layout, err := t.registryClient.NewLayout(layoutDir) - if err != nil { - return err - } - - t.layout = layout - t.logs = logs - return nil -} - -func (t *ociLayout) add(im string) (string, error) { - n, err := image.NewName(im) - if err != nil { - return "", err - } - - dig, err := t.layout.Add(n) - if err != nil { - return "", err - } - - return dig.String(), nil -} diff --git a/pkg/relocator/relocator.go b/pkg/relocator/relocator.go new file mode 100644 index 00000000..1b8458ee --- /dev/null +++ b/pkg/relocator/relocator.go @@ -0,0 +1,89 @@ +package relocator + +import ( + _ "crypto/sha256" // ensure SHA-256 is loaded + "fmt" + "io" + + "github.com/deislabs/cnab-go/bundle" + "github.com/pivotal/image-relocation/pkg/image" + + "github.com/deislabs/duffle/pkg/imagestore" +) + +type Relocator struct { + bun *bundle.Bundle + mapping Mapping + imageStore imagestore.Store + out io.Writer +} + +type Mapping func(image.Name) image.Name + +func NewRelocator(bun *bundle.Bundle, mapping Mapping, is imagestore.Store, logs io.Writer) (*Relocator, error) { + return &Relocator{ + bun: bun, + mapping: mapping, + imageStore: is, + out: logs, + }, nil +} + +func (r *Relocator) Relocate(relMap map[string]string) error { + for i := range r.bun.InvocationImages { + ii := r.bun.InvocationImages[i] + err := r.relocateImage(&ii.BaseImage, relMap) + if err != nil { + return err + } + } + + for k := range r.bun.Images { + im := r.bun.Images[k] + err := r.relocateImage(&im.BaseImage, relMap) + if err != nil { + return err + } + } + + return nil +} + +func (r *Relocator) relocateImage(i *bundle.BaseImage, relMap map[string]string) error { + if !isOCI(i.ImageType) && !isDocker(i.ImageType) { + return fmt.Errorf("cannot relocate image %s with imageType %s: only oci and docker image types are currently supported", i.Image, i.ImageType) + } + // map the image name + n, err := image.NewName(i.Image) + if err != nil { + return err + } + rn := r.mapping(n) + + dig := n.Digest() + if dig == image.EmptyDigest && i.Digest != "" { + var err error + dig, err = image.NewDigest(i.Digest) + if err != nil { + return err + } + } + + fmt.Fprintf(r.out, "writing %s to %s\n", i.Image, rn.String()) + err = r.imageStore.Push(dig, n, rn) + if err != nil { + return err + } + + // update the relocation mapping + relMap[i.Image] = rn.String() + return nil +} + +func isOCI(imageType string) bool { + return imageType == "" || imageType == "oci" +} + +func isDocker(imageType string) bool { + return imageType == "docker" +} diff --git a/pkg/relocator/relocator_test.go b/pkg/relocator/relocator_test.go new file mode 100644 index 00000000..b8b73f2a --- /dev/null +++ b/pkg/relocator/relocator_test.go @@ -0,0 +1,222 @@ +package relocator_test + +import ( + "fmt" + "io/ioutil" + "reflect" + "testing" + + "github.com/pivotal/image-relocation/pkg/image" + + "github.com/deislabs/duffle/pkg/imagestore/imagestoremocks" + "github.com/deislabs/duffle/pkg/relocator" + + "github.com/deislabs/cnab-go/bundle" + + "github.com/deislabs/duffle/pkg/imagestore" +) + +func TestRelocator_Relocate(t *testing.T) { + const ( + expectedOriginalShortRef = "ubuntu" + expectedOriginalRef = "docker.io/library/ubuntu" + expectedMappedRef = "docker.io/library/ubuntu-mapped" + expectedDigest = "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + expectedOriginalShortDigestedRef = expectedOriginalShortRef + "@" + expectedDigest + expectedOriginalDigestedRef = expectedOriginalRef + "@" + expectedDigest + ) + + type fields struct { + bun *bundle.Bundle + mapping relocator.Mapping + imageStore imagestore.Store + } + tests := []struct { + name string + fields fields + wantErr bool + expectedRelMap map[string]string + }{ + { + "invocation image", + fields{ + &bundle.Bundle{ + InvocationImages: []bundle.InvocationImage{ + { + BaseImage: bundle.BaseImage{ + Image: expectedOriginalShortRef, + }, + }, + }, + }, + func(ref image.Name) image.Name { + mappedRef, err := image.NewName(fmt.Sprintf("%s/%s-mapped", ref.Host(), ref.Path())) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + return mappedRef + }, + &imagestoremocks.MockStore{ + PushStub: func(dig image.Digest, src image.Name, dst image.Name) error { + if dig != image.EmptyDigest { + t.Errorf("expected digest %s, got %s", image.EmptyDigest, dig) + } + if src.String() != expectedOriginalRef { + t.Errorf("expected source image %s, got %s", expectedOriginalRef, src.String()) + } + if dst.String() != expectedMappedRef { + t.Errorf("expected destination image %s, got %s", expectedMappedRef, dst.String()) + } + return nil + }, + }, + }, + false, + map[string]string{ + expectedOriginalShortRef: expectedMappedRef, + }, + }, + { + "image", + fields{ + &bundle.Bundle{ + Images: map[string]bundle.Image{ + "i1": { + BaseImage: bundle.BaseImage{ + Image: expectedOriginalShortRef, + ImageType: "docker", + }, + }, + }, + }, + func(ref image.Name) image.Name { + mappedRef, err := image.NewName(fmt.Sprintf("%s/%s-mapped", ref.Host(), ref.Path())) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + return mappedRef + }, + &imagestoremocks.MockStore{ + PushStub: func(dig image.Digest, src image.Name, dst image.Name) error { + if dig != image.EmptyDigest { + t.Errorf("expected digest %s, got %s", image.EmptyDigest, dig) + } + + if src.String() != expectedOriginalRef { + t.Errorf("expected source image %s, got %s", expectedOriginalRef, src.String()) + } + + if dst.String() != expectedMappedRef { + t.Errorf("expected destination image %s, got %s", expectedMappedRef, dst.String()) + } + return nil + }, + }, + }, + false, + map[string]string{ + expectedOriginalShortRef: expectedMappedRef, + }, + }, + { + "digested image", + fields{ + &bundle.Bundle{ + Images: map[string]bundle.Image{ + "i1": { + BaseImage: bundle.BaseImage{ + Image: expectedOriginalShortDigestedRef, + }, + }, + }, + }, + func(ref image.Name) image.Name { + mappedRef, err := image.NewName(fmt.Sprintf("%s/%s-mapped", ref.Host(), ref.Path())) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + return mappedRef + }, + &imagestoremocks.MockStore{ + PushStub: func(dig image.Digest, src image.Name, dst image.Name) error { + if dig.String() != expectedDigest { + t.Errorf("expected digest %s, got %s", expectedDigest, dig) + } + + if src.String() != expectedOriginalDigestedRef { + t.Errorf("expected source image %s, got %s", expectedOriginalDigestedRef, src.String()) + } + + if dst.String() != expectedMappedRef { + t.Errorf("expected destination image %s, got %s", expectedMappedRef, dst.String()) + } + return nil + }, + }, + }, + false, + map[string]string{ + expectedOriginalShortDigestedRef: expectedMappedRef, + }, + }, + { + "image with declared digest", + fields{ + &bundle.Bundle{ + Images: map[string]bundle.Image{ + "i1": { + BaseImage: bundle.BaseImage{ + Image: expectedOriginalShortRef, + Digest: expectedDigest, + }, + }, + }, + }, + func(ref image.Name) image.Name { + mappedRef, err := image.NewName(fmt.Sprintf("%s/%s-mapped", ref.Host(), ref.Path())) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + return mappedRef + }, + &imagestoremocks.MockStore{ + PushStub: func(dig image.Digest, src image.Name, dst image.Name) error { + if dig.String() != expectedDigest { + t.Errorf("expected digest %s, got %s", expectedDigest, dig) + } + + if src.String() != expectedOriginalRef { + t.Errorf("expected source image %s, got %s", expectedOriginalRef, src.String()) + } + + if dst.String() != expectedMappedRef { + t.Errorf("expected destination image %s, got %s", expectedMappedRef, dst.String()) + } + return nil + }, + }, + }, + false, + map[string]string{ + expectedOriginalShortRef: expectedMappedRef, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + relMap := make(map[string]string) + r, err := relocator.NewRelocator(tt.fields.bun, tt.fields.mapping, tt.fields.imageStore, ioutil.Discard) + if err != nil { + t.Fatalf("NewRelocate failed: %v", err) + } + if err := r.Relocate(relMap); (err != nil) != tt.wantErr { + t.Errorf("Relocator.Relocate() error = %v, wantErr %v", err, tt.wantErr) + } + + if !reflect.DeepEqual(relMap, tt.expectedRelMap) { + t.Errorf("output relocation mapping file has unexpected content: %v (expected %v)", + relMap, tt.expectedRelMap) + } + }) + } +} diff --git a/vendor/github.com/pivotal/image-relocation/pkg/image/name.go b/vendor/github.com/pivotal/image-relocation/pkg/image/name.go index 78505821..8170f84b 100644 --- a/vendor/github.com/pivotal/image-relocation/pkg/image/name.go +++ b/vendor/github.com/pivotal/image-relocation/pkg/image/name.go @@ -44,7 +44,10 @@ func init() { // NewName returns the Name for the given image reference or an error if the image reference is invalid. func NewName(i string) (Name, error) { ref, err := reference.ParseNormalizedNamed(i) - return Name{ref}, err + if err != nil { + return Name{}, fmt.Errorf("invalid image reference: %q", i) + } + return Name{ref}, nil } // Normalize returns a fully-qualified equivalent to the Name. Useful on synonyms. diff --git a/vendor/github.com/pivotal/image-relocation/pkg/registry/client.go b/vendor/github.com/pivotal/image-relocation/pkg/registry/client.go index 4517d8b4..7a1445f3 100644 --- a/vendor/github.com/pivotal/image-relocation/pkg/registry/client.go +++ b/vendor/github.com/pivotal/image-relocation/pkg/registry/client.go @@ -28,8 +28,9 @@ type Client interface { // Digest returns the digest of the given image or an error if the image does not exist or the digest is unavailable. Digest(image.Name) (image.Digest, error) - // Copy copies the given source image to the given target and returns the image's digest (which is preserved). - Copy(source image.Name, target image.Name) (image.Digest, error) + // Copy copies the given source image to the given target and returns the image's digest (which is preserved) and + // the size in bytes of the raw image manifest. + Copy(source image.Name, target image.Name) (image.Digest, int64, error) // NewLayout creates a Layout for the Client and creates a corresponding directory containing a new OCI image layout at // the given file system path. @@ -67,21 +68,31 @@ func (r *client) Digest(n image.Name) (image.Digest, error) { return image.NewDigest(hash.String()) } -func (r *client) Copy(source image.Name, target image.Name) (image.Digest, error) { +func (r *client) Copy(source image.Name, target image.Name) (image.Digest, int64, error) { img, err := r.readRemoteImage(source) if err != nil { - return image.EmptyDigest, fmt.Errorf("failed to read image %v: %v", source, err) + return image.EmptyDigest, 0, fmt.Errorf("failed to read image %v: %v", source, err) } hash, err := img.Digest() if err != nil { - return image.EmptyDigest, fmt.Errorf("failed to read digest of image %v: %v", source, err) + return image.EmptyDigest, 0, fmt.Errorf("failed to read digest of image %v: %v", source, err) } err = r.writeRemoteImage(img, target) if err != nil { - return image.EmptyDigest, fmt.Errorf("failed to write image %v: %v", target, err) + return image.EmptyDigest, 0, fmt.Errorf("failed to write image %v: %v", target, err) } - return image.NewDigest(hash.String()) + dig, err := image.NewDigest(hash.String()) + if err != nil { + return image.EmptyDigest, 0, err + } + + rawManifest, err := img.RawManifest() + if err != nil { + return image.EmptyDigest, 0, fmt.Errorf("failed to get raw manifest of image %v: %v", source, err) + } + + return dig, int64(len(rawManifest)), nil } diff --git a/vendor/github.com/pivotal/image-relocation/pkg/registry/layout.go b/vendor/github.com/pivotal/image-relocation/pkg/registry/layout.go index f3e13e2b..b58f3455 100644 --- a/vendor/github.com/pivotal/image-relocation/pkg/registry/layout.go +++ b/vendor/github.com/pivotal/image-relocation/pkg/registry/layout.go @@ -17,9 +17,10 @@ package registry import ( + "fmt" "os" - v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/pivotal/image-relocation/pkg/image" @@ -27,6 +28,7 @@ import ( const ( outputDirPermissions = 0755 + refNameAnnotation = "org.opencontainers.image.ref.name" ) // A Layout allows a registry client to interact with an OCI image layout on disk. @@ -36,6 +38,9 @@ type Layout interface { // Push pushes the image with the given digest from the layout to the given image reference. Push(digest image.Digest, name image.Name) error + + // Find returns the digest of an image in the layout with the given image reference. + Find(n image.Name) (image.Digest, error) } func (r *client) NewLayout(path string) (Layout, error) { @@ -72,7 +77,19 @@ func (r *client) ReadLayout(path string) (Layout, error) { type imageLayout struct { registryClient *client - layoutPath layout.Path + layoutPath LayoutPath +} + +func NewImageLayout(registryClient *client, layoutPath LayoutPath) Layout { + return &imageLayout{ + registryClient: registryClient, + layoutPath: layoutPath, + } +} + +type LayoutPath interface { + AppendImage(img v1.Image, options ...layout.Option) error + ImageIndex() (v1.ImageIndex, error) } func (l *imageLayout) Add(n image.Name) (image.Digest, error) { @@ -82,7 +99,7 @@ func (l *imageLayout) Add(n image.Name) (image.Digest, error) { } annotations := map[string]string{ - "org.opencontainers.image.ref.name": n.String(), + refNameAnnotation: n.String(), } if err := l.layoutPath.AppendImage(img, layout.WithAnnotations(annotations)); err != nil { return image.EmptyDigest, err @@ -112,3 +129,28 @@ func (l *imageLayout) Push(digest image.Digest, n image.Name) error { return l.registryClient.writeRemoteImage(i, n) } + +func (l *imageLayout) Find(n image.Name) (image.Digest, error) { + imageIndex, err := l.layoutPath.ImageIndex() + if err != nil { + return image.EmptyDigest, err + } + indexMan, err := imageIndex.IndexManifest() + if err != nil { + return image.EmptyDigest, err + } + + for _, imageMan := range indexMan.Manifests { + if ref, ok := imageMan.Annotations[refNameAnnotation]; ok { + r, err := image.NewName(ref) + if err != nil { + return image.EmptyDigest, err + } + if r == n { + return image.NewDigest(imageMan.Digest.String()) + } + } + } + + return image.EmptyDigest, fmt.Errorf("image %v not found in layout", n) +}