diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 1507ff819b..81c95b8a16 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -30,6 +30,7 @@ import ( "github.com/buildpack/pack/archive" "github.com/buildpack/pack/cache" + "github.com/buildpack/pack/lifecycle" h "github.com/buildpack/pack/testhelpers" ) @@ -37,12 +38,11 @@ var ( packPath string dockerCli *client.Client registryConfig *h.TestRegistryConfig - lifecycleVersion = "0.1.0" runImage = "pack-test/run" buildImage = "pack-test/build" runImageMirror string - packHome string builder string + lifecycleVersion = lifecycle.DefaultLifecycleVersion ) func TestAcceptance(t *testing.T) { @@ -64,15 +64,11 @@ func TestAcceptance(t *testing.T) { } defer os.RemoveAll(packTmpDir) } - var err error dockerCli, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) h.AssertNil(t, err) registryConfig = h.RunRegistry(t, false) defer registryConfig.StopRegistry(t) - if version, ok := os.LookupEnv("LIFECYCLE_VERSION"); ok { - lifecycleVersion = version - } runImageMirror = registryConfig.RepoName(runImage) createStack(t, dockerCli) builder = createBuilder(t, runImageMirror) @@ -134,7 +130,7 @@ func testAcceptance(t *testing.T, when spec.G, it spec.S) { dockerCli.ContainerKill(context.TODO(), containerName, "SIGKILL") dockerCli.ContainerRemove(context.TODO(), containerName, dockertypes.ContainerRemoveOptions{Force: true}) dockerCli.ImageRemove(context.TODO(), repoName, dockertypes.ImageRemoveOptions{Force: true, PruneChildren: true}) - ref, err := name.ParseReference(repoName, name.WeakValidation) + ref, err := name.ParseReference(repoName, name.WeakValidation) h.AssertNil(t, err) cacheImage := cache.New(ref, dockerCli) cacheImage.Clear(context.TODO()) @@ -161,7 +157,7 @@ func testAcceptance(t *testing.T, when spec.G, it spec.S) { assertMockAppRunsWithOutput(t, repoName, "Launch Dep Contents", "Cached Dep Contents") t.Log("it uses the default run image as a base image") - assertHasBase(t, repoName, "packs/run:"+lifecycleVersion) + assertHasBase(t, repoName, runImage) t.Log("sets the run image metadata") runImageLabel := imageLabel(t, dockerCli, repoName, metadata.AppMetadataLabel) @@ -190,8 +186,8 @@ func testAcceptance(t *testing.T, when spec.G, it spec.S) { h.AssertContainsMatch(t, output, `\[analyzer] using cached launch layer 'simple/layers:cached-launch-layer'`) t.Log("exporter and cacher reuse unchanged layers") - h.AssertContainsMatch(t, output, `\[exporter] reusing layer 'simple/layers:cached-launch-layer'`) - h.AssertContainsMatch(t, output, `\[cacher] reusing layer 'simple/layers:cached-launch-layer'`) + h.AssertContainsMatch(t, output, `(?i)\[exporter] reusing layer 'simple/layers:cached-launch-layer'`) + h.AssertContainsMatch(t, output, `(?i)\[cacher] reusing layer 'simple/layers:cached-launch-layer'`) t.Log("rebuild with --clear-cache") cmd = packCmd("build", repoName, "-p", "testdata/mock_app/.", "--clear-cache") @@ -205,10 +201,14 @@ func testAcceptance(t *testing.T, when spec.G, it spec.S) { h.AssertContains(t, output, "Skipping 'analyze' due to clearing cache") t.Log("exporter reuses unchanged layers") - h.AssertContainsMatch(t, output, `\[exporter] reusing layer 'simple/layers:cached-launch-layer'`) + h.AssertContainsMatch(t, output, `(?i)\[exporter] reusing layer 'simple/layers:cached-launch-layer'`) t.Log("cacher adds layers") - h.AssertContainsMatch(t, output, `\[cacher] adding layer 'simple/layers:cached-launch-layer'`) + if lifecycleVersion == "0.1.0" { + h.AssertContainsMatch(t, output, `\[cacher] adding layer 'simple/layers:cached-launch-layer'`) + } else { + h.AssertContainsMatch(t, output, `\[cacher] Caching layer 'simple/layers:cached-launch-layer'`) + } }) when("--buildpack", func() { @@ -677,38 +677,18 @@ func testAcceptance(t *testing.T, when spec.G, it spec.S) { when("pack inspect-builder", func() { it("displays configuration for a builder (local and remote)", func() { - configuredRunImage := "some-registry.com/some/run1" - - builderImageName := h.CreateImageOnRemote(t, dockerCli, registryConfig, "some/builder", - fmt.Sprintf(` - FROM scratch - ENV CNB_USER_ID=1234 - ENV CNB_GROUP_ID=4321 - LABEL %s="{\"stack\":{\"runImage\":{\"image\":\"some/run1\",\"mirrors\":[\"gcr.io/some/run1\"]}},\"buildpacks\":[{\"id\":\"test.bp.one\",\"version\":\"0.0.1\",\"latest\":false},{\"id\":\"test.bp.two\",\"version\":\"0.0.2\",\"latest\":true}],\"groups\":[{\"buildpacks\":[{\"id\":\"test.bp.one\",\"version\":\"0.0.1\"},{\"id\":\"test.bp.two\",\"version\":\"0.0.2\"}]},{\"buildpacks\":[{\"id\":\"test.bp.one\",\"version\":\"0.0.1\"}]}]}" - LABEL io.buildpacks.stack.id=some.test.stack - `, "io.buildpacks.builder.metadata")) - - h.CreateImageOnLocal(t, dockerCli, builderImageName, - fmt.Sprintf(` - FROM scratch - ENV CNB_USER_ID=1234 - ENV CNB_GROUP_ID=4321 - LABEL %s="{\"stack\":{\"runImage\":{\"image\":\"some/run1\",\"mirrors\":[\"gcr.io/some/run2\"]}},\"buildpacks\":[{\"id\":\"test.bp.one\",\"version\":\"0.0.1\",\"latest\":false},{\"id\":\"test.bp.two\",\"version\":\"0.0.2\",\"latest\":true}],\"groups\":[{\"buildpacks\":[{\"id\":\"test.bp.one\",\"version\":\"0.0.1\"},{\"id\":\"test.bp.two\",\"version\":\"0.0.2\"}]},{\"buildpacks\":[{\"id\":\"test.bp.one\",\"version\":\"0.0.1\"}]}]}" - LABEL io.buildpacks.stack.id=some.test.stack - `, "io.buildpacks.builder.metadata")) - defer h.DockerRmi(dockerCli, builderImageName) - - cmd := packCmd("set-run-image-mirrors", "some/run1", "--mirror", configuredRunImage) + configuredRunImage := "some-registry.com/pack-test/run1" + cmd := packCmd("set-run-image-mirrors", "pack-test/run", "--mirror", configuredRunImage) output := h.Run(t, cmd) - h.AssertEq(t, output, "Run Image 'some/run1' configured with mirror 'some-registry.com/some/run1'\n") + h.AssertEq(t, output, "Run Image 'pack-test/run' configured with mirror 'some-registry.com/pack-test/run1'\n") - cmd = packCmd("inspect-builder", builderImageName) + cmd = packCmd("inspect-builder", builder) output = h.Run(t, cmd) expected, err := ioutil.ReadFile(filepath.Join("testdata", "inspect_builder_output.txt")) h.AssertNil(t, err) - h.AssertEq(t, output, fmt.Sprintf(string(expected), builderImageName)) + h.AssertEq(t, output, fmt.Sprintf(string(expected), builder, lifecycleVersion, runImageMirror, lifecycleVersion, runImageMirror)) }) }) } @@ -720,29 +700,48 @@ func createBuilder(t *testing.T, runImageMirror string) string { tmpDir, err := ioutil.TempDir("", "create-test-builder") h.AssertNil(t, err) defer os.RemoveAll(tmpDir) + h.RecursiveCopy(t, filepath.Join("testdata", "mock_buildpacks"), tmpDir) + builderConfigFile, err := os.OpenFile(filepath.Join(tmpDir, "builder.toml"), os.O_RDWR|os.O_APPEND, 0666) h.AssertNil(t, err) - _, err = builderConfigFile.Write([]byte( - fmt.Sprintf(`run-image-mirrors = ["%s"]`, runImageMirror))) + + _, err = builderConfigFile.Write([]byte(fmt.Sprintf("run-image-mirrors = [\"%s\"]\n", runImageMirror))) + h.AssertNil(t, err) + + _, err = builderConfigFile.Write([]byte("[lifecycle]\n")) h.AssertNil(t, err) + if lifecyclePath, ok := os.LookupEnv("LIFECYCLE_PATH"); ok { + lifecycleVersion = "Unknown" + if !filepath.IsAbs(lifecyclePath) { + t.Fatal("LIFECYCLE_PATH must be an absolute path") + } + _, err = builderConfigFile.Write([]byte(fmt.Sprintf("uri = \"%s\"\n", lifecyclePath))) + h.AssertNil(t, err) + } + if lcver, ok := os.LookupEnv("LIFECYCLE_VERSION"); ok { + lifecycleVersion = lcver + _, err = builderConfigFile.Write([]byte(fmt.Sprintf("version = \"%s\"\n", lifecycleVersion))) + h.AssertNil(t, err) + } + builderConfigFile.Close() - builder := "some-org/" + h.RandString(10) + builder := registryConfig.RepoName("some-org/" + h.RandString(10)) cmd := exec.Command(packPath, "create-builder", "--no-color", builder, "-b", filepath.Join(tmpDir, "builder.toml")) output := h.Run(t, cmd) - h.AssertContains(t, output, fmt.Sprintf("Successfully created builder image '%s'", builder)) + h.AssertNil(t, h.PushImage(dockerCli, builder, registryConfig)) + return builder } func createStack(t *testing.T, dockerCli *client.Client) { t.Log("create stack images") - createStackImage(t, dockerCli, runImage, filepath.Join("testdata", "mock_stack", "run")) - createStackImage(t, dockerCli, buildImage, filepath.Join("testdata", "mock_stack", "build")) - err := dockerCli.ImageTag(context.Background(), runImage, runImageMirror) - h.AssertNil(t, err) + createStackImage(t, dockerCli, runImage, filepath.Join("testdata", "mock_stack")) + h.AssertNil(t, dockerCli.ImageTag(context.Background(), runImage, buildImage)) + h.AssertNil(t, dockerCli.ImageTag(context.Background(), runImage, runImageMirror)) h.AssertNil(t, h.PushImage(dockerCli, runImageMirror, registryConfig)) } @@ -754,7 +753,6 @@ func createStackImage(t *testing.T, dockerCli *client.Client, repoName string, d Tags: []string{repoName}, Remove: true, ForceRemove: true, - BuildArgs: map[string]*string{"lifecycleVersion": &lifecycleVersion}, }) h.AssertNil(t, err) @@ -807,7 +805,7 @@ func fetchHostPort(t *testing.T, dockerID string) string { func imgSHAFromOutput(txt, repoName string) (string, error) { for _, m := range regexp.MustCompile(`\*\*\* Image: (.+)@(.+)`).FindAllStringSubmatch(txt, -1) { // remove the :latest tag check once we fix tag + sha output error in lifecycle - if m[1] == repoName || m[1] == repoName + ":latest" { + if m[1] == repoName || m[1] == repoName+":latest" { return m[2], nil } } @@ -819,9 +817,10 @@ func runDockerImageExposePort(t *testing.T, containerName, repoName string) stri ctx := context.Background() ctr, err := dockerCli.ContainerCreate(ctx, &container.Config{ - Image: repoName, - Env: []string{"PORT=8080"}, - Healthcheck: nil, + Image: repoName, + Env: []string{"PORT=8080"}, + ExposedPorts: map[nat.Port]struct{}{"8080/tcp": {}}, + Healthcheck: nil, }, &container.HostConfig{ PortBindings: nat.PortMap{ "8080/tcp": []nat.PortBinding{{}}, diff --git a/acceptance/testdata/inspect_builder_output.txt b/acceptance/testdata/inspect_builder_output.txt index f2c56cc0df..2fabc5c238 100644 --- a/acceptance/testdata/inspect_builder_output.txt +++ b/acceptance/testdata/inspect_builder_output.txt @@ -3,43 +3,45 @@ Inspecting builder: '%s' Remote ------ -Stack: some.test.stack +Stack: pack.test.stack + +Lifecycle Version: %s Run Images: - some-registry.com/some/run1 (user-configured) - some/run1 - gcr.io/some/run1 + some-registry.com/pack-test/run1 (user-configured) + pack-test/run + %s Buildpacks: - ID VERSION LATEST - test.bp.one 0.0.1 false - test.bp.two 0.0.2 true + ID VERSION LATEST + simple/layers simple-layers-version false + read/env read-env-version false + noop.buildpack noop.buildpack.version true Detection Order: Group #1: - test.bp.one@0.0.1 - test.bp.two@0.0.2 - Group #2: - test.bp.one@0.0.1 + simple/layers@simple-layers-version + read/env@read-env-version Local ----- -Stack: some.test.stack +Stack: pack.test.stack + +Lifecycle Version: %s Run Images: - some-registry.com/some/run1 (user-configured) - some/run1 - gcr.io/some/run2 + some-registry.com/pack-test/run1 (user-configured) + pack-test/run + %s Buildpacks: - ID VERSION LATEST - test.bp.one 0.0.1 false - test.bp.two 0.0.2 true + ID VERSION LATEST + simple/layers simple-layers-version false + read/env read-env-version false + noop.buildpack noop.buildpack.version true Detection Order: Group #1: - test.bp.one@0.0.1 - test.bp.two@0.0.2 - Group #2: - test.bp.one@0.0.1 + simple/layers@simple-layers-version + read/env@read-env-version diff --git a/acceptance/testdata/mock_buildpacks/builder.toml b/acceptance/testdata/mock_buildpacks/builder.toml index d14669044a..9ca4acf40d 100644 --- a/acceptance/testdata/mock_buildpacks/builder.toml +++ b/acceptance/testdata/mock_buildpacks/builder.toml @@ -25,4 +25,4 @@ build-image = "pack-test/build" run-image = "pack-test/run" -# run-image-mirror is append by accpetance tests +# run-image-mirror and lifecycle are appended by accpetance tests diff --git a/acceptance/testdata/mock_stack/Dockerfile b/acceptance/testdata/mock_stack/Dockerfile new file mode 100644 index 0000000000..b0ec6e0f5b --- /dev/null +++ b/acceptance/testdata/mock_stack/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:bionic + +ENV CNB_USER_ID=2222 +ENV CNB_GROUP_ID=3333 + +RUN \ + groupadd pack --gid 3333 && \ + useradd --uid 2222 --gid 3333 -m -s /bin/bash pack + +RUN apt-get update && apt-get -yq install netcat +LABEL io.buildpacks.stack.id=pack.test.stack + +RUN mkdir /layers && chown pack:pack /layers + +USER pack +WORKDIR /layers + + diff --git a/acceptance/testdata/mock_stack/build/Dockerfile b/acceptance/testdata/mock_stack/build/Dockerfile deleted file mode 100644 index f4b1df6892..0000000000 --- a/acceptance/testdata/mock_stack/build/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -ARG lifecycleVersion="0.1.0" -FROM packs/build:${lifecycleVersion} - -LABEL io.buildpacks.stack.id=pack.test.stack - diff --git a/acceptance/testdata/mock_stack/create-stack.sh b/acceptance/testdata/mock_stack/create-stack.sh index a5ae0f1f24..878c1017f4 100755 --- a/acceptance/testdata/mock_stack/create-stack.sh +++ b/acceptance/testdata/mock_stack/create-stack.sh @@ -2,5 +2,5 @@ dir="$(cd $(dirname $0) && pwd)" -docker build --tag pack-test/build "$dir/build" -docker build --tag pack-test/run "$dir/run" \ No newline at end of file +docker build --tag pack-test/build "$dir" +docker tag pack-test/build pack-test/run \ No newline at end of file diff --git a/acceptance/testdata/mock_stack/run/Dockerfile b/acceptance/testdata/mock_stack/run/Dockerfile deleted file mode 100644 index f39784e2bb..0000000000 --- a/acceptance/testdata/mock_stack/run/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -ARG lifecycleVersion="0.1.0" -FROM packs/run:${lifecycleVersion} - -USER root -RUN apt-get update && apt-get -yq install netcat -USER pack - -LABEL io.buildpacks.stack.id=pack.test.stack - diff --git a/build/lifecycle.go b/build/lifecycle.go index fadbecc55d..c9bc61c3a4 100644 --- a/build/lifecycle.go +++ b/build/lifecycle.go @@ -65,6 +65,12 @@ func (l *Lifecycle) Execute(ctx context.Context, opts LifecycleOptions) error { l.Setup(opts.AppDir, opts.Builder) defer l.Cleanup() + if lifecycleVersion := l.Builder.GetLifecycleVersion(); lifecycleVersion == "" { + l.logger.Verbose("Warning: lifecycle version unknown") + }else { + l.logger.Verbose("Executing lifecycle version %s", style.Symbol(lifecycleVersion)) + } + l.logger.Verbose(style.Step("DETECTING")) if err := l.Detect(ctx); err != nil { return err diff --git a/build/testdata/fake-lifecycle/phase.go b/build/testdata/fake-lifecycle/phase.go index 34c99e1951..b3c4d1040a 100644 --- a/build/testdata/fake-lifecycle/phase.go +++ b/build/testdata/fake-lifecycle/phase.go @@ -78,7 +78,7 @@ func testRegistryAccess(repoName string) { } _, err = v1remote.Image(ref, v1remote.WithAuth(auth)) if err != nil { - fmt.Println("failed to access image") + fmt.Println("failed to access image:", err) os.Exit(6) } } diff --git a/build_test.go b/build_test.go index 893a0344cb..36ba801667 100644 --- a/build_test.go +++ b/build_test.go @@ -96,8 +96,9 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { clientConfig, logging.NewLogger(logOut, logErr, true, false), fakeImageFetcher, + buildpack.NewFetcher(NewDownloader(logging.NewLogger(logOut, logErr, true, false), tmpDir)), + nil, fakeLifecycle, - buildpack.NewFetcher(logging.NewLogger(logOut, logErr, true, false), tmpDir), docker, ) }) diff --git a/builder/builder.go b/builder/builder.go index d5e2250e17..f553e97aa6 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -17,6 +17,7 @@ import ( "github.com/buildpack/pack/archive" "github.com/buildpack/pack/buildpack" + "github.com/buildpack/pack/lifecycle" "github.com/buildpack/pack/stack" "github.com/buildpack/pack/style" ) @@ -24,18 +25,20 @@ import ( const ( buildpacksDir = "/buildpacks" platformDir = "/platform" + lifecycleDir = "/lifecycle" stackLabel = "io.buildpacks.stack.id" envUID = "CNB_USER_ID" envGID = "CNB_GROUP_ID" ) type Builder struct { - image imgutil.Image - buildpacks []buildpack.Buildpack - metadata Metadata - env map[string]string - UID, GID int - StackID string + image imgutil.Image + lifecyclePath string + buildpacks []buildpack.Buildpack + metadata Metadata + env map[string]string + UID, GID int + StackID string } func GetBuilder(img imgutil.Image) (*Builder, error) { @@ -72,6 +75,10 @@ func GetBuilder(img imgutil.Image) (*Builder, error) { }, nil } +func (b *Builder) GetLifecycleVersion() string { + return b.metadata.Lifecycle.Version +} + func (b *Builder) GetBuildpacks() []BuildpackMetadata { return b.metadata.Buildpacks } @@ -134,6 +141,12 @@ func (b *Builder) AddBuildpack(bp buildpack.Buildpack) error { return nil } +func (b *Builder) SetLifecycle(md lifecycle.Metadata) error { + b.metadata.Lifecycle.Version = md.Version + b.lifecyclePath = md.Dir + return nil +} + func (b *Builder) SetEnv(env map[string]string) { b.env = env } @@ -201,6 +214,16 @@ func (b *Builder) Save() error { return errors.Wrap(err, "adding env layer") } + if b.lifecyclePath != "" { + lifecycleTar, err := b.lifecycleLayer(tmpDir) + if err != nil { + return err + } + if err := b.image.AddLayer(lifecycleTar); err != nil { + return errors.Wrap(err, "adding lifecycle layer") + } + } + for _, bp := range b.buildpacks { layerTar, err := b.buildpackLayer(tmpDir, bp) if err != nil { @@ -365,3 +388,45 @@ func (b *Builder) envLayer(dest string, env map[string]string) (string, error) { return fh.Name(), nil } + +func (b *Builder) lifecycleLayer(dest string) (string, error) { + fh, err := os.Create(filepath.Join(dest, "lifecycle.tar")) + if err != nil { + return "", err + } + defer fh.Close() + + tw := tar.NewWriter(fh) + defer tw.Close() + + now := time.Now() + + for _, binary := range []string{"detector", "restorer", "analyzer", "builder", "exporter", "cacher", "launcher"} { + if err := writeLifecycleBinary(b.lifecyclePath, binary, tw, now); err != nil { + return "", err + } + } + + if err := tw.WriteHeader(&tar.Header{Typeflag: tar.TypeDir, Name: lifecycleDir, Mode: 0555, ModTime: now}); err != nil { + return "", err + } + + return fh.Name(), nil +} + +func writeLifecycleBinary(lifecyclePath, binary string, tw *tar.Writer, now time.Time) error { + buf, err := ioutil.ReadFile(filepath.Join(lifecyclePath, binary)) + if err != nil { + return errors.Wrap(err, "reading lifecycle binary") + } + + if err := tw.WriteHeader(&tar.Header{Name: lifecycleDir + "/" + binary, Size: int64(len(buf)), Mode: 0555, ModTime: now}); err != nil { + return err + } + + if _, err := tw.Write([]byte(buf)); err != nil { + return err + } + + return nil +} diff --git a/builder/builder_test.go b/builder/builder_test.go index 30c1861eea..d0decebd5a 100644 --- a/builder/builder_test.go +++ b/builder/builder_test.go @@ -16,7 +16,7 @@ import ( "github.com/buildpack/pack/builder" "github.com/buildpack/pack/buildpack" - + "github.com/buildpack/pack/lifecycle" h "github.com/buildpack/pack/testhelpers" ) @@ -119,6 +119,34 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) { }) }) + // TODO : check lifecycle bin owner or perms + when("#SetLifecycle", func() { + it.Before(func() { + h.AssertNil(t, subject.SetLifecycle(lifecycle.Metadata{ + Version: "1.2.3", + Dir: filepath.Join("testdata", "lifecycle"), + })) + h.AssertNil(t, subject.Save()) + h.AssertEq(t, baseImage.IsSaved(), true) + }) + + it("should set the lifecycle version successfully", func() { + h.AssertEq(t, subject.GetLifecycleVersion(), "1.2.3") + }) + + it("should add the lifecycle binaries as an image layer", func() { + layerTar, err := baseImage.FindLayerWithPath("/lifecycle") + h.AssertNil(t, err) + assertTarFileContents(t, layerTar, "/lifecycle/detector", "detector") + assertTarFileContents(t, layerTar, "/lifecycle/restorer", "restorer") + assertTarFileContents(t, layerTar, "/lifecycle/analyzer", "analyzer") + assertTarFileContents(t, layerTar, "/lifecycle/builder", "builder") + assertTarFileContents(t, layerTar, "/lifecycle/exporter", "exporter") + assertTarFileContents(t, layerTar, "/lifecycle/cacher", "cacher") + assertTarFileContents(t, layerTar, "/lifecycle/launcher", "launcher") + }) + }) + when("#AddBuildpack", func() { when("buildpack has matching stack", func() { it.Before(func() { diff --git a/builder/config.go b/builder/config.go index ca3dd9fa36..d6f696377a 100644 --- a/builder/config.go +++ b/builder/config.go @@ -3,7 +3,8 @@ package builder type Config struct { Buildpacks []BuildpackConfig `toml:"buildpacks"` Groups []GroupMetadata `toml:"groups"` - Stack StackConfig + Stack StackConfig `toml:"stack"` + Lifecycle LifecycleConfig `toml:"lifecycle"` } type BuildpackConfig struct { @@ -18,3 +19,8 @@ type StackConfig struct { RunImage string `toml:"run-image"` RunImageMirrors []string `toml:"run-image-mirrors,omitempty"` } + +type LifecycleConfig struct { + URI string `toml:"uri"` + Version string `toml:"version"` +} diff --git a/builder/metadata.go b/builder/metadata.go index 7e6ad6b6a9..ad71e3063f 100644 --- a/builder/metadata.go +++ b/builder/metadata.go @@ -1,6 +1,7 @@ package builder import ( + "github.com/buildpack/pack/lifecycle" "github.com/buildpack/pack/stack" ) @@ -10,6 +11,7 @@ type Metadata struct { Buildpacks []BuildpackMetadata `json:"buildpacks"` Groups []GroupMetadata `json:"groups"` Stack stack.Metadata `json:"stack"` + Lifecycle lifecycle.Metadata `json:"lifecycle"` } type BuildpackMetadata struct { diff --git a/builder/testdata/lifecycle/analyzer b/builder/testdata/lifecycle/analyzer new file mode 100644 index 0000000000..2c7cce34c1 --- /dev/null +++ b/builder/testdata/lifecycle/analyzer @@ -0,0 +1 @@ +analyzer \ No newline at end of file diff --git a/builder/testdata/lifecycle/builder b/builder/testdata/lifecycle/builder new file mode 100644 index 0000000000..b05c21cd9d --- /dev/null +++ b/builder/testdata/lifecycle/builder @@ -0,0 +1 @@ +builder \ No newline at end of file diff --git a/builder/testdata/lifecycle/cacher b/builder/testdata/lifecycle/cacher new file mode 100644 index 0000000000..3ad3632e14 --- /dev/null +++ b/builder/testdata/lifecycle/cacher @@ -0,0 +1 @@ +cacher \ No newline at end of file diff --git a/builder/testdata/lifecycle/detector b/builder/testdata/lifecycle/detector new file mode 100644 index 0000000000..4ca7e105c9 --- /dev/null +++ b/builder/testdata/lifecycle/detector @@ -0,0 +1 @@ +detector \ No newline at end of file diff --git a/builder/testdata/lifecycle/exporter b/builder/testdata/lifecycle/exporter new file mode 100644 index 0000000000..76a0149ce4 --- /dev/null +++ b/builder/testdata/lifecycle/exporter @@ -0,0 +1 @@ +exporter \ No newline at end of file diff --git a/builder/testdata/lifecycle/launcher b/builder/testdata/lifecycle/launcher new file mode 100644 index 0000000000..89f76d0bc2 --- /dev/null +++ b/builder/testdata/lifecycle/launcher @@ -0,0 +1 @@ +launcher \ No newline at end of file diff --git a/builder/testdata/lifecycle/restorer b/builder/testdata/lifecycle/restorer new file mode 100644 index 0000000000..f6d18366f2 --- /dev/null +++ b/builder/testdata/lifecycle/restorer @@ -0,0 +1 @@ +restorer \ No newline at end of file diff --git a/buildpack/fetcher.go b/buildpack/fetcher.go index 72bcc8baca..02d2f1dba6 100644 --- a/buildpack/fetcher.go +++ b/buildpack/fetcher.go @@ -1,23 +1,16 @@ package buildpack import ( - "crypto/sha256" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "os" "path/filepath" "github.com/BurntSushi/toml" "github.com/pkg/errors" - - "github.com/buildpack/pack/archive" ) -type Logger interface { - Verbose(format string, a ...interface{}) +//go:generate mockgen -package mocks -destination mocks/downloader.go github.com/buildpack/pack/lifecycle Downloader + +type Downloader interface { + Download(uri string) (string, error) } type buildpackTOML struct { @@ -29,31 +22,17 @@ type buildpackTOML struct { } type Fetcher struct { - Logger Logger - CacheDir string + downloader Downloader } -func NewFetcher(logger Logger, cacheDir string) *Fetcher { - return &Fetcher{ - Logger: logger, - CacheDir: filepath.Join(cacheDir, "dl-cache"), - } +func NewFetcher(downloader Downloader) *Fetcher { + return &Fetcher{downloader: downloader} } func (f *Fetcher) FetchBuildpack(uri string) (Buildpack, error) { - bpURL, err := url.Parse(uri) + dir, err := f.downloader.Download(uri) if err != nil { - return Buildpack{}, err - } - - var dir string - switch bpURL.Scheme { - case "", "file": - dir, err = f.handleFile(bpURL) - case "http", "https": - dir, err = f.handleHTTP(uri) - default: - return Buildpack{}, fmt.Errorf("unsupported protocol in URI %q", uri) + return Buildpack{}, errors.Wrap(err, "fetching buildpack") } data, err := readTOML(filepath.Join(dir, "buildpack.toml")) @@ -77,107 +56,3 @@ func readTOML(path string) (buildpackTOML, error) { } return data, nil } - -func (f *Fetcher) handleFile(bpURL *url.URL) (string, error) { - path := bpURL.Path - - if filepath.Ext(path) != ".tgz" { - return path, nil - } - - file, err := os.Open(path) - if err != nil { - return "", errors.Wrapf(err, "could not open file to untar: %q", path) - } - defer file.Close() - - tmpDir, err := ioutil.TempDir("", "") - if err != nil { - return "", fmt.Errorf(`failed to create temporary directory: %s`, err) - } - - if err = archive.ExtractTarGZ(file, tmpDir); err != nil { - return "", err - } - - return tmpDir, nil -} - -func (f *Fetcher) handleHTTP(uri string) (string, error) { - bpCache := filepath.Join(f.CacheDir, fmt.Sprintf("%x", sha256.Sum256([]byte(uri)))) - if err := os.MkdirAll(bpCache, 0744); err != nil { - return "", err - } - - etagFile := bpCache + ".etag" - etagExists, err := fileExists(etagFile) - if err != nil { - return "", err - } - - etag := "" - if etagExists { - bytes, err := ioutil.ReadFile(etagFile) - if err != nil { - return "", err - } - etag = string(bytes) - } - - reader, etag, err := f.downloadAsStream(uri, etag) - if err != nil { - return "", errors.Wrapf(err, "failed to download from %q", uri) - } else if reader == nil { - return bpCache, nil - } - defer reader.Close() - - if err = archive.ExtractTarGZ(reader, bpCache); err != nil { - return "", err - } - - if err = ioutil.WriteFile(etagFile, []byte(etag), 0744); err != nil { - return "", err - } - - return bpCache, nil -} - -func (f *Fetcher) downloadAsStream(uri string, etag string) (io.ReadCloser, string, error) { - req, err := http.NewRequest("GET", uri, nil) - if err != nil { - return nil, "", err - } - - if etag != "" { - req.Header.Set("If-None-Match", etag) - } - - resp, err := (&http.Client{}).Do(req) - if err != nil { - return nil, "", err - } - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - f.Logger.Verbose("Downloading from %q\n", uri) - return resp.Body, resp.Header.Get("Etag"), nil - } - - if resp.StatusCode == 304 { - f.Logger.Verbose("Using cached version of %q\n", uri) - return nil, etag, nil - } - - return nil, "", fmt.Errorf("could not download from %q, code http status %d", uri, resp.StatusCode) -} - -func fileExists(file string) (bool, error) { - _, err := os.Stat(file) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - return true, nil -} diff --git a/buildpack/fetcher_test.go b/buildpack/fetcher_test.go index e38c572055..5efb91c08b 100644 --- a/buildpack/fetcher_test.go +++ b/buildpack/fetcher_test.go @@ -1,120 +1,49 @@ package buildpack_test import ( - "io/ioutil" - "net/http" - "os" "path/filepath" - "runtime" "testing" - "github.com/fatih/color" - "github.com/onsi/gomega/ghttp" + "github.com/golang/mock/gomock" "github.com/sclevine/spec" "github.com/sclevine/spec/report" + "github.com/buildpack/pack/lifecycle/mocks" + "github.com/buildpack/pack/buildpack" h "github.com/buildpack/pack/testhelpers" ) func TestBuildpackFetcher(t *testing.T) { - h.RequireDocker(t) - color.NoColor = true - if runtime.GOOS == "windows" { - t.Skip("create builder is not implemented on windows") - } spec.Run(t, "BuildpackFetcher", testBuildpackFetcher, spec.Parallel(), spec.Report(report.Terminal{})) } -type emptyLogger struct { -} - -func (e *emptyLogger) Verbose(format string, a ...interface{}) { -} - func testBuildpackFetcher(t *testing.T, when spec.G, it spec.S) { when("#FetchBuildpack", func() { var ( - err error - tmpDir string - cacheDir string - subject *buildpack.Fetcher + mockController *gomock.Controller + mockDownloader *mocks.MockDownloader + subject *buildpack.Fetcher ) it.Before(func() { - tmpDir, err = ioutil.TempDir("", "") - h.AssertNil(t, err) - - cacheDir, err = ioutil.TempDir("", "") - h.AssertNil(t, err) + mockController = gomock.NewController(t) + mockDownloader = mocks.NewMockDownloader(mockController) - subject = buildpack.NewFetcher(&emptyLogger{}, cacheDir) + subject = buildpack.NewFetcher(mockDownloader) }) it.After(func() { - os.RemoveAll(tmpDir) - os.RemoveAll(cacheDir) - }) - - it("fetches from a relative directory", func() { - out, err := subject.FetchBuildpack(filepath.Join("testdata", "buildpack")) - h.AssertNil(t, err) - h.AssertEq(t, out.ID, "bp.one") - h.AssertEq(t, out.Version, "some-buildpack-version") - h.AssertEq(t, out.Stacks[0].ID, "some.stack.id") - h.AssertEq(t, out.Stacks[1].ID, "other.stack.id") - h.AssertNotEq(t, out.Dir, "") - h.AssertDirContainsFileWithContents(t, out.Dir, "bin/detect", "I come from a directory\n") - h.AssertDirContainsFileWithContents(t, out.Dir, "bin/build", "I come from a directory\n") - }) - - it("fetches from a relative tgz", func() { - out, err := subject.FetchBuildpack(filepath.Join("testdata", "buildpack.tgz")) - h.AssertNil(t, err) - h.AssertEq(t, out.ID, "bp.one") - h.AssertEq(t, out.Version, "some-buildpack-version") - h.AssertEq(t, out.Stacks[0].ID, "some.stack.id") - h.AssertEq(t, out.Stacks[1].ID, "other.stack.id") - h.AssertNotEq(t, out.Dir, "") - h.AssertDirContainsFileWithContents(t, out.Dir, "bin/detect", "I come from an archive\n") - h.AssertDirContainsFileWithContents(t, out.Dir, "bin/build", "I come from an archive\n") - }) - - it("fetches from an absolute directory", func() { - absPath, err := filepath.Abs(filepath.Join("testdata", "buildpack")) - h.AssertNil(t, err) - - out, err := subject.FetchBuildpack(absPath) - h.AssertNil(t, err) - h.AssertEq(t, out.ID, "bp.one") - h.AssertEq(t, out.Version, "some-buildpack-version") - h.AssertEq(t, out.Stacks[0].ID, "some.stack.id") - h.AssertEq(t, out.Stacks[1].ID, "other.stack.id") - h.AssertNotEq(t, out.Dir, "") - h.AssertDirContainsFileWithContents(t, out.Dir, "bin/detect", "I come from a directory\n") - h.AssertDirContainsFileWithContents(t, out.Dir, "bin/build", "I come from a directory\n") + mockController.Finish() }) - it("fetches from an absolute tgz", func() { - absPath, err := filepath.Abs(filepath.Join("testdata", "buildpack.tgz")) - h.AssertNil(t, err) + it("fetches a buildpack", func() { + downloadPath := filepath.Join("testdata", "buildpack") + mockDownloader.EXPECT(). + Download(downloadPath). + Return(downloadPath, nil) - out, err := subject.FetchBuildpack(absPath) - h.AssertNil(t, err) - h.AssertEq(t, out.ID, "bp.one") - h.AssertEq(t, out.Version, "some-buildpack-version") - h.AssertEq(t, out.Stacks[0].ID, "some.stack.id") - h.AssertEq(t, out.Stacks[1].ID, "other.stack.id") - h.AssertNotEq(t, out.Dir, "") - h.AssertDirContainsFileWithContents(t, out.Dir, "bin/detect", "I come from an archive\n") - h.AssertDirContainsFileWithContents(t, out.Dir, "bin/build", "I come from an archive\n") - }) - - it("fetches from a 'file://' URI directory", func() { - absPath, err := filepath.Abs(filepath.Join("testdata", "buildpack")) - h.AssertNil(t, err) - - out, err := subject.FetchBuildpack("file://" + absPath) + out, err := subject.FetchBuildpack(downloadPath) h.AssertNil(t, err) h.AssertEq(t, out.ID, "bp.one") h.AssertEq(t, out.Version, "some-buildpack-version") @@ -124,39 +53,5 @@ func testBuildpackFetcher(t *testing.T, when spec.G, it spec.S) { h.AssertDirContainsFileWithContents(t, out.Dir, "bin/detect", "I come from a directory\n") h.AssertDirContainsFileWithContents(t, out.Dir, "bin/build", "I come from a directory\n") }) - - it("fetches from a 'file://' URI tgz", func() { - absPath, err := filepath.Abs(filepath.Join("testdata", "buildpack.tgz")) - h.AssertNil(t, err) - - out, err := subject.FetchBuildpack("file://" + absPath) - h.AssertNil(t, err) - h.AssertEq(t, out.ID, "bp.one") - h.AssertEq(t, out.Version, "some-buildpack-version") - h.AssertEq(t, out.Stacks[0].ID, "some.stack.id") - h.AssertEq(t, out.Stacks[1].ID, "other.stack.id") - h.AssertNotEq(t, out.Dir, "") - h.AssertDirContainsFileWithContents(t, out.Dir, "bin/detect", "I come from an archive\n") - h.AssertDirContainsFileWithContents(t, out.Dir, "bin/build", "I come from an archive\n") - }) - - it("fetches from a 'http(s)://' URI tgz", func() { - server := ghttp.NewServer() - server.AppendHandlers(func(w http.ResponseWriter, r *http.Request) { - path := filepath.Join("testdata", r.URL.Path) - http.ServeFile(w, r, path) - }) - defer server.Close() - - out, err := subject.FetchBuildpack(server.URL() + "/buildpack.tgz") - h.AssertNil(t, err) - h.AssertEq(t, out.ID, "bp.one") - h.AssertEq(t, out.Version, "some-buildpack-version") - h.AssertEq(t, out.Stacks[0].ID, "some.stack.id") - h.AssertEq(t, out.Stacks[1].ID, "other.stack.id") - h.AssertNotEq(t, out.Dir, "") - h.AssertDirContainsFileWithContents(t, out.Dir, "bin/detect", "I come from an archive\n") - h.AssertDirContainsFileWithContents(t, out.Dir, "bin/build", "I come from an archive\n") - }) }) } diff --git a/buildpack/testdata/buildpack.tgz b/buildpack/testdata/buildpack.tgz deleted file mode 100644 index 3f333f41c2..0000000000 Binary files a/buildpack/testdata/buildpack.tgz and /dev/null differ diff --git a/client.go b/client.go index ea9f175cdf..8a16749034 100644 --- a/client.go +++ b/client.go @@ -1,8 +1,12 @@ package pack import ( + "path/filepath" + "github.com/docker/docker/client" + "github.com/buildpack/pack/lifecycle" + "github.com/buildpack/pack/build" "github.com/buildpack/pack/buildpack" "github.com/buildpack/pack/config" @@ -15,6 +19,7 @@ type Client struct { logger *logging.Logger imageFetcher ImageFetcher buildpackFetcher BuildpackFetcher + lifecycleFetcher LifecycleFetcher lifecycle Lifecycle docker *client.Client } @@ -23,8 +28,9 @@ func NewClient( config *config.Config, logger *logging.Logger, imageFetcher ImageFetcher, - lifecycle Lifecycle, buildpackFetcher BuildpackFetcher, + lifecycleFetcher LifecycleFetcher, + lifecycle Lifecycle, docker *client.Client, ) *Client { return &Client{ @@ -32,6 +38,7 @@ func NewClient( logger: logger, imageFetcher: imageFetcher, buildpackFetcher: buildpackFetcher, + lifecycleFetcher: lifecycleFetcher, lifecycle: lifecycle, docker: docker, } @@ -43,11 +50,14 @@ func DefaultClient(config *config.Config, logger *logging.Logger) (*Client, erro return nil, err } + downloader := NewDownloader(logger, filepath.Join(config.Path(), "download-cache")) + return &Client{ config: config, logger: logger, imageFetcher: image.NewFetcher(logger, dockerClient), - buildpackFetcher: buildpack.NewFetcher(logger, config.Path()), + buildpackFetcher: buildpack.NewFetcher(downloader), + lifecycleFetcher: lifecycle.NewFetcher(downloader), lifecycle: build.NewLifecycle(dockerClient, logger), docker: dockerClient, }, nil diff --git a/commands/create_builder.go b/commands/create_builder.go index 0cbe56faad..41276652e0 100644 --- a/commands/create_builder.go +++ b/commands/create_builder.go @@ -64,20 +64,40 @@ func readBuilderConfig(path string) (builder.Config, error) { if err != nil { return builder.Config{}, err } + builderConfig := builder.Config{} if _, err = toml.DecodeFile(path, &builderConfig); err != nil { return builderConfig, fmt.Errorf(`failed to decode builder config from file %s: %s`, path, err) } + for i, bp := range builderConfig.Buildpacks { - bpURL, err := url.Parse(bp.URI) + uri, err := transformRelativePath(bp.URI, builderDir) if err != nil { - return builder.Config{}, err + return builder.Config{}, errors.Wrap(err, "transforming buildpack URI") } - if bpURL.Scheme == "" || bpURL.Scheme == "file" { - if !filepath.IsAbs(bpURL.Path) { - builderConfig.Buildpacks[i].URI = fmt.Sprintf("file://" + filepath.Join(builderDir, bpURL.Path)) - } + builderConfig.Buildpacks[i].URI = uri + } + + if builderConfig.Lifecycle.URI != "" { + uri, err := transformRelativePath(builderConfig.Lifecycle.URI, builderDir) + if err != nil { + return builder.Config{}, errors.Wrap(err, "transforming lifecycle URI") } + builderConfig.Lifecycle.URI = uri } + return builderConfig, nil } + +func transformRelativePath(uri, relativeTo string) (string, error) { + parsed, err := url.Parse(uri) + if err != nil { + return "", err + } + if parsed.Scheme == "" { + if !filepath.IsAbs(parsed.Path) { + return fmt.Sprintf("file://" + filepath.Join(relativeTo, parsed.Path)), nil + } + } + return uri, nil +} diff --git a/commands/inspect_builder.go b/commands/inspect_builder.go index e939d03911..7d96112241 100644 --- a/commands/inspect_builder.go +++ b/commands/inspect_builder.go @@ -62,13 +62,24 @@ func inspectBuilderOutput(logger *logging.Logger, client PackClient, imageName s logger.Info("\nStack: %s\n", info.Stack) - logger.Info("Run Images:") - for _, r := range info.LocalRunImageMirrors { - logger.Info(" %s (user-configured)", r) + lcycleVer := info.LifecycleVersion + if lcycleVer == "" { + lcycleVer = "Unknown" } - logger.Info(" %s", info.RunImage) - for _, r := range info.RunImageMirrors { - logger.Info(" %s", r) + logger.Info("Lifecycle Version: %s\n", lcycleVer) + + if info.RunImage == "" { + logger.Info("\nWarning: '%s' does not specify a run image", imageName) + logger.Info(" Users must build with an explicitly specified run image") + } else { + logger.Info("Run Images:") + for _, r := range info.LocalRunImageMirrors { + logger.Info(" %s (user-configured)", r) + } + logger.Info(" %s", info.RunImage) + for _, r := range info.RunImageMirrors { + logger.Info(" %s", r) + } } if len(info.Buildpacks) == 0 { @@ -89,12 +100,12 @@ func inspectBuilderOutput(logger *logging.Logger, client PackClient, imageName s func logBuildpacksInfo(logger *logging.Logger, info *pack.BuilderInfo) { buf := &bytes.Buffer{} tabWriter := new(tabwriter.Writer).Init(buf, 0, 0, 8, ' ', 0) - if _, err := fmt.Fprint(tabWriter, "\n ID\tVERSION\tLATEST\t"); err != nil { + if _, err := fmt.Fprint(tabWriter, "\n ID\tVERSION\tLATEST"); err != nil { logger.Error(err.Error()) } for _, bp := range info.Buildpacks { - if _, err := fmt.Fprint(tabWriter, fmt.Sprintf("\n %s\t%s\t%t\t", bp.ID, bp.Version, bp.Latest)); err != nil { + if _, err := fmt.Fprint(tabWriter, fmt.Sprintf("\n %s\t%s\t%t", bp.ID, bp.Version, bp.Latest)); err != nil { logger.Error(err.Error()) } } diff --git a/commands/inspect_builder_test.go b/commands/inspect_builder_test.go index 57b612f7d3..9889a3e1a1 100644 --- a/commands/inspect_builder_test.go +++ b/commands/inspect_builder_test.go @@ -105,6 +105,17 @@ ERROR: some local error h.AssertContains(t, outBuf.String(), "Warning: 'some/image' does not specify detection order") h.AssertContains(t, outBuf.String(), "Users must build with explicitly specified buildpacks") }) + + it("missing run image logs a warning", func() { + h.AssertNil(t, command.Execute()) + h.AssertContains(t, outBuf.String(), "Warning: 'some/image' does not specify a run image") + h.AssertContains(t, outBuf.String(), "Users must build with an explicitly specified run image") + }) + + it("missing lifecycle version prints Unknown", func() { + h.AssertNil(t, command.Execute()) + h.AssertContains(t, outBuf.String(), "Lifecycle Version: Unknown") + }) }) when("is successful", func() { @@ -124,6 +135,7 @@ ERROR: some local error {ID: "test.bp.one", Version: "1.0.0", Optional: true}, {ID: "test.bp.two", Version: "2.0.0"}, }}}, + LifecycleVersion: "6.7.8", } mockClient.EXPECT().InspectBuilder("some/image", false).Return(remoteInfo, nil) @@ -137,6 +149,7 @@ ERROR: some local error {Buildpacks: []builder.GroupBuildpack{{ID: "test.bp.one", Version: "1.0.0"}}}, {Buildpacks: []builder.GroupBuildpack{{ID: "test.bp.two", Version: "2.0.0", Optional: true}}}, }, + LifecycleVersion: "4.5.6", } mockClient.EXPECT().InspectBuilder("some/image", true).Return(localInfo, nil) }) @@ -163,6 +176,8 @@ Remote Stack: test.stack.id +Lifecycle Version: 6.7.8 + Run Images: first/image (user-configured) second/image (user-configured) @@ -171,8 +186,8 @@ Run Images: second/default Buildpacks: - ID VERSION LATEST - test.bp.one 1.0.0 true + ID VERSION LATEST + test.bp.one 1.0.0 true test.bp.two 2.0.0 false Detection Order: @@ -187,6 +202,8 @@ Local Stack: test.stack.id +Lifecycle Version: 4.5.6 + Run Images: first/local (user-configured) second/local (user-configured) @@ -195,8 +212,8 @@ Run Images: second/local-default Buildpacks: - ID VERSION LATEST - test.bp.one 1.0.0 true + ID VERSION LATEST + test.bp.one 1.0.0 true test.bp.two 2.0.0 false Detection Order: diff --git a/create_builder.go b/create_builder.go index e16be50e35..4ea0cfd362 100644 --- a/create_builder.go +++ b/create_builder.go @@ -61,10 +61,22 @@ func (c *Client) CreateBuilder(ctx context.Context, opts CreateBuilderOptions) e return err } } + if err := builderImage.SetOrder(opts.BuilderConfig.Groups); err != nil { return errors.Wrap(err, "builder config has invalid groups") } + builderImage.SetStackInfo(opts.BuilderConfig.Stack) + + lifecycleMd, err := c.lifecycleFetcher.Fetch(opts.BuilderConfig.Lifecycle.Version, opts.BuilderConfig.Lifecycle.URI) + if err != nil { + return errors.Wrap(err, "fetching lifecycle") + } + + if err := builderImage.SetLifecycle(lifecycleMd); err != nil { + return errors.Wrap(err, "setting lifecycle") + } + return builderImage.Save() } diff --git a/create_builder_test.go b/create_builder_test.go index 399ed6b1e8..93d2b3c0e8 100644 --- a/create_builder_test.go +++ b/create_builder_test.go @@ -1,8 +1,11 @@ package pack_test import ( + "archive/tar" "bytes" "context" + "io" + "os" "path/filepath" "runtime" "testing" @@ -13,6 +16,8 @@ import ( "github.com/sclevine/spec" "github.com/sclevine/spec/report" + "github.com/buildpack/pack/lifecycle" + "github.com/buildpack/pack" "github.com/buildpack/pack/builder" "github.com/buildpack/pack/buildpack" @@ -34,20 +39,22 @@ func TestCreateBuilder(t *testing.T) { func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { when("#CreateBuilder", func() { var ( - mockController *gomock.Controller - mockBPFetcher *mocks.MockBuildpackFetcher - imageFetcher *h.FakeImageFetcher - fakeBuildImage *fakes.Image - fakeRunImage *fakes.Image - fakeRunImageMirror *fakes.Image - logOut, logErr *bytes.Buffer - opts pack.CreateBuilderOptions - subject *pack.Client + mockController *gomock.Controller + mockBPFetcher *mocks.MockBuildpackFetcher + mockLifecycleFetcher *mocks.MockLifecycleFetcher + imageFetcher *h.FakeImageFetcher + fakeBuildImage *fakes.Image + fakeRunImage *fakes.Image + fakeRunImageMirror *fakes.Image + logOut, logErr *bytes.Buffer + opts pack.CreateBuilderOptions + subject *pack.Client ) it.Before(func() { mockController = gomock.NewController(t) mockBPFetcher = mocks.NewMockBuildpackFetcher(mockController) + mockLifecycleFetcher = mocks.NewMockLifecycleFetcher(mockController) fakeBuildImage = fakes.NewImage("some/build-image", "", "") h.AssertNil(t, fakeBuildImage.SetLabel("io.buildpacks.stack.id", "some.stack.id")) @@ -75,14 +82,17 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { mockBPFetcher.EXPECT().FetchBuildpack(gomock.Any()).Return(bp, nil).AnyTimes() + mockLifecycleFetcher.EXPECT().Fetch(gomock.Any(), gomock.Any()).Return(lifecycle.Metadata{Dir: filepath.Join("testdata", "lifecycle"), Version: "3.4.5"}, nil).AnyTimes() + logOut, logErr = &bytes.Buffer{}, &bytes.Buffer{} subject = pack.NewClient( &config.Config{}, logging.NewLogger(logOut, logErr, true, false), imageFetcher, - nil, mockBPFetcher, + mockLifecycleFetcher, + nil, nil, ) @@ -103,6 +113,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { RunImage: "some/run-image", RunImageMirrors: []string{"localhost:5000/some-run-image"}, }, + Lifecycle: builder.LifecycleConfig{Version: "3.4.5"}, }, Publish: false, NoPull: false, @@ -188,6 +199,49 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { Optional: false, }}, }}) + h.AssertEq(t, builderImage.GetLifecycleVersion(), "3.4.5") + + layerTar, err := fakeBuildImage.FindLayerWithPath("/lifecycle") + h.AssertNil(t, err) + assertTarHasFile(t, layerTar, "/lifecycle/detector") + assertTarHasFile(t, layerTar, "/lifecycle/restorer") + assertTarHasFile(t, layerTar, "/lifecycle/analyzer") + assertTarHasFile(t, layerTar, "/lifecycle/builder") + assertTarHasFile(t, layerTar, "/lifecycle/exporter") + assertTarHasFile(t, layerTar, "/lifecycle/cacher") + assertTarHasFile(t, layerTar, "/lifecycle/launcher") }) }) } + +func assertTarHasFile(t *testing.T, tarFile, path string) { + t.Helper() + + exist := tarHasFile(t, tarFile, path) + if !exist { + t.Fatalf("%s does not exist in %s", path, tarFile) + } +} + +func tarHasFile(t *testing.T, tarFile, path string) (exist bool) { + t.Helper() + + r, err := os.Open(tarFile) + h.AssertNil(t, err) + defer r.Close() + + tr := tar.NewReader(r) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + h.AssertNil(t, err) + + if header.Name == path { + return true + } + } + + return false +} diff --git a/downloader.go b/downloader.go new file mode 100644 index 0000000000..21f5c81f4f --- /dev/null +++ b/downloader.go @@ -0,0 +1,152 @@ +package pack + +import ( + "crypto/sha256" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + + "github.com/pkg/errors" + + "github.com/buildpack/pack/archive" +) + +type Logger interface { + Verbose(format string, a ...interface{}) +} + +type Downloader struct { + logger Logger + cacheDir string +} + +func NewDownloader(logger Logger, cacheDir string) *Downloader { + return &Downloader{ + logger: logger, + cacheDir: cacheDir, + } +} + +func (d *Downloader) Download(uri string) (string, error) { + url, err := url.Parse(uri) + if err != nil { + return "", err + } + + switch url.Scheme { + case "", "file": + return d.handleFile(url) + case "http", "https": + return d.handleHTTP(uri) + default: + return "", fmt.Errorf("unsupported protocol in URI %q", uri) + } +} + +func (d *Downloader) handleFile(bpURL *url.URL) (string, error) { + path := bpURL.Path + + if filepath.Ext(path) != ".tgz" { + return path, nil + } + + file, err := os.Open(path) + if err != nil { + return "", errors.Wrapf(err, "could not open file to untar: %q", path) + } + defer file.Close() + + tmpDir, err := ioutil.TempDir("", "") + if err != nil { + return "", fmt.Errorf(`failed to create temporary directory: %s`, err) + } + + if err = archive.ExtractTarGZ(file, tmpDir); err != nil { + return "", err + } + + return tmpDir, nil +} + +func (d *Downloader) handleHTTP(uri string) (string, error) { + bpCache := filepath.Join(d.cacheDir, fmt.Sprintf("%x", sha256.Sum256([]byte(uri)))) + if err := os.MkdirAll(bpCache, 0744); err != nil { + return "", err + } + + etagFile := bpCache + ".etag" + etagExists, err := fileExists(etagFile) + if err != nil { + return "", err + } + + etag := "" + if etagExists { + bytes, err := ioutil.ReadFile(etagFile) + if err != nil { + return "", err + } + etag = string(bytes) + } + + reader, etag, err := d.downloadAsStream(uri, etag) + if err != nil { + return "", errors.Wrapf(err, "failed to download from %q", uri) + } else if reader == nil { + return bpCache, nil + } + defer reader.Close() + + if err = archive.ExtractTarGZ(reader, bpCache); err != nil { + return "", err + } + + if err = ioutil.WriteFile(etagFile, []byte(etag), 0744); err != nil { + return "", err + } + + return bpCache, nil +} + +func (d *Downloader) downloadAsStream(uri string, etag string) (io.ReadCloser, string, error) { + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + return nil, "", err + } + + if etag != "" { + req.Header.Set("If-None-Match", etag) + } + + resp, err := (&http.Client{}).Do(req) + if err != nil { + return nil, "", err + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + d.logger.Verbose("Downloading from %q\n", uri) + return resp.Body, resp.Header.Get("Etag"), nil + } + + if resp.StatusCode == 304 { + d.logger.Verbose("Using cached version of %q\n", uri) + return nil, etag, nil + } + + return nil, "", fmt.Errorf("could not download from %q, code http status %d", uri, resp.StatusCode) +} + +func fileExists(file string) (bool, error) { + _, err := os.Stat(file) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/downloader_test.go b/downloader_test.go new file mode 100644 index 0000000000..e57e67e844 --- /dev/null +++ b/downloader_test.go @@ -0,0 +1,124 @@ +package pack + +import ( + "io/ioutil" + "net/http" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/onsi/gomega/ghttp" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + h "github.com/buildpack/pack/testhelpers" +) + +func TestDownloader(t *testing.T) { + spec.Run(t, "Downloader", testDownloader, spec.Parallel(), spec.Report(report.Terminal{})) +} + +type emptyLogger struct { +} + +func (e *emptyLogger) Verbose(format string, a ...interface{}) { +} + +func testDownloader(t *testing.T, when spec.G, it spec.S) { + when("#Download", func() { + var ( + err error + tmpDir string + cacheDir string + subject *Downloader + ) + + it.Before(func() { + if runtime.GOOS == "windows" { + t.Skip("do not run on windows") + } + + tmpDir, err = ioutil.TempDir("", "") + h.AssertNil(t, err) + + cacheDir, err = ioutil.TempDir("", "") + h.AssertNil(t, err) + + subject = NewDownloader(&emptyLogger{}, cacheDir) + }) + + it.After(func() { + h.AssertNil(t, os.RemoveAll(tmpDir)) + h.AssertNil(t, os.RemoveAll(cacheDir)) + }) + + it("download from a relative directory", func() { + out, err := subject.Download(filepath.Join("testdata", "downloader", "dirA")) + h.AssertNil(t, err) + h.AssertNotEq(t, out, "") + h.AssertDirContainsFileWithContents(t, out, "file.txt", "some file contents") + }) + + it("download from a relative tgz", func() { + out, err := subject.Download(filepath.Join("testdata", "downloader", "dirA.tgz")) + h.AssertNil(t, err) + h.AssertNotEq(t, out, "") + h.AssertDirContainsFileWithContents(t, out, "file.txt", "some file contents") + }) + + it("download from an absolute directory", func() { + absPath, err := filepath.Abs(filepath.Join("testdata", "downloader", "dirA")) + h.AssertNil(t, err) + + out, err := subject.Download(absPath) + h.AssertNil(t, err) + h.AssertNotEq(t, out, "") + h.AssertDirContainsFileWithContents(t, out, "file.txt", "some file contents") + }) + + it("download from an absolute tgz", func() { + absPath, err := filepath.Abs(filepath.Join("testdata", "downloader", "dirA.tgz")) + h.AssertNil(t, err) + + out, err := subject.Download(absPath) + h.AssertNil(t, err) + h.AssertNotEq(t, out, "") + h.AssertDirContainsFileWithContents(t, out, "file.txt", "some file contents") + }) + + it("download from a 'file://' URI directory", func() { + absPath, err := filepath.Abs(filepath.Join("testdata", "downloader", "dirA")) + h.AssertNil(t, err) + + out, err := subject.Download("file://" + absPath) + h.AssertNil(t, err) + h.AssertNotEq(t, out, "") + h.AssertDirContainsFileWithContents(t, out, "file.txt", "some file contents") + }) + + it("download from a 'file://' URI tgz", func() { + absPath, err := filepath.Abs(filepath.Join("testdata", "downloader", "dirA.tgz")) + h.AssertNil(t, err) + + out, err := subject.Download("file://" + absPath) + h.AssertNil(t, err) + h.AssertNotEq(t, out, "") + h.AssertDirContainsFileWithContents(t, out, "file.txt", "some file contents") + }) + + it("download from a 'http(s)://' URI tgz", func() { + server := ghttp.NewServer() + server.AppendHandlers(func(w http.ResponseWriter, r *http.Request) { + path := filepath.Join("testdata", r.URL.Path) + http.ServeFile(w, r, path) + }) + defer server.Close() + + out, err := subject.Download(server.URL() + "/downloader/dirA.tgz") + h.AssertNil(t, err) + h.AssertNotEq(t, out, "") + h.AssertDirContainsFileWithContents(t, out, "file.txt", "some file contents") + }) + }) +} diff --git a/generate_external_mocks.go b/generate_external_mocks.go deleted file mode 100644 index c32fe720b3..0000000000 --- a/generate_external_mocks.go +++ /dev/null @@ -1,3 +0,0 @@ -package pack - -//go:generate mockgen -package mocks -destination mocks/image.go github.com/buildpack/lifecycle/image Image diff --git a/inspect_builder.go b/inspect_builder.go index 0ed189f608..ad0dd32c8a 100644 --- a/inspect_builder.go +++ b/inspect_builder.go @@ -17,6 +17,7 @@ type BuilderInfo struct { LocalRunImageMirrors []string Buildpacks []builder.BuildpackMetadata Groups []builder.GroupMetadata + LifecycleVersion string } type BuildpackInfo struct { @@ -53,5 +54,6 @@ func (c *Client) InspectBuilder(name string, daemon bool) (*BuilderInfo, error) LocalRunImageMirrors: localMirrors, Buildpacks: bldr.GetBuildpacks(), Groups: bldr.GetOrder(), + LifecycleVersion: bldr.GetLifecycleVersion(), }, nil } diff --git a/inspect_builder_test.go b/inspect_builder_test.go index 079015d460..c5b864c8cc 100644 --- a/inspect_builder_test.go +++ b/inspect_builder_test.go @@ -47,9 +47,10 @@ func testInspectBuilder(t *testing.T, when spec.G, it spec.S) { }, logging.NewLogger(ioutil.Discard, ioutil.Discard, false, false), mockImageFetcher, - nil, mockBPFetcher, nil, + nil, + nil, ) builderImage = fakes.NewImage("some/builder", "", "") h.AssertNil(t, builderImage.SetLabel("io.buildpacks.stack.id", "test.stack.id")) @@ -101,7 +102,8 @@ func testInspectBuilder(t *testing.T, when spec.G, it spec.S) { } ] } - ] + ], + "lifecycle": {"version": "some-lifecycle-version"} }`)) }) @@ -141,6 +143,12 @@ func testInspectBuilder(t *testing.T, when spec.G, it spec.S) { Version: "1.0.0", }) }) + + it("sets the lifecycle version", func() { + builderInfo, err := client.InspectBuilder("some/builder", useDaemon) + h.AssertNil(t, err) + h.AssertEq(t, builderInfo.LifecycleVersion, "some-lifecycle-version") + }) }) }) } diff --git a/interfaces.go b/interfaces.go index fea87c21a0..fab62cef0c 100644 --- a/interfaces.go +++ b/interfaces.go @@ -6,6 +6,7 @@ import ( "github.com/buildpack/imgutil" "github.com/buildpack/pack/buildpack" + "github.com/buildpack/pack/lifecycle" ) //go:generate mockgen -package mocks -destination mocks/image_fetcher.go github.com/buildpack/pack ImageFetcher @@ -19,3 +20,9 @@ type ImageFetcher interface { type BuildpackFetcher interface { FetchBuildpack(uri string) (buildpack.Buildpack, error) } + +//go:generate mockgen -package mocks -destination mocks/lifecycle_fetcher.go github.com/buildpack/pack LifecycleFetcher + +type LifecycleFetcher interface { + Fetch(version, uri string) (lifecycle.Metadata, error) +} diff --git a/lifecycle/fetcher.go b/lifecycle/fetcher.go new file mode 100644 index 0000000000..418b1cd3d2 --- /dev/null +++ b/lifecycle/fetcher.go @@ -0,0 +1,84 @@ +package lifecycle + +import ( + "fmt" + "io/ioutil" + "path/filepath" + + "github.com/pkg/errors" + + "github.com/buildpack/pack/style" +) + +const ( + DefaultLifecycleVersion = "0.1.0" +) + +//go:generate mockgen -package mocks -destination mocks/downloader.go github.com/buildpack/pack/lifecycle Downloader + +type Downloader interface { + Download(uri string) (string, error) +} + +type Fetcher struct { + downloader Downloader +} + +func NewFetcher(downloader Downloader) *Fetcher { + return &Fetcher{downloader: downloader} +} + +func (f *Fetcher) Fetch(version, uri string) (Metadata, error) { + if version == "" && uri == "" { + version = DefaultLifecycleVersion + } + + if uri == "" { + uri = fmt.Sprintf("https://github.com/buildpack/lifecycle/releases/download/v%s/lifecycle-v%s+linux.x86-64.tgz", version, version) + } + + downloadDir, err := f.downloader.Download(uri) + if err != nil { + return Metadata{}, errors.Wrapf(err, "retrieving lifecycle from %s", uri) + } + + dir, err := getLifecycleParentDir(downloadDir) + if err != nil { + return Metadata{}, errors.Wrapf(err, "invalid lifecycle") + } + + return Metadata{Version: version, Dir: dir}, nil +} + +func getLifecycleParentDir(root string) (string, error) { + fis, err := ioutil.ReadDir(root) + if err != nil { + return "", err + } + if len(fis) == 1 && fis[0].IsDir() { + return getLifecycleParentDir(filepath.Join(root, fis[0].Name())) + } + + bins := map[string]bool{ + "detector": false, + "restorer": false, + "analyzer": false, + "builder": false, + "exporter": false, + "cacher": false, + "launcher": false, + } + + for _, fi := range fis { + if _, ok := bins[fi.Name()]; ok { + bins[fi.Name()] = true + } + } + + for bin, found := range bins { + if !found { + return "", fmt.Errorf("missing required lifecycle binary %s", style.Symbol(bin)) + } + } + return root, nil +} diff --git a/lifecycle/fetcher_test.go b/lifecycle/fetcher_test.go new file mode 100644 index 0000000000..41b9b8ede0 --- /dev/null +++ b/lifecycle/fetcher_test.go @@ -0,0 +1,126 @@ +package lifecycle_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/golang/mock/gomock" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpack/pack/lifecycle" + "github.com/buildpack/pack/lifecycle/mocks" + h "github.com/buildpack/pack/testhelpers" +) + +func TestFetcher(t *testing.T) { + spec.Run(t, "Fetcher", testFetcher, spec.Report(report.Terminal{})) +} + +func testFetcher(t *testing.T, when spec.G, it spec.S) { + var ( + mockController *gomock.Controller + mockDownloader *mocks.MockDownloader + subject *lifecycle.Fetcher + ) + + it.Before(func() { + mockController = gomock.NewController(t) + mockDownloader = mocks.NewMockDownloader(mockController) + subject = lifecycle.NewFetcher(mockDownloader) + }) + + it.After(func() { + mockController.Finish() + }) + + when("#Fetch", func() { + when("only a version is provided", func() { + it("returns a release from github", func() { + mockDownloader.EXPECT(). + Download("https://github.com/buildpack/lifecycle/releases/download/v1.2.3/lifecycle-v1.2.3+linux.x86-64.tgz"). + Return(filepath.Join("testdata", "download-dir"), nil) + + md, err := subject.Fetch("1.2.3", "") + h.AssertNil(t, err) + h.AssertEq(t, md.Version, "1.2.3") + h.AssertEq(t, md.Dir, filepath.Join("testdata", "download-dir", "fake-lifecycle")) + }) + }) + + when("only a uri is provided", func() { + it("returns the lifecycle from the uri", func() { + mockDownloader.EXPECT(). + Download("https://lifecycle.example.com"). + Return(filepath.Join("testdata", "download-dir"), nil) + + md, err := subject.Fetch("", "https://lifecycle.example.com") + h.AssertNil(t, err) + h.AssertEq(t, md.Version, "") + h.AssertEq(t, md.Dir, filepath.Join("testdata", "download-dir", "fake-lifecycle")) + }) + }) + + when("a uri and version are provided", func() { + it("returns the lifecycle from the uri", func() { + mockDownloader.EXPECT(). + Download("https://lifecycle.example.com"). + Return(filepath.Join("testdata", "download-dir"), nil) + + md, err := subject.Fetch("1.2.3", "https://lifecycle.example.com") + h.AssertNil(t, err) + h.AssertEq(t, md.Version, "1.2.3") + h.AssertEq(t, md.Dir, filepath.Join("testdata", "download-dir", "fake-lifecycle")) + }) + }) + + when("neither is uri nor version is provided", func() { + it("returns the default lifecycle", func() { + mockDownloader.EXPECT(). + Download("https://github.com/buildpack/lifecycle/releases/download/v0.1.0/lifecycle-v0.1.0+linux.x86-64.tgz"). + Return(filepath.Join("testdata", "download-dir"), nil) + + md, err := subject.Fetch("", "") + h.AssertNil(t, err) + h.AssertEq(t, md.Version, "0.1.0") + h.AssertEq(t, md.Dir, filepath.Join("testdata", "download-dir", "fake-lifecycle")) + }) + }) + + when("the lifecycle is missing binaries", func() { + it("returns an error", func() { + tmp, err := ioutil.TempDir("", "") + h.AssertNil(t, err) + defer os.RemoveAll(tmp) + + mockDownloader.EXPECT(). + Download("https://github.com/buildpack/lifecycle/releases/download/v0.1.0/lifecycle-v0.1.0+linux.x86-64.tgz"). + Return(tmp, nil) + + _, err = subject.Fetch("", "") + h.AssertError(t, err, "invalid lifecycle") + }) + }) + + when("the lifecycle has incomplete list of binaries", func() { + it("returns an error", func() { + tmp, err := ioutil.TempDir("", "") + h.AssertNil(t, err) + defer os.RemoveAll(tmp) + + h.AssertNil(t, ioutil.WriteFile(filepath.Join(tmp, "analyzer"), []byte("content"), os.ModePerm)) + h.AssertNil(t, ioutil.WriteFile(filepath.Join(tmp, "detector"), []byte("content"), os.ModePerm)) + h.AssertNil(t, ioutil.WriteFile(filepath.Join(tmp, "builder"), []byte("content"), os.ModePerm)) + + mockDownloader.EXPECT(). + Download("https://github.com/buildpack/lifecycle/releases/download/v0.1.0/lifecycle-v0.1.0+linux.x86-64.tgz"). + Return(tmp, nil) + + _, err = subject.Fetch("", "") + h.AssertError(t, err, "invalid lifecycle") + }) + }) + }) +} diff --git a/lifecycle/metadata.go b/lifecycle/metadata.go new file mode 100644 index 0000000000..111166bb1c --- /dev/null +++ b/lifecycle/metadata.go @@ -0,0 +1,6 @@ +package lifecycle + +type Metadata struct { + Version string `json:"version"` + Dir string `json:"-"` +} diff --git a/lifecycle/mocks/downloader.go b/lifecycle/mocks/downloader.go new file mode 100644 index 0000000000..1dce064589 --- /dev/null +++ b/lifecycle/mocks/downloader.go @@ -0,0 +1,46 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/buildpack/pack/lifecycle (interfaces: Downloader) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockDownloader is a mock of Downloader interface +type MockDownloader struct { + ctrl *gomock.Controller + recorder *MockDownloaderMockRecorder +} + +// MockDownloaderMockRecorder is the mock recorder for MockDownloader +type MockDownloaderMockRecorder struct { + mock *MockDownloader +} + +// NewMockDownloader creates a new mock instance +func NewMockDownloader(ctrl *gomock.Controller) *MockDownloader { + mock := &MockDownloader{ctrl: ctrl} + mock.recorder = &MockDownloaderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockDownloader) EXPECT() *MockDownloaderMockRecorder { + return m.recorder +} + +// Download mocks base method +func (m *MockDownloader) Download(arg0 string) (string, error) { + ret := m.ctrl.Call(m, "Download", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Download indicates an expected call of Download +func (mr *MockDownloaderMockRecorder) Download(arg0 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Download", reflect.TypeOf((*MockDownloader)(nil).Download), arg0) +} diff --git a/lifecycle/testdata/download-dir/fake-lifecycle/analyzer b/lifecycle/testdata/download-dir/fake-lifecycle/analyzer new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lifecycle/testdata/download-dir/fake-lifecycle/builder b/lifecycle/testdata/download-dir/fake-lifecycle/builder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lifecycle/testdata/download-dir/fake-lifecycle/cacher b/lifecycle/testdata/download-dir/fake-lifecycle/cacher new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lifecycle/testdata/download-dir/fake-lifecycle/detector b/lifecycle/testdata/download-dir/fake-lifecycle/detector new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lifecycle/testdata/download-dir/fake-lifecycle/exporter b/lifecycle/testdata/download-dir/fake-lifecycle/exporter new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lifecycle/testdata/download-dir/fake-lifecycle/launcher b/lifecycle/testdata/download-dir/fake-lifecycle/launcher new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lifecycle/testdata/download-dir/fake-lifecycle/restorer b/lifecycle/testdata/download-dir/fake-lifecycle/restorer new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mocks/cache.go b/mocks/cache.go deleted file mode 100644 index cee166a253..0000000000 --- a/mocks/cache.go +++ /dev/null @@ -1,58 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/buildpack/pack (interfaces: Cache) - -// Package mocks is a generated GoMock package. -package mocks - -import ( - context "context" - gomock "github.com/golang/mock/gomock" - reflect "reflect" -) - -// MockCache is a mock of Cache interface -type MockCache struct { - ctrl *gomock.Controller - recorder *MockCacheMockRecorder -} - -// MockCacheMockRecorder is the mock recorder for MockCache -type MockCacheMockRecorder struct { - mock *MockCache -} - -// NewMockCache creates a new mock instance -func NewMockCache(ctrl *gomock.Controller) *MockCache { - mock := &MockCache{ctrl: ctrl} - mock.recorder = &MockCacheMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockCache) EXPECT() *MockCacheMockRecorder { - return m.recorder -} - -// Clear mocks base method -func (m *MockCache) Clear(arg0 context.Context) error { - ret := m.ctrl.Call(m, "Clear", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Clear indicates an expected call of Clear -func (mr *MockCacheMockRecorder) Clear(arg0 interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clear", reflect.TypeOf((*MockCache)(nil).Clear), arg0) -} - -// Image mocks base method -func (m *MockCache) Image() string { - ret := m.ctrl.Call(m, "Image") - ret0, _ := ret[0].(string) - return ret0 -} - -// Image indicates an expected call of Image -func (mr *MockCacheMockRecorder) Image() *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Image", reflect.TypeOf((*MockCache)(nil).Image)) -} diff --git a/mocks/lifecycle_fetcher.go b/mocks/lifecycle_fetcher.go new file mode 100644 index 0000000000..157ff75f7f --- /dev/null +++ b/mocks/lifecycle_fetcher.go @@ -0,0 +1,47 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/buildpack/pack (interfaces: LifecycleFetcher) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + lifecycle "github.com/buildpack/pack/lifecycle" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockLifecycleFetcher is a mock of LifecycleFetcher interface +type MockLifecycleFetcher struct { + ctrl *gomock.Controller + recorder *MockLifecycleFetcherMockRecorder +} + +// MockLifecycleFetcherMockRecorder is the mock recorder for MockLifecycleFetcher +type MockLifecycleFetcherMockRecorder struct { + mock *MockLifecycleFetcher +} + +// NewMockLifecycleFetcher creates a new mock instance +func NewMockLifecycleFetcher(ctrl *gomock.Controller) *MockLifecycleFetcher { + mock := &MockLifecycleFetcher{ctrl: ctrl} + mock.recorder = &MockLifecycleFetcherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockLifecycleFetcher) EXPECT() *MockLifecycleFetcherMockRecorder { + return m.recorder +} + +// Fetch mocks base method +func (m *MockLifecycleFetcher) Fetch(arg0, arg1 string) (lifecycle.Metadata, error) { + ret := m.ctrl.Call(m, "Fetch", arg0, arg1) + ret0, _ := ret[0].(lifecycle.Metadata) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Fetch indicates an expected call of Fetch +func (mr *MockLifecycleFetcherMockRecorder) Fetch(arg0, arg1 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockLifecycleFetcher)(nil).Fetch), arg0, arg1) +} diff --git a/rebase_test.go b/rebase_test.go index 0a46da3e98..ee7852eae5 100644 --- a/rebase_test.go +++ b/rebase_test.go @@ -55,6 +55,7 @@ func testRebase(t *testing.T, when spec.G, it spec.S) { nil, nil, nil, + nil, ) }) diff --git a/testdata/downloader/dirA.tgz b/testdata/downloader/dirA.tgz new file mode 100644 index 0000000000..d4d3d6e132 Binary files /dev/null and b/testdata/downloader/dirA.tgz differ diff --git a/testdata/downloader/dirA/file.txt b/testdata/downloader/dirA/file.txt new file mode 100644 index 0000000000..c6187e4d11 --- /dev/null +++ b/testdata/downloader/dirA/file.txt @@ -0,0 +1 @@ +some file contents \ No newline at end of file diff --git a/testdata/lifecycle/analyzer b/testdata/lifecycle/analyzer new file mode 100644 index 0000000000..2c7cce34c1 --- /dev/null +++ b/testdata/lifecycle/analyzer @@ -0,0 +1 @@ +analyzer \ No newline at end of file diff --git a/testdata/lifecycle/builder b/testdata/lifecycle/builder new file mode 100644 index 0000000000..b05c21cd9d --- /dev/null +++ b/testdata/lifecycle/builder @@ -0,0 +1 @@ +builder \ No newline at end of file diff --git a/testdata/lifecycle/cacher b/testdata/lifecycle/cacher new file mode 100644 index 0000000000..3ad3632e14 --- /dev/null +++ b/testdata/lifecycle/cacher @@ -0,0 +1 @@ +cacher \ No newline at end of file diff --git a/testdata/lifecycle/detector b/testdata/lifecycle/detector new file mode 100644 index 0000000000..4ca7e105c9 --- /dev/null +++ b/testdata/lifecycle/detector @@ -0,0 +1 @@ +detector \ No newline at end of file diff --git a/testdata/lifecycle/exporter b/testdata/lifecycle/exporter new file mode 100644 index 0000000000..76a0149ce4 --- /dev/null +++ b/testdata/lifecycle/exporter @@ -0,0 +1 @@ +exporter \ No newline at end of file diff --git a/testdata/lifecycle/launcher b/testdata/lifecycle/launcher new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testdata/lifecycle/restorer b/testdata/lifecycle/restorer new file mode 100644 index 0000000000..f6d18366f2 --- /dev/null +++ b/testdata/lifecycle/restorer @@ -0,0 +1 @@ +restorer \ No newline at end of file