From c55e2a4a44d334b8e89cd89274df31f4051e5f25 Mon Sep 17 00:00:00 2001 From: Danny Joyce Date: Thu, 13 Jun 2019 17:35:20 -0400 Subject: [PATCH] Write analyzed.toml * Export to multiple tags * Use analyzed.toml in exporter [buildpack/lifecycle#138] Signed-off-by: Andrew Meyer --- analyzer.go | 44 +- analyzer_test.go | 58 ++- cmd/analyzer/main.go | 39 +- cmd/cmd.go | 64 +-- cmd/exporter/main.go | 66 ++- cmd/exporter/main_test.go | 32 ++ exporter.go | 53 +- exporter_test.go | 188 ++++--- go.mod | 2 +- go.sum | 2 + metadata/metadata.go | 12 + .../buildpack/imgutil/fakes/image.go | 66 ++- vendor/github.com/buildpack/imgutil/local.go | 5 + .../golang/mock/mockgen/model/model.go | 461 ------------------ vendor/modules.txt | 3 +- 15 files changed, 400 insertions(+), 695 deletions(-) create mode 100644 cmd/exporter/main_test.go delete mode 100644 vendor/github.com/golang/mock/mockgen/model/model.go diff --git a/analyzer.go b/analyzer.go index 4c23d9746..58320161e 100644 --- a/analyzer.go +++ b/analyzer.go @@ -11,13 +11,14 @@ import ( ) type Analyzer struct { - Buildpacks []*Buildpack - AppDir string - LayersDir string - In []byte - Out, Err *log.Logger - UID int - GID int + Buildpacks []*Buildpack + AppDir string + AnalyzedPath string + LayersDir string + In []byte + Out, Err *log.Logger + UID int + GID int } func (a *Analyzer) Analyze(image imgutil.Image) error { @@ -25,6 +26,7 @@ func (a *Analyzer) Analyze(image imgutil.Image) error { if err != nil { return err } + for _, buildpack := range a.Buildpacks { cache, err := readBuildpackLayersDir(a.LayersDir, *buildpack) if err != nil { @@ -73,12 +75,34 @@ func (a *Analyzer) Analyze(image imgutil.Image) error { } // if analyzer is running as root it needs to fix the ownership of the layers dir - if current := os.Getuid(); err != nil { - return err - } else if current == 0 { + if current := os.Getuid(); current == 0 { if err := recursiveChown(a.LayersDir, a.UID, a.GID); err != nil { return errors.Wrapf(err, "chowning layers dir to '%d/%d'", a.UID, a.GID) } } + + return a.writeAnalyzedMetadata(image) +} + +func (a *Analyzer) writeAnalyzedMetadata(image imgutil.Image) error { + var ( + err error + digest string + ) + if image.Found() { + digest, err = image.Digest() + if err != nil { + return errors.Wrap(err, "retrieve image digest") + } + } + + md := metadata.AnalyzedMetadata{ + Repository: image.Name(), + Digest: digest, + } + if err := WriteTOML(a.AnalyzedPath, md); err != nil { + return errors.Wrap(err, "write analyzed.toml") + } + return nil } diff --git a/analyzer_test.go b/analyzer_test.go index beca917f8..151496104 100644 --- a/analyzer_test.go +++ b/analyzer_test.go @@ -33,30 +33,35 @@ func testAnalyzer(t *testing.T, when spec.G, it spec.S) { stdout, stderr *bytes.Buffer layerDir string appDir string + tmpDir string ) it.Before(func() { var err error + + tmpDir, err = ioutil.TempDir("", "analyzer-tests") + h.AssertNil(t, err) + layerDir, err = ioutil.TempDir("", "lifecycle-layer-dir") - if err != nil { - t.Fatalf("Error: %s\n", err) - } - appDir = filepath.Join(layerDir, "some-app-dir") + h.AssertNil(t, err) + appDir = filepath.Join(layerDir, "some-app-dir") stdout, stderr = &bytes.Buffer{}, &bytes.Buffer{} analyzer = &lifecycle.Analyzer{ - Buildpacks: []*lifecycle.Buildpack{{ID: "metdata.buildpack"}, {ID: "no.cache.buildpack"}, {ID: "no.metadata.buildpack"}}, - AppDir: appDir, - LayersDir: layerDir, - Out: log.New(stdout, "", 0), - Err: log.New(stderr, "", 0), - UID: 1234, - GID: 4321, + Buildpacks: []*lifecycle.Buildpack{{ID: "metdata.buildpack"}, {ID: "no.cache.buildpack"}, {ID: "no.metadata.buildpack"}}, + AppDir: appDir, + LayersDir: layerDir, + AnalyzedPath: filepath.Join(tmpDir, "some-previous-file.toml"), + Out: log.New(stdout, "", 0), + Err: log.New(stderr, "", 0), + UID: 1234, + GID: 4321, } mockCtrl = gomock.NewController(t) }) it.After(func() { + os.RemoveAll(tmpDir) os.RemoveAll(layerDir) mockCtrl.Finish() }) @@ -68,8 +73,7 @@ func testAnalyzer(t *testing.T, when spec.G, it spec.S) { ) it.Before(func() { - image = fakes.NewImage("image-repo-name", "", "") - + image = fakes.NewImage("image-repo-name", "", "s0m3D1g3sT") ref = testmock.NewMockReference(mockCtrl) ref.EXPECT().Name().AnyTimes() }) @@ -459,7 +463,7 @@ func testAnalyzer(t *testing.T, when spec.G, it spec.S) { when("analyzer is running as root", func() { it.Before(func() { if os.Getuid() != 0 { - t.Skip() + t.Skip("Skipped when not running as root") } }) @@ -471,10 +475,22 @@ func testAnalyzer(t *testing.T, when spec.G, it spec.S) { h.AssertUidGid(t, filepath.Join(layerDir, "no.cache.buildpack", "go.toml"), 1234, 4321) }) }) + + when("analyzed path is provided", func() { + it("should write analyzed TOML at provided path", func() { + err := analyzer.Analyze(image) + h.AssertNil(t, err) + + b, err := ioutil.ReadFile(analyzer.AnalyzedPath) + h.AssertNil(t, err) + + h.AssertEq(t, string(b), "repository = \"image-repo-name\"\ndigest = \"s0m3D1g3sT\"\n") + }) + }) }) }) - when("the image cannot found", func() { + when("the image cannot be found", func() { it.Before(func() { h.AssertNil(t, image.Delete()) }) @@ -494,6 +510,18 @@ func testAnalyzer(t *testing.T, when spec.G, it spec.S) { t.Fatalf("Missing some-app-dir") } }) + + it("should write an analyzed.toml without a digest", func() { + h.AssertNil(t, image.Delete()) + + err := analyzer.Analyze(image) + h.AssertNil(t, err) + + b, err := ioutil.ReadFile(analyzer.AnalyzedPath) + h.AssertNil(t, err) + + h.AssertEq(t, string(b), "repository = \"image-repo-name\"\ndigest = \"\"\n") + }) }) when("the image does not have the required label", func() { diff --git a/cmd/analyzer/main.go b/cmd/analyzer/main.go index 89a0b3f4b..19e61f083 100644 --- a/cmd/analyzer/main.go +++ b/cmd/analyzer/main.go @@ -19,24 +19,26 @@ import ( ) var ( - repoName string - layersDir string - appDir string - groupPath string - useDaemon bool - useHelpers bool - uid int - gid int + repoName string + appDir string + layersDir string + analyzedPath string + groupPath string + useDaemon bool + useHelpers bool + uid int + gid int ) func init() { - cmd.FlagLayersDir(&layersDir) cmd.FlagAppDir(&appDir) + cmd.FlagGID(&gid) cmd.FlagGroupPath(&groupPath) + cmd.FlagLayersDir(&layersDir) + cmd.FlagAnalyzedPath(&analyzedPath) + cmd.FlagUID(&uid) cmd.FlagUseDaemon(&useDaemon) cmd.FlagUseCredHelpers(&useHelpers) - cmd.FlagUID(&uid) - cmd.FlagGID(&gid) } func main() { @@ -67,13 +69,14 @@ func analyzer() error { } analyzer := &lifecycle.Analyzer{ - Buildpacks: group.Buildpacks, - AppDir: appDir, - LayersDir: layersDir, - Out: log.New(os.Stdout, "", 0), - Err: log.New(os.Stderr, "", 0), - UID: uid, - GID: gid, + Buildpacks: group.Buildpacks, + AppDir: appDir, + LayersDir: layersDir, + AnalyzedPath: analyzedPath, + Out: log.New(os.Stdout, "", 0), + Err: log.New(os.Stderr, "", 0), + UID: uid, + GID: gid, } var err error diff --git a/cmd/cmd.go b/cmd/cmd.go index f50cb3af2..7a9af1d41 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -17,12 +17,14 @@ const ( DefaultOrderPath = "/buildpacks/order.toml" DefaultGroupPath = "./group.toml" DefaultStackPath = "/buildpacks/stack.toml" + DefaultAnalyzedPath = "./analyzed.toml" DefaultPlanPath = "./plan.toml" EnvLayersDir = "CNB_LAYERS_DIR" EnvAppDir = "CNB_APP_DIR" EnvBuildpacksDir = "CNB_BUILDPACKS_DIR" EnvPlatformDir = "CNB_PLATFORM_DIR" + EnvAnalyzedPath = "CNB_ANALYZED_PATH" EnvOrderPath = "CNB_ORDER_PATH" EnvGroupPath = "CNB_GROUP_PATH" EnvStackPath = "CNB_STACK_PATH" @@ -38,68 +40,72 @@ const ( EnvRegistryAuth = "CNB_REGISTRY_AUTH" ) -func FlagLayersDir(dir *string) { - flag.StringVar(dir, "layers", envWithDefault(EnvLayersDir, DefaultLayersDir), "path to layers directory") -} - func FlagAppDir(dir *string) { - flag.StringVar(dir, "app", envWithDefault(EnvAppDir, DefaultAppDir), "path to app directory") + flag.StringVar(dir, "app", envOrDefault(EnvAppDir, DefaultAppDir), "path to app directory") } func FlagBuildpacksDir(dir *string) { - flag.StringVar(dir, "buildpacks", envWithDefault(EnvBuildpacksDir, DefaultBuildpacksDir), "path to buildpacks directory") + flag.StringVar(dir, "buildpacks", envOrDefault(EnvBuildpacksDir, DefaultBuildpacksDir), "path to buildpacks directory") } -func FlagPlatformDir(dir *string) { - flag.StringVar(dir, "platform", envWithDefault(EnvPlatformDir, DefaultPlatformDir), "path to platform directory") +func FlagCacheDir(dir *string) { + flag.StringVar(dir, "path", os.Getenv(EnvCacheDir), "path to cache directory") } -func FlagOrderPath(path *string) { - flag.StringVar(path, "order", envWithDefault(EnvOrderPath, DefaultOrderPath), "path to order.toml") +func FlagCacheImage(image *string) { + flag.StringVar(image, "image", os.Getenv(EnvCacheImage), "cache image tag name") } -func FlagGroupPath(path *string) { - flag.StringVar(path, "group", envWithDefault(EnvGroupPath, DefaultGroupPath), "path to group.toml") +func FlagGID(gid *int) { + flag.IntVar(gid, "gid", intEnv(EnvGID), "GID of user's group in the stack's build and run images") } -func FlagStackPath(path *string) { - flag.StringVar(path, "stack", envWithDefault(EnvStackPath, DefaultStackPath), "path to stack.toml") +func FlagGroupPath(path *string) { + flag.StringVar(path, "group", envOrDefault(EnvGroupPath, DefaultGroupPath), "path to group.toml") } func FlagLaunchCacheDir(dir *string) { flag.StringVar(dir, "launch-cache", os.Getenv(EnvLaunchCacheDir), "path to launch cache directory") } -func FlagPlanPath(path *string) { - flag.StringVar(path, "plan", envWithDefault(EnvPlanPath, DefaultPlanPath), "path to plan.toml") +func FlagLayersDir(dir *string) { + flag.StringVar(dir, "layers", envOrDefault(EnvLayersDir, DefaultLayersDir), "path to layers directory") } -func FlagRunImage(image *string) { - flag.StringVar(image, "image", os.Getenv(EnvRunImage), "reference to run image") +func FlagOrderPath(path *string) { + flag.StringVar(path, "order", envOrDefault(EnvOrderPath, DefaultOrderPath), "path to order.toml") } -func FlagCacheImage(image *string) { - flag.StringVar(image, "image", os.Getenv(EnvCacheImage), "cache image tag name") +func FlagPlanPath(path *string) { + flag.StringVar(path, "plan", envOrDefault(EnvPlanPath, DefaultPlanPath), "path to plan.toml") } -func FlagCacheDir(dir *string) { - flag.StringVar(dir, "path", os.Getenv(EnvCacheDir), "path to cache directory") +func FlagPlatformDir(dir *string) { + flag.StringVar(dir, "platform", envOrDefault(EnvPlatformDir, DefaultPlatformDir), "path to platform directory") } -func FlagUseDaemon(use *bool) { - flag.BoolVar(use, "daemon", boolEnv(EnvUseDaemon), "export to docker daemon") +func FlagAnalyzedPath(dir *string) { + flag.StringVar(dir, "analyzed", envOrDefault(EnvAnalyzedPath, DefaultAnalyzedPath), "path to analyzed.toml") } -func FlagUseCredHelpers(use *bool) { - flag.BoolVar(use, "helpers", boolEnv(EnvUseHelpers), "use credential helpers") +func FlagRunImage(image *string) { + flag.StringVar(image, "image", os.Getenv(EnvRunImage), "reference to run image") +} + +func FlagStackPath(path *string) { + flag.StringVar(path, "stack", envOrDefault(EnvStackPath, DefaultStackPath), "path to stack.toml") } func FlagUID(uid *int) { flag.IntVar(uid, "uid", intEnv(EnvUID), "UID of user in the stack's build and run images") } -func FlagGID(gid *int) { - flag.IntVar(gid, "gid", intEnv(EnvGID), "GID of user's group in the stack's build and run images") +func FlagUseCredHelpers(use *bool) { + flag.BoolVar(use, "helpers", boolEnv(EnvUseHelpers), "use credential helpers") +} + +func FlagUseDaemon(use *bool) { + flag.BoolVar(use, "daemon", boolEnv(EnvUseDaemon), "export to docker daemon") } const ( @@ -173,7 +179,7 @@ func boolEnv(k string) bool { return b } -func envWithDefault(key string, defaultVal string) string { +func envOrDefault(key string, defaultVal string) string { if envVal := os.Getenv(key); envVal != "" { return envVal } diff --git a/cmd/exporter/main.go b/cmd/exporter/main.go index 1137d74ab..892dcc7b7 100644 --- a/cmd/exporter/main.go +++ b/cmd/exporter/main.go @@ -1,9 +1,7 @@ package main import ( - "errors" "flag" - "fmt" "io/ioutil" "log" "os" @@ -11,6 +9,7 @@ import ( "github.com/BurntSushi/toml" "github.com/buildpack/imgutil" + "github.com/pkg/errors" "github.com/buildpack/lifecycle" "github.com/buildpack/lifecycle/cache" @@ -22,11 +21,12 @@ import ( ) var ( - repoName string + repoNames []string runImageRef string layersDir string appDir string groupPath string + analyzedPath string stackPath string launchCacheDir string useDaemon bool @@ -42,6 +42,7 @@ func init() { cmd.FlagLayersDir(&layersDir) cmd.FlagAppDir(&appDir) cmd.FlagGroupPath(&groupPath) + cmd.FlagAnalyzedPath(&analyzedPath) cmd.FlagStackPath(&stackPath) cmd.FlagLaunchCacheDir(&launchCacheDir) cmd.FlagUseDaemon(&useDaemon) @@ -55,18 +56,21 @@ func main() { log.SetOutput(ioutil.Discard) flag.Parse() - if flag.NArg() > 1 { - cmd.Exit(cmd.FailErrCode(fmt.Errorf("received %d args expected 1", flag.NArg()), cmd.CodeInvalidArgs, "parse arguments")) + + for _, v := range flag.Args() { + if v != "" { + repoNames = append(repoNames, v) + } } - if flag.Arg(0) == "" { - cmd.Exit(cmd.FailErrCode(errors.New("image argument is required"), cmd.CodeInvalidArgs, "parse arguments")) + + if len(repoNames) == 0 { + cmd.Exit(cmd.FailErrCode(errors.New("at least one image argument is required"), cmd.CodeInvalidArgs, "parse arguments")) } if launchCacheDir != "" && !useDaemon { cmd.Exit(cmd.FailErrCode(errors.New("launch cache can only be used when exporting to a Docker daemon"), cmd.CodeInvalidArgs, "parse arguments")) } - repoName = flag.Arg(0) cmd.Exit(export()) } @@ -95,30 +99,40 @@ func export() error { ArtifactsDir: artifactsDir, } - var stack metadata.StackMetadata - _, err = toml.DecodeFile(stackPath, &stack) + var analyzedMD metadata.AnalyzedMetadata + _, err = toml.DecodeFile(analyzedPath, &analyzedMD) + if err != nil { + return cmd.FailErrCode(errors.Wrapf(err, "no analyzed.toml found at path '%s'", analyzedPath), cmd.CodeInvalidArgs, "parse arguments") + } + + if err := validateSingleRegistry(append(repoNames, analyzedMD.Repository)...); err != nil { + return cmd.FailErrCode(err, cmd.CodeInvalidArgs, "parse arguments") + } + + var stackMD metadata.StackMetadata + _, err = toml.DecodeFile(stackPath, &stackMD) if err != nil { outLog.Printf("no stack.toml found at path '%s', stack metadata will not be exported\n", stackPath) } if runImageRef == "" { - if stack.RunImage.Image == "" { + if stackMD.RunImage.Image == "" { return cmd.FailErrCode(errors.New("-image is required when there is no stack metadata available"), cmd.CodeInvalidArgs, "parse arguments") } - runImageRef, err = runImageFromStackToml(stack) + runImageRef, err = runImageFromStackToml(stackMD, analyzedMD.Repository) if err != nil { return err } } if useHelpers { - if err := lifecycle.SetupCredHelpers(filepath.Join(os.Getenv("HOME"), ".docker"), repoName, runImageRef); err != nil { + if err := lifecycle.SetupCredHelpers(filepath.Join(os.Getenv("HOME"), ".docker"), analyzedMD.Repository, runImageRef); err != nil { return cmd.FailErr(err, "setup credential helpers") } } - var runImage, origImage imgutil.Image + var runImage, analyzedImage imgutil.Image if useDaemon { dockerClient, err := docker.DefaultClient() if err != nil { @@ -138,7 +152,7 @@ func export() error { runImage = lifecycle.NewCachingImage(runImage, volumeCache) } - origImage, err = imgutil.NewLocalImage(repoName, dockerClient) + analyzedImage, err = imgutil.NewLocalImage(analyzedMD.FullName(), dockerClient) if err != nil { return cmd.FailErr(err, "access previous image") } @@ -147,20 +161,21 @@ func export() error { if err != nil { return cmd.FailErr(err, "access run image") } - origImage, err = imgutil.NewRemoteImage(repoName, auth.DefaultEnvKeychain()) + + analyzedImage, err = imgutil.NewRemoteImage(analyzedMD.FullName(), auth.DefaultEnvKeychain()) if err != nil { return cmd.FailErr(err, "access previous image") } } - if err := exporter.Export(layersDir, appDir, runImage, origImage, launcherPath, stack); err != nil { + if err := exporter.Export(layersDir, appDir, runImage, analyzedImage, repoNames, launcherPath, stackMD); err != nil { return cmd.FailErr(err, "export") } return nil } -func runImageFromStackToml(stack metadata.StackMetadata) (string, error) { +func runImageFromStackToml(stack metadata.StackMetadata, repoName string) (string, error) { registry, err := image.ParseRegistry(repoName) if err != nil { return "", cmd.FailErrCode(err, cmd.CodeInvalidArgs, "parse image name") @@ -174,3 +189,18 @@ func runImageFromStackToml(stack metadata.StackMetadata) (string, error) { } return runImageRef, nil } + +func validateSingleRegistry(repoNames ...string) error { + set := make(map[string]interface{}) + for _, repoName := range repoNames { + registry, err := image.ParseRegistry(repoName) + if err != nil { + return errors.Wrapf(err, "parsing registry from repo %s", repoName) + } + set[registry] = nil + } + if len(set) != 1 { + return errors.New("exporting to multiple registries is unsupported") + } + return nil +} diff --git a/cmd/exporter/main_test.go b/cmd/exporter/main_test.go new file mode 100644 index 000000000..fd7cbffb0 --- /dev/null +++ b/cmd/exporter/main_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + h "github.com/buildpack/lifecycle/testhelpers" +) + +func TestAnalyzer(t *testing.T) { + spec.Run(t, "exporter", testExporter, spec.Report(report.Terminal{})) +} + +func testExporter(t *testing.T, when spec.G, it spec.S) { + when("#validateSingleRegistry", func() { + when("multiple registries are provided", func() { + it("errors as unsupported", func() { + err := validateSingleRegistry("some/repo", "gcr.io/other-repo:latest", "example.com/final-repo") + h.AssertError(t, err, "exporting to multiple registries is unsupported") + }) + }) + + when("a single registry is provided", func() { + it("does not return an error", func() { + err := validateSingleRegistry("gcr.io/some/repo", "gcr.io/other-repo:latest", "gcr.io/final-repo") + h.AssertNil(t, err) + }) + }) + }) +} diff --git a/exporter.go b/exporter.go index d432902db..5a80b0265 100644 --- a/exporter.go +++ b/exporter.go @@ -22,7 +22,15 @@ type Exporter struct { UID, GID int } -func (e *Exporter) Export(layersDir, appDir string, runImage, origImage imgutil.Image, launcher string, stack metadata.StackMetadata) error { +func (e *Exporter) Export( + layersDir, + appDir string, + runImage, + analyzedImage imgutil.Image, + repoNames []string, + launcher string, + stack metadata.StackMetadata, +) error { var err error meta := metadata.AppImageMetadata{} @@ -39,25 +47,25 @@ func (e *Exporter) Export(layersDir, appDir string, runImage, origImage imgutil. meta.Stack = stack - origMetadata, err := metadata.GetAppMetadata(origImage) + origMetadata, err := metadata.GetAppMetadata(analyzedImage) if err != nil { return errors.Wrap(err, "metadata for previous image") } - runImage.Rename(origImage.Name()) - appImage := runImage + runImage.Rename(analyzedImage.Name()) + workingImage := runImage - meta.App.SHA, err = e.addOrReuseLayer(appImage, &layer{path: appDir, identifier: "app"}, origMetadata.App.SHA) + meta.App.SHA, err = e.addOrReuseLayer(workingImage, &layer{path: appDir, identifier: "app"}, origMetadata.App.SHA) if err != nil { return errors.Wrap(err, "exporting app layer") } - meta.Config.SHA, err = e.addOrReuseLayer(appImage, &layer{path: filepath.Join(layersDir, "config"), identifier: "config"}, origMetadata.Config.SHA) + meta.Config.SHA, err = e.addOrReuseLayer(workingImage, &layer{path: filepath.Join(layersDir, "config"), identifier: "config"}, origMetadata.Config.SHA) if err != nil { return errors.Wrap(err, "exporting config layer") } - meta.Launcher.SHA, err = e.addOrReuseLayer(appImage, &layer{path: launcher, identifier: "launcher"}, origMetadata.Launcher.SHA) + meta.Launcher.SHA, err = e.addOrReuseLayer(workingImage, &layer{path: launcher, identifier: "launcher"}, origMetadata.Launcher.SHA) if err != nil { return errors.Wrap(err, "exporting launcher layer") } @@ -77,7 +85,7 @@ func (e *Exporter) Export(layersDir, appDir string, runImage, origImage imgutil. if layer.hasLocalContents() { origLayerMetadata := origMetadata.MetadataForBuildpack(bp.ID).Layers[layer.name()] - lmd.SHA, err = e.addOrReuseLayer(appImage, &layer, origLayerMetadata.SHA) + lmd.SHA, err = e.addOrReuseLayer(workingImage, &layer, origLayerMetadata.SHA) if err != nil { return err } @@ -91,7 +99,7 @@ func (e *Exporter) Export(layersDir, appDir string, runImage, origImage imgutil. } e.Out.Printf("Reusing layer '%s' with SHA %s\n", layer.Identifier(), origLayerMetadata.SHA) - if err := appImage.ReuseLayer(origLayerMetadata.SHA); err != nil { + if err := workingImage.ReuseLayer(origLayerMetadata.SHA); err != nil { return errors.Wrapf(err, "reusing layer: '%s'", layer.Identifier()) } lmd.SHA = origLayerMetadata.SHA @@ -114,31 +122,40 @@ func (e *Exporter) Export(layersDir, appDir string, runImage, origImage imgutil. if err != nil { return errors.Wrap(err, "marshall metadata") } - if err := appImage.SetLabel(metadata.AppMetadataLabel, string(data)); err != nil { + if err := workingImage.SetLabel(metadata.AppMetadataLabel, string(data)); err != nil { return errors.Wrap(err, "set app image metadata label") } - if err := appImage.SetEnv(cmd.EnvLayersDir, layersDir); err != nil { + if err := workingImage.SetEnv(cmd.EnvLayersDir, layersDir); err != nil { return errors.Wrapf(err, "set app image env %s", cmd.EnvLayersDir) } - if err := appImage.SetEnv(cmd.EnvAppDir, appDir); err != nil { + if err := workingImage.SetEnv(cmd.EnvAppDir, appDir); err != nil { return errors.Wrapf(err, "set app image env %s", cmd.EnvAppDir) } - if err := appImage.SetEntrypoint(launcher); err != nil { + if err := workingImage.SetEntrypoint(launcher); err != nil { return errors.Wrap(err, "setting entrypoint") } - if err := appImage.SetCmd(); err != nil { // Note: Command intentionally empty + if err := workingImage.SetCmd(); err != nil { // Note: Command intentionally empty return errors.Wrap(err, "setting cmd") } - sha, err := appImage.Save() - if err != nil { - return errors.Wrap(err, "saving") + var sha string + for _, repoName := range repoNames { + workingImage.Rename(repoName) + + sha, err = workingImage.Save() + if err != nil { + return errors.Wrap(err, "saving") + } + } + + e.Out.Printf("\n*** Digest: %s\n", sha) + for _, repoName := range repoNames { + e.Out.Printf("*** Image: %s\n", repoName) } - e.Out.Printf("\n*** Image: %s@%s\n", runImage.Name(), sha) return nil } diff --git a/exporter_test.go b/exporter_test.go index caad853b6..1146a9e63 100644 --- a/exporter_test.go +++ b/exporter_test.go @@ -32,7 +32,7 @@ func TestExporter(t *testing.T) { func testExporter(t *testing.T, when spec.G, it spec.S) { var ( exporter *lifecycle.Exporter - fakeImage *fakes.Image + fakeRunImage *fakes.Image stderr bytes.Buffer stdout bytes.Buffer layersDir string @@ -42,6 +42,7 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { uid = 1234 gid = 4321 stack = metadata.StackMetadata{} + repoNames []string ) it.Before(func() { @@ -57,7 +58,9 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, os.Mkdir(layersDir, 0777)) h.AssertNil(t, ioutil.WriteFile(filepath.Join(tmpDir, "launcher"), []byte("some-launcher"), 0777)) - fakeImage = fakes.NewImage("runImageName", "some-top-layer-sha", "some-run-image-digest") + fakeRunImage = fakes.NewImage("runImageName", "some-top-layer-sha", "some-run-image-digest") + + repoNames = []string{"some-repo/app-image:foo", "some-repo/app-image:bar"} exporter = &lifecycle.Exporter{ ArtifactsDir: tmpDir, @@ -73,7 +76,7 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it.After(func() { - fakeImage.Cleanup() + fakeRunImage.Cleanup() if err := os.RemoveAll(tmpDir); err != nil { t.Fatal(err) @@ -96,9 +99,9 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { launcherSHA := h.ComputeSHA256ForPath(t, launcherPath, uid, gid) fakeOriginalImage = fakes.NewImage("app/original-Image-Name", "original-top-layer-sha", "some-original-run-image-digest") - fakeImage.AddPreviousLayer("sha256:"+localReusableLayerSha, "") - fakeImage.AddPreviousLayer("sha256:"+launcherSHA, "") - fakeImage.AddPreviousLayer("sha256:orig-launch-layer-no-local-dir-sha", "") + fakeRunImage.AddPreviousLayer("sha256:"+localReusableLayerSha, "") + fakeRunImage.AddPreviousLayer("sha256:"+launcherSHA, "") + fakeRunImage.AddPreviousLayer("sha256:orig-launch-layer-no-local-dir-sha", "") _ = fakeOriginalImage.SetLabel("io.buildpacks.lifecycle.metadata", fmt.Sprintf(`{ "buildpacks": [ @@ -139,9 +142,9 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("creates app layer on Run image", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - appLayerPath := fakeImage.AppLayerPath() + appLayerPath := fakeRunImage.AppLayerPath() assertTarFileContents(t, appLayerPath, filepath.Join(appDir, ".hidden.txt"), "some-hidden-text\n") assertTarFileOwner(t, appLayerPath, appDir, uid, gid) @@ -149,9 +152,9 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("creates config layer on Run image", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - configLayerPath := fakeImage.ConfigLayerPath() + configLayerPath := fakeRunImage.ConfigLayerPath() assertTarFileContents(t, configLayerPath, @@ -164,31 +167,31 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { it("reuses launcher layer if the sha matches the sha in the metadata", func() { launcherLayerSHA := h.ComputeSHA256ForPath(t, launcherPath, uid, gid) - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) - h.AssertContains(t, fakeImage.ReusedLayers(), "sha256:"+launcherLayerSHA) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) + h.AssertContains(t, fakeRunImage.ReusedLayers(), "sha256:"+launcherLayerSHA) assertReuseLayerLog(t, stdout, "launcher", launcherLayerSHA) }) it("reuses launch layers when only layer.toml is present", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - h.AssertContains(t, fakeImage.ReusedLayers(), "sha256:orig-launch-layer-no-local-dir-sha") + h.AssertContains(t, fakeRunImage.ReusedLayers(), "sha256:orig-launch-layer-no-local-dir-sha") assertReuseLayerLog(t, stdout, "buildpack.id:launch-layer-no-local-dir", "orig-launch-layer-no-local-dir-sha") }) it("reuses cached launch layers if the local sha matches the sha in the metadata", func() { layer5sha := h.ComputeSHA256ForPath(t, filepath.Join(layersDir, "other.buildpack.id/local-reusable-layer"), uid, gid) - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - h.AssertContains(t, fakeImage.ReusedLayers(), "sha256:"+layer5sha) + h.AssertContains(t, fakeRunImage.ReusedLayers(), "sha256:"+layer5sha) assertReuseLayerLog(t, stdout, "other.buildpack.id:local-reusable-layer", layer5sha) }) it("adds new launch layers", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - layer2Path, err := fakeImage.FindLayerWithPath(filepath.Join(layersDir, "buildpack.id/new-launch-layer")) + layer2Path, err := fakeRunImage.FindLayerWithPath(filepath.Join(layersDir, "buildpack.id/new-launch-layer")) h.AssertNil(t, err) assertTarFileContents(t, @@ -200,9 +203,9 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("adds new launch layers from a second buildpack", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - layer3Path, err := fakeImage.FindLayerWithPath(filepath.Join(layersDir, "other.buildpack.id/new-launch-layer")) + layer3Path, err := fakeRunImage.FindLayerWithPath(filepath.Join(layersDir, "other.buildpack.id/new-launch-layer")) h.AssertNil(t, err) assertTarFileContents(t, @@ -214,37 +217,37 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("only creates expected layers", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) var applayer, configLayer, layer2, layer3 = 1, 1, 1, 1 - h.AssertEq(t, fakeImage.NumberOfAddedLayers(), applayer+configLayer+layer2+layer3) + h.AssertEq(t, fakeRunImage.NumberOfAddedLayers(), applayer+configLayer+layer2+layer3) }) it("only reuses expected layers", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) var launcherLayer, layer1, layer5 = 1, 1, 1 - h.AssertEq(t, len(fakeImage.ReusedLayers()), launcherLayer+layer1+layer5) + h.AssertEq(t, len(fakeRunImage.ReusedLayers()), launcherLayer+layer1+layer5) }) it("saves lifecycle metadata with layer info", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - appLayerPath := fakeImage.AppLayerPath() + appLayerPath := fakeRunImage.AppLayerPath() appLayerSHA := h.ComputeSHA256ForFile(t, appLayerPath) - configLayerPath := fakeImage.ConfigLayerPath() + configLayerPath := fakeRunImage.ConfigLayerPath() configLayerSHA := h.ComputeSHA256ForFile(t, configLayerPath) - newLayerPath, err := fakeImage.FindLayerWithPath(filepath.Join(layersDir, "buildpack.id/new-launch-layer")) + newLayerPath, err := fakeRunImage.FindLayerWithPath(filepath.Join(layersDir, "buildpack.id/new-launch-layer")) h.AssertNil(t, err) newLayerSHA := h.ComputeSHA256ForFile(t, newLayerPath) - secondBPLayerPath, err := fakeImage.FindLayerWithPath(filepath.Join(layersDir, "other.buildpack.id/new-launch-layer")) + secondBPLayerPath, err := fakeRunImage.FindLayerWithPath(filepath.Join(layersDir, "other.buildpack.id/new-launch-layer")) h.AssertNil(t, err) secondBPLayerPathSHA := h.ComputeSHA256ForFile(t, secondBPLayerPath) - metadataJSON, err := fakeImage.Label("io.buildpacks.lifecycle.metadata") + metadataJSON, err := fakeRunImage.Label("io.buildpacks.lifecycle.metadata") h.AssertNil(t, err) var meta metadata.AppImageMetadata @@ -286,9 +289,9 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { Mirrors: []string{"registry.example.com/some/run", "other.example.com/some/run"}, }, } - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - metadataJSON, err := fakeImage.Label("io.buildpacks.lifecycle.metadata") + metadataJSON, err := fakeRunImage.Label("io.buildpacks.lifecycle.metadata") h.AssertNil(t, err) var meta metadata.AppImageMetadata @@ -301,55 +304,50 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("sets CNB_LAYERS_DIR", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - val, err := fakeImage.Env("CNB_LAYERS_DIR") + val, err := fakeRunImage.Env("CNB_LAYERS_DIR") h.AssertNil(t, err) h.AssertEq(t, val, layersDir) }) it("sets CNB_APP_DIR", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - val, err := fakeImage.Env("CNB_APP_DIR") + val, err := fakeRunImage.Env("CNB_APP_DIR") h.AssertNil(t, err) h.AssertEq(t, val, appDir) }) it("sets ENTRYPOINT to launcher", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - val, err := fakeImage.Entrypoint() + val, err := fakeRunImage.Entrypoint() h.AssertNil(t, err) h.AssertEq(t, val, []string{launcherPath}) }) it("sets empty CMD", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - val, err := fakeImage.Cmd() + val, err := fakeRunImage.Cmd() h.AssertNil(t, err) h.AssertEq(t, val, []string(nil)) }) - it("sets name to match old run image", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) - - h.AssertEq(t, fakeImage.Name(), "app/original-Image-Name") - }) - it("saves run image", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - h.AssertEq(t, fakeImage.IsSaved(), true) + h.AssertEq(t, fakeRunImage.IsSaved(), true) }) it("outputs image name and digest", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - if !strings.Contains(stdout.String(), "Image: app/original-Image-Name@saved-digest-from-fake-run-image") { - t.Fatalf("output should contain Image: app/original-Image-Name@some-digest, got '%s'", stdout.String()) - } + output := strings.Split(stdout.String(), "\n") + h.AssertContains(t, output, "*** Digest: saved-digest-from-fake-run-image") + h.AssertContains(t, output, "*** Image: "+repoNames[0]) + h.AssertContains(t, output, "*** Image: "+repoNames[1]) }) when("previous image metadata is missing buildpack for reused layer", func() { @@ -360,7 +358,7 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { it("returns an error", func() { h.AssertError( t, - exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack), + exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack), "cannot reuse 'buildpack.id:launch-layer-no-local-dir', previous image has no metadata for layer 'buildpack.id:launch-layer-no-local-dir'", ) }) @@ -376,11 +374,16 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { it("returns an error", func() { h.AssertError( t, - exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack), + exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack), "cannot reuse 'buildpack.id:launch-layer-no-local-dir', previous image has no metadata for layer 'buildpack.id:launch-layer-no-local-dir'", ) }) }) + + it("saves the image for all provided repoNames", func() { + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) + h.AssertEq(t, fakeRunImage.SavedRepoNames(), repoNames) + }) }) when("previous image doesn't exist", func() { @@ -404,9 +407,9 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("creates app layer on Run image", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, nonExistingOriginalImage, repoNames, launcherPath, stack)) - appLayerPath := fakeImage.AppLayerPath() + appLayerPath := fakeRunImage.AppLayerPath() assertTarFileContents(t, appLayerPath, filepath.Join(appDir, ".hidden.txt"), "some-hidden-text\n") assertTarFileOwner(t, appLayerPath, appDir, uid, gid) @@ -414,9 +417,9 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("creates config layer on Run image", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, nonExistingOriginalImage, repoNames, launcherPath, stack)) - configLayerPath := fakeImage.ConfigLayerPath() + configLayerPath := fakeRunImage.ConfigLayerPath() assertTarFileContents(t, configLayerPath, @@ -428,9 +431,9 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("creates a launcher layer", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, nonExistingOriginalImage, repoNames, launcherPath, stack)) - launcherLayerPath, err := fakeImage.FindLayerWithPath(launcherPath) + launcherLayerPath, err := fakeRunImage.FindLayerWithPath(launcherPath) h.AssertNil(t, err) assertTarFileContents(t, launcherLayerPath, @@ -441,9 +444,9 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("adds launch layers", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, nonExistingOriginalImage, repoNames, launcherPath, stack)) - layer1Path, err := fakeImage.FindLayerWithPath(filepath.Join(layersDir, "buildpack.id/layer1")) + layer1Path, err := fakeRunImage.FindLayerWithPath(filepath.Join(layersDir, "buildpack.id/layer1")) h.AssertNil(t, err) assertTarFileContents(t, layer1Path, @@ -452,7 +455,7 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { assertTarFileOwner(t, layer1Path, filepath.Join(layersDir, "buildpack.id/layer1"), uid, gid) assertAddLayerLog(t, stdout, "buildpack.id:layer1", layer1Path) - layer2Path, err := fakeImage.FindLayerWithPath(filepath.Join(layersDir, "buildpack.id/layer2")) + layer2Path, err := fakeRunImage.FindLayerWithPath(filepath.Join(layersDir, "buildpack.id/layer2")) h.AssertNil(t, err) assertTarFileContents(t, layer2Path, @@ -463,34 +466,34 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("only creates expected layers", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, nonExistingOriginalImage, repoNames, launcherPath, stack)) var applayer, configLayer, launcherLayer, layer1, layer2 = 1, 1, 1, 1, 1 - h.AssertEq(t, fakeImage.NumberOfAddedLayers(), applayer+configLayer+launcherLayer+layer1+layer2) + h.AssertEq(t, fakeRunImage.NumberOfAddedLayers(), applayer+configLayer+launcherLayer+layer1+layer2) }) it("saves metadata with layer info", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, nonExistingOriginalImage, repoNames, launcherPath, stack)) - appLayerPath := fakeImage.AppLayerPath() + appLayerPath := fakeRunImage.AppLayerPath() appLayerSHA := h.ComputeSHA256ForFile(t, appLayerPath) - configLayerPath := fakeImage.ConfigLayerPath() + configLayerPath := fakeRunImage.ConfigLayerPath() configLayerSHA := h.ComputeSHA256ForFile(t, configLayerPath) - launcherLayerPath, err := fakeImage.FindLayerWithPath(launcherPath) + launcherLayerPath, err := fakeRunImage.FindLayerWithPath(launcherPath) h.AssertNil(t, err) launcherLayerSHA := h.ComputeSHA256ForFile(t, launcherLayerPath) - layer1Path, err := fakeImage.FindLayerWithPath(filepath.Join(layersDir, "buildpack.id/layer1")) + layer1Path, err := fakeRunImage.FindLayerWithPath(filepath.Join(layersDir, "buildpack.id/layer1")) h.AssertNil(t, err) buildpackLayer1SHA := h.ComputeSHA256ForFile(t, layer1Path) - layer2Path, err := fakeImage.FindLayerWithPath(filepath.Join(layersDir, "buildpack.id/layer2")) + layer2Path, err := fakeRunImage.FindLayerWithPath(filepath.Join(layersDir, "buildpack.id/layer2")) h.AssertNil(t, err) buildpackLayer2SHA := h.ComputeSHA256ForFile(t, layer2Path) - metadataJSON, err := fakeImage.Label("io.buildpacks.lifecycle.metadata") + metadataJSON, err := fakeRunImage.Label("io.buildpacks.lifecycle.metadata") h.AssertNil(t, err) var meta metadata.AppImageMetadata @@ -516,47 +519,40 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("sets CNB_LAYERS_DIR", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, nonExistingOriginalImage, repoNames, launcherPath, stack)) - val, err := fakeImage.Env("CNB_LAYERS_DIR") + val, err := fakeRunImage.Env("CNB_LAYERS_DIR") h.AssertNil(t, err) h.AssertEq(t, val, layersDir) }) it("sets CNB_APP_DIR", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, nonExistingOriginalImage, repoNames, launcherPath, stack)) - val, err := fakeImage.Env("CNB_APP_DIR") + val, err := fakeRunImage.Env("CNB_APP_DIR") h.AssertNil(t, err) h.AssertEq(t, val, appDir) }) it("sets ENTRYPOINT to launcher", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, nonExistingOriginalImage, repoNames, launcherPath, stack)) - val, err := fakeImage.Entrypoint() + val, err := fakeRunImage.Entrypoint() h.AssertNil(t, err) h.AssertEq(t, val, []string{launcherPath}) }) it("sets empty CMD", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, nonExistingOriginalImage, repoNames, launcherPath, stack)) - val, err := fakeImage.Cmd() + val, err := fakeRunImage.Cmd() h.AssertNil(t, err) h.AssertEq(t, val, []string(nil)) }) - it("sets name to match original image", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack)) - - h.AssertEq(t, fakeImage.Name(), "app/original-Image-Name") - }) - - it("saves run image", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack)) - - h.AssertEq(t, fakeImage.IsSaved(), true) + it("saves the image for all provided repoNames", func() { + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, nonExistingOriginalImage, repoNames, launcherPath, stack)) + h.AssertEq(t, fakeRunImage.SavedRepoNames(), repoNames) }) }) @@ -588,9 +584,9 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("exports layers from the escaped id path", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - layerPath, err := fakeImage.FindLayerWithPath(filepath.Join(layersDir, "some_escaped_bp_id/some-layer")) + layerPath, err := fakeRunImage.FindLayerWithPath(filepath.Join(layersDir, "some_escaped_bp_id/some-layer")) h.AssertNil(t, err) assertTarFileContents(t, @@ -602,9 +598,9 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { }) it("exports buildpack metadata with unescaped id", func() { - h.AssertNil(t, exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack)) + h.AssertNil(t, exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack)) - metadataJSON, err := fakeImage.Label("io.buildpacks.lifecycle.metadata") + metadataJSON, err := fakeRunImage.Label("io.buildpacks.lifecycle.metadata") h.AssertNil(t, err) var meta metadata.AppImageMetadata @@ -641,7 +637,7 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { it("returns an error", func() { h.AssertError( t, - exporter.Export(layersDir, appDir, fakeImage, nonExistingOriginalImage, launcherPath, stack), + exporter.Export(layersDir, appDir, fakeRunImage, nonExistingOriginalImage, repoNames, launcherPath, stack), "failed to parse metadata for layers '[buildpack.id:bad-layer]'", ) }) @@ -681,7 +677,7 @@ func testExporter(t *testing.T, when spec.G, it spec.S) { it("returns an error", func() { h.AssertError( t, - exporter.Export(layersDir, appDir, fakeImage, fakeOriginalImage, launcherPath, stack), + exporter.Export(layersDir, appDir, fakeRunImage, fakeOriginalImage, repoNames, launcherPath, stack), "layer 'buildpack.id:cache-layer-no-contents' is cache=true but has no contents", ) }) diff --git a/go.mod b/go.mod index 15c5c99b8..8241dd23d 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/buildpack/lifecycle require ( github.com/BurntSushi/toml v0.3.1 - github.com/buildpack/imgutil v0.0.0-20190509214933-76de939dfb34 + github.com/buildpack/imgutil v0.0.0-20190613212649-bc74f5cf6410 github.com/docker/docker v0.7.3-0.20190307005417-54dddadc7d5d github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.4.0 // indirect diff --git a/go.sum b/go.sum index dd5650519..29267f1fa 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/buildpack/imgutil v0.0.0-20190509214311-ef7b838ea54e h1:+XO65Ne4CWNdW github.com/buildpack/imgutil v0.0.0-20190509214311-ef7b838ea54e/go.mod h1:hucbKwghPRl30WTj6YxhsoOYTLzpLo3hDSSMPmCAYf4= github.com/buildpack/imgutil v0.0.0-20190509214933-76de939dfb34 h1:cCzM7734kk/1RmfIY8zDvOQDaqBaUoGpGpesl6i5nD0= github.com/buildpack/imgutil v0.0.0-20190509214933-76de939dfb34/go.mod h1:hucbKwghPRl30WTj6YxhsoOYTLzpLo3hDSSMPmCAYf4= +github.com/buildpack/imgutil v0.0.0-20190613212649-bc74f5cf6410 h1:N8WxkgrR/B6SWnIMs/zz+zkwVuPJ86suwAFjVZv8blA= +github.com/buildpack/imgutil v0.0.0-20190613212649-bc74f5cf6410/go.mod h1:hucbKwghPRl30WTj6YxhsoOYTLzpLo3hDSSMPmCAYf4= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/metadata/metadata.go b/metadata/metadata.go index 23998c988..285893ed6 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -22,6 +22,18 @@ type AppMetadata struct { SHA string `json:"sha"` } +type AnalyzedMetadata struct { + Repository string `toml:"repository"` + Digest string `toml:"digest"` +} + +func (a AnalyzedMetadata) FullName() string { + if a.Digest == "" { + return a.Repository + } + return a.Repository + "@" + a.Digest +} + type ConfigMetadata struct { SHA string `json:"sha"` } diff --git a/vendor/github.com/buildpack/imgutil/fakes/image.go b/vendor/github.com/buildpack/imgutil/fakes/image.go index 54ba1ffbd..3f725aa03 100644 --- a/vendor/github.com/buildpack/imgutil/fakes/image.go +++ b/vendor/github.com/buildpack/imgutil/fakes/image.go @@ -19,37 +19,39 @@ import ( func NewImage(name, topLayerSha, digest string) *Image { return &Image{ - alreadySaved: false, - labels: map[string]string{}, - env: map[string]string{}, - topLayerSha: topLayerSha, - digest: digest, - name: name, - cmd: []string{"initialCMD"}, - layersMap: map[string]string{}, - prevLayersMap: map[string]string{}, - createdAt: time.Now(), + alreadySaved: false, + labels: map[string]string{}, + env: map[string]string{}, + topLayerSha: topLayerSha, + digest: digest, + name: name, + cmd: []string{"initialCMD"}, + layersMap: map[string]string{}, + prevLayersMap: map[string]string{}, + createdAt: time.Now(), + savedRepoNames: make(map[string]interface{}), } } type Image struct { - alreadySaved bool - deleted bool - layers []string - layersMap map[string]string - prevLayersMap map[string]string - reusedLayers []string - labels map[string]string - env map[string]string - topLayerSha string - digest string - name string - entryPoint []string - cmd []string - base string - createdAt time.Time - layerDir string - workingDir string + alreadySaved bool + deleted bool + layers []string + layersMap map[string]string + prevLayersMap map[string]string + reusedLayers []string + labels map[string]string + env map[string]string + topLayerSha string + digest string + name string + entryPoint []string + cmd []string + base string + createdAt time.Time + layerDir string + workingDir string + savedRepoNames map[string]interface{} } func (f *Image) CreatedAt() (time.Time, error) { @@ -174,6 +176,8 @@ func (f *Image) Save() (string, error) { f.layers[i] = filepath.Join(f.layerDir, filepath.Base(layerPath)) } + f.savedRepoNames[f.name] = nil + return "saved-digest-from-fake-run-image", nil } @@ -300,3 +304,11 @@ func (f *Image) IsSaved() bool { func (f *Image) Base() string { return f.base } + +func (f *Image) SavedRepoNames() []string { + var t []string + for k := range f.savedRepoNames { + t = append(t, k) + } + return t +} diff --git a/vendor/github.com/buildpack/imgutil/local.go b/vendor/github.com/buildpack/imgutil/local.go index 543ba109b..86a260e63 100644 --- a/vendor/github.com/buildpack/imgutil/local.go +++ b/vendor/github.com/buildpack/imgutil/local.go @@ -193,6 +193,11 @@ func (l *localImage) SetLabel(key, val string) error { if l.inspect.Config == nil { return fmt.Errorf("failed to set label, image '%s' does not exist", l.repoName) } + + if l.inspect.Config.Labels == nil { + l.inspect.Config.Labels = map[string]string{} + } + l.inspect.Config.Labels[key] = val return nil } diff --git a/vendor/github.com/golang/mock/mockgen/model/model.go b/vendor/github.com/golang/mock/mockgen/model/model.go deleted file mode 100644 index 8113e3d39..000000000 --- a/vendor/github.com/golang/mock/mockgen/model/model.go +++ /dev/null @@ -1,461 +0,0 @@ -// Copyright 2012 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package model contains the data model necessary for generating mock implementations. -package model - -import ( - "encoding/gob" - "fmt" - "io" - "reflect" - "strings" -) - -// pkgPath is the importable path for package model -const pkgPath = "github.com/golang/mock/mockgen/model" - -// Package is a Go package. It may be a subset. -type Package struct { - Name string - Interfaces []*Interface - DotImports []string -} - -func (pkg *Package) Print(w io.Writer) { - fmt.Fprintf(w, "package %s\n", pkg.Name) - for _, intf := range pkg.Interfaces { - intf.Print(w) - } -} - -// Imports returns the imports needed by the Package as a set of import paths. -func (pkg *Package) Imports() map[string]bool { - im := make(map[string]bool) - for _, intf := range pkg.Interfaces { - intf.addImports(im) - } - return im -} - -// Interface is a Go interface. -type Interface struct { - Name string - Methods []*Method -} - -func (intf *Interface) Print(w io.Writer) { - fmt.Fprintf(w, "interface %s\n", intf.Name) - for _, m := range intf.Methods { - m.Print(w) - } -} - -func (intf *Interface) addImports(im map[string]bool) { - for _, m := range intf.Methods { - m.addImports(im) - } -} - -// Method is a single method of an interface. -type Method struct { - Name string - In, Out []*Parameter - Variadic *Parameter // may be nil -} - -func (m *Method) Print(w io.Writer) { - fmt.Fprintf(w, " - method %s\n", m.Name) - if len(m.In) > 0 { - fmt.Fprintf(w, " in:\n") - for _, p := range m.In { - p.Print(w) - } - } - if m.Variadic != nil { - fmt.Fprintf(w, " ...:\n") - m.Variadic.Print(w) - } - if len(m.Out) > 0 { - fmt.Fprintf(w, " out:\n") - for _, p := range m.Out { - p.Print(w) - } - } -} - -func (m *Method) addImports(im map[string]bool) { - for _, p := range m.In { - p.Type.addImports(im) - } - if m.Variadic != nil { - m.Variadic.Type.addImports(im) - } - for _, p := range m.Out { - p.Type.addImports(im) - } -} - -// Parameter is an argument or return parameter of a method. -type Parameter struct { - Name string // may be empty - Type Type -} - -func (p *Parameter) Print(w io.Writer) { - n := p.Name - if n == "" { - n = `""` - } - fmt.Fprintf(w, " - %v: %v\n", n, p.Type.String(nil, "")) -} - -// Type is a Go type. -type Type interface { - String(pm map[string]string, pkgOverride string) string - addImports(im map[string]bool) -} - -func init() { - gob.Register(&ArrayType{}) - gob.Register(&ChanType{}) - gob.Register(&FuncType{}) - gob.Register(&MapType{}) - gob.Register(&NamedType{}) - gob.Register(&PointerType{}) - - // Call gob.RegisterName to make sure it has the consistent name registered - // for both gob decoder and encoder. - // - // For a non-pointer type, gob.Register will try to get package full path by - // calling rt.PkgPath() for a name to register. If your project has vendor - // directory, it is possible that PkgPath will get a path like this: - // ../../../vendor/github.com/golang/mock/mockgen/model - gob.RegisterName(pkgPath+".PredeclaredType", PredeclaredType("")) -} - -// ArrayType is an array or slice type. -type ArrayType struct { - Len int // -1 for slices, >= 0 for arrays - Type Type -} - -func (at *ArrayType) String(pm map[string]string, pkgOverride string) string { - s := "[]" - if at.Len > -1 { - s = fmt.Sprintf("[%d]", at.Len) - } - return s + at.Type.String(pm, pkgOverride) -} - -func (at *ArrayType) addImports(im map[string]bool) { at.Type.addImports(im) } - -// ChanType is a channel type. -type ChanType struct { - Dir ChanDir // 0, 1 or 2 - Type Type -} - -func (ct *ChanType) String(pm map[string]string, pkgOverride string) string { - s := ct.Type.String(pm, pkgOverride) - if ct.Dir == RecvDir { - return "<-chan " + s - } - if ct.Dir == SendDir { - return "chan<- " + s - } - return "chan " + s -} - -func (ct *ChanType) addImports(im map[string]bool) { ct.Type.addImports(im) } - -// ChanDir is a channel direction. -type ChanDir int - -const ( - RecvDir ChanDir = 1 - SendDir ChanDir = 2 -) - -// FuncType is a function type. -type FuncType struct { - In, Out []*Parameter - Variadic *Parameter // may be nil -} - -func (ft *FuncType) String(pm map[string]string, pkgOverride string) string { - args := make([]string, len(ft.In)) - for i, p := range ft.In { - args[i] = p.Type.String(pm, pkgOverride) - } - if ft.Variadic != nil { - args = append(args, "..."+ft.Variadic.Type.String(pm, pkgOverride)) - } - rets := make([]string, len(ft.Out)) - for i, p := range ft.Out { - rets[i] = p.Type.String(pm, pkgOverride) - } - retString := strings.Join(rets, ", ") - if nOut := len(ft.Out); nOut == 1 { - retString = " " + retString - } else if nOut > 1 { - retString = " (" + retString + ")" - } - return "func(" + strings.Join(args, ", ") + ")" + retString -} - -func (ft *FuncType) addImports(im map[string]bool) { - for _, p := range ft.In { - p.Type.addImports(im) - } - if ft.Variadic != nil { - ft.Variadic.Type.addImports(im) - } - for _, p := range ft.Out { - p.Type.addImports(im) - } -} - -// MapType is a map type. -type MapType struct { - Key, Value Type -} - -func (mt *MapType) String(pm map[string]string, pkgOverride string) string { - return "map[" + mt.Key.String(pm, pkgOverride) + "]" + mt.Value.String(pm, pkgOverride) -} - -func (mt *MapType) addImports(im map[string]bool) { - mt.Key.addImports(im) - mt.Value.addImports(im) -} - -// NamedType is an exported type in a package. -type NamedType struct { - Package string // may be empty - Type string // TODO: should this be typed Type? -} - -func (nt *NamedType) String(pm map[string]string, pkgOverride string) string { - // TODO: is this right? - if pkgOverride == nt.Package { - return nt.Type - } - prefix := pm[nt.Package] - if prefix != "" { - return prefix + "." + nt.Type - } else { - return nt.Type - } -} -func (nt *NamedType) addImports(im map[string]bool) { - if nt.Package != "" { - im[nt.Package] = true - } -} - -// PointerType is a pointer to another type. -type PointerType struct { - Type Type -} - -func (pt *PointerType) String(pm map[string]string, pkgOverride string) string { - return "*" + pt.Type.String(pm, pkgOverride) -} -func (pt *PointerType) addImports(im map[string]bool) { pt.Type.addImports(im) } - -// PredeclaredType is a predeclared type such as "int". -type PredeclaredType string - -func (pt PredeclaredType) String(pm map[string]string, pkgOverride string) string { return string(pt) } -func (pt PredeclaredType) addImports(im map[string]bool) {} - -// The following code is intended to be called by the program generated by ../reflect.go. - -func InterfaceFromInterfaceType(it reflect.Type) (*Interface, error) { - if it.Kind() != reflect.Interface { - return nil, fmt.Errorf("%v is not an interface", it) - } - intf := &Interface{} - - for i := 0; i < it.NumMethod(); i++ { - mt := it.Method(i) - // TODO: need to skip unexported methods? or just raise an error? - m := &Method{ - Name: mt.Name, - } - - var err error - m.In, m.Variadic, m.Out, err = funcArgsFromType(mt.Type) - if err != nil { - return nil, err - } - - intf.Methods = append(intf.Methods, m) - } - - return intf, nil -} - -// t's Kind must be a reflect.Func. -func funcArgsFromType(t reflect.Type) (in []*Parameter, variadic *Parameter, out []*Parameter, err error) { - nin := t.NumIn() - if t.IsVariadic() { - nin-- - } - var p *Parameter - for i := 0; i < nin; i++ { - p, err = parameterFromType(t.In(i)) - if err != nil { - return - } - in = append(in, p) - } - if t.IsVariadic() { - p, err = parameterFromType(t.In(nin).Elem()) - if err != nil { - return - } - variadic = p - } - for i := 0; i < t.NumOut(); i++ { - p, err = parameterFromType(t.Out(i)) - if err != nil { - return - } - out = append(out, p) - } - return -} - -func parameterFromType(t reflect.Type) (*Parameter, error) { - tt, err := typeFromType(t) - if err != nil { - return nil, err - } - return &Parameter{Type: tt}, nil -} - -var errorType = reflect.TypeOf((*error)(nil)).Elem() - -var byteType = reflect.TypeOf(byte(0)) - -func typeFromType(t reflect.Type) (Type, error) { - // Hack workaround for https://golang.org/issue/3853. - // This explicit check should not be necessary. - if t == byteType { - return PredeclaredType("byte"), nil - } - - if imp := t.PkgPath(); imp != "" { - return &NamedType{ - Package: impPath(imp), - Type: t.Name(), - }, nil - } - - // only unnamed or predeclared types after here - - // Lots of types have element types. Let's do the parsing and error checking for all of them. - var elemType Type - switch t.Kind() { - case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice: - var err error - elemType, err = typeFromType(t.Elem()) - if err != nil { - return nil, err - } - } - - switch t.Kind() { - case reflect.Array: - return &ArrayType{ - Len: t.Len(), - Type: elemType, - }, nil - case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, - reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String: - return PredeclaredType(t.Kind().String()), nil - case reflect.Chan: - var dir ChanDir - switch t.ChanDir() { - case reflect.RecvDir: - dir = RecvDir - case reflect.SendDir: - dir = SendDir - } - return &ChanType{ - Dir: dir, - Type: elemType, - }, nil - case reflect.Func: - in, variadic, out, err := funcArgsFromType(t) - if err != nil { - return nil, err - } - return &FuncType{ - In: in, - Out: out, - Variadic: variadic, - }, nil - case reflect.Interface: - // Two special interfaces. - if t.NumMethod() == 0 { - return PredeclaredType("interface{}"), nil - } - if t == errorType { - return PredeclaredType("error"), nil - } - case reflect.Map: - kt, err := typeFromType(t.Key()) - if err != nil { - return nil, err - } - return &MapType{ - Key: kt, - Value: elemType, - }, nil - case reflect.Ptr: - return &PointerType{ - Type: elemType, - }, nil - case reflect.Slice: - return &ArrayType{ - Len: -1, - Type: elemType, - }, nil - case reflect.Struct: - if t.NumField() == 0 { - return PredeclaredType("struct{}"), nil - } - } - - // TODO: Struct, UnsafePointer - return nil, fmt.Errorf("can't yet turn %v (%v) into a model.Type", t, t.Kind()) -} - -// impPath sanitizes the package path returned by `PkgPath` method of a reflect Type so that -// it is importable. PkgPath might return a path that includes "vendor". These paths do not -// compile, so we need to remove everything up to and including "/vendor/". -// See https://github.com/golang/go/issues/12019. -func impPath(imp string) string { - if strings.HasPrefix(imp, "vendor/") { - imp = "/" + imp - } - if i := strings.LastIndex(imp, "/vendor/"); i != -1 { - imp = imp[i+len("/vendor/"):] - } - return imp -} diff --git a/vendor/modules.txt b/vendor/modules.txt index eaddc78df..557397fd6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -2,7 +2,7 @@ github.com/BurntSushi/toml # github.com/Microsoft/go-winio v0.4.12 github.com/Microsoft/go-winio -# github.com/buildpack/imgutil v0.0.0-20190509214933-76de939dfb34 +# github.com/buildpack/imgutil v0.0.0-20190613212649-bc74f5cf6410 github.com/buildpack/imgutil github.com/buildpack/imgutil/fakes # github.com/docker/distribution v2.7.1+incompatible @@ -36,7 +36,6 @@ github.com/docker/go-units # github.com/gogo/protobuf v1.2.1 github.com/gogo/protobuf/proto # github.com/golang/mock v1.3.0 -github.com/golang/mock/mockgen/model github.com/golang/mock/gomock # github.com/google/go-cmp v0.3.0 github.com/google/go-cmp/cmp