diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 092474aa3..c9d4a4f86 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -97,7 +97,7 @@ func testPack(t *testing.T, when spec.G, it spec.S) { if err != nil { t.Fatal(err) } - exec.Command("cp", "-r", "fixtures/node_app/.", sourceCodePath).Run() + exec.Command("cp", "-r", "testdata/node_app/.", sourceCodePath).Run() repo = "some-org/" + randString(10) repoName = "localhost:" + registryPort + "/" + repo @@ -171,156 +171,77 @@ func testPack(t *testing.T, when spec.G, it spec.S) { }, spec.Parallel(), spec.Report(report.Terminal{})) }, spec.Parallel(), spec.Report(report.Terminal{})) - when("create", func() { - var detectImageName, buildImageName string + when("create-builder", func() { + var ( + builderTOML string + builderRepoName string + appRepoName string + containerName string + tmpDir string + ) it.Before(func() { - detectImageName = "some-org/detect-" + randString(10) - buildImageName = "some-org/build-" + randString(10) - docker = NewDockerDaemon() - }) - it.After(func() { - docker.RemoveImage(detectImageName, buildImageName) - }) - - when("provided with output detect and build images", func() { - it("creates detect and build images on the daemon", func() { - cmd := exec.Command(pack, "create", detectImageName, buildImageName, "--from-base-image", "sclevine/test") - cmd.Env = append(os.Environ(), "HOME="+homeDir) - cmd.Dir = "./fixtures/buildpacks" - run(t, cmd) - - t.Log("images exist") - detectConfig := docker.InspectImage(t, detectImageName) - buildConfig := docker.InspectImage(t, buildImageName) - - t.Log("both images have buildpacks") - for _, image := range []string{detectImageName, buildImageName} { - info := docker.FileFromImage(t, image, "/buildpacks/order.toml") - if info.Name != "order.toml" || info.Size != 130 { - t.Fatalf("Expected %s to contain /buildpacks/order.toml: %v", image, info) - } - } - - t.Log("both images have ENTRYPOINTs") - if diff := cmp.Diff(detectConfig.Config.Entrypoint, []string{"/packs/detector"}); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(buildConfig.Config.Entrypoint, []string{"/packs/builder"}); diff != "" { - t.Fatal(diff) - } - - t.Log("detect image has desired ENV variables") - if contains(detectConfig.Config.Env, `"PACK_BP_ORDER_PATH=/buildpacks/order.toml"`) { - t.Fatalf("Expected %v to contain %s", detectConfig.Config.Env, `"PACK_BP_ORDER_PATH=/buildpacks/order.toml"`) - } - - t.Log("build image has desired extra ENV variables") - if contains(buildConfig.Config.Env, `"PACK_METADATA_PATH=/launch/config/metadata.toml"`) { - t.Fatalf("Expected %v to contain %s", buildConfig.Config.Env, `"PACK_METADATA_PATH=/launch/config/metadata.toml"`) - } - }) - - when("publishing", func() { - var registryContainerName, registryPort string - var registry *DockerRegistry - it.Before(func() { - registryContainerName = "test-registry-" + randString(10) - run(t, exec.Command("docker", "run", "-d", "--rm", "-p", ":5000", "--name", registryContainerName, "registry:2")) - registryPort = fetchHostPort(t, registryContainerName) - registry = NewDockerRegistry() - - detectImageName = "localhost:" + registryPort + "/" + detectImageName - buildImageName = "localhost:" + registryPort + "/" + buildImageName - }) - it.After(func() { - docker.Kill(registryContainerName) - }) - - it("creates detect and build images on the registry", func() { - cmd := exec.Command(pack, "create", detectImageName, buildImageName, "--publish", "--from-base-image", "sclevine/test") - cmd.Env = append(os.Environ(), "HOME="+homeDir) - cmd.Dir = "./fixtures/buildpacks" - run(t, cmd) - - t.Log("images exist on registry") - detectConfig := registry.InspectImage(t, detectImageName) - buildConfig := registry.InspectImage(t, buildImageName) - - t.Log("both images have ENTRYPOINTs") - if diff := cmp.Diff(detectConfig.Config.Entrypoint, []string{"/packs/detector"}); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(buildConfig.Config.Entrypoint, []string{"/packs/builder"}); diff != "" { - t.Fatal(diff) - } - - t.Log("detect image has desired ENV variables") - if contains(detectConfig.Config.Env, `"PACK_BP_ORDER_PATH=/buildpacks/order.toml"`) { - t.Fatalf("Expected %v to contain %s", detectConfig.Config.Env, `"PACK_BP_ORDER_PATH=/buildpacks/order.toml"`) - } - - t.Log("build image has desired extra ENV variables") - if contains(buildConfig.Config.Env, `"PACK_METADATA_PATH=/launch/config/metadata.toml"`) { - t.Fatalf("Expected %v to contain %s", buildConfig.Config.Env, `"PACK_METADATA_PATH=/launch/config/metadata.toml"`) - } - }) - }) - }) - }, spec.Parallel(), spec.Report(report.Terminal{})) - - when.Pend("create, build, run", func() { - var tmpDir, detectImageName, buildImageName, repoName, containerName, registryContainerName, registry string - - it.Before(func() { - uid := randString(10) - registryContainerName = "test-registry-" + uid - run(t, exec.Command("docker", "run", "-d", "--rm", "-p", ":5000", "--name", registryContainerName, "registry:2")) - registry = "localhost:" + fetchHostPort(t, registryContainerName) + "/" + builderTOML = filepath.Join("testdata", "builder.toml") var err error tmpDir, err = ioutil.TempDir("", "pack.build.node_app.") assertNil(t, err) assertNil(t, os.Mkdir(filepath.Join(tmpDir, "app"), 0755)) - run(t, exec.Command("cp", "-r", "fixtures/node_app/.", filepath.Join(tmpDir, "app"))) - assertNil(t, os.Mkdir(filepath.Join(tmpDir, "buildpacks"), 0755)) - run(t, exec.Command("cp", "-r", "fixtures/buildpacks/.", filepath.Join(tmpDir, "buildpacks"))) - - repoName = registry + "some-org/output-" + uid - detectImageName = registry + "some-org/detect-" + uid - buildImageName = registry + "some-org/build-" + uid - containerName = "test-" + uid + run(t, exec.Command("cp", "-r", "testdata/node_app/.", filepath.Join(tmpDir, "app"))) - txt, err := ioutil.ReadFile(filepath.Join(tmpDir, "buildpacks", "order.toml")) - assertNil(t, err) - txt2 := strings.Replace(string(txt), "some-build-image", buildImageName, -1) - txt2 = strings.Replace(txt2, "some-run-image", buildImageName, -1) - assertNil(t, ioutil.WriteFile(filepath.Join(tmpDir, "buildpacks", "order.toml"), []byte(txt2), 0644)) + builderRepoName = "some-org/" + randString(10) + appRepoName = "some-org/" + randString(10) + containerName = "test-" + randString(10) }) it.After(func() { - docker.Kill(containerName, registryContainerName) - docker.RemoveImage(repoName, detectImageName, buildImageName) + docker.Kill(containerName) + docker.RemoveImage(builderRepoName, appRepoName) if tmpDir != "" { os.RemoveAll(tmpDir) } }) - it("run works", func() { - t.Log("create detect image:") - cmd := exec.Command(pack, "create", detectImageName, buildImageName, "--from-base-image", "packsdev/v3:latest", "-p", filepath.Join(tmpDir, "buildpacks"), "--publish") - cmd.Env = append(os.Environ(), "HOME="+homeDir) - run(t, cmd) + it("creates a builder image", func() { + t.Log("create builder image") + cmd := exec.Command(pack, "create-builder", builderRepoName, "-b", builderTOML) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("create-builder command failed: %s: %s", output, err) + } + + t.Log("builder image has order toml and buildpacks") + dockerRunOutput := run(t, exec.Command("docker", "run", "--rm=true", "-t", builderRepoName, "ls", "/buildpacks")) + + if !strings.Contains(dockerRunOutput, "order.toml") { + t.Fatalf("expected /buildpacks to contain order.toml, got '%s'", dockerRunOutput) + } + if !strings.Contains(dockerRunOutput, "com.example.sample.bp") { + t.Fatalf("expected /buildpacks to contain com.example.sample.bp, got '%s'", dockerRunOutput) + } - t.Log("build image from detect:") - docker.Pull(t, detectImageName, "latest") - docker.Pull(t, buildImageName, "latest") - cmd = exec.Command(pack, "build", repoName, "-p", filepath.Join(tmpDir, "app"), "--detect-image", detectImageName, "--publish") + dockerRunOutput = run(t, exec.Command("docker", "run", "--rm=true", "-t", builderRepoName, "cat", "/buildpacks/order.toml")) + sanitzedOutput := strings.Replace(dockerRunOutput, "\r", "", -1) + expectedGroups := `[[groups]] + + [[groups.buildpacks]] + id = "com.example.sample.bp" + version = "1.2.3" +` + + if diff := cmp.Diff(sanitzedOutput, expectedGroups); diff != "" { + t.Fatalf("expected order.toml to contain '%s', got diff '%s'", expectedGroups, diff) + } + + t.Log("build app with builder:", builderRepoName) + cmd = exec.Command(pack, "build", appRepoName, "-p", filepath.Join(tmpDir, "app"), "--builder", builderRepoName) cmd.Env = append(os.Environ(), "HOME="+homeDir) run(t, cmd) - t.Log("run image:", repoName) - docker.Pull(t, repoName, "latest") - run(t, exec.Command("docker", "run", "--name="+containerName, "--rm=true", "-d", "-e", "PORT=8080", "-p", ":8080", repoName)) + t.Log("run image:", appRepoName) + txt := run(t, exec.Command("docker", "run", "--name="+containerName, "--rm=true", appRepoName)) + if !strings.Contains(txt, "Hi from Sample BP") { + t.Fatalf("expected '%s' to be contained in:\n%s", "Hi from Sample BP", txt) + } }) }, spec.Parallel(), spec.Report(report.Terminal{})) } diff --git a/acceptance/docker_utils_test.go b/acceptance/docker_utils_test.go index 708667ec1..7cb030fa0 100644 --- a/acceptance/docker_utils_test.go +++ b/acceptance/docker_utils_test.go @@ -42,7 +42,6 @@ func NewDockerDaemon() *DockerDaemon { } func (d *DockerDaemon) Do(method, path string, query map[string]string, data interface{}) (io.ReadCloser, http.Header, error) { - // fmt.Println("DOCKER:", method, path, query, data) var postData io.Reader if data != nil { b, err := json.Marshal(data) @@ -164,7 +163,6 @@ func (d *DockerDaemon) Pull(t *testing.T, imageName, tag string) { if err := json.NewDecoder(body).Decode(&out); err != nil { break } - // fmt.Printf("OUT: %#v\n", out) if out["message"] != nil { t.Fatal(out["message"]) } diff --git a/acceptance/fixtures/buildpacks/order.toml b/acceptance/fixtures/buildpacks/order.toml deleted file mode 100644 index 75e696508..000000000 --- a/acceptance/fixtures/buildpacks/order.toml +++ /dev/null @@ -1,5 +0,0 @@ -[[groups]] -build-image = "some-build-image" -run-image = "some-run-image" -buildpacks = [{ id = "sample_bp", version = "latest" }] - diff --git a/acceptance/fixtures/buildpacks/sample_bp/latest/bin/detect b/acceptance/fixtures/buildpacks/sample_bp/latest/bin/detect deleted file mode 100755 index 577c2e385..000000000 --- a/acceptance/fixtures/buildpacks/sample_bp/latest/bin/detect +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -echo "sample_bp = { version = \"latest\" }" -exit 0 diff --git a/acceptance/testdata/builder.toml b/acceptance/testdata/builder.toml new file mode 100644 index 000000000..5885ce0f5 --- /dev/null +++ b/acceptance/testdata/builder.toml @@ -0,0 +1,6 @@ +[[buildpacks]] +id = "com.example.sample.bp" +uri = "file://testdata/buildpacks/sample_bp" + +[[groups]] +buildpacks = [{ id = "com.example.sample.bp", version = "1.2.3" }] \ No newline at end of file diff --git a/acceptance/fixtures/buildpacks/sample_bp/latest/bin/build b/acceptance/testdata/buildpacks/sample_bp/bin/build similarity index 88% rename from acceptance/fixtures/buildpacks/sample_bp/latest/bin/build rename to acceptance/testdata/buildpacks/sample_bp/bin/build index e85eb6fc6..360d2dec0 100755 --- a/acceptance/fixtures/buildpacks/sample_bp/latest/bin/build +++ b/acceptance/testdata/buildpacks/sample_bp/bin/build @@ -7,6 +7,7 @@ launch_dir=$3 mkdir -p "$launch_dir/layer/bin" echo -e "#!/usr/bin/env bash\necho Hi from Sample BP" > "$launch_dir/layer/bin/my-run" +chmod +x "$launch_dir/layer/bin/my-run" touch "$launch_dir/layer.toml" echo 'processes = [{ type = "web", command = "my-run"}]' > "$launch_dir/launch.toml" diff --git a/acceptance/testdata/buildpacks/sample_bp/bin/detect b/acceptance/testdata/buildpacks/sample_bp/bin/detect new file mode 100755 index 000000000..126e15f52 --- /dev/null +++ b/acceptance/testdata/buildpacks/sample_bp/bin/detect @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +echo "com.example.sample.bp = { version = \"1.2.3\" }" +exit 0 diff --git a/acceptance/testdata/buildpacks/sample_bp/buildpack.toml b/acceptance/testdata/buildpacks/sample_bp/buildpack.toml new file mode 100644 index 000000000..5bb528317 --- /dev/null +++ b/acceptance/testdata/buildpacks/sample_bp/buildpack.toml @@ -0,0 +1,4 @@ +[buildpack] +id = "com.example.sample.bp" +version = "1.2.3" +name = "Sample Buildpack" \ No newline at end of file diff --git a/acceptance/fixtures/node_app/app.js b/acceptance/testdata/node_app/app.js similarity index 100% rename from acceptance/fixtures/node_app/app.js rename to acceptance/testdata/node_app/app.js diff --git a/acceptance/fixtures/node_app/package.json b/acceptance/testdata/node_app/package.json similarity index 100% rename from acceptance/fixtures/node_app/package.json rename to acceptance/testdata/node_app/package.json diff --git a/build.go b/build.go index 8a75bebb1..5b4ff63f3 100644 --- a/build.go +++ b/build.go @@ -16,20 +16,20 @@ import ( func Build(appDir, buildImage, runImage, repoName string, publish bool) error { return (&BuildFlags{ - AppDir: appDir, - BuildImage: buildImage, - RunImage: runImage, - RepoName: repoName, - Publish: publish, + AppDir: appDir, + Builder: buildImage, + RunImage: runImage, + RepoName: repoName, + Publish: publish, }).Run() } type BuildFlags struct { - AppDir string - BuildImage string - RunImage string - RepoName string - Publish bool + AppDir string + Builder string + RunImage string + RepoName string + Publish bool } func (b *BuildFlags) Run() error { @@ -45,19 +45,19 @@ func (b *BuildFlags) Run() error { defer exec.Command("docker", "volume", "rm", "-f", workspaceVolume).Run() fmt.Println("*** COPY APP TO VOLUME:") - if err := copyToVolume(b.BuildImage, workspaceVolume, b.AppDir, "app"); err != nil { + if err := copyToVolume(b.Builder, workspaceVolume, b.AppDir, "app"); err != nil { return err } fmt.Println("*** DETECTING:") - cmd := exec.Command("docker", "run", "--rm", "-v", workspaceVolume+":/workspace", b.BuildImage, "/lifecycle/detector") + cmd := exec.Command("docker", "run", "--rm", "-v", workspaceVolume+":/workspace", b.Builder, "/lifecycle/detector") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return err } - group, err := groupToml(workspaceVolume, b.BuildImage) + group, err := groupToml(workspaceVolume, b.Builder) if err != nil { return err } @@ -71,7 +71,7 @@ func (b *BuildFlags) Run() error { if err := analyzer(group, analyzeTmpDir, b.RepoName, !b.Publish); err != nil { return err } - if err := copyToVolume(b.BuildImage, workspaceVolume, analyzeTmpDir, ""); err != nil { + if err := copyToVolume(b.Builder, workspaceVolume, analyzeTmpDir, ""); err != nil { return err } @@ -80,7 +80,7 @@ func (b *BuildFlags) Run() error { "--rm", "-v", workspaceVolume+":/workspace", "-v", cacheVolume+":/cache", - b.BuildImage, + b.Builder, "/lifecycle/builder", ) cmd.Stdout = os.Stdout @@ -99,7 +99,7 @@ func (b *BuildFlags) Run() error { fmt.Println("*** EXPORTING:") if b.Publish { - localWorkspaceDir, cleanup, err := exportVolume(b.BuildImage, workspaceVolume) + localWorkspaceDir, cleanup, err := exportVolume(b.Builder, workspaceVolume) if err != nil { return err } diff --git a/cmd/pack/main.go b/cmd/pack/main.go index 03647dad7..e6fe6e31c 100644 --- a/cmd/pack/main.go +++ b/cmd/pack/main.go @@ -8,11 +8,23 @@ import ( ) func main() { + buildCmd := buildCommand() + createBuilderCmd := createBuilderCommand() + + rootCmd := &cobra.Command{Use: "pack"} + rootCmd.AddCommand(buildCmd) + rootCmd.AddCommand(createBuilderCmd) + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +func buildCommand() *cobra.Command { wd, _ := os.Getwd() var buildFlags pack.BuildFlags buildCommand := &cobra.Command{ - Use: "build [IMAGE NAME]", + Use: "build ", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { buildFlags.RepoName = args[0] @@ -20,28 +32,30 @@ func main() { }, } buildCommand.Flags().StringVarP(&buildFlags.AppDir, "path", "p", wd, "path to app dir") - buildCommand.Flags().StringVar(&buildFlags.BuildImage, "build-image", "packs/build:0.0.1-rc.170", "build image") - buildCommand.Flags().StringVar(&buildFlags.RunImage, "run-image", "packs/run:0.0.1-rc.170", "run image") + buildCommand.Flags().StringVar(&buildFlags.Builder, "builder", "packs/samples", "builder image") + buildCommand.Flags().StringVar(&buildFlags.RunImage, "run-image", "packs/run", "run image") buildCommand.Flags().BoolVar(&buildFlags.Publish, "publish", false, "publish to registry") + return buildCommand +} - var createFlags pack.Create - createCommand := &cobra.Command{ - Use: "create [DETECT IMAGE NAME] [BUILD IMAGE NAME]", - Args: cobra.MinimumNArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - createFlags.DetectImage = args[0] - createFlags.BuildImage = args[1] - return createFlags.Run() +func createBuilderCommand() *cobra.Command { + builderFactory := pack.BuilderFactory{ + DefaultStack: pack.Stack{ + ID: "", + BuildImage: "packs/build", + RunImage: "packs/run", }, } - createCommand.Flags().StringVarP(&createFlags.BPDir, "path", "p", wd, "path to dir with buildpacks and order.toml") - createCommand.Flags().StringVar(&createFlags.BaseImage, "from-base-image", "packs/v3:latest", "from base image") - createCommand.Flags().BoolVar(&createFlags.Publish, "publish", false, "publish to registry") - rootCmd := &cobra.Command{Use: "pack"} - rootCmd.AddCommand(buildCommand) - rootCmd.AddCommand(createCommand) - if err := rootCmd.Execute(); err != nil { - os.Exit(1) + var createBuilderFlags pack.CreateBuilderFlags + createBuilderCommand := &cobra.Command{ + Use: "create-builder -b ", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + createBuilderFlags.RepoName = args[0] + return builderFactory.Create(createBuilderFlags) + }, } + createBuilderCommand.Flags().StringVarP(&createBuilderFlags.BuilderTomlPath, "builder-config", "b", "", "path to builder.toml file") + return createBuilderCommand } diff --git a/create.go b/create.go deleted file mode 100644 index dcfe230ac..000000000 --- a/create.go +++ /dev/null @@ -1,155 +0,0 @@ -package pack - -import ( - "archive/tar" - "compress/gzip" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - - "github.com/buildpack/packs/img" - "github.com/google/go-containerregistry/pkg/v1/mutate" -) - -type Create struct { - BPDir string - BaseImage string - DetectImage string - BuildImage string - Publish bool -} - -func (c *Create) Run() error { - useDaemon := !c.Publish - tmpDir, err := ioutil.TempDir("", "pack.create.") - if err != nil { - return err - } - defer os.RemoveAll(tmpDir) - - baseImage, err := readImage(c.BaseImage, useDaemon) - if err != nil { - return err - } - if baseImage == nil { - return fmt.Errorf("base-image not found: %s", c.BaseImage) - } - - if err := createTarFile(filepath.Join(tmpDir, "buildpacks.tar"), c.BPDir, "/buildpacks"); err != nil { - return err - } - newImage, _, err := img.Append(baseImage, filepath.Join(tmpDir, "buildpacks.tar")) - if err != nil { - return err - } - - configFile, err := newImage.ConfigFile() - if err != nil { - return err - } - config := *configFile.Config.DeepCopy() - config.Cmd = []string{} - config.User = "packs" - config.Entrypoint = []string{"/packs/detector"} - config.Env = append( - config.Env, - "PACK_BP_PATH=/buildpacks", - "PACK_BP_ORDER_PATH=/buildpacks/order.toml", - "PACK_BP_GROUP_PATH=./group.toml", - "PACK_DETECT_INFO_PATH=./detect.toml", - "PACK_STACK_NAME=", - ) - newImage, err = mutate.Config(newImage, config) - if err != nil { - return err - } - - detectStore, err := repoStore(c.DetectImage, useDaemon) - if err != nil { - return err - } - if err := detectStore.Write(newImage); err != nil { - return err - } - - config.Entrypoint = []string{"/packs/builder"} - config.Env = append( - config.Env, - "PACK_METADATA_PATH=/launch/config/metadata.toml", - ) - newImage, err = mutate.Config(newImage, config) - if err != nil { - return err - } - - buildStore, err := repoStore(c.BuildImage, useDaemon) - if err != nil { - return err - } - if err := buildStore.Write(newImage); err != nil { - return err - } - - return nil -} - -// TODO share between here and exporter. -func createTarFile(tarFile, fsDir, tarDir string) error { - fh, err := os.Create(tarFile) - if err != nil { - return fmt.Errorf("create file for tar: %s", err) - } - defer fh.Close() - gzw := gzip.NewWriter(fh) - defer gzw.Close() - tw := tar.NewWriter(gzw) - defer tw.Close() - - return filepath.Walk(fsDir, func(file string, fi os.FileInfo, err error) error { - if err != nil { - return err - } - if fi.Mode().IsDir() { - return nil - } - relPath, err := filepath.Rel(fsDir, file) - if err != nil { - return err - } - - var header *tar.Header - if fi.Mode()&os.ModeSymlink != 0 { - target, err := os.Readlink(file) - if err != nil { - return err - } - header, err = tar.FileInfoHeader(fi, target) - if err != nil { - return err - } - } else { - header, err = tar.FileInfoHeader(fi, fi.Name()) - if err != nil { - return err - } - } - header.Name = filepath.Join(tarDir, relPath) - - if err := tw.WriteHeader(header); err != nil { - return err - } - if fi.Mode().IsRegular() { - f, err := os.Open(file) - if err != nil { - return err - } - defer f.Close() - if _, err := io.Copy(tw, f); err != nil { - return err - } - } - return nil - }) -} diff --git a/create_builder.go b/create_builder.go new file mode 100644 index 000000000..ffab86b65 --- /dev/null +++ b/create_builder.go @@ -0,0 +1,227 @@ +package pack + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "github.com/BurntSushi/toml" + "github.com/buildpack/lifecycle" + "github.com/buildpack/lifecycle/img" + "github.com/pkg/errors" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" +) + +type CreateBuilderFlags struct { + RepoName string + BuilderTomlPath string +} + +type Buildpack struct { + ID string + URI string +} + +type Builder struct { + Buildpacks []Buildpack `toml:"buildpacks"` + Groups []lifecycle.BuildpackGroup `toml:"groups"` +} + +type BuilderFactory struct { + DefaultStack Stack +} + +func (f *BuilderFactory) Create(flags CreateBuilderFlags) error { + if out, err := exec.Command("docker", "pull", f.DefaultStack.BuildImage).CombinedOutput(); err != nil { + fmt.Println(string(out)) + return err + } + builderStore, err := repoStore(flags.RepoName, true) + if err != nil { + return err + } + + baseImage, err := readImage(f.DefaultStack.BuildImage, true) + if err != nil { + return err + } + + tmpDir, err := ioutil.TempDir("", "buildpack") + if err != nil { + return err + } + defer os.Remove(tmpDir) + + builder := Builder{} + _, err = toml.DecodeFile(flags.BuilderTomlPath, &builder) + if err != nil { + return err + } + + buildpackDir, err := f.buildpackDir(tmpDir, &builder) + if err != nil { + return err + } + if err := createTarFile(filepath.Join(tmpDir, "buildpacks.tar"), buildpackDir, "/buildpacks"); err != nil { + return err + } + builderImage, _, err := img.Append(baseImage, filepath.Join(tmpDir, "buildpacks.tar")) + if err != nil { + return err + } + + return builderStore.Write(builderImage) +} + +type order struct { + Groups []lifecycle.BuildpackGroup `toml:"groups"` +} + +func (f *BuilderFactory) buildpackDir(dest string, builder *Builder) (string, error) { + buildpackDir := filepath.Join(dest, "buildpack") + err := os.Mkdir(buildpackDir, 0755) + if err != nil { + return "", err + } + for _, buildpack := range builder.Buildpacks { + dir := strings.TrimPrefix(buildpack.URI, "file://") + var data struct { + BP struct { + ID string `toml:"id"` + Version string `toml:"version"` + } `toml:"buildpack"` + } + _, err := toml.DecodeFile(filepath.Join(dir, "buildpack.toml"), &data) + if err != nil { + return "", errors.Wrapf(err, "reading buildpack.toml from buildpack: %s", filepath.Join(dir, "buildpack.toml")) + } + bp := data.BP + if buildpack.ID != bp.ID { + return "", fmt.Errorf("buildpack ids did not match: %s != %s", buildpack.ID, bp.ID) + } + if bp.Version == "" { + return "", fmt.Errorf("buildpack.toml must provide version: %s", filepath.Join(dir, "buildpack.toml")) + } + err = recursiveCopy(dir, filepath.Join(buildpackDir, buildpack.ID, bp.Version)) + if err != nil { + return "", err + } + } + + orderFile, err := os.Create(filepath.Join(buildpackDir, "order.toml")) + if err != nil { + return "", err + } + defer orderFile.Close() + err = toml.NewEncoder(orderFile).Encode(order{Groups: builder.Groups}) + if err != nil { + return "", err + } + return buildpackDir, nil +} + +func recursiveCopy(src, dest string) error { + return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + relPath, err := filepath.Rel(src, path) + if err != nil { + return err + } + destFile := filepath.Join(dest, relPath) + if info.IsDir() { + err := os.MkdirAll(destFile, info.Mode()) + if err != nil { + return err + } + } + if info.Mode()&os.ModeSymlink != 0 { + target, err := os.Readlink(path) + if err != nil { + return err + } + os.Symlink(destFile, target) + } + if info.Mode().IsRegular() { + s, err := os.Open(path) + if err != nil { + return err + } + defer s.Close() + + d, err := os.OpenFile(destFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + defer d.Close() + if _, err := io.Copy(d, s); err != nil { + return err + } + } + return nil + }) +} + +// TODO share between here and exporter. +func createTarFile(tarFile, fsDir, tarDir string) error { + fh, err := os.Create(tarFile) + if err != nil { + return fmt.Errorf("create file for tar: %s", err) + } + defer fh.Close() + gzw := gzip.NewWriter(fh) + defer gzw.Close() + tw := tar.NewWriter(gzw) + defer tw.Close() + + return filepath.Walk(fsDir, func(file string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if fi.Mode().IsDir() { + return nil + } + relPath, err := filepath.Rel(fsDir, file) + if err != nil { + return err + } + + var header *tar.Header + if fi.Mode()&os.ModeSymlink != 0 { + target, err := os.Readlink(file) + if err != nil { + return err + } + header, err = tar.FileInfoHeader(fi, target) + if err != nil { + return err + } + } else { + header, err = tar.FileInfoHeader(fi, fi.Name()) + if err != nil { + return err + } + } + header.Name = filepath.Join(tarDir, relPath) + + if err := tw.WriteHeader(header); err != nil { + return err + } + if fi.Mode().IsRegular() { + f, err := os.Open(file) + if err != nil { + return err + } + defer f.Close() + if _, err := io.Copy(tw, f); err != nil { + return err + } + } + return nil + }) +} diff --git a/create_builder_test.go b/create_builder_test.go new file mode 100644 index 000000000..efa176a26 --- /dev/null +++ b/create_builder_test.go @@ -0,0 +1 @@ +package pack diff --git a/exporter.go b/exporter.go index 0e921d8e8..8da380b54 100644 --- a/exporter.go +++ b/exporter.go @@ -201,7 +201,6 @@ func addDockerfileToTar(runImage, repoName string, buildpacks []string, r io.Rea for _, b := range buildpacks { isBuildpack[b] = true } - // fmt.Println(isBuildpack) go func() { defer pw.Close() @@ -218,7 +217,6 @@ func addDockerfileToTar(runImage, repoName string, buildpacks []string, r io.Rea errChan <- errors.Wrap(err, "tr.Next") return } - // fmt.Printf("File: %s\n", hdr.Name) tw.WriteHeader(hdr) @@ -267,7 +265,6 @@ func addDockerfileToTar(runImage, repoName string, buildpacks []string, r io.Rea layers := sortedKeys(tomlFiles[buildpack]) for _, layer := range layers { layerNames = append(layerNames, dockerfileLayer{buildpack, layer, tomlFiles[buildpack][layer]}) - // fmt.Println("Buildpack:", buildpack, "Layer:", layer, "DIRS:", dirs) if dirs[buildpack][layer] { dockerFile += fmt.Sprintf("ADD --chown=pack:pack /workspace/%s/%s /workspace/%s/%s\n", buildpack, layer, buildpack, layer) } else { @@ -280,10 +277,6 @@ func addDockerfileToTar(runImage, repoName string, buildpacks []string, r io.Rea dockerFile = "FROM " + repoName + " AS prev\n\n" + dockerFile } - fmt.Println(tomlFiles) - fmt.Println(dirs) - // fmt.Println(dockerFile) - if err := tw.WriteHeader(&tar.Header{Name: "Dockerfile", Size: int64(len(dockerFile)), Mode: 0666}); err != nil { layerChan <- nil errChan <- errors.Wrap(err, "write tar header for Dockerfile") diff --git a/stack.go b/stack.go new file mode 100644 index 000000000..e14399748 --- /dev/null +++ b/stack.go @@ -0,0 +1,7 @@ +package pack + +type Stack struct { + ID string + BuildImage string + RunImage string +}