diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 93adb4841a..5ed29e09c4 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -3296,6 +3296,185 @@ include = [ "*.jar", "media/mountain.jpg", "/media/person.png", ] }) }) }) + + when("multi-platform", func() { + var ( + tmpDir string + multiArchBuildpackPackage string + builderTomlPath string + remoteRunImage string + remoteBuildImage string + err error + ) + + it.Before(func() { + h.SkipIf(t, !pack.SupportsFeature(invoke.MultiPlatformBuildersAndBuildPackages), "multi-platform builders and buildpack packages are available since 0.34.0") + + tmpDir, err = os.MkdirTemp("", "multi-platform-builder-create-tests") + assert.Nil(err) + + // used to avoid authentication issues with the local registry + os.Setenv("DOCKER_CONFIG", registryConfig.DockerConfigDir) + + // create a multi-platform buildpack and push it to a registry + multiArchBuildpackPackage = registryConfig.RepoName("simple-multi-platform-buildpack" + h.RandString(8)) + sourceDir := filepath.Join("testdata", "mock_buildpacks") + path := filepath.Join(tmpDir, "simple-layers-buildpack") + err = buildpacks.BpFolderSimpleLayers.Prepare(sourceDir, tmpDir) + h.AssertNil(t, err) + + output := pack.RunSuccessfully( + "buildpack", "package", multiArchBuildpackPackage, + "--path", path, + "--publish", + "--target", "linux/amd64", + "--target", "windows/amd64", + ) + assertions.NewOutputAssertionManager(t, output).ReportsPackagePublished(multiArchBuildpackPackage) + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulIndexPushed(multiArchBuildpackPackage) + h.AssertRemoteImageIndex(t, multiArchBuildpackPackage, types.OCIImageIndex, 2) + + // runImage and buildImage are saved in the daemon, for this test we want them to be available in a registry + remoteRunImage = registryConfig.RepoName(runImage + h.RandString(8)) + remoteBuildImage = registryConfig.RepoName(buildImage + h.RandString(8)) + + imageManager.TagImage(runImage, remoteRunImage) + imageManager.TagImage(buildImage, remoteBuildImage) + + h.AssertNil(t, h.PushImage(dockerCli, remoteRunImage, registryConfig)) + h.AssertNil(t, h.PushImage(dockerCli, remoteBuildImage, registryConfig)) + }) + + it.After(func() { + imageManager.CleanupImages(remoteBuildImage) + imageManager.CleanupImages(remoteRunImage) + os.RemoveAll(tmpDir) + }) + + generateMultiPlatformBuilderToml := func(template, buildpackURI, buildImage, runImage string) string { + t.Helper() + buildpackToml, err := os.CreateTemp(tmpDir, "buildpack-*.toml") + assert.Nil(err) + + pack.FixtureManager().TemplateFixtureToFile( + template, + buildpackToml, + map[string]interface{}{ + "BuildpackURI": buildpackURI, + "BuildImage": buildImage, + "RunImage": runImage, + }, + ) + assert.Nil(buildpackToml.Close()) + return buildpackToml.Name() + } + + when("builder.toml has no targets", func() { + when("--publish", func() { + it.Before(func() { + builderName = registryConfig.RepoName("remote-multi-platform-builder" + h.RandString(8)) + + // We need to configure our builder.toml with image references that points to our ephemeral registry + builderTomlPath = generateMultiPlatformBuilderToml("builder_multi_platform-no-targets.toml", multiArchBuildpackPackage, remoteBuildImage, remoteRunImage) + }) + + it("publishes builder images for each requested target to the registry and creates an image index", func() { + output := pack.RunSuccessfully( + "builder", "create", builderName, + "--config", builderTomlPath, + "--publish", + "--target", "linux/amd64", + "--target", "windows/amd64", + ) + + defer imageManager.CleanupImages(builderName) + assertions.NewOutputAssertionManager(t, output).ReportsBuilderCreated(builderName) + + assertImage.CanBePulledFromRegistry(builderName) + + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulIndexPushed(builderName) + h.AssertRemoteImageIndex(t, builderName, types.OCIImageIndex, 2) + }) + }) + + when("--daemon", func() { + it.Before(func() { + builderName = registryConfig.RepoName("local-multi-platform-builder" + h.RandString(8)) + + // We need to configure our builder.toml with image references that points to our ephemeral registry + builderTomlPath = generateMultiPlatformBuilderToml("builder_multi_platform-no-targets.toml", multiArchBuildpackPackage, buildImage, runImage) + }) + + it("publishes builder image to the daemon for the given target", func() { + platform := "linux/amd64" + if runtime.GOOS == "windows" { + platform = "windows/amd64" + } + + output := pack.RunSuccessfully( + "builder", "create", builderName, + "--config", builderTomlPath, + "--target", platform, + ) + + defer imageManager.CleanupImages(builderName) + assertions.NewOutputAssertionManager(t, output).ReportsBuilderCreated(builderName) + }) + }) + }) + + when("builder.toml has targets", func() { + when("--publish", func() { + it.Before(func() { + builderName = registryConfig.RepoName("remote-multi-platform-builder" + h.RandString(8)) + + // We need to configure our builder.toml with image references that points to our ephemeral registry + builderTomlPath = generateMultiPlatformBuilderToml("builder_multi_platform.toml", multiArchBuildpackPackage, remoteBuildImage, remoteRunImage) + }) + + it("publishes builder images for each configured target to the registry and creates an image index", func() { + output := pack.RunSuccessfully( + "builder", "create", builderName, + "--config", builderTomlPath, + "--publish", + ) + + defer imageManager.CleanupImages(builderName) + assertions.NewOutputAssertionManager(t, output).ReportsBuilderCreated(builderName) + + assertImage.CanBePulledFromRegistry(builderName) + + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulIndexPushed(builderName) + h.AssertRemoteImageIndex(t, builderName, types.OCIImageIndex, 2) + }) + }) + + when("--daemon", func() { + it.Before(func() { + builderName = registryConfig.RepoName("local-multi-platform-builder" + h.RandString(8)) + + // We need to configure our builder.toml with image references that points to our ephemeral registry + builderTomlPath = generateMultiPlatformBuilderToml("builder_multi_platform.toml", multiArchBuildpackPackage, buildImage, runImage) + }) + + it("publishes builder image to the daemon for the given target", func() { + platform := "linux/amd64" + if runtime.GOOS == "windows" { + platform = "windows/amd64" + } + + output := pack.RunSuccessfully( + "builder", "create", builderName, + "--config", builderTomlPath, + "--target", platform, + ) + + defer imageManager.CleanupImages(builderName) + assertions.NewOutputAssertionManager(t, output).ReportsBuilderCreated(builderName) + }) + }) + }) + }) }) when("builder create", func() { diff --git a/acceptance/assertions/output.go b/acceptance/assertions/output.go index 65f206a86e..d64be0e171 100644 --- a/acceptance/assertions/output.go +++ b/acceptance/assertions/output.go @@ -173,6 +173,12 @@ func (o OutputAssertionManager) IncludesUsagePrompt() { o.assert.Contains(o.output, "Run 'pack --help' for usage.") } +func (o OutputAssertionManager) ReportsBuilderCreated(name string) { + o.testObject.Helper() + + o.assert.ContainsF(o.output, "Successfully created builder image '%s'", name) +} + func (o OutputAssertionManager) ReportsSettingDefaultBuilder(name string) { o.testObject.Helper() diff --git a/acceptance/testdata/pack_fixtures/builder_multi_platform-no-targets.toml b/acceptance/testdata/pack_fixtures/builder_multi_platform-no-targets.toml new file mode 100644 index 0000000000..8e6998332d --- /dev/null +++ b/acceptance/testdata/pack_fixtures/builder_multi_platform-no-targets.toml @@ -0,0 +1,19 @@ +[[buildpacks]] +id = "simple/layers" +version = "simple-layers-version" +uri = "{{ .BuildpackURI }}" + +[[order]] +[[order.group]] +id = "simple/layers" +version = "simple-layers-version" + +[build] +image = "{{ .BuildImage }}" + +[run] +[[run.images]] +image = "{{ .RunImage }}" + + + diff --git a/acceptance/testdata/pack_fixtures/builder_multi_platform.toml b/acceptance/testdata/pack_fixtures/builder_multi_platform.toml new file mode 100644 index 0000000000..d67d820515 --- /dev/null +++ b/acceptance/testdata/pack_fixtures/builder_multi_platform.toml @@ -0,0 +1,28 @@ +[[buildpacks]] +id = "simple/layers" +version = "simple-layers-version" +uri = "{{ .BuildpackURI }}" + +[[order]] +[[order.group]] +id = "simple/layers" +version = "simple-layers-version" + +# Targets the buildpack will work with +[[targets]] +os = "linux" +arch = "amd64" + +[[targets]] +os = "windows" +arch = "arm64" + +[build] +image = "{{ .BuildImage }}" + +[run] +[[run.images]] +image = "{{ .RunImage }}" + + + diff --git a/internal/commands/builder_create.go b/internal/commands/builder_create.go index 3730dbc2e1..0893c974df 100644 --- a/internal/commands/builder_create.go +++ b/internal/commands/builder_create.go @@ -94,7 +94,7 @@ Creating a custom builder allows you to control what buildpacks are used and wha } if len(multiArchCfg.Targets()) == 0 { - logger.Warnf("A new '--target' flag is available to set the platform") + logger.Infof("Pro tip: use --targets flag OR [[targets]] in builder.toml to specify the desired platform") } imageName := args[0] diff --git a/internal/commands/builder_create_test.go b/internal/commands/builder_create_test.go index 54c5e524d3..db8baeda0f 100644 --- a/internal/commands/builder_create_test.go +++ b/internal/commands/builder_create_test.go @@ -463,79 +463,94 @@ func testCreateCommand(t *testing.T, when spec.G, it spec.S) { }) when("multi-platform builder is expected to be created", func() { - when("--target", func() { - when("builder config has no targets defined", func() { - it.Before(func() { - h.AssertNil(t, os.WriteFile(builderConfigPath, []byte(validConfig), 0666)) - }) - when("daemon", func() { - it("errors when exporting to daemon", func() { - command.SetArgs([]string{ - "some/builder", - "--config", builderConfigPath, - "--target", "linux/amd64", - "--target", "windows/amd64", - }) - err := command.Execute() - h.AssertNotNil(t, err) - h.AssertError(t, err, "when exporting to daemon only one target is allowed") + when("builder config has no targets defined", func() { + it.Before(func() { + h.AssertNil(t, os.WriteFile(builderConfigPath, []byte(validConfig), 0666)) + }) + when("daemon", func() { + it("errors when exporting to daemon", func() { + command.SetArgs([]string{ + "some/builder", + "--config", builderConfigPath, + "--target", "linux/amd64", + "--target", "windows/amd64", }) + err := command.Execute() + h.AssertNotNil(t, err) + h.AssertError(t, err, "when exporting to daemon only one target is allowed") }) + }) - when("--publish", func() { - it.Before(func() { - mockClient.EXPECT().CreateBuilder(gomock.Any(), EqCreateBuilderOptionsTargets([]dist.Target{ - {OS: "linux", Arch: "amd64"}, - {OS: "windows", Arch: "amd64"}, - })).Return(nil) - }) + when("--publish", func() { + it.Before(func() { + mockClient.EXPECT().CreateBuilder(gomock.Any(), EqCreateBuilderOptionsTargets([]dist.Target{ + {OS: "linux", Arch: "amd64"}, + {OS: "windows", Arch: "amd64"}, + })).Return(nil) + }) - it("creates a builder with the given targets", func() { - command.SetArgs([]string{ - "some/builder", - "--config", builderConfigPath, - "--target", "linux/amd64", - "--target", "windows/amd64", - "--publish", - }) - h.AssertNil(t, command.Execute()) + it("creates a builder with the given targets", func() { + command.SetArgs([]string{ + "some/builder", + "--config", builderConfigPath, + "--target", "linux/amd64", + "--target", "windows/amd64", + "--publish", }) + h.AssertNil(t, command.Execute()) }) }) + }) + + when("builder config has targets defined", func() { + it.Before(func() { + h.AssertNil(t, os.WriteFile(builderConfigPath, []byte(validConfigWithTargets), 0666)) + }) - when("builder config has targets defined", func() { + when("--publish", func() { it.Before(func() { - h.AssertNil(t, os.WriteFile(builderConfigPath, []byte(validConfigWithTargets), 0666)) + mockClient.EXPECT().CreateBuilder(gomock.Any(), EqCreateBuilderOptionsTargets([]dist.Target{ + {OS: "linux", Arch: "amd64"}, + {OS: "linux", Arch: "arm64"}, + })).Return(nil) }) - when("--publish", func() { - it.Before(func() { - mockClient.EXPECT().CreateBuilder(gomock.Any(), EqCreateBuilderOptionsTargets([]dist.Target{ - {OS: "linux", Arch: "amd64"}, - {OS: "linux", Arch: "arm64"}, - })).Return(nil) + it("creates a builder with the given targets", func() { + command.SetArgs([]string{ + "some/builder", + "--config", builderConfigPath, + "--publish", }) + h.AssertNil(t, command.Execute()) + }) + }) - it("creates a builder with the given targets", func() { - command.SetArgs([]string{ - "some/builder", - "--config", builderConfigPath, - "--publish", - }) - h.AssertNil(t, command.Execute()) + when("invalid target flag is used", func() { + it("errors with a message when invalid target flag is used", func() { + command.SetArgs([]string{ + "some/builder", + "--config", builderConfigPath, + "--target", "something/wrong", + "--publish", }) + h.AssertNotNil(t, command.Execute()) + }) + }) + + when("--targets", func() { + it.Before(func() { + mockClient.EXPECT().CreateBuilder(gomock.Any(), EqCreateBuilderOptionsTargets([]dist.Target{ + {OS: "linux", Arch: "amd64"}, + })).Return(nil) }) - when("invalid target flag is used", func() { - it("errors with a message when invalid target flag is used", func() { - command.SetArgs([]string{ - "some/builder", - "--config", builderConfigPath, - "--target", "something/wrong", - "--publish", - }) - h.AssertNotNil(t, command.Execute()) + it("creates a builder with the given targets", func() { + command.SetArgs([]string{ + "some/builder", + "--target", "linux/amd64", + "--config", builderConfigPath, }) + h.AssertNil(t, command.Execute()) }) }) }) diff --git a/internal/commands/buildpack_package.go b/internal/commands/buildpack_package.go index 88f87e6f44..bfa2b09dcb 100644 --- a/internal/commands/buildpack_package.go +++ b/internal/commands/buildpack_package.go @@ -108,6 +108,7 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa if err != nil { return err } + daemon := !flags.Publish && flags.Format == "" multiArchCfg, err := processMultiArchitectureConfig(logger, flags.Targets, targets, daemon) if err != nil { @@ -115,8 +116,13 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa } if len(multiArchCfg.Targets()) == 0 { - logger.Warnf("A new '--target' flag is available to set the platform, using '%s' as default", bpPackageCfg.Platform.OS) + if isCompositeBP { + logger.Infof("Pro tip: use --targets flag OR [[targets]] in package.toml to specify the desired platform (os/arch/variant); using os %s", style.Symbol(bpPackageCfg.Platform.OS)) + } else { + logger.Infof("Pro tip: use --targets flag OR [[targets]] in buildpack.toml to specify the desired platform (os/arch/variant); using os %s", style.Symbol(bpPackageCfg.Platform.OS)) + } } else if !isCompositeBP { + // FIXME: Check if we can copy the config files during layers creation. filesToClean, err := multiArchCfg.CopyConfigFiles(bpPath) if err != nil { return err