diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 5e7c1766ff..e7f52815d0 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -2699,6 +2699,21 @@ include = [ "*.jar", "media/mountain.jpg", "/media/person.png", ] }) }) }) + + when("--platform", func() { + it.Before(func() { + h.SkipIf(t, !pack.SupportsFeature(invoke.PlatformOption), "") + }) + + it("uses the builder with the desired platform", func() { + output, _ := pack.Run( + "build", repoName, + "-p", filepath.Join("testdata", "mock_app"), + "--platform", "linux/not-exist-arch", + ) + h.AssertContainsMatch(t, output, "Pulling image '.*test/builder.*' with platform 'linux/not-exist-arch") + }) + }) }) when("build --buildpack ", func() { diff --git a/acceptance/invoke/pack.go b/acceptance/invoke/pack.go index b5ebb9ae94..9346bdce5a 100644 --- a/acceptance/invoke/pack.go +++ b/acceptance/invoke/pack.go @@ -239,6 +239,7 @@ const ( FlattenBuilderCreationV2 FixesRunImageMetadata ManifestCommands + PlatformOption MultiPlatformBuildersAndBuildPackages ) @@ -279,6 +280,9 @@ var featureTests = map[Feature]func(i *PackInvoker) bool{ ManifestCommands: func(i *PackInvoker) bool { return i.atLeast("v0.34.0") }, + PlatformOption: func(i *PackInvoker) bool { + return i.atLeast("v0.34.0") + }, MultiPlatformBuildersAndBuildPackages: func(i *PackInvoker) bool { return i.atLeast("v0.34.0") }, diff --git a/internal/commands/build.go b/internal/commands/build.go index d32f96cf8b..50ccebeabd 100644 --- a/internal/commands/build.go +++ b/internal/commands/build.go @@ -35,6 +35,7 @@ type BuildFlags struct { Builder string Registry string RunImage string + Platform string Policy string Network string DescriptorPath string @@ -132,6 +133,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob if err != nil { return errors.Wrapf(err, "parsing pull policy %s", flags.Policy) } + var lifecycleImage string if flags.LifecycleImage != "" { ref, err := name.ParseReference(flags.LifecycleImage) @@ -140,6 +142,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob } lifecycleImage = ref.Name() } + var gid = -1 if cmd.Flags().Changed("gid") { gid = flags.GID @@ -165,6 +168,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob Image: inputImageName.Name(), Publish: flags.Publish, DockerHost: flags.DockerHost, + Platform: flags.Platform, PullPolicy: pullPolicy, ClearCache: flags.ClearCache, TrustBuilder: func(string) bool { @@ -257,6 +261,7 @@ Special value 'inherit' may be used in which case DOCKER_HOST environment variab This option may set DOCKER_HOST environment variable for the build container if needed. `) cmd.Flags().StringVar(&buildFlags.LifecycleImage, "lifecycle-image", cfg.LifecycleImage, `Custom lifecycle image to use for analysis, restore, and export when builder is untrusted.`) + cmd.Flags().StringVar(&buildFlags.Platform, "platform", "", `Platform to build on (e.g., "linux/amd64").`) cmd.Flags().StringVar(&buildFlags.Policy, "pull-policy", "", `Pull policy to use. Accepted values are always, never, and if-not-present. (default "always")`) cmd.Flags().StringVarP(&buildFlags.Registry, "buildpack-registry", "r", cfg.DefaultRegistryName, "Buildpack Registry by name") cmd.Flags().StringVar(&buildFlags.RunImage, "run-image", "", "Run image (defaults to default stack's run image)") diff --git a/internal/commands/build_test.go b/internal/commands/build_test.go index 6a59810004..07561e175a 100644 --- a/internal/commands/build_test.go +++ b/internal/commands/build_test.go @@ -148,6 +148,17 @@ func testBuildCommand(t *testing.T, when spec.G, it spec.S) { }) }) + when("--platform", func() { + it("sets platform", func() { + mockClient.EXPECT(). + Build(gomock.Any(), EqBuildOptionsWithPlatform("linux/amd64")). + Return(nil) + + command.SetArgs([]string{"image", "--builder", "my-builder", "--platform", "linux/amd64"}) + h.AssertNil(t, command.Execute()) + }) + }) + when("--pull-policy", func() { it("sets pull-policy=never", func() { mockClient.EXPECT(). @@ -958,6 +969,15 @@ func EqBuildOptionsDefaultProcess(defaultProc string) gomock.Matcher { } } +func EqBuildOptionsWithPlatform(platform string) gomock.Matcher { + return buildOptionsMatcher{ + description: fmt.Sprintf("Platform=%s", platform), + equals: func(o client.BuildOptions) bool { + return o.Platform == platform + }, + } +} + func EqBuildOptionsWithPullPolicy(policy image.PullPolicy) gomock.Matcher { return buildOptionsMatcher{ description: fmt.Sprintf("PullPolicy=%s", policy), diff --git a/pkg/client/build.go b/pkg/client/build.go index 4491aa95ac..aea26e89bc 100644 --- a/pkg/client/build.go +++ b/pkg/client/build.go @@ -159,6 +159,9 @@ type BuildOptions struct { // Process type that will be used when setting container start command. DefaultProcessType string + // Platform is the desired platform to build on (e.g., linux/amd64) + Platform string + // Strategy for updating local images before a build. PullPolicy image.PullPolicy @@ -320,19 +323,43 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { return errors.Wrapf(err, "invalid builder '%s'", opts.Builder) } - rawBuilderImage, err := c.imageFetcher.Fetch(ctx, builderRef.Name(), image.FetchOptions{Daemon: true, PullPolicy: opts.PullPolicy}) + requestedTarget := func() *dist.Target { + if opts.Platform == "" { + return nil + } + parts := strings.Split(opts.Platform, "/") + switch len(parts) { + case 0: + return nil + case 1: + return &dist.Target{OS: parts[0]} + case 2: + return &dist.Target{OS: parts[0], Arch: parts[1]} + default: + return &dist.Target{OS: parts[0], Arch: parts[1], ArchVariant: parts[2]} + } + }() + + rawBuilderImage, err := c.imageFetcher.Fetch( + ctx, + builderRef.Name(), + image.FetchOptions{ + Daemon: true, + Target: requestedTarget, + PullPolicy: opts.PullPolicy}, + ) if err != nil { 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") + var targetToUse *dist.Target + if requestedTarget != nil { + targetToUse = requestedTarget + } else { + targetToUse, err = getTargetFromBuilder(rawBuilderImage) + if err != nil { + return err + } } bldr, err := c.getBuilder(rawBuilderImage) @@ -340,12 +367,10 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { return errors.Wrapf(err, "invalid builder %s", style.Symbol(opts.Builder)) } - target := &dist.Target{OS: builderOS, Arch: builderArch} - fetchOptions := image.FetchOptions{ Daemon: !opts.Publish, PullPolicy: opts.PullPolicy, - Target: target, + Target: targetToUse, } runImageName := c.resolveRunImage(opts.RunImage, imgRegistry, builderRef.Context().RegistryStr(), bldr.DefaultRunImage(), opts.AdditionalMirrors, opts.Publish, fetchOptions) @@ -374,12 +399,12 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { return err } - fetchedBPs, order, err := c.processBuildpacks(ctx, bldr.Image(), bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts) + fetchedBPs, order, err := c.processBuildpacks(ctx, bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts, targetToUse) if err != nil { return err } - fetchedExs, orderExtensions, err := c.processExtensions(ctx, bldr.Image(), bldr.Extensions(), bldr.OrderExtensions(), bldr.StackID, opts) + fetchedExs, orderExtensions, err := c.processExtensions(ctx, bldr.Extensions(), opts, targetToUse) if err != nil { return err } @@ -420,7 +445,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { image.FetchOptions{ Daemon: true, PullPolicy: opts.PullPolicy, - Target: target, + Target: targetToUse, }, ) if err != nil { @@ -492,7 +517,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { defer c.docker.ImageRemove(context.Background(), ephemeralBuilder.Name(), types.RemoveOptions{Force: true}) if len(bldr.OrderExtensions()) > 0 || len(ephemeralBuilder.OrderExtensions()) > 0 { - if builderOS == "windows" { + if targetToUse.OS == "windows" { return fmt.Errorf("builder contains image extensions which are not supported for Windows builds") } if !(opts.PullPolicy == image.PullAlways) { @@ -504,7 +529,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { opts.ContainerConfig.Volumes = appendLayoutVolumes(opts.ContainerConfig.Volumes, pathsConfig) } - processedVolumes, warnings, err := processVolumes(builderOS, opts.ContainerConfig.Volumes) + processedVolumes, warnings, err := processVolumes(targetToUse.OS, opts.ContainerConfig.Volumes) if err != nil { return err } @@ -735,6 +760,26 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { return c.logImageNameAndSha(ctx, opts.Publish, imageRef) } +func getTargetFromBuilder(builderImage imgutil.Image) (*dist.Target, error) { + builderOS, err := builderImage.OS() + if err != nil { + return nil, fmt.Errorf("failed to get builder OS: %w", err) + } + builderArch, err := builderImage.Architecture() + if err != nil { + return nil, fmt.Errorf("failed to get builder architecture: %w", err) + } + builderArchVariant, err := builderImage.Variant() + if err != nil { + return nil, fmt.Errorf("failed to get builder architecture variant: %w", err) + } + return &dist.Target{ + OS: builderOS, + Arch: builderArch, + ArchVariant: builderArchVariant, + }, nil +} + func extractSupportedLifecycleApis(labels map[string]string) ([]string, error) { // sample contents of labels: // {io.buildpacks.builder.metadata:\"{\"lifecycle\":{\"version\":\"0.15.3\"},\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}}", @@ -1087,7 +1132,7 @@ func (c *Client) processProxyConfig(config *ProxyConfig) ProxyConfig { // ---------- // - group: // - A -func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Image, builderBPs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions) (fetchedBPs []buildpack.BuildModule, order dist.Order, err error) { +func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions, targetToUse *dist.Target) (fetchedBPs []buildpack.BuildModule, order dist.Order, err error) { relativeBaseDir := opts.RelativeBaseDir declaredBPs := opts.Buildpacks @@ -1130,7 +1175,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima order = newOrder } default: - newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderImage, builderBPs, opts, buildpack.KindBuildpack) + newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse) if err != nil { return fetchedBPs, order, err } @@ -1164,7 +1209,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima if len(preBuildpacks) > 0 || len(postBuildpacks) > 0 { order = builderOrder for _, bp := range preBuildpacks { - newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderImage, builderBPs, opts, buildpack.KindBuildpack) + newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse) if err != nil { return fetchedBPs, order, err } @@ -1173,7 +1218,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima } for _, bp := range postBuildpacks { - newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderImage, builderBPs, opts, buildpack.KindBuildpack) + newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse) if err != nil { return fetchedBPs, order, err } @@ -1186,7 +1231,7 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima return fetchedBPs, order, nil } -func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir string, builderImage imgutil.Image, builderBPs []dist.ModuleInfo, opts BuildOptions, kind string) ([]buildpack.BuildModule, *dist.ModuleInfo, error) { +func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir string, builderBPs []dist.ModuleInfo, opts BuildOptions, kind string, targetToUse *dist.Target) ([]buildpack.BuildModule, *dist.ModuleInfo, error) { pullPolicy := opts.PullPolicy publish := opts.Publish registry := opts.Registry @@ -1206,19 +1251,9 @@ func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir Version: version, } default: - 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 builder architecture") - } - downloadOptions := buildpack.DownloadOptions{ RegistryName: registry, - Target: &dist.Target{OS: builderOS, Arch: builderArch}, + Target: targetToUse, RelativeBaseDir: relativeBaseDir, Daemon: !publish, PullPolicy: pullPolicy, @@ -1322,7 +1357,7 @@ func prependBuildpackToOrder(order dist.Order, bpInfo dist.ModuleInfo) (newOrder return newOrder } -func (c *Client) processExtensions(ctx context.Context, builderImage imgutil.Image, builderExs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions) (fetchedExs []buildpack.BuildModule, orderExtensions dist.Order, err error) { +func (c *Client) processExtensions(ctx context.Context, builderExs []dist.ModuleInfo, opts BuildOptions, targetToUse *dist.Target) (fetchedExs []buildpack.BuildModule, orderExtensions dist.Order, err error) { relativeBaseDir := opts.RelativeBaseDir declaredExs := opts.Extensions @@ -1339,7 +1374,7 @@ func (c *Client) processExtensions(ctx context.Context, builderImage imgutil.Ima case buildpack.FromBuilderLocator: return nil, nil, errors.New("from builder is not supported for extensions") default: - newFetchedExs, moduleInfo, err := c.fetchBuildpack(ctx, ex, relativeBaseDir, builderImage, builderExs, opts, buildpack.KindExtension) + newFetchedExs, moduleInfo, err := c.fetchBuildpack(ctx, ex, relativeBaseDir, builderExs, opts, buildpack.KindExtension, targetToUse) if err != nil { return fetchedExs, orderExtensions, err } diff --git a/pkg/client/build_test.go b/pkg/client/build_test.go index 4d2b76c323..ec6a5d39a1 100644 --- a/pkg/client/build_test.go +++ b/pkg/client/build_test.go @@ -1122,81 +1122,7 @@ api = "0.2" var fakePackage *fakes.Image it.Before(func() { - metaBuildpackTar := ifakes.CreateBuildpackTar(t, tmpDir, dist.BuildpackDescriptor{ - WithAPI: api.MustParse("0.3"), - WithInfo: dist.ModuleInfo{ - ID: "meta.buildpack.id", - Version: "meta.buildpack.version", - Homepage: "http://meta.buildpack", - }, - WithStacks: nil, - WithOrder: dist.Order{{ - Group: []dist.ModuleRef{{ - ModuleInfo: dist.ModuleInfo{ - ID: "child.buildpack.id", - Version: "child.buildpack.version", - }, - Optional: false, - }}, - }}, - }) - - childBuildpackTar := ifakes.CreateBuildpackTar(t, tmpDir, dist.BuildpackDescriptor{ - WithAPI: api.MustParse("0.3"), - WithInfo: dist.ModuleInfo{ - ID: "child.buildpack.id", - Version: "child.buildpack.version", - Homepage: "http://child.buildpack", - }, - WithStacks: []dist.Stack{ - {ID: defaultBuilderStackID}, - }, - }) - - bpLayers := dist.ModuleLayers{ - "meta.buildpack.id": { - "meta.buildpack.version": { - API: api.MustParse("0.3"), - Order: dist.Order{{ - Group: []dist.ModuleRef{{ - ModuleInfo: dist.ModuleInfo{ - ID: "child.buildpack.id", - Version: "child.buildpack.version", - }, - Optional: false, - }}, - }}, - LayerDiffID: diffIDForFile(t, metaBuildpackTar), - }, - }, - "child.buildpack.id": { - "child.buildpack.version": { - API: api.MustParse("0.3"), - Stacks: []dist.Stack{ - {ID: defaultBuilderStackID}, - }, - LayerDiffID: diffIDForFile(t, childBuildpackTar), - }, - }, - } - - md := buildpack.Metadata{ - ModuleInfo: dist.ModuleInfo{ - ID: "meta.buildpack.id", - Version: "meta.buildpack.version", - }, - Stacks: []dist.Stack{ - {ID: defaultBuilderStackID}, - }, - } - - fakePackage = fakes.NewImage("example.com/some/package", "", nil) - h.AssertNil(t, dist.SetLabel(fakePackage, "io.buildpacks.buildpack.layers", bpLayers)) - h.AssertNil(t, dist.SetLabel(fakePackage, "io.buildpacks.buildpackage.metadata", md)) - - h.AssertNil(t, fakePackage.AddLayer(metaBuildpackTar)) - h.AssertNil(t, fakePackage.AddLayer(childBuildpackTar)) - + fakePackage = makeFakePackage(t, tmpDir, defaultBuilderStackID) fakeImageFetcher.LocalImages[fakePackage.Name()] = fakePackage }) @@ -2261,6 +2187,84 @@ api = "0.2" }) }) + when("Platform option", func() { + var fakePackage imgutil.Image + + it.Before(func() { + fakePackage = makeFakePackage(t, tmpDir, defaultBuilderStackID) + fakeImageFetcher.LocalImages[fakePackage.Name()] = fakePackage + }) + + when("provided", func() { + it("uses the provided platform to pull the builder, run image, packages, and lifecycle image", func() { + h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: defaultBuilderName, + Buildpacks: []string{ + "example.com/some/package", + }, + Platform: "linux/arm64", + PullPolicy: image.PullAlways, + })) + + args := fakeImageFetcher.FetchCalls[defaultBuilderName] + h.AssertEq(t, args.Daemon, true) + h.AssertEq(t, args.PullPolicy, image.PullAlways) + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/arm64") + + args = fakeImageFetcher.FetchCalls["default/run"] + h.AssertEq(t, args.Daemon, true) + h.AssertEq(t, args.PullPolicy, image.PullAlways) + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/arm64") + + args = fakeImageFetcher.FetchCalls[fakePackage.Name()] + h.AssertEq(t, args.Daemon, true) + h.AssertEq(t, args.PullPolicy, image.PullAlways) + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/arm64") + + args = fakeImageFetcher.FetchCalls[fmt.Sprintf("%s:%s", cfg.DefaultLifecycleImageRepo, builder.DefaultLifecycleVersion)] + h.AssertEq(t, args.Daemon, true) + h.AssertEq(t, args.PullPolicy, image.PullAlways) + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/arm64") + }) + }) + + when("not provided", func() { + it("defaults to builder os/arch", func() { + // defaultBuilderImage has linux/amd64 + + h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: defaultBuilderName, + Buildpacks: []string{ + "example.com/some/package", + }, + PullPolicy: image.PullAlways, + })) + + args := fakeImageFetcher.FetchCalls[defaultBuilderName] + h.AssertEq(t, args.Daemon, true) + h.AssertEq(t, args.PullPolicy, image.PullAlways) + h.AssertEq(t, args.Target, (*dist.Target)(nil)) + + args = fakeImageFetcher.FetchCalls["default/run"] + h.AssertEq(t, args.Daemon, true) + h.AssertEq(t, args.PullPolicy, image.PullAlways) + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/amd64") + + args = fakeImageFetcher.FetchCalls[fakePackage.Name()] + h.AssertEq(t, args.Daemon, true) + h.AssertEq(t, args.PullPolicy, image.PullAlways) + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/amd64") + + args = fakeImageFetcher.FetchCalls[fmt.Sprintf("%s:%s", cfg.DefaultLifecycleImageRepo, builder.DefaultLifecycleVersion)] + h.AssertEq(t, args.Daemon, true) + h.AssertEq(t, args.PullPolicy, image.PullAlways) + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/amd64") + }) + }) + }) + when("PullPolicy", func() { when("never", func() { it("uses the local builder and run images without updating", func() { @@ -3173,6 +3177,85 @@ api = "0.2" }) } +func makeFakePackage(t *testing.T, tmpDir string, stackID string) *fakes.Image { + metaBuildpackTar := ifakes.CreateBuildpackTar(t, tmpDir, dist.BuildpackDescriptor{ + WithAPI: api.MustParse("0.3"), + WithInfo: dist.ModuleInfo{ + ID: "meta.buildpack.id", + Version: "meta.buildpack.version", + Homepage: "http://meta.buildpack", + }, + WithStacks: nil, + WithOrder: dist.Order{{ + Group: []dist.ModuleRef{{ + ModuleInfo: dist.ModuleInfo{ + ID: "child.buildpack.id", + Version: "child.buildpack.version", + }, + Optional: false, + }}, + }}, + }) + + childBuildpackTar := ifakes.CreateBuildpackTar(t, tmpDir, dist.BuildpackDescriptor{ + WithAPI: api.MustParse("0.3"), + WithInfo: dist.ModuleInfo{ + ID: "child.buildpack.id", + Version: "child.buildpack.version", + Homepage: "http://child.buildpack", + }, + WithStacks: []dist.Stack{ + {ID: stackID}, + }, + }) + + bpLayers := dist.ModuleLayers{ + "meta.buildpack.id": { + "meta.buildpack.version": { + API: api.MustParse("0.3"), + Order: dist.Order{{ + Group: []dist.ModuleRef{{ + ModuleInfo: dist.ModuleInfo{ + ID: "child.buildpack.id", + Version: "child.buildpack.version", + }, + Optional: false, + }}, + }}, + LayerDiffID: diffIDForFile(t, metaBuildpackTar), + }, + }, + "child.buildpack.id": { + "child.buildpack.version": { + API: api.MustParse("0.3"), + Stacks: []dist.Stack{ + {ID: stackID}, + }, + LayerDiffID: diffIDForFile(t, childBuildpackTar), + }, + }, + } + + md := buildpack.Metadata{ + ModuleInfo: dist.ModuleInfo{ + ID: "meta.buildpack.id", + Version: "meta.buildpack.version", + }, + Stacks: []dist.Stack{ + {ID: stackID}, + }, + } + + fakePackage := fakes.NewImage("example.com/some/package", "", nil) + h.AssertNil(t, dist.SetLabel(fakePackage, "io.buildpacks.buildpack.layers", bpLayers)) + h.AssertNil(t, dist.SetLabel(fakePackage, "io.buildpacks.buildpackage.metadata", md)) + + h.AssertNil(t, fakePackage.AddLayer(metaBuildpackTar)) + h.AssertNil(t, fakePackage.AddLayer(childBuildpackTar)) + + return fakePackage +} + func diffIDForFile(t *testing.T, path string) string { file, err := os.Open(path) h.AssertNil(t, err) diff --git a/pkg/image/fetcher.go b/pkg/image/fetcher.go index 5b61a3aba5..ec12b10226 100644 --- a/pkg/image/fetcher.go +++ b/pkg/image/fetcher.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "fmt" "io" "strings" @@ -109,17 +110,22 @@ func (f *Fetcher) Fetch(ctx context.Context, name string, options FetchOptions) } } - f.logger.Debugf("Pulling image %s", style.Symbol(name)) - platform := "" + msg := fmt.Sprintf("Pulling image %s", style.Symbol(name)) if options.Target != nil { platform = options.Target.ValuesAsPlatform() + msg = fmt.Sprintf("Pulling image %s with platform %s", style.Symbol(name), style.Symbol(platform)) } - + f.logger.Debug(msg) if err = f.pullImage(ctx, name, platform); err != nil { - // sample error from docker engine: - // image with reference was found but does not match the specified platform: wanted linux/amd64, actual: linux - if strings.Contains(err.Error(), "does not match the specified platform") { + // FIXME: this matching is brittle and the fallback should be removed when https://github.com/buildpacks/pack/issues/2079 + // has been fixed for a sufficient amount of time. + // Sample error from docker engine: + // `image with reference was found but does not match the specified platform: wanted linux/amd64, actual: linux` + if strings.Contains(err.Error(), "does not match the specified platform") && + (strings.HasSuffix(strings.TrimSpace(err.Error()), "actual: linux") || + strings.HasSuffix(strings.TrimSpace(err.Error()), "actual: windows")) { + f.logger.Debugf(fmt.Sprintf("Pulling image %s", style.Symbol(name))) err = f.pullImage(ctx, name, "") } } diff --git a/pkg/image/fetcher_test.go b/pkg/image/fetcher_test.go index 85f5d6aa60..fb6f30a7cf 100644 --- a/pkg/image/fetcher_test.go +++ b/pkg/image/fetcher_test.go @@ -273,7 +273,7 @@ func testFetcher(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, img.Save()) }) - it("retry without setting platform", func() { + it("retries without setting platform", func() { _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Target: &dist.Target{OS: osType, Arch: runtime.GOARCH}}) h.AssertNil(t, err) })