diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 393b765af7..f1b8489baa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,8 +4,13 @@ updates: - package-ecosystem: "gomod" directory: "/" schedule: - # Check for updates to GitHub Actions every weekday - interval: "daily" + interval: "weekly" + groups: + # Group all minor/patch go dependencies into a single PR. + go-dependencies: + update-types: + - "minor" + - "patch" labels: - "dependencies" - "go" @@ -15,8 +20,7 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - # Check for updates to GitHub Actions every weekday - interval: "daily" + interval: "weekly" labels: - "dependencies" - "github_actions" diff --git a/.github/workflows/delivery-docker.yml b/.github/workflows/delivery-docker.yml index 5db2c7e4b2..c956195175 100644 --- a/.github/workflows/delivery-docker.yml +++ b/.github/workflows/delivery-docker.yml @@ -16,12 +16,21 @@ on: default: false env: - BUILDER: "paketobuildpacks/builder-jammy-tiny" IMG_NAME: 'pack' USERNAME: 'buildpacksio' jobs: deliver-docker: + strategy: + matrix: + config: [tiny, base] + include: + - config: tiny + base_image: gcr.io/distroless/static + suffix: + - config: base + base_image: ubuntu:jammy + suffix: -base runs-on: ubuntu-latest steps: - name: Determine version @@ -42,16 +51,6 @@ jobs: uses: actions/checkout@v4 with: ref: v${{ steps.version.outputs.result }} - # This has to come after the first checkout, so it isn't clobbered - - name: Checkout delivery configuration - uses: actions/checkout@v4 - with: - path: ./head - - name: Setup Working Dir - shell: bash - run: | - rm project.toml || true - cp head/.github/workflows/delivery/docker/project.toml project.toml - name: Determine App Name run: 'echo "IMG_NAME=${{ env.USERNAME }}/${{ env.IMG_NAME }}" >> $GITHUB_ENV' - name: Login to Dockerhub @@ -65,12 +64,17 @@ jobs: - name: Buildx Build/Publish run: | docker buildx build . \ - --tag ${{ env.IMG_NAME }}:${{ steps.version.outputs.result }} \ + --tag ${{ env.IMG_NAME }}:${{ steps.version.outputs.result }}${{ matrix.suffix }} \ --platform linux/amd64,linux/arm64 \ --build-arg pack_version=${{ steps.version.outputs.result }} \ + --build-arg base_image=${{ matrix.base_image }} \ --provenance=false \ --push + - name: Tag Image as Base + if: ${{ (github.event.release != '' || github.event.inputs.tag_latest) && matrix.config == 'base' }} + run: | + crane copy ${{ env.IMG_NAME }}:${{ steps.version.outputs.result }} ${{ env.IMG_NAME }}:base - name: Tag Image as Latest - if: ${{ github.event.release != '' || github.event.inputs.tag_latest }} + if: ${{ (github.event.release != '' || github.event.inputs.tag_latest) && matrix.config != 'base' }} run: | crane copy ${{ env.IMG_NAME }}:${{ steps.version.outputs.result }} ${{ env.IMG_NAME }}:latest diff --git a/.github/workflows/delivery/docker/project.toml b/.github/workflows/delivery/docker/project.toml deleted file mode 100644 index da9c5c982c..0000000000 --- a/.github/workflows/delivery/docker/project.toml +++ /dev/null @@ -1,7 +0,0 @@ -[project] -version = "1.0.2" -source-url = "https://github.com/buildpacks/pack" - -[[build.env]] -name = "BP_GO_TARGETS" -value = "./cmd/pack" diff --git a/Dockerfile b/Dockerfile index 70c75ed45e..c81b2f0c04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,5 @@ +ARG base_image=gcr.io/distroless/static + FROM golang:1.20 as builder ARG pack_version ENV PACK_VERSION=$pack_version @@ -5,8 +7,6 @@ WORKDIR /app COPY . . RUN make build -FROM scratch +FROM ${base_image} COPY --from=builder /app/out/pack /usr/local/bin/pack -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -COPY --from=builder /tmp /tmp ENTRYPOINT [ "/usr/local/bin/pack" ] diff --git a/acceptance/testdata/pack_fixtures/report_output.txt b/acceptance/testdata/pack_fixtures/report_output.txt index 58e3baa5d7..beaae617bd 100644 --- a/acceptance/testdata/pack_fixtures/report_output.txt +++ b/acceptance/testdata/pack_fixtures/report_output.txt @@ -2,7 +2,7 @@ Pack: Version: {{ .Version }} OS/Arch: {{ .OS }}/{{ .Arch }} -Default Lifecycle Version: 0.17.1 +Default Lifecycle Version: 0.17.2 Supported Platform APIs: 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.10, 0.11, 0.12 diff --git a/builder/config_reader.go b/builder/config_reader.go index d719c5df8c..6ebd688d55 100644 --- a/builder/config_reader.go +++ b/builder/config_reader.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/BurntSushi/toml" "github.com/pkg/errors" @@ -70,7 +71,25 @@ type RunImageConfig struct { // BuildConfig build image configuration type BuildConfig struct { - Image string `toml:"image"` + Image string `toml:"image"` + Env []BuildConfigEnv `toml:"env"` +} + +type Suffix string + +const ( + NONE Suffix = "" + DEFAULT Suffix = "default" + OVERRIDE Suffix = "override" + APPEND Suffix = "append" + PREPEND Suffix = "prepend" +) + +type BuildConfigEnv struct { + Name string `toml:"name"` + Value string `toml:"value"` + Suffix Suffix `toml:"suffix,omitempty"` + Delim string `toml:"delim,omitempty"` } // ReadConfig reads a builder configuration from the file path provided and returns the @@ -162,3 +181,92 @@ func parseConfig(file *os.File) (Config, error) { return builderConfig, nil } + +func ParseBuildConfigEnv(env []BuildConfigEnv, path string) (envMap map[string]string, warnings []string, err error) { + envMap = map[string]string{} + var appendOrPrependWithoutDelim = 0 + for _, v := range env { + if name := v.Name; name == "" || len(name) == 0 { + return nil, nil, errors.Wrapf(errors.Errorf("env name should not be empty"), "parse contents of '%s'", path) + } + if val := v.Value; val == "" || len(val) == 0 { + warnings = append(warnings, fmt.Sprintf("empty value for key/name %s", style.Symbol(v.Name))) + } + suffixName, delimName, err := getBuildConfigEnvFileName(v) + if err != nil { + return envMap, warnings, err + } + if val, e := envMap[suffixName]; e { + warnings = append(warnings, fmt.Sprintf(errors.Errorf("overriding env with name: %s and suffix: %s from %s to %s", style.Symbol(v.Name), style.Symbol(string(v.Suffix)), style.Symbol(val), style.Symbol(v.Value)).Error(), "parse contents of '%s'", path)) + } + if val, e := envMap[delimName]; e { + warnings = append(warnings, fmt.Sprintf(errors.Errorf("overriding env with name: %s and delim: %s from %s to %s", style.Symbol(v.Name), style.Symbol(v.Delim), style.Symbol(val), style.Symbol(v.Value)).Error(), "parse contents of '%s'", path)) + } + if delim := v.Delim; (delim != "" || len(delim) != 0) && (delimName != "" || len(delimName) != 0) { + envMap[delimName] = delim + } + envMap[suffixName] = v.Value + } + + for k := range envMap { + name, suffix, err := getFilePrefixSuffix(k) + if err != nil { + continue + } + if _, ok := envMap[name+".delim"]; (suffix == "append" || suffix == "prepend") && !ok { + warnings = append(warnings, fmt.Sprintf(errors.Errorf("env with name/key %s with suffix %s must to have a %s value", style.Symbol(name), style.Symbol(suffix), style.Symbol("delim")).Error(), "parse contents of '%s'", path)) + appendOrPrependWithoutDelim++ + } + } + if appendOrPrependWithoutDelim > 0 { + return envMap, warnings, errors.Errorf("error parsing [[build.env]] in file '%s'", path) + } + return envMap, warnings, err +} + +func getBuildConfigEnvFileName(env BuildConfigEnv) (suffixName, delimName string, err error) { + suffix, err := getActionType(env.Suffix) + if err != nil { + return suffixName, delimName, err + } + if suffix == "" { + suffixName = env.Name + } else { + suffixName = env.Name + suffix + } + if delim := env.Delim; delim != "" || len(delim) != 0 { + delimName = env.Name + ".delim" + } + return suffixName, delimName, err +} + +func getActionType(suffix Suffix) (suffixString string, err error) { + const delim = "." + switch suffix { + case NONE: + return "", nil + case DEFAULT: + return delim + string(DEFAULT), nil + case OVERRIDE: + return delim + string(OVERRIDE), nil + case APPEND: + return delim + string(APPEND), nil + case PREPEND: + return delim + string(PREPEND), nil + default: + return suffixString, errors.Errorf("unknown action type %s", style.Symbol(string(suffix))) + } +} + +func getFilePrefixSuffix(filename string) (prefix, suffix string, err error) { + val := strings.Split(filename, ".") + if len(val) <= 1 { + return val[0], suffix, errors.Errorf("Suffix might be null") + } + if len(val) == 2 { + suffix = val[1] + } else { + suffix = strings.Join(val[1:], ".") + } + return val[0], suffix, err +} diff --git a/builder/config_reader_test.go b/builder/config_reader_test.go index 93a1bf851d..6c5512f897 100644 --- a/builder/config_reader_test.go +++ b/builder/config_reader_test.go @@ -229,4 +229,111 @@ uri = "noop-buildpack.tgz" h.AssertError(t, builder.ValidateConfig(config), "build.image is required") }) }) + when("#ParseBuildConfigEnv()", func() { + it("should return an error when name is not defined", func() { + _, _, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{ + { + Name: "", + Value: "vaiue", + }, + }, "") + h.AssertNotNil(t, err) + }) + it("should warn when the value is nil or empty string", func() { + env, warn, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{ + { + Name: "key", + Value: "", + Suffix: "override", + }, + }, "") + + h.AssertNotNil(t, warn) + h.AssertNil(t, err) + h.AssertMapContains[string, string](t, env, h.NewKeyValue[string, string]("key.override", "")) + }) + it("should return an error when unknown suffix is specified", func() { + _, _, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{ + { + Name: "key", + Value: "", + Suffix: "invalid", + }, + }, "") + + h.AssertNotNil(t, err) + }) + it("should override and show a warning when suffix or delim is defined multiple times", func() { + env, warn, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{ + { + Name: "key1", + Value: "value1", + Suffix: "append", + Delim: "%", + }, + { + Name: "key1", + Value: "value2", + Suffix: "append", + Delim: ",", + }, + { + Name: "key1", + Value: "value3", + Suffix: "default", + Delim: ";", + }, + { + Name: "key1", + Value: "value4", + Suffix: "prepend", + Delim: ":", + }, + }, "") + + h.AssertNotNil(t, warn) + h.AssertNil(t, err) + h.AssertMapContains[string, string]( + t, + env, + h.NewKeyValue[string, string]("key1.append", "value2"), + h.NewKeyValue[string, string]("key1.default", "value3"), + h.NewKeyValue[string, string]("key1.prepend", "value4"), + h.NewKeyValue[string, string]("key1.delim", ":"), + ) + h.AssertMapNotContains[string, string]( + t, + env, + h.NewKeyValue[string, string]("key1.append", "value1"), + h.NewKeyValue[string, string]("key1.delim", "%"), + h.NewKeyValue[string, string]("key1.delim", ","), + h.NewKeyValue[string, string]("key1.delim", ";"), + ) + }) + it("should return an error when `suffix` is defined as `append` or `prepend` without a `delim`", func() { + _, warn, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{ + { + Name: "key", + Value: "value", + Suffix: "append", + }, + }, "") + + h.AssertNotNil(t, warn) + h.AssertNotNil(t, err) + }) + it("when suffix is NONE or omitted should default to `override`", func() { + env, warn, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{ + { + Name: "key", + Value: "value", + Suffix: "", + }, + }, "") + + h.AssertNotNil(t, warn) + h.AssertNil(t, err) + h.AssertMapContains[string, string](t, env, h.NewKeyValue[string, string]("key", "value")) + }) + }) } diff --git a/go.mod b/go.mod index af6b065132..863d6caada 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Microsoft/go-winio v0.6.1 github.com/apex/log v1.9.0 github.com/buildpacks/imgutil v0.0.0-20230626185301-726f02e4225c - github.com/buildpacks/lifecycle v0.17.1 + github.com/buildpacks/lifecycle v0.17.2 github.com/docker/cli v24.0.6+incompatible github.com/docker/docker v24.0.6+incompatible github.com/docker/go-connections v0.4.0 diff --git a/go.sum b/go.sum index f9eea35414..71b3ec690c 100644 --- a/go.sum +++ b/go.sum @@ -97,8 +97,8 @@ github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230522190001- github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/buildpacks/imgutil v0.0.0-20230626185301-726f02e4225c h1:HlRuSz+JGAzudNtNCfHIzXe0AEuHX6Vx8uZgmjvX02o= github.com/buildpacks/imgutil v0.0.0-20230626185301-726f02e4225c/go.mod h1:mBG5M3GJW5nknCEOOqtmMHyPYnSpw/5GEiciuYU/COw= -github.com/buildpacks/lifecycle v0.17.1 h1:sCNj83TH1YE8Z3+CKHoFx/HK+llCVF1RlQUbj3xdNBQ= -github.com/buildpacks/lifecycle v0.17.1/go.mod h1:WFzcNp1WG4bwgHuXtKxMg4tdU3AguL44ZlP3knANeVs= +github.com/buildpacks/lifecycle v0.17.2 h1:CfJYWHIC5v996idgjDamYHBTk+G+c1Qt7Yk80MlbWpw= +github.com/buildpacks/lifecycle v0.17.2/go.mod h1:h8MrqltqMM+HQnn2F2JOQaKWmeybZ54qvlNV3pAiAqw= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4= github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM= diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 51b5a4a00e..fa7dd6e9d3 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -32,6 +32,8 @@ import ( lifecycleplatform "github.com/buildpacks/lifecycle/platform" ) +var buildConfigDir = cnbBuildConfigDir() + const ( packName = "Pack CLI" @@ -67,6 +69,7 @@ const ( // Builder represents a pack builder, used to build images type Builder struct { baseImageName string + buildConfigEnv map[string]string image imgutil.Image layerWriterFactory archive.TarWriterFactory lifecycle Lifecycle @@ -146,6 +149,7 @@ func constructBuilder(img imgutil.Image, newName string, errOnMissingLabel bool, metadata: metadata, lifecycleDescriptor: constructLifecycleDescriptor(metadata), env: map[string]string{}, + buildConfigEnv: map[string]string{}, validateMixins: true, additionalBuildpacks: *buildpack.NewModuleManager(opts.flatten, opts.depth), additionalExtensions: *buildpack.NewModuleManager(opts.flatten, opts.depth), @@ -349,6 +353,11 @@ func (b *Builder) SetEnv(env map[string]string) { b.env = env } +// SetBuildConfigEnv sets an environment variable to a value that will take action on platform environment variables basedon filename suffix +func (b *Builder) SetBuildConfigEnv(env map[string]string) { + b.buildConfigEnv = env +} + // SetOrder sets the order of the builder func (b *Builder) SetOrder(order dist.Order) { b.order = order @@ -525,6 +534,18 @@ func (b *Builder) Save(logger logging.Logger, creatorMetadata CreatorMetadata) e return errors.Wrap(err, "adding run.tar layer") } + if len(b.buildConfigEnv) > 0 { + logger.Debugf("Provided Build Config Environment Variables\n %s", style.Map(b.env, " ", "\n")) + buildConfigEnvTar, err := b.buildConfigEnvLayer(tmpDir, b.buildConfigEnv) + if err != nil { + return errors.Wrap(err, "retrieving build-config-env layer") + } + + if err := b.image.AddLayer(buildConfigEnvTar); err != nil { + return errors.Wrap(err, "adding build-config-env layer") + } + } + if len(b.env) > 0 { logger.Debugf("Provided Environment Variables\n %s", style.Map(b.env, " ", "\n")) } @@ -903,7 +924,7 @@ func (b *Builder) defaultDirsLayer(dest string) (string, error) { } // can't use filepath.Join(), to ensure Windows doesn't transform it to Windows join - for _, path := range []string{cnbDir, dist.BuildpacksDir, dist.ExtensionsDir, platformDir, platformDir + "/env"} { + for _, path := range []string{cnbDir, dist.BuildpacksDir, dist.ExtensionsDir, platformDir, platformDir + "/env", buildConfigDir, buildConfigDir + "/env"} { if err := lw.WriteHeader(b.rootOwnedDir(path, ts)); err != nil { return "", errors.Wrapf(err, "creating %s dir in layer", style.Symbol(path)) } @@ -1102,6 +1123,31 @@ func (b *Builder) envLayer(dest string, env map[string]string) (string, error) { return fh.Name(), nil } +func (b *Builder) buildConfigEnvLayer(dest string, env map[string]string) (string, error) { + fh, err := os.Create(filepath.Join(dest, "build-config-env.tar")) + if err != nil { + return "", err + } + defer fh.Close() + lw := b.layerWriterFactory.NewWriter(fh) + defer lw.Close() + for k, v := range env { + if err := lw.WriteHeader(&tar.Header{ + Name: path.Join(cnbBuildConfigDir(), "env", k), + Size: int64(len(v)), + Mode: 0644, + ModTime: archive.NormalizedDateTime, + }); err != nil { + return "", err + } + if _, err := lw.Write([]byte(v)); err != nil { + return "", err + } + } + + return fh.Name(), nil +} + func (b *Builder) whiteoutLayer(tmpDir string, i int, bpInfo dist.ModuleInfo) (string, error) { bpWhiteoutsTmpDir := filepath.Join(tmpDir, strconv.Itoa(i)+"_whiteouts") if err := os.MkdirAll(bpWhiteoutsTmpDir, os.ModePerm); err != nil { @@ -1257,3 +1303,11 @@ func (e errModuleTar) Info() dist.ModuleInfo { func (e errModuleTar) Path() string { return "" } + +func cnbBuildConfigDir() string { + if env, ok := os.LookupEnv("CNB_BUILD_CONFIG_DIR"); ok { + return env + } + + return "/cnb/build-config" +} diff --git a/internal/builder/builder_test.go b/internal/builder/builder_test.go index 13a958c72e..37124533da 100644 --- a/internal/builder/builder_test.go +++ b/internal/builder/builder_test.go @@ -384,6 +384,19 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) { ) }) + it("creates the build-config dir", func() { + h.AssertNil(t, subject.Save(logger, builder.CreatorMetadata{})) + h.AssertEq(t, baseImage.IsSaved(), true) + + layerTar, err := baseImage.FindLayerWithPath("/cnb/build-config") + h.AssertNil(t, err) + h.AssertOnTarEntry(t, layerTar, "/cnb/build-config", + h.IsDirectory(), + h.HasOwnerAndGroup(0, 0), + h.HasFileMode(0755), + h.HasModTime(archive.NormalizedDateTime), + ) + }) it("creates the buildpacks dir", func() { h.AssertNil(t, subject.Save(logger, builder.CreatorMetadata{})) h.AssertEq(t, baseImage.IsSaved(), true) @@ -1607,6 +1620,71 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) { }) }) + when("when CNB_BUILD_CONFIG_DIR is defined", func() { + var buildConfigEnvName = "CNB_BUILD_CONFIG_DIR" + var buildConfigEnvValue = "/cnb/dup-build-config-dir" + it.Before(func() { + os.Setenv(buildConfigEnvName, buildConfigEnvValue) + subject.SetBuildConfigEnv(map[string]string{ + "SOME_KEY": "some-val", + "OTHER_KEY.append": "other-val", + "OTHER_KEY.delim": ":", + }) + h.AssertNil(t, subject.Save(logger, builder.CreatorMetadata{})) + h.AssertEq(t, baseImage.IsSaved(), true) + }) + it.After(func() { + os.Unsetenv(buildConfigEnvName) + }) + + it("adds the env vars as files to the image", func() { + layerTar, err := baseImage.FindLayerWithPath(buildConfigEnvValue + "/env/SOME_KEY") + h.AssertNil(t, err) + h.AssertOnTarEntry(t, layerTar, buildConfigEnvValue+"/env/SOME_KEY", + h.ContentEquals(`some-val`), + h.HasModTime(archive.NormalizedDateTime), + ) + h.AssertOnTarEntry(t, layerTar, buildConfigEnvValue+"/env/OTHER_KEY.append", + h.ContentEquals(`other-val`), + h.HasModTime(archive.NormalizedDateTime), + ) + h.AssertOnTarEntry(t, layerTar, buildConfigEnvValue+"/env/OTHER_KEY.delim", + h.ContentEquals(`:`), + h.HasModTime(archive.NormalizedDateTime), + ) + }) + }) + + when("#SetBuildConfigEnv", func() { + it.Before(func() { + os.Unsetenv("CNB_BUILD_CONFIG_DIR") + subject.SetBuildConfigEnv(map[string]string{ + "SOME_KEY": "some-val", + "OTHER_KEY.append": "other-val", + "OTHER_KEY.delim": ":", + }) + h.AssertNil(t, subject.Save(logger, builder.CreatorMetadata{})) + h.AssertEq(t, baseImage.IsSaved(), true) + }) + + it("adds the env vars as files to the image", func() { + layerTar, err := baseImage.FindLayerWithPath("/cnb/build-config/env/SOME_KEY") + h.AssertNil(t, err) + h.AssertOnTarEntry(t, layerTar, "/cnb/build-config/env/SOME_KEY", + h.ContentEquals(`some-val`), + h.HasModTime(archive.NormalizedDateTime), + ) + h.AssertOnTarEntry(t, layerTar, "/cnb/build-config/env/OTHER_KEY.append", + h.ContentEquals(`other-val`), + h.HasModTime(archive.NormalizedDateTime), + ) + h.AssertOnTarEntry(t, layerTar, "/cnb/build-config/env/OTHER_KEY.delim", + h.ContentEquals(`:`), + h.HasModTime(archive.NormalizedDateTime), + ) + }) + }) + when("#SetEnv", func() { it.Before(func() { subject.SetEnv(map[string]string{ diff --git a/internal/builder/lifecycle.go b/internal/builder/lifecycle.go index 3184279021..6c3193d47b 100644 --- a/internal/builder/lifecycle.go +++ b/internal/builder/lifecycle.go @@ -14,7 +14,7 @@ import ( // A snapshot of the latest tested lifecycle version values const ( - DefaultLifecycleVersion = "0.17.1" + DefaultLifecycleVersion = "0.17.2" DefaultBuildpackAPIVersion = "0.2" ) diff --git a/internal/commands/builder_create.go b/internal/commands/builder_create.go index 1b0d709895..c3c1ccb90b 100644 --- a/internal/commands/builder_create.go +++ b/internal/commands/builder_create.go @@ -75,9 +75,18 @@ Creating a custom builder allows you to control what buildpacks are used and wha return errors.Wrap(err, "getting absolute path for config") } + envMap, warnings, err := builder.ParseBuildConfigEnv(builderConfig.Build.Env, flags.BuilderTomlPath) + for _, v := range warnings { + logger.Warn(v) + } + if err != nil { + return err + } + imageName := args[0] if err := pack.CreateBuilder(cmd.Context(), client.CreateBuilderOptions{ RelativeBaseDir: relativeBaseDir, + BuildConfigEnv: envMap, BuilderName: imageName, Config: builderConfig, Publish: flags.Publish, diff --git a/internal/commands/builder_create_test.go b/internal/commands/builder_create_test.go index 12c89465f4..4ddf115b32 100644 --- a/internal/commands/builder_create_test.go +++ b/internal/commands/builder_create_test.go @@ -13,6 +13,7 @@ import ( "github.com/sclevine/spec/report" "github.com/spf13/cobra" + "github.com/buildpacks/pack/builder" "github.com/buildpacks/pack/internal/commands" "github.com/buildpacks/pack/internal/commands/testmocks" "github.com/buildpacks/pack/internal/config" @@ -47,6 +48,110 @@ const validConfigWithExtensions = ` ` +var BuildConfigEnvSuffixNone = builder.BuildConfigEnv{ + Name: "suffixNone", + Value: "suffixNoneValue", +} + +var BuildConfigEnvSuffixNoneWithEmptySuffix = builder.BuildConfigEnv{ + Name: "suffixNoneWithEmptySuffix", + Value: "suffixNoneWithEmptySuffixValue", + Suffix: "", +} + +var BuildConfigEnvSuffixDefault = builder.BuildConfigEnv{ + Name: "suffixDefault", + Value: "suffixDefaultValue", + Suffix: "default", +} + +var BuildConfigEnvSuffixOverride = builder.BuildConfigEnv{ + Name: "suffixOverride", + Value: "suffixOverrideValue", + Suffix: "override", +} + +var BuildConfigEnvSuffixAppend = builder.BuildConfigEnv{ + Name: "suffixAppend", + Value: "suffixAppendValue", + Suffix: "append", + Delim: ":", +} + +var BuildConfigEnvSuffixPrepend = builder.BuildConfigEnv{ + Name: "suffixPrepend", + Value: "suffixPrependValue", + Suffix: "prepend", + Delim: ":", +} + +var BuildConfigEnvDelimWithoutSuffix = builder.BuildConfigEnv{ + Name: "delimWithoutSuffix", + Delim: ":", +} + +var BuildConfigEnvSuffixUnknown = builder.BuildConfigEnv{ + Name: "suffixUnknown", + Value: "suffixUnknownValue", + Suffix: "unknown", +} + +var BuildConfigEnvSuffixMultiple = []builder.BuildConfigEnv{ + { + Name: "MY_VAR", + Value: "suffixAppendValueValue", + Suffix: "append", + Delim: ";", + }, + { + Name: "MY_VAR", + Value: "suffixDefaultValue", + Suffix: "default", + Delim: "%", + }, + { + Name: "MY_VAR", + Value: "suffixPrependValue", + Suffix: "prepend", + Delim: ":", + }, +} + +var BuildConfigEnvEmptyValue = builder.BuildConfigEnv{ + Name: "warning", + Value: "", +} + +var BuildConfigEnvEmptyName = builder.BuildConfigEnv{ + Name: "", + Value: "suffixUnknownValue", + Suffix: "default", +} + +var BuildConfigEnvSuffixPrependWithoutDelim = builder.BuildConfigEnv{ + Name: "suffixPrepend", + Value: "suffixPrependValue", + Suffix: "prepend", +} + +var BuildConfigEnvDelimWithoutSuffixAppendOrPrepend = builder.BuildConfigEnv{ + Name: "delimWithoutActionAppendOrPrepend", + Value: "some-value", + Delim: ":", +} + +var BuildConfigEnvDelimWithSameSuffixAndName = []builder.BuildConfigEnv{ + { + Name: "MY_VAR", + Value: "some-value", + Suffix: "", + }, + { + Name: "MY_VAR", + Value: "some-value", + }, +} + func TestCreateCommand(t *testing.T) { color.Disable(true) defer color.Disable(false) @@ -171,6 +276,115 @@ func testCreateCommand(t *testing.T, when spec.G, it spec.S) { }) }) + when("#ParseBuildpackConfigEnv", func() { + it("should create envMap as expected when suffix is omitted", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{BuildConfigEnvSuffixNone}, "") + h.AssertEq(t, envMap, map[string]string{ + BuildConfigEnvSuffixNone.Name: BuildConfigEnvSuffixNone.Value, + }) + h.AssertEq(t, len(warnings), 0) + h.AssertNil(t, err) + }) + it("should create envMap as expected when suffix is empty string", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{BuildConfigEnvSuffixNoneWithEmptySuffix}, "") + h.AssertEq(t, envMap, map[string]string{ + BuildConfigEnvSuffixNoneWithEmptySuffix.Name: BuildConfigEnvSuffixNoneWithEmptySuffix.Value, + }) + h.AssertEq(t, len(warnings), 0) + h.AssertNil(t, err) + }) + it("should create envMap as expected when suffix is `default`", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{BuildConfigEnvSuffixDefault}, "") + h.AssertEq(t, envMap, map[string]string{ + BuildConfigEnvSuffixDefault.Name + "." + string(BuildConfigEnvSuffixDefault.Suffix): BuildConfigEnvSuffixDefault.Value, + }) + h.AssertEq(t, len(warnings), 0) + h.AssertNil(t, err) + }) + it("should create envMap as expected when suffix is `override`", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{BuildConfigEnvSuffixOverride}, "") + h.AssertEq(t, envMap, map[string]string{ + BuildConfigEnvSuffixOverride.Name + "." + string(BuildConfigEnvSuffixOverride.Suffix): BuildConfigEnvSuffixOverride.Value, + }) + h.AssertEq(t, len(warnings), 0) + h.AssertNil(t, err) + }) + it("should create envMap as expected when suffix is `append`", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{BuildConfigEnvSuffixAppend}, "") + h.AssertEq(t, envMap, map[string]string{ + BuildConfigEnvSuffixAppend.Name + "." + string(BuildConfigEnvSuffixAppend.Suffix): BuildConfigEnvSuffixAppend.Value, + BuildConfigEnvSuffixAppend.Name + ".delim": BuildConfigEnvSuffixAppend.Delim, + }) + h.AssertEq(t, len(warnings), 0) + h.AssertNil(t, err) + }) + it("should create envMap as expected when suffix is `prepend`", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{BuildConfigEnvSuffixPrepend}, "") + h.AssertEq(t, envMap, map[string]string{ + BuildConfigEnvSuffixPrepend.Name + "." + string(BuildConfigEnvSuffixPrepend.Suffix): BuildConfigEnvSuffixPrepend.Value, + BuildConfigEnvSuffixPrepend.Name + ".delim": BuildConfigEnvSuffixPrepend.Delim, + }) + h.AssertEq(t, len(warnings), 0) + h.AssertNil(t, err) + }) + it("should create envMap as expected when delim is specified", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{BuildConfigEnvDelimWithoutSuffix}, "") + h.AssertEq(t, envMap, map[string]string{ + BuildConfigEnvDelimWithoutSuffix.Name: BuildConfigEnvDelimWithoutSuffix.Value, + BuildConfigEnvDelimWithoutSuffix.Name + ".delim": BuildConfigEnvDelimWithoutSuffix.Delim, + }) + h.AssertNotEq(t, len(warnings), 0) + h.AssertNil(t, err) + }) + it("should create envMap with a warning when `value` is empty", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{BuildConfigEnvEmptyValue}, "") + h.AssertEq(t, envMap, map[string]string{ + BuildConfigEnvEmptyValue.Name: BuildConfigEnvEmptyValue.Value, + }) + h.AssertNotEq(t, len(warnings), 0) + h.AssertNil(t, err) + }) + it("should return an error when `name` is empty", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{BuildConfigEnvEmptyName}, "") + h.AssertEq(t, envMap, map[string]string(nil)) + h.AssertEq(t, len(warnings), 0) + h.AssertNotNil(t, err) + }) + it("should return warnings when `apprend` or `prepend` is used without `delim`", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{BuildConfigEnvSuffixPrependWithoutDelim}, "") + h.AssertEq(t, envMap, map[string]string{ + BuildConfigEnvSuffixPrependWithoutDelim.Name + "." + string(BuildConfigEnvSuffixPrependWithoutDelim.Suffix): BuildConfigEnvSuffixPrependWithoutDelim.Value, + }) + h.AssertNotEq(t, len(warnings), 0) + h.AssertNotNil(t, err) + }) + it("should return an error when unknown `suffix` is used", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv([]builder.BuildConfigEnv{BuildConfigEnvSuffixUnknown}, "") + h.AssertEq(t, envMap, map[string]string{}) + h.AssertEq(t, len(warnings), 0) + h.AssertNotNil(t, err) + }) + it("should override with the last specified delim when `[[build.env]]` has multiple delims with same `name` with a `append` or `prepend` suffix", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv(BuildConfigEnvSuffixMultiple, "") + h.AssertEq(t, envMap, map[string]string{ + BuildConfigEnvSuffixMultiple[0].Name + "." + string(BuildConfigEnvSuffixMultiple[0].Suffix): BuildConfigEnvSuffixMultiple[0].Value, + BuildConfigEnvSuffixMultiple[1].Name + "." + string(BuildConfigEnvSuffixMultiple[1].Suffix): BuildConfigEnvSuffixMultiple[1].Value, + BuildConfigEnvSuffixMultiple[2].Name + "." + string(BuildConfigEnvSuffixMultiple[2].Suffix): BuildConfigEnvSuffixMultiple[2].Value, + BuildConfigEnvSuffixMultiple[2].Name + ".delim": BuildConfigEnvSuffixMultiple[2].Delim, + }) + h.AssertNotEq(t, len(warnings), 0) + h.AssertNil(t, err) + }) + it("should override `value` with the last read value when a `[[build.env]]` has same `name` with same `suffix`", func() { + envMap, warnings, err := builder.ParseBuildConfigEnv(BuildConfigEnvDelimWithSameSuffixAndName, "") + h.AssertEq(t, envMap, map[string]string{ + BuildConfigEnvDelimWithSameSuffixAndName[1].Name: BuildConfigEnvDelimWithSameSuffixAndName[1].Value, + }) + h.AssertNotEq(t, len(warnings), 0) + h.AssertNil(t, err) + }) + }) + when("no config provided", func() { it("errors with a descriptive message", func() { command.SetArgs([]string{ diff --git a/internal/commands/buildpack_package.go b/internal/commands/buildpack_package.go index 6cac0ad74d..dff2bf3981 100644 --- a/internal/commands/buildpack_package.go +++ b/internal/commands/buildpack_package.go @@ -118,14 +118,14 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa } action := "created" + location := "docker daemon" if flags.Publish { action = "published" + location = "registry" } - location := "docker daemon" if flags.Format == client.FormatFile { location = "file" } - logger.Infof("Successfully %s package %s and saved to %s", action, style.Symbol(name), location) return nil }), diff --git a/internal/commands/extension_package.go b/internal/commands/extension_package.go index 69370c4604..f8d729b2c3 100644 --- a/internal/commands/extension_package.go +++ b/internal/commands/extension_package.go @@ -84,15 +84,16 @@ func ExtensionPackage(logger logging.Logger, cfg config.Config, packager Extensi }); err != nil { return err } + action := "created" + location := "docker daemon" if flags.Publish { action = "published" + location = "registry" } - location := "docker daemon" if flags.Format == client.FormatFile { location = "file" } - logger.Infof("Successfully %s package %s and saved to %s", action, style.Symbol(name), location) return nil }), diff --git a/pkg/buildpack/downloader.go b/pkg/buildpack/downloader.go index 41a5a61cdb..454afda0cb 100644 --- a/pkg/buildpack/downloader.go +++ b/pkg/buildpack/downloader.go @@ -67,6 +67,9 @@ type DownloadOptions struct { // The OS of the builder image ImageOS string + // The OS/Architecture to download + Platform string + // Deprecated: the older alternative to buildpack URI ImageName string @@ -102,7 +105,11 @@ func (c *buildpackDownloader) Download(ctx context.Context, moduleURI string, op case PackageLocator: imageName := ParsePackageLocator(moduleURI) c.logger.Debugf("Downloading %s from image: %s", kind, style.Symbol(imageName)) - mainBP, depBPs, err = extractPackaged(ctx, kind, imageName, c.imageFetcher, image.FetchOptions{Daemon: opts.Daemon, PullPolicy: opts.PullPolicy}) + mainBP, depBPs, err = extractPackaged(ctx, kind, imageName, c.imageFetcher, image.FetchOptions{ + Daemon: opts.Daemon, + PullPolicy: opts.PullPolicy, + Platform: opts.Platform, + }) if err != nil { return nil, nil, errors.Wrapf(err, "extracting from registry %s", style.Symbol(moduleURI)) } @@ -113,7 +120,11 @@ func (c *buildpackDownloader) Download(ctx context.Context, moduleURI string, op return nil, nil, errors.Wrapf(err, "locating in registry: %s", style.Symbol(moduleURI)) } - mainBP, depBPs, err = extractPackaged(ctx, kind, address, c.imageFetcher, image.FetchOptions{Daemon: opts.Daemon, PullPolicy: opts.PullPolicy}) + mainBP, depBPs, err = extractPackaged(ctx, kind, address, c.imageFetcher, image.FetchOptions{ + Daemon: opts.Daemon, + PullPolicy: opts.PullPolicy, + Platform: opts.Platform, + }) if err != nil { return nil, nil, errors.Wrapf(err, "extracting from registry %s", style.Symbol(moduleURI)) } diff --git a/pkg/buildpack/downloader_test.go b/pkg/buildpack/downloader_test.go index 16c30b6e10..2aefdb571d 100644 --- a/pkg/buildpack/downloader_test.go +++ b/pkg/buildpack/downloader_test.go @@ -127,8 +127,12 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { downloadOptions = buildpack.DownloadOptions{ImageOS: "linux"} ) - shouldFetchPackageImageWith := func(demon bool, pull image.PullPolicy) { - mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), image.FetchOptions{Daemon: demon, PullPolicy: pull}).Return(packageImage, nil) + shouldFetchPackageImageWith := func(demon bool, pull image.PullPolicy, platform string) { + mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), image.FetchOptions{ + Daemon: demon, + PullPolicy: pull, + Platform: platform, + }).Return(packageImage, nil) } when("package image lives in cnb registry", func() { @@ -141,11 +145,12 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { downloadOptions = buildpack.DownloadOptions{ RegistryName: "some-registry", ImageOS: "linux", + Platform: "linux/amd64", Daemon: true, PullPolicy: image.PullAlways, } - shouldFetchPackageImageWith(true, image.PullAlways) + shouldFetchPackageImageWith(true, image.PullAlways, "linux/amd64") mainBP, _, err := buildpackDownloader.Download(context.TODO(), "urn:cnb:registry:example/foo@1.1.0", downloadOptions) h.AssertNil(t, err) h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo") @@ -161,7 +166,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { PullPolicy: image.PullAlways, } - shouldFetchPackageImageWith(true, image.PullAlways) + shouldFetchPackageImageWith(true, image.PullAlways, "") mainBP, _, err := buildpackDownloader.Download(context.TODO(), "example/foo@1.1.0", downloadOptions) h.AssertNil(t, err) h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo") @@ -185,10 +190,11 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { Daemon: true, PullPolicy: image.PullAlways, ImageOS: "linux", + Platform: "linux/amd64", ImageName: "some/package:tag", } - shouldFetchPackageImageWith(true, image.PullAlways) + shouldFetchPackageImageWith(true, image.PullAlways, "linux/amd64") mainBP, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions) h.AssertNil(t, err) h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo") @@ -204,7 +210,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { PullPolicy: image.PullAlways, } - shouldFetchPackageImageWith(true, image.PullAlways) + shouldFetchPackageImageWith(true, image.PullAlways, "") mainBP, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions) h.AssertNil(t, err) h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo") @@ -220,7 +226,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { PullPolicy: image.PullAlways, } - shouldFetchPackageImageWith(false, image.PullAlways) + shouldFetchPackageImageWith(false, image.PullAlways, "") mainBP, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions) h.AssertNil(t, err) h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo") @@ -234,7 +240,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { Daemon: false, PullPolicy: image.PullAlways, } - shouldFetchPackageImageWith(false, image.PullAlways) + shouldFetchPackageImageWith(false, image.PullAlways, "") mainBP, _, err := buildpackDownloader.Download(context.TODO(), packageImage.Name(), downloadOptions) h.AssertNil(t, err) h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo") @@ -250,7 +256,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { PullPolicy: image.PullNever, } - shouldFetchPackageImageWith(false, image.PullNever) + shouldFetchPackageImageWith(false, image.PullNever, "") mainBP, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions) h.AssertNil(t, err) h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo") diff --git a/pkg/client/build.go b/pkg/client/build.go index 058dd89c54..4371e311b0 100644 --- a/pkg/client/build.go +++ b/pkg/client/build.go @@ -318,6 +318,16 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { return errors.Wrapf(err, "failed to fetch builder image '%s'", builderRef.Name()) } + builderOS, err := rawBuilderImage.OS() + if err != nil { + return errors.Wrapf(err, "getting builder OS") + } + + builderArch, err := rawBuilderImage.Architecture() + if err != nil { + return errors.Wrapf(err, "getting builder architecture") + } + bldr, err := c.getBuilder(rawBuilderImage) if err != nil { return errors.Wrapf(err, "invalid builder %s", style.Symbol(opts.Builder)) @@ -325,7 +335,11 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { runImageName := c.resolveRunImage(opts.RunImage, imgRegistry, builderRef.Context().RegistryStr(), bldr.DefaultRunImage(), opts.AdditionalMirrors, opts.Publish) - fetchOptions := image.FetchOptions{Daemon: !opts.Publish, PullPolicy: opts.PullPolicy} + fetchOptions := image.FetchOptions{ + Daemon: !opts.Publish, + PullPolicy: opts.PullPolicy, + Platform: fmt.Sprintf("%s/%s", builderOS, builderArch), + } if opts.Layout() { targetRunImagePath, err := layout.ParseRefToPath(runImageName) if err != nil { @@ -361,11 +375,6 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { return err } - imgOS, err := rawBuilderImage.OS() - if err != nil { - return errors.Wrapf(err, "getting builder OS") - } - // Default mode: if the TrustBuilder option is not set, trust the suggested builders. if opts.TrustBuilder == nil { opts.TrustBuilder = IsSuggestedBuilderFunc @@ -396,15 +405,14 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { lifecycleImageName = fmt.Sprintf("%s:%s", internalConfig.DefaultLifecycleImageRepo, lifecycleVersion.String()) } - imgArch, err := rawBuilderImage.Architecture() - if err != nil { - return errors.Wrapf(err, "getting builder architecture") - } - lifecycleImage, err := c.imageFetcher.Fetch( ctx, lifecycleImageName, - image.FetchOptions{Daemon: true, PullPolicy: opts.PullPolicy, Platform: fmt.Sprintf("%s/%s", imgOS, imgArch)}, + image.FetchOptions{ + Daemon: true, + PullPolicy: opts.PullPolicy, + Platform: fmt.Sprintf("%s/%s", builderOS, builderArch), + }, ) if err != nil { return fmt.Errorf("fetching lifecycle image: %w", err) @@ -455,7 +463,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { if !c.experimental { return fmt.Errorf("experimental features must be enabled when builder contains image extensions") } - if imgOS == "windows" { + if builderOS == "windows" { return fmt.Errorf("builder contains image extensions which are not supported for Windows builds") } if !(opts.PullPolicy == image.PullAlways) { @@ -467,7 +475,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { opts.ContainerConfig.Volumes = appendLayoutVolumes(opts.ContainerConfig.Volumes, pathsConfig) } - processedVolumes, warnings, err := processVolumes(imgOS, opts.ContainerConfig.Volumes) + processedVolumes, warnings, err := processVolumes(builderOS, opts.ContainerConfig.Volumes) if err != nil { return err } @@ -1024,13 +1032,19 @@ func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir Version: version, } default: - imageOS, err := builderImage.OS() + builderOS, err := builderImage.OS() + if err != nil { + return nil, nil, errors.Wrapf(err, "getting builder OS") + } + + builderArch, err := builderImage.Architecture() if err != nil { - return nil, nil, errors.Wrapf(err, "getting OS from %s", style.Symbol(builderImage.Name())) + return nil, nil, errors.Wrapf(err, "getting builder architecture") } downloadOptions := buildpack.DownloadOptions{ RegistryName: registry, - ImageOS: imageOS, + ImageOS: builderOS, + Platform: fmt.Sprintf("%s/%s", builderOS, builderArch), RelativeBaseDir: relativeBaseDir, Daemon: !publish, PullPolicy: pullPolicy, @@ -1068,6 +1082,7 @@ func (c *Client) fetchBuildpackDependencies(ctx context.Context, bp string, pack mainBP, deps, err := c.buildpackDownloader.Download(ctx, dep.URI, buildpack.DownloadOptions{ RegistryName: downloadOptions.RegistryName, ImageOS: downloadOptions.ImageOS, + Platform: downloadOptions.Platform, Daemon: downloadOptions.Daemon, PullPolicy: downloadOptions.PullPolicy, RelativeBaseDir: filepath.Join(bp, packageCfg.Buildpack.URI), diff --git a/pkg/client/build_test.go b/pkg/client/build_test.go index 06b215104b..ed5693f7c9 100644 --- a/pkg/client/build_test.go +++ b/pkg/client/build_test.go @@ -1240,6 +1240,8 @@ api = "0.2" Version: "child.buildpack.version", }, }) + args := fakeImageFetcher.FetchCalls[fakePackage.Name()] + h.AssertEq(t, args.Platform, "linux/amd64") }) it("fails when no metadata label on package", func() { @@ -2085,11 +2087,12 @@ api = "0.2" })) h.AssertEq(t, fakeLifecycle.Opts.Publish, true) - args := fakeImageFetcher.FetchCalls["default/run"] - h.AssertEq(t, args.Daemon, false) - - args = fakeImageFetcher.FetchCalls[defaultBuilderName] + args := fakeImageFetcher.FetchCalls[defaultBuilderName] h.AssertEq(t, args.Daemon, true) + + args = fakeImageFetcher.FetchCalls["default/run"] + h.AssertEq(t, args.Daemon, false) + h.AssertEq(t, args.Platform, "linux/amd64") }) when("builder is untrusted", func() { diff --git a/pkg/client/create_builder.go b/pkg/client/create_builder.go index 28cd2fa7c5..b697313b33 100644 --- a/pkg/client/create_builder.go +++ b/pkg/client/create_builder.go @@ -29,6 +29,9 @@ type CreateBuilderOptions struct { // Name of the builder. BuilderName string + // BuildConfigEnv for Builder + BuildConfigEnv map[string]string + // Configuration that defines the functionality a builder provides. Config pubbldr.Config @@ -79,6 +82,7 @@ func (c *Client) CreateBuilder(ctx context.Context, opts CreateBuilderOptions) e bldr.SetStack(opts.Config.Stack) } bldr.SetRunImage(opts.Config.Run) + bldr.SetBuildConfigEnv(opts.BuildConfigEnv) return bldr.Save(c.logger, builder.CreatorMetadata{Version: c.version}) } @@ -191,6 +195,7 @@ func (c *Client) createBaseBuilder(ctx context.Context, opts CreateBuilderOption } bldr.SetLifecycle(lifecycle) + bldr.SetBuildConfigEnv(opts.BuildConfigEnv) return bldr, nil } @@ -256,14 +261,20 @@ func (c *Client) addExtensionsToBuilder(ctx context.Context, opts CreateBuilderO func (c *Client) addConfig(ctx context.Context, kind string, config pubbldr.ModuleConfig, opts CreateBuilderOptions, bldr *builder.Builder) error { c.logger.Debugf("Looking up %s %s", kind, style.Symbol(config.DisplayString())) - imageOS, err := bldr.Image().OS() + builderOS, err := bldr.Image().OS() + if err != nil { + return errors.Wrapf(err, "getting builder OS") + } + builderArch, err := bldr.Image().Architecture() if err != nil { - return errors.Wrapf(err, "getting OS from %s", style.Symbol(bldr.Image().Name())) + return errors.Wrapf(err, "getting builder architecture") } + mainBP, depBPs, err := c.buildpackDownloader.Download(ctx, config.URI, buildpack.DownloadOptions{ Daemon: !opts.Publish, ImageName: config.ImageName, - ImageOS: imageOS, + ImageOS: builderOS, + Platform: fmt.Sprintf("%s/%s", builderOS, builderArch), ModuleKind: kind, PullPolicy: opts.PullPolicy, RegistryName: opts.Registry, diff --git a/pkg/client/create_builder_test.go b/pkg/client/create_builder_test.go index 3b4cfc41ef..ea0c95a939 100644 --- a/pkg/client/create_builder_test.go +++ b/pkg/client/create_builder_test.go @@ -843,12 +843,22 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { buildpackBlob := blob.NewBlob(filepath.Join("testdata", "buildpack-api-0.4")) bp, err := buildpack.FromBuildpackRootBlob(buildpackBlob, archive.DefaultTarWriterFactory()) h.AssertNil(t, err) - mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/bp-one-with-api-4.tgz", gomock.Any()).Return(bp, bpDependencies, nil) + mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/bp-one-with-api-4.tgz", gomock.Any()).DoAndReturn( + func(ctx context.Context, buildpackURI string, opts buildpack.DownloadOptions) (buildpack.BuildModule, []buildpack.BuildModule, error) { + // test options + h.AssertEq(t, opts.Platform, "linux/amd64") + return bp, bpDependencies, nil + }) extensionBlob := blob.NewBlob(filepath.Join("testdata", "extension-api-0.9")) extension, err := buildpack.FromExtensionRootBlob(extensionBlob, archive.DefaultTarWriterFactory()) h.AssertNil(t, err) - mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/ext-one-with-api-9.tgz", gomock.Any()).Return(extension, nil, nil) + mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/ext-one-with-api-9.tgz", gomock.Any()).DoAndReturn( + func(ctx context.Context, buildpackURI string, opts buildpack.DownloadOptions) (buildpack.BuildModule, []buildpack.BuildModule, error) { + // test options + h.AssertEq(t, opts.Platform, "linux/amd64") + return extension, nil, nil + }) successfullyCreateDeterministicBuilder() diff --git a/pkg/client/rebase.go b/pkg/client/rebase.go index 168c727573..276c56026e 100644 --- a/pkg/client/rebase.go +++ b/pkg/client/rebase.go @@ -2,6 +2,7 @@ package client import ( "context" + "fmt" "os" "path/filepath" @@ -60,6 +61,16 @@ func (c *Client) Rebase(ctx context.Context, opts RebaseOptions) error { return err } + appOS, err := appImage.OS() + if err != nil { + return errors.Wrapf(err, "getting app OS") + } + + appArch, err := appImage.Architecture() + if err != nil { + return errors.Wrapf(err, "getting app architecture") + } + var md files.LayersMetadataCompat if ok, err := dist.GetLabel(appImage, platform.LifecycleMetadataLabel, &md); err != nil { return err @@ -90,7 +101,11 @@ func (c *Client) Rebase(ctx context.Context, opts RebaseOptions) error { return errors.New("run image must be specified") } - baseImage, err := c.imageFetcher.Fetch(ctx, runImageName, image.FetchOptions{Daemon: !opts.Publish, PullPolicy: opts.PullPolicy}) + baseImage, err := c.imageFetcher.Fetch(ctx, runImageName, image.FetchOptions{ + Daemon: !opts.Publish, + PullPolicy: opts.PullPolicy, + Platform: fmt.Sprintf("%s/%s", appOS, appArch), + }) if err != nil { return err } diff --git a/pkg/client/rebase_test.go b/pkg/client/rebase_test.go index 86eff76de9..101fb683a9 100644 --- a/pkg/client/rebase_test.go +++ b/pkg/client/rebase_test.go @@ -258,6 +258,8 @@ func testRebase(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, fakeAppImage.Base(), "some/run") lbl, _ := fakeAppImage.Label("io.buildpacks.lifecycle.metadata") h.AssertContains(t, lbl, `"runImage":{"topLayer":"remote-top-layer-sha","reference":"remote-digest"`) + args := fakeImageFetcher.FetchCalls["some/run"] + h.AssertEq(t, args.Platform, "linux/amd64") }) }) }) diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 7868691a3c..3eeb4b6431 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -179,6 +179,33 @@ func AssertNotContains(t *testing.T, actual, expected string) { } } +type KeyValue[k comparable, v any] struct { + key k + value v +} + +func NewKeyValue[k comparable, v any](key k, value v) KeyValue[k, v] { + return KeyValue[k, v]{key: key, value: value} +} + +func AssertMapContains[key comparable, value any](t *testing.T, actual map[key]value, expected ...KeyValue[key, value]) { + t.Helper() + for _, i := range expected { + if v, ok := actual[i.key]; !ok || !reflect.DeepEqual(v, i.value) { + t.Fatalf("Expected %s to contain elements %s", reflect.ValueOf(actual), reflect.ValueOf(expected)) + } + } +} + +func AssertMapNotContains[key comparable, value any](t *testing.T, actual map[key]value, expected ...KeyValue[key, value]) { + t.Helper() + for _, i := range expected { + if v, ok := actual[i.key]; ok && reflect.DeepEqual(v, i.value) { + t.Fatalf("Expected %s to not contain elements %s", reflect.ValueOf(actual), reflect.ValueOf(expected)) + } + } +} + func AssertSliceContains(t *testing.T, slice []string, expected ...string) { t.Helper() _, missing, _ := stringset.Compare(slice, expected)