diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index c5ced80561..5e7c1766ff 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -209,6 +209,25 @@ func testWithoutSpecificBuilderRequirement( return packageTomlFile.Name() } + generateMultiPlatformCompositeBuildpackPackageToml := func(buildpackURI, dependencyURI string) string { + t.Helper() + packageTomlFile, err := os.CreateTemp(tmpDir, "package_multi_platform-*.toml") + assert.Nil(err) + + pack.FixtureManager().TemplateFixtureToFile( + "package_multi_platform.toml", + packageTomlFile, + map[string]interface{}{ + "BuildpackURI": buildpackURI, + "PackageName": dependencyURI, + }, + ) + + assert.Nil(packageTomlFile.Close()) + + return packageTomlFile.Name() + } + when("no --format is provided", func() { it("creates the package as image", func() { packageName := "test/package-" + h.RandString(10) @@ -255,34 +274,206 @@ func testWithoutSpecificBuilderRequirement( }) when("--publish", func() { - it("publishes image to registry", func() { - packageTomlPath := generatePackageTomlWithOS(t, assert, pack, tmpDir, simplePackageConfigFixtureName, imageManager.HostOS()) - nestedPackageName := registryConfig.RepoName("test/package-" + h.RandString(10)) + it.Before(func() { + // used to avoid authentication issues with the local registry + os.Setenv("DOCKER_CONFIG", registryConfig.DockerConfigDir) + }) - nestedPackage := buildpacks.NewPackageImage( - t, - pack, - nestedPackageName, - packageTomlPath, - buildpacks.WithRequiredBuildpacks(buildpacks.BpSimpleLayers), - buildpacks.WithPublish(), - ) - buildpackManager.PrepareBuildModules(tmpDir, nestedPackage) + when("no --targets", func() { + it("publishes image to registry", func() { + packageTomlPath := generatePackageTomlWithOS(t, assert, pack, tmpDir, simplePackageConfigFixtureName, imageManager.HostOS()) + nestedPackageName := registryConfig.RepoName("test/package-" + h.RandString(10)) - aggregatePackageToml := generateAggregatePackageToml("simple-layers-parent-buildpack.tgz", nestedPackageName, imageManager.HostOS()) - packageName := registryConfig.RepoName("test/package-" + h.RandString(10)) + nestedPackage := buildpacks.NewPackageImage( + t, + pack, + nestedPackageName, + packageTomlPath, + buildpacks.WithRequiredBuildpacks(buildpacks.BpSimpleLayers), + buildpacks.WithPublish(), + ) + buildpackManager.PrepareBuildModules(tmpDir, nestedPackage) - output := pack.RunSuccessfully( - "buildpack", "package", packageName, - "-c", aggregatePackageToml, - "--publish", - ) + aggregatePackageToml := generateAggregatePackageToml("simple-layers-parent-buildpack.tgz", nestedPackageName, imageManager.HostOS()) + packageName := registryConfig.RepoName("test/package-" + h.RandString(10)) - defer imageManager.CleanupImages(packageName) - assertions.NewOutputAssertionManager(t, output).ReportsPackagePublished(packageName) + output := pack.RunSuccessfully( + "buildpack", "package", packageName, + "-c", aggregatePackageToml, + "--publish", + ) - assertImage.NotExistsLocally(packageName) - assertImage.CanBePulledFromRegistry(packageName) + defer imageManager.CleanupImages(packageName) + assertions.NewOutputAssertionManager(t, output).ReportsPackagePublished(packageName) + + assertImage.NotExistsLocally(packageName) + assertImage.CanBePulledFromRegistry(packageName) + }) + }) + + when("--targets", func() { + var packageName string + + it.Before(func() { + h.SkipIf(t, !pack.SupportsFeature(invoke.MultiPlatformBuildersAndBuildPackages), "multi-platform builders and buildpack packages are available since 0.34.0") + packageName = registryConfig.RepoName("simple-multi-platform-buildpack" + h.RandString(8)) + }) + + when("simple buildpack on disk", func() { + var path string + + it.Before(func() { + // create a simple buildpack on disk + sourceDir := filepath.Join("testdata", "mock_buildpacks") + path = filepath.Join(tmpDir, "simple-layers-buildpack") + err := buildpacks.BpFolderSimpleLayers.Prepare(sourceDir, tmpDir) + h.AssertNil(t, err) + }) + + it("publishes images for each requested target to the registry and creates an image index", func() { + output := pack.RunSuccessfully( + "buildpack", "package", packageName, + "--path", path, + "--publish", + "--target", "linux/amd64", + "--target", "windows/amd64", + ) + + defer imageManager.CleanupImages(packageName) + assertions.NewOutputAssertionManager(t, output).ReportsPackagePublished(packageName) + + assertImage.NotExistsLocally(packageName) + assertImage.CanBePulledFromRegistry(packageName) + + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulIndexPushed(packageName) + h.AssertRemoteImageIndex(t, packageName, types.OCIImageIndex, 2) + }) + }) + + when("composite buildpack on disk", func() { + var packageTomlPath string + + when("dependencies are not available in a registry", func() { + it.Before(func() { + // creates a composite buildpack on disk + sourceDir := filepath.Join("testdata", "mock_buildpacks") + + err := buildpacks.MetaBpDependency.Prepare(sourceDir, tmpDir) + h.AssertNil(t, err) + + err = buildpacks.MetaBpFolder.Prepare(sourceDir, tmpDir) + h.AssertNil(t, err) + + packageTomlPath = filepath.Join(tmpDir, "meta-buildpack", "package.toml") + }) + + it("errors with a descriptive message", func() { + output, err := pack.Run( + "buildpack", "package", packageName, + "--config", packageTomlPath, + "--publish", + "--target", "linux/amd64", + "--target", "windows/amd64", + "--verbose", + ) + assert.NotNil(err) + h.AssertContains(t, output, "uri '../meta-buildpack-dependency' is not allowed when creating a composite multi-platform buildpack; push your dependencies to a registry and use 'docker://' instead") + }) + }) + + when("dependencies are available in a registry", func() { + var depPackageName string + + it.Before(func() { + // multi-platform composite buildpacks require the dependencies to be available in a registry + // let's push it + + // first creates the simple buildpack dependency on disk + depSourceDir := filepath.Join("testdata", "mock_buildpacks") + depPath := filepath.Join(tmpDir, "meta-buildpack-dependency") + err := buildpacks.MetaBpDependency.Prepare(depSourceDir, tmpDir) + h.AssertNil(t, err) + + // push the dependency to a registry + depPackageName = registryConfig.RepoName("simple-multi-platform-buildpack" + h.RandString(8)) + output := pack.RunSuccessfully( + "buildpack", "package", depPackageName, + "--path", depPath, + "--publish", + "--target", "linux/amd64", + "--target", "windows/amd64", + ) + assertions.NewOutputAssertionManager(t, output).ReportsPackagePublished(depPackageName) + assertImage.CanBePulledFromRegistry(depPackageName) + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulIndexPushed(depPackageName) + + // let's prepare the composite buildpack to use the simple buildpack dependency prepared above + packageTomlPath = generateMultiPlatformCompositeBuildpackPackageToml(".", depPackageName) + + // We need to copy the buildpack toml to the folder where the packageTomlPath was created + packageTomlDir := filepath.Dir(packageTomlPath) + sourceDir := filepath.Join("testdata", "mock_buildpacks", "meta-buildpack", "buildpack.toml") + h.CopyFile(t, sourceDir, filepath.Join(packageTomlDir, "buildpack.toml")) + }) + + it("publishes images for each requested target to the registry and creates an image index", func() { + output := pack.RunSuccessfully( + "buildpack", "package", packageName, + "--config", packageTomlPath, + "--publish", + "--target", "linux/amd64", + "--target", "windows/amd64", + ) + + defer imageManager.CleanupImages(packageName) + assertions.NewOutputAssertionManager(t, output).ReportsPackagePublished(packageName) + + assertImage.NotExistsLocally(packageName) + assertImage.CanBePulledFromRegistry(packageName) + + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulIndexPushed(packageName) + h.AssertRemoteImageIndex(t, packageName, types.OCIImageIndex, 2) + }) + }) + }) + }) + + when("new multi-platform folder structure is used", func() { + var packageName string + + it.Before(func() { + h.SkipIf(t, !pack.SupportsFeature(invoke.MultiPlatformBuildersAndBuildPackages), "multi-platform builders and buildpack packages are available since 0.34.0") + packageName = registryConfig.RepoName("simple-multi-platform-buildpack" + h.RandString(8)) + }) + + when("simple buildpack on disk", func() { + var path string + + it.Before(func() { + // create a simple buildpack on disk + sourceDir := filepath.Join("testdata", "mock_buildpacks") + path = filepath.Join(tmpDir, "multi-platform-buildpack") + err := buildpacks.MultiPlatformFolderBP.Prepare(sourceDir, tmpDir) + h.AssertNil(t, err) + }) + + it("publishes images for each target specified in buildpack.toml to the registry and creates an image index", func() { + output := pack.RunSuccessfully( + "buildpack", "package", packageName, + "--path", path, + "--publish", "--verbose", + ) + + defer imageManager.CleanupImages(packageName) + assertions.NewOutputAssertionManager(t, output).ReportsPackagePublished(packageName) + + assertImage.NotExistsLocally(packageName) + assertImage.CanBePulledFromRegistry(packageName) + + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulIndexPushed(packageName) + h.AssertRemoteImageIndex(t, packageName, types.OCIImageIndex, 2) + }) + }) }) }) @@ -3105,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 but the user provides --target", 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 imageManager.HostOS() == "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 imageManager.HostOS() == "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/buildpacks/folder_buildpack.go b/acceptance/buildpacks/folder_buildpack.go index 7da935d845..387d7ae115 100644 --- a/acceptance/buildpacks/folder_buildpack.go +++ b/acceptance/buildpacks/folder_buildpack.go @@ -47,4 +47,5 @@ var ( ExtFolderSimpleLayers = folderBuildModule{name: "simple-layers-extension"} MetaBpFolder = folderBuildModule{name: "meta-buildpack"} MetaBpDependency = folderBuildModule{name: "meta-buildpack-dependency"} + MultiPlatformFolderBP = folderBuildModule{name: "multi-platform-buildpack"} ) diff --git a/acceptance/invoke/pack.go b/acceptance/invoke/pack.go index d63170d1eb..b5ebb9ae94 100644 --- a/acceptance/invoke/pack.go +++ b/acceptance/invoke/pack.go @@ -239,6 +239,7 @@ const ( FlattenBuilderCreationV2 FixesRunImageMetadata ManifestCommands + MultiPlatformBuildersAndBuildPackages ) var featureTests = map[Feature]func(i *PackInvoker) bool{ @@ -278,6 +279,9 @@ var featureTests = map[Feature]func(i *PackInvoker) bool{ ManifestCommands: func(i *PackInvoker) bool { return i.atLeast("v0.34.0") }, + MultiPlatformBuildersAndBuildPackages: func(i *PackInvoker) bool { + return i.atLeast("v0.34.0") + }, } func (i *PackInvoker) SupportsFeature(f Feature) bool { diff --git a/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/buildpack.toml b/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/buildpack.toml new file mode 100644 index 0000000000..e00f93e2de --- /dev/null +++ b/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/buildpack.toml @@ -0,0 +1,17 @@ +api = "0.10" + +[buildpack] + id = "simple/layers" + version = "simple-layers-version" + name = "Simple Layers Buildpack" + +[[targets]] +os = "linux" +arch = "amd64" + +[[targets]] +os = "windows" +arch = "amd64" + +[[stacks]] + id = "*" diff --git a/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/linux/amd64/bin/build b/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/linux/amd64/bin/build new file mode 100755 index 0000000000..88c1b8049b --- /dev/null +++ b/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/linux/amd64/bin/build @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo "---> Build: NOOP Buildpack" diff --git a/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/linux/amd64/bin/detect b/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/linux/amd64/bin/detect new file mode 100755 index 0000000000..d1813055aa --- /dev/null +++ b/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/linux/amd64/bin/detect @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +## always detect \ No newline at end of file diff --git a/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/windows/amd64/bin/build.bat b/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/windows/amd64/bin/build.bat new file mode 100644 index 0000000000..39731e4422 --- /dev/null +++ b/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/windows/amd64/bin/build.bat @@ -0,0 +1,3 @@ +@echo off + +echo ---- Build: NOOP Buildpack diff --git a/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/windows/amd64/bin/detect.bat b/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/windows/amd64/bin/detect.bat new file mode 100644 index 0000000000..15823e73f1 --- /dev/null +++ b/acceptance/testdata/mock_buildpacks/multi-platform-buildpack/windows/amd64/bin/detect.bat @@ -0,0 +1,2 @@ +@echo off +:: always detect 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..cc95496e30 --- /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 = "amd64" + +[build] +image = "{{ .BuildImage }}" + +[run] +[[run.images]] +image = "{{ .RunImage }}" + + + diff --git a/acceptance/testdata/pack_fixtures/package_aggregate.toml b/acceptance/testdata/pack_fixtures/package_aggregate.toml index e6a5e2c980..0f8cc5d93a 100644 --- a/acceptance/testdata/pack_fixtures/package_aggregate.toml +++ b/acceptance/testdata/pack_fixtures/package_aggregate.toml @@ -5,4 +5,4 @@ uri = "{{ .BuildpackURI }}" image = "{{ .PackageName }}" [platform] -os = "{{ .OS }}" \ No newline at end of file +os = "{{ .OS }}" diff --git a/acceptance/testdata/pack_fixtures/package_multi_platform.toml b/acceptance/testdata/pack_fixtures/package_multi_platform.toml new file mode 100644 index 0000000000..02b4ff47a6 --- /dev/null +++ b/acceptance/testdata/pack_fixtures/package_multi_platform.toml @@ -0,0 +1,5 @@ +[buildpack] +uri = "{{ .BuildpackURI }}" + +[[dependencies]] +uri = "{{ .PackageName }}" diff --git a/builder/config_reader.go b/builder/config_reader.go index 6ebd688d55..becdd94660 100644 --- a/builder/config_reader.go +++ b/builder/config_reader.go @@ -25,6 +25,7 @@ type Config struct { Lifecycle LifecycleConfig `toml:"lifecycle"` Run RunConfig `toml:"run"` Build BuildConfig `toml:"build"` + Targets []dist.Target `toml:"targets"` } // ModuleCollection is a list of ModuleConfigs diff --git a/buildpackage/config_reader.go b/buildpackage/config_reader.go index d8b8d7d436..ea5af009fe 100644 --- a/buildpackage/config_reader.go +++ b/buildpackage/config_reader.go @@ -19,7 +19,11 @@ type Config struct { Buildpack dist.BuildpackURI `toml:"buildpack"` Extension dist.BuildpackURI `toml:"extension"` Dependencies []dist.ImageOrURI `toml:"dependencies"` - Platform dist.Platform `toml:"platform"` + // deprecated + Platform dist.Platform `toml:"platform"` + + // Define targets for composite buildpacks + Targets []dist.Target `toml:"targets"` } func DefaultConfig() Config { @@ -117,6 +121,17 @@ func (r *ConfigReader) Read(path string) (Config, error) { return packageConfig, nil } +func (r *ConfigReader) ReadBuildpackDescriptor(path string) (dist.BuildpackDescriptor, error) { + buildpackCfg := dist.BuildpackDescriptor{} + + _, err := toml.DecodeFile(path, &buildpackCfg) + if err != nil { + return dist.BuildpackDescriptor{}, err + } + + return buildpackCfg, nil +} + func validateURI(uri, relativeBaseDir string) error { locatorType, err := buildpack.GetLocatorType(uri, relativeBaseDir, nil) if err != nil { diff --git a/go.mod b/go.mod index 12d3c8f7be..ce4b382349 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/Microsoft/go-winio v0.6.2 github.com/apex/log v1.9.0 - github.com/buildpacks/imgutil v0.0.0-20240507132533-9f7b96c3d09d + github.com/buildpacks/imgutil v0.0.0-20240514200737-4af87862ff7e github.com/buildpacks/lifecycle v0.19.6 github.com/docker/cli v26.1.1+incompatible github.com/docker/docker v26.1.1+incompatible diff --git a/go.sum b/go.sum index 4fe11ffdee..1b4ae4166d 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/buildpacks/imgutil v0.0.0-20240507132533-9f7b96c3d09d h1:GVRuY/C8j4pjOddeeZelbKKLMMX+dYR3TlxE4L1hECU= -github.com/buildpacks/imgutil v0.0.0-20240507132533-9f7b96c3d09d/go.mod h1:n2R6VRuWsAX3cyHCp/u0Z4WJcixny0gYg075J39owrk= +github.com/buildpacks/imgutil v0.0.0-20240514200737-4af87862ff7e h1:IBH3oJu2okeB8W+bMTCYsRqbDT1+cjt6GuFtE52tAxM= +github.com/buildpacks/imgutil v0.0.0-20240514200737-4af87862ff7e/go.mod h1:n2R6VRuWsAX3cyHCp/u0Z4WJcixny0gYg075J39owrk= github.com/buildpacks/lifecycle v0.19.6 h1:/bmfMs35aSkxyzYDF+iHl9VnYmUBBbHBmnvo8XNEINk= github.com/buildpacks/lifecycle v0.19.6/go.mod h1:sWrBJzf/7dWrcHrWiV/P2+3jS8G8Ki5tczq8jO3XVRQ= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= diff --git a/internal/builder/lifecycle.go b/internal/builder/lifecycle.go index 92c860ae9d..4f2d4df948 100644 --- a/internal/builder/lifecycle.go +++ b/internal/builder/lifecycle.go @@ -120,3 +120,8 @@ func (l *lifecycle) binaries() []string { } return binaries } + +// SupportedLinuxArchitecture returns true for each binary architecture available at https://github.com/buildpacks/lifecycle/releases/ +func SupportedLinuxArchitecture(arch string) bool { + return arch == "arm64" || arch == "ppc64le" || arch == "s390x" +} diff --git a/internal/commands/builder_create.go b/internal/commands/builder_create.go index 56ac071fe2..0893c974df 100644 --- a/internal/commands/builder_create.go +++ b/internal/commands/builder_create.go @@ -23,6 +23,7 @@ type BuilderCreateFlags struct { Registry string Policy string Flatten []string + Targets []string Label map[string]string } @@ -87,6 +88,15 @@ Creating a custom builder allows you to control what buildpacks are used and wha return err } + multiArchCfg, err := processMultiArchitectureConfig(logger, flags.Targets, builderConfig.Targets, !flags.Publish) + if err != nil { + return err + } + + if len(multiArchCfg.Targets()) == 0 { + logger.Infof("Pro tip: use --targets flag OR [[targets]] in builder.toml to specify the desired platform") + } + imageName := args[0] if err := pack.CreateBuilder(cmd.Context(), client.CreateBuilderOptions{ RelativeBaseDir: relativeBaseDir, @@ -98,6 +108,7 @@ Creating a custom builder allows you to control what buildpacks are used and wha PullPolicy: pullPolicy, Flatten: toFlatten, Labels: flags.Label, + Targets: multiArchCfg.Targets(), }); err != nil { return err } @@ -116,6 +127,12 @@ Creating a custom builder allows you to control what buildpacks are used and wha cmd.Flags().StringVar(&flags.Policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, and if-not-present. The default is always") cmd.Flags().StringArrayVar(&flags.Flatten, "flatten", nil, "List of buildpacks to flatten together into a single layer (format: '@,@'") cmd.Flags().StringToStringVarP(&flags.Label, "label", "l", nil, "Labels to add to the builder image, in the form of '='") + cmd.Flags().StringSliceVarP(&flags.Targets, "target", "t", nil, + `Target platforms to build for.\nTargets should be in the format '[os][/arch][/variant]:[distroname@osversion@anotherversion];[distroname@osversion]'. +- To specify two different architectures: '--target "linux/amd64" --target "linux/arm64"' +- To specify the distribution version: '--target "linux/arm/v6:ubuntu@14.04"' +- To specify multiple distribution versions: '--target "linux/arm/v6:ubuntu@14.04" --target "linux/arm/v6:ubuntu@16.04"' + `) AddHelpFlag(cmd, "create") return cmd diff --git a/internal/commands/builder_create_test.go b/internal/commands/builder_create_test.go index b89e7ab534..db8baeda0f 100644 --- a/internal/commands/builder_create_test.go +++ b/internal/commands/builder_create_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "testing" "github.com/golang/mock/gomock" @@ -17,6 +18,8 @@ import ( "github.com/buildpacks/pack/internal/commands" "github.com/buildpacks/pack/internal/commands/testmocks" "github.com/buildpacks/pack/internal/config" + "github.com/buildpacks/pack/pkg/client" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/logging" h "github.com/buildpacks/pack/testhelpers" ) @@ -31,6 +34,23 @@ const validConfig = ` ` +const validConfigWithTargets = ` +[[buildpacks]] +id = "some.buildpack" + +[[order]] +[[order.group]] +id = "some.buildpack" + +[[targets]] +os = "linux" +arch = "amd64" + +[[targets]] +os = "linux" +arch = "arm64" +` + const validConfigWithExtensions = ` [[buildpacks]] id = "some.buildpack" @@ -441,5 +461,127 @@ func testCreateCommand(t *testing.T, when spec.G, it spec.S) { }) }) }) + + when("multi-platform builder is expected to be created", 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("--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()) + }) + }) + }) + + when("builder config has targets defined", func() { + it.Before(func() { + h.AssertNil(t, os.WriteFile(builderConfigPath, []byte(validConfigWithTargets), 0666)) + }) + + 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()) + }) + }) + + 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) + }) + + it("creates a builder with the given targets", func() { + command.SetArgs([]string{ + "some/builder", + "--target", "linux/amd64", + "--config", builderConfigPath, + }) + h.AssertNil(t, command.Execute()) + }) + }) + }) + }) }) } + +func EqCreateBuilderOptionsTargets(targets []dist.Target) gomock.Matcher { + return createbuilderOptionsMatcher{ + description: fmt.Sprintf("Target=%v", targets), + equals: func(o client.CreateBuilderOptions) bool { + if len(o.Targets) != len(targets) { + return false + } + return reflect.DeepEqual(o.Targets, targets) + }, + } +} + +type createbuilderOptionsMatcher struct { + equals func(options client.CreateBuilderOptions) bool + description string +} + +func (m createbuilderOptionsMatcher) Matches(x interface{}) bool { + if b, ok := x.(client.CreateBuilderOptions); ok { + return m.equals(b) + } + return false +} + +func (m createbuilderOptionsMatcher) String() string { + return "is a CreateBuilderOption with " + m.description +} diff --git a/internal/commands/buildpack_package.go b/internal/commands/buildpack_package.go index adb95c3e03..bfa2b09dcb 100644 --- a/internal/commands/buildpack_package.go +++ b/internal/commands/buildpack_package.go @@ -2,6 +2,7 @@ package commands import ( "context" + "os" "path/filepath" "strings" @@ -12,6 +13,7 @@ import ( "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/pkg/client" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/image" "github.com/buildpacks/pack/pkg/logging" ) @@ -24,6 +26,7 @@ type BuildpackPackageFlags struct { BuildpackRegistry string Path string FlattenExclude []string + Targets []string Label map[string]string Publish bool Flatten bool @@ -37,6 +40,7 @@ type BuildpackPackager interface { // PackageConfigReader reads BuildpackPackage configs type PackageConfigReader interface { Read(path string) (pubbldpkg.Config, error) + ReadBuildpackDescriptor(path string) (dist.BuildpackDescriptor, error) } // BuildpackPackage packages (a) buildpack(s) into OCI format, based on a package config @@ -100,6 +104,32 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa logger.Warn("Flattening a buildpack package could break the distribution specification. Please use it with caution.") } + targets, isCompositeBP, err := processBuildpackPackageTargets(flags.Path, packageConfigReader, bpPackageCfg) + if err != nil { + return err + } + + daemon := !flags.Publish && flags.Format == "" + multiArchCfg, err := processMultiArchitectureConfig(logger, flags.Targets, targets, daemon) + if err != nil { + return err + } + + if len(multiArchCfg.Targets()) == 0 { + 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 + } + defer clean(filesToClean) + } + if err := packager.PackageBuildpack(cmd.Context(), client.PackageBuildpackOptions{ RelativeBaseDir: relativeBaseDir, Name: name, @@ -111,6 +141,7 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa Flatten: flags.Flatten, FlattenExclude: flags.FlattenExclude, Labels: flags.Label, + Targets: multiArchCfg.Targets(), }); err != nil { return err } @@ -138,6 +169,13 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa cmd.Flags().BoolVar(&flags.Flatten, "flatten", false, "Flatten the buildpack into a single layer") cmd.Flags().StringSliceVarP(&flags.FlattenExclude, "flatten-exclude", "e", nil, "Buildpacks to exclude from flattening, in the form of '@'") cmd.Flags().StringToStringVarP(&flags.Label, "label", "l", nil, "Labels to add to packaged Buildpack, in the form of '='") + cmd.Flags().StringSliceVarP(&flags.Targets, "target", "t", nil, + `Target platforms to build for. +Targets should be in the format '[os][/arch][/variant]:[distroname@osversion@anotherversion];[distroname@osversion]'. +- To specify two different architectures: '--target "linux/amd64" --target "linux/arm64"' +- To specify the distribution version: '--target "linux/arm/v6:ubuntu@14.04"' +- To specify multiple distribution versions: '--target "linux/arm/v6:ubuntu@14.04" --target "linux/arm/v6:ubuntu@16.04"' + `) if !cfg.Experimental { cmd.Flags().MarkHidden("flatten") cmd.Flags().MarkHidden("flatten-exclude") @@ -169,3 +207,41 @@ func validateBuildpackPackageFlags(cfg config.Config, p *BuildpackPackageFlags) } return nil } + +// processBuildpackPackageTargets returns the list of targets defined in the configuration file; it could be the buildpack.toml or +// the package.toml if the buildpack is a composite buildpack +func processBuildpackPackageTargets(path string, packageConfigReader PackageConfigReader, bpPackageCfg pubbldpkg.Config) ([]dist.Target, bool, error) { + var ( + targets []dist.Target + order dist.Order + isCompositeBP bool + ) + + // Read targets from buildpack.toml + pathToBuildpackToml := filepath.Join(path, "buildpack.toml") + if _, err := os.Stat(pathToBuildpackToml); err == nil { + buildpackCfg, err := packageConfigReader.ReadBuildpackDescriptor(pathToBuildpackToml) + if err != nil { + return nil, false, err + } + targets = buildpackCfg.Targets() + order = buildpackCfg.Order() + isCompositeBP = len(order) > 0 + } + + // When composite buildpack, targets are defined in package.toml - See RFC-0128 + if isCompositeBP { + targets = bpPackageCfg.Targets + } + return targets, isCompositeBP, nil +} + +func clean(paths []string) error { + // we need to clean the buildpack.toml for each place where we copied to + if len(paths) > 0 { + for _, path := range paths { + os.Remove(path) + } + } + return nil +} diff --git a/internal/commands/buildpack_package_test.go b/internal/commands/buildpack_package_test.go index f527c01d08..d43548caaf 100644 --- a/internal/commands/buildpack_package_test.go +++ b/internal/commands/buildpack_package_test.go @@ -203,6 +203,7 @@ func testPackageCommand(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, receivedOptions.PullPolicy, image.PullAlways) }) }) + when("no --pull-policy", func() { var pullPolicyArgs = []string{ "some-image-name", @@ -235,6 +236,35 @@ func testPackageCommand(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, receivedOptions.PullPolicy, image.PullNever) }) }) + + when("composite buildpack", func() { + when("multi-platform", func() { + var ( + targets []dist.Target + descriptor dist.BuildpackDescriptor + config pubbldpkg.Config + path string + ) + + it.Before(func() { + targets = []dist.Target{ + {OS: "linux", Arch: "amd64"}, + {OS: "windows", Arch: "amd64"}, + } + config = pubbldpkg.Config{Buildpack: dist.BuildpackURI{URI: "test"}} + descriptor = dist.BuildpackDescriptor{WithTargets: targets} + path = "testdata" + }) + + it("creates a multi-platform buildpack package", func() { + cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager), withPackageConfigReader(fakes.NewFakePackageConfigReader(whereReadReturns(config, nil), whereReadBuildpackDescriptor(descriptor, nil)))) + cmd.SetArgs([]string{"some-name", "-p", path}) + + h.AssertNil(t, cmd.Execute()) + h.AssertEq(t, fakeBuildpackPackager.CreateCalledWithOptions.Targets, targets) + }) + }) + }) }) when("no config path is specified", func() { @@ -249,13 +279,43 @@ func testPackageCommand(t *testing.T, when spec.G, it spec.S) { }) }) when("a path is specified", func() { - it("creates a default config with the appropriate path", func() { - cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager)) - cmd.SetArgs([]string{"some-name", "-p", ".."}) - h.AssertNil(t, cmd.Execute()) - bpPath, _ := filepath.Abs("..") - receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions - h.AssertEq(t, receivedOptions.Config.Buildpack.URI, bpPath) + when("not multi-platform", func() { + it("creates a default config with the appropriate path", func() { + cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager)) + cmd.SetArgs([]string{"some-name", "-p", ".."}) + h.AssertNil(t, cmd.Execute()) + bpPath, _ := filepath.Abs("..") + receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions + h.AssertEq(t, receivedOptions.Config.Buildpack.URI, bpPath) + }) + }) + + when("multi-platform", func() { + var ( + targets []dist.Target + descriptor dist.BuildpackDescriptor + path string + ) + + when("single buildpack", func() { + it.Before(func() { + targets = []dist.Target{ + {OS: "linux", Arch: "amd64"}, + {OS: "windows", Arch: "amd64"}, + } + + descriptor = dist.BuildpackDescriptor{WithTargets: targets} + path = "testdata" + }) + + it("creates a multi-platform buildpack package", func() { + cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager), withPackageConfigReader(fakes.NewFakePackageConfigReader(whereReadBuildpackDescriptor(descriptor, nil)))) + cmd.SetArgs([]string{"some-name", "-p", path}) + + h.AssertNil(t, cmd.Execute()) + h.AssertEq(t, fakeBuildpackPackager.CreateCalledWithOptions.Targets, targets) + }) + }) }) }) }) @@ -330,6 +390,20 @@ func testPackageCommand(t *testing.T, when spec.G, it spec.S) { h.AssertError(t, err, "invalid argument \"name+value\" for \"-l, --label\" flag: name+value must be formatted as key=value") }) }) + + when("--target cannot be parsed", func() { + it("errors with a descriptive message", func() { + cmd := packageCommand() + cmd.SetArgs([]string{ + "some-image-name", "--config", "/path/to/some/file", + "--target", "something/wrong", "--publish", + }) + + err := cmd.Execute() + h.AssertNotNil(t, err) + h.AssertError(t, err, "unknown target: 'something/wrong'") + }) + }) }) } @@ -413,3 +487,10 @@ func whereReadReturns(config pubbldpkg.Config, err error) func(*fakes.FakePackag r.ReadReturnError = err } } + +func whereReadBuildpackDescriptor(descriptor dist.BuildpackDescriptor, err error) func(*fakes.FakePackageConfigReader) { + return func(r *fakes.FakePackageConfigReader) { + r.ReadBuildpackDescriptorReturn = descriptor + r.ReadBuildpackDescriptorReturnError = err + } +} diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 734344dba0..1308b35ec0 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -13,7 +13,10 @@ import ( "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/internal/style" + "github.com/buildpacks/pack/internal/target" + "github.com/buildpacks/pack/pkg/buildpack" "github.com/buildpacks/pack/pkg/client" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/logging" ) @@ -128,3 +131,27 @@ func parseFormatFlag(value string) (types.MediaType, error) { } return format, nil } + +// processMultiArchitectureConfig takes an array of targets with format: [os][/arch][/variant]:[distroname@osversion@anotherversion];[distroname@osversion] +// and a list of targets defined in a configuration file (buildpack.toml or package.toml) and creates a multi-architecture configuration +func processMultiArchitectureConfig(logger logging.Logger, userTargets []string, configTargets []dist.Target, daemon bool) (*buildpack.MultiArchConfig, error) { + var ( + expectedTargets []dist.Target + err error + ) + if len(userTargets) > 0 { + if expectedTargets, err = target.ParseTargets(userTargets, logger); err != nil { + return &buildpack.MultiArchConfig{}, err + } + if len(expectedTargets) > 1 && daemon { + // when we are exporting to daemon, only 1 target is allow + return &buildpack.MultiArchConfig{}, errors.Errorf("when exporting to daemon only one target is allowed") + } + } + + multiArchCfg, err := buildpack.NewMultiArchConfig(configTargets, expectedTargets, logger) + if err != nil { + return &buildpack.MultiArchConfig{}, err + } + return multiArchCfg, nil +} diff --git a/internal/commands/fakes/fake_package_config_reader.go b/internal/commands/fakes/fake_package_config_reader.go index e0610a33da..d3b402fcb9 100644 --- a/internal/commands/fakes/fake_package_config_reader.go +++ b/internal/commands/fakes/fake_package_config_reader.go @@ -2,12 +2,17 @@ package fakes import ( pubbldpkg "github.com/buildpacks/pack/buildpackage" + "github.com/buildpacks/pack/pkg/dist" ) type FakePackageConfigReader struct { ReadCalledWithArg string ReadReturnConfig pubbldpkg.Config ReadReturnError error + + ReadBuildpackDescriptorCalledWithArg string + ReadBuildpackDescriptorReturn dist.BuildpackDescriptor + ReadBuildpackDescriptorReturnError error } func (r *FakePackageConfigReader) Read(path string) (pubbldpkg.Config, error) { @@ -16,6 +21,12 @@ func (r *FakePackageConfigReader) Read(path string) (pubbldpkg.Config, error) { return r.ReadReturnConfig, r.ReadReturnError } +func (r *FakePackageConfigReader) ReadBuildpackDescriptor(path string) (dist.BuildpackDescriptor, error) { + r.ReadBuildpackDescriptorCalledWithArg = path + + return r.ReadBuildpackDescriptorReturn, r.ReadBuildpackDescriptorReturnError +} + func NewFakePackageConfigReader(ops ...func(*FakePackageConfigReader)) *FakePackageConfigReader { fakePackageConfigReader := &FakePackageConfigReader{ ReadReturnConfig: pubbldpkg.Config{}, diff --git a/internal/commands/testdata/buildpack.toml b/internal/commands/testdata/buildpack.toml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/fakes/fake_image_fetcher.go b/internal/fakes/fake_image_fetcher.go index 9e09ecfde2..857438d529 100644 --- a/internal/fakes/fake_image_fetcher.go +++ b/internal/fakes/fake_image_fetcher.go @@ -6,14 +6,15 @@ import ( "github.com/buildpacks/imgutil" "github.com/pkg/errors" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/image" ) type FetchArgs struct { Daemon bool PullPolicy image.PullPolicy - Platform string LayoutOption image.LayoutOption + Target *dist.Target } type FakeImageFetcher struct { @@ -31,7 +32,7 @@ func NewFakeImageFetcher() *FakeImageFetcher { } func (f *FakeImageFetcher) Fetch(ctx context.Context, name string, options image.FetchOptions) (imgutil.Image, error) { - f.FetchCalls[name] = &FetchArgs{Daemon: options.Daemon, PullPolicy: options.PullPolicy, Platform: options.Platform, LayoutOption: options.LayoutOption} + f.FetchCalls[name] = &FetchArgs{Daemon: options.Daemon, PullPolicy: options.PullPolicy, Target: options.Target, LayoutOption: options.LayoutOption} ri, remoteFound := f.RemoteImages[name] diff --git a/internal/registry/git_test.go b/internal/registry/git_test.go index cd25829c50..060df13ba4 100644 --- a/internal/registry/git_test.go +++ b/internal/registry/git_test.go @@ -4,6 +4,7 @@ import ( "bytes" "os" "path/filepath" + "runtime" "testing" "github.com/go-git/go-git/v5" @@ -42,7 +43,10 @@ func testGit(t *testing.T, when spec.G, it spec.S) { }) it.After(func() { - h.AssertNil(t, os.RemoveAll(tmpDir)) + if runtime.GOOS != "windows" { + h.AssertNil(t, os.RemoveAll(tmpDir)) + } + os.RemoveAll(tmpDir) }) when("#GitCommit", func() { diff --git a/pkg/buildpack/builder.go b/pkg/buildpack/builder.go index 14006b6b67..6d6c835497 100644 --- a/pkg/buildpack/builder.go +++ b/pkg/buildpack/builder.go @@ -27,7 +27,7 @@ import ( ) type ImageFactory interface { - NewImage(repoName string, local bool, imageOS string) (imgutil.Image, error) + NewImage(repoName string, local bool, target dist.Target) (imgutil.Image, error) } type WorkableImage interface { @@ -352,12 +352,12 @@ func (b *PackageBuilder) resolvedStacks() []dist.Stack { return stacks } -func (b *PackageBuilder) SaveAsFile(path, imageOS string, labels map[string]string) error { +func (b *PackageBuilder) SaveAsFile(path string, target dist.Target, labels map[string]string) error { if err := b.validate(); err != nil { return err } - layoutImage, err := newLayoutImage(imageOS) + layoutImage, err := newLayoutImage(target) if err != nil { return errors.Wrap(err, "creating layout image") } @@ -417,7 +417,7 @@ func (b *PackageBuilder) SaveAsFile(path, imageOS string, labels map[string]stri return archive.WriteDirToTar(tw, layoutDir, "/", 0, 0, 0755, true, false, nil) } -func newLayoutImage(imageOS string) (*layoutImage, error) { +func newLayoutImage(target dist.Target) (*layoutImage, error) { i := empty.Image configFile, err := i.ConfigFile() @@ -425,13 +425,14 @@ func newLayoutImage(imageOS string) (*layoutImage, error) { return nil, err } - configFile.OS = imageOS + configFile.OS = target.OS + configFile.Architecture = target.Arch i, err = mutate.ConfigFile(i, configFile) if err != nil { return nil, err } - if imageOS == "windows" { + if target.OS == "windows" { opener := func() (io.ReadCloser, error) { reader, err := layer.WindowsBaseLayer() return io.NopCloser(reader), err @@ -451,12 +452,12 @@ func newLayoutImage(imageOS string) (*layoutImage, error) { return &layoutImage{Image: i}, nil } -func (b *PackageBuilder) SaveAsImage(repoName string, publish bool, imageOS string, labels map[string]string) (imgutil.Image, error) { +func (b *PackageBuilder) SaveAsImage(repoName string, publish bool, target dist.Target, labels map[string]string) (imgutil.Image, error) { if err := b.validate(); err != nil { return nil, err } - image, err := b.imageFactory.NewImage(repoName, !publish, imageOS) + image, err := b.imageFactory.NewImage(repoName, !publish, target) if err != nil { return nil, errors.Wrapf(err, "creating image") } diff --git a/pkg/buildpack/builder_test.go b/pkg/buildpack/builder_test.go index 3abdeecb24..613fed7f98 100644 --- a/pkg/buildpack/builder_test.go +++ b/pkg/buildpack/builder_test.go @@ -55,7 +55,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { if expectedImageOS != "" { fakePackageImage := fakes.NewImage("some/package", "", nil) - imageFactory.EXPECT().NewImage("some/package", true, expectedImageOS).Return(fakePackageImage, nil).MaxTimes(1) + imageFactory.EXPECT().NewImage("some/package", true, dist.Target{OS: expectedImageOS}).Return(fakePackageImage, nil).MaxTimes(1) } return imageFactory @@ -72,24 +72,27 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }) when("validation", func() { + linux := dist.Target{OS: "linux"} + windows := dist.Target{OS: "windows"} + for _, _test := range []*struct { name string expectedImageOS string fn func(*buildpack.PackageBuilder) error }{ {name: "SaveAsImage", expectedImageOS: "linux", fn: func(builder *buildpack.PackageBuilder) error { - _, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{}) + _, err := builder.SaveAsImage("some/package", false, linux, map[string]string{}) return err }}, {name: "SaveAsImage", expectedImageOS: "windows", fn: func(builder *buildpack.PackageBuilder) error { - _, err := builder.SaveAsImage("some/package", false, "windows", map[string]string{}) + _, err := builder.SaveAsImage("some/package", false, windows, map[string]string{}) return err }}, {name: "SaveAsFile", expectedImageOS: "linux", fn: func(builder *buildpack.PackageBuilder) error { - return builder.SaveAsFile(path.Join(tmpDir, "package.cnb"), "linux", map[string]string{}) + return builder.SaveAsFile(path.Join(tmpDir, "package.cnb"), linux, map[string]string{}) }}, {name: "SaveAsFile", expectedImageOS: "windows", fn: func(builder *buildpack.PackageBuilder) error { - return builder.SaveAsFile(path.Join(tmpDir, "package.cnb"), "windows", map[string]string{}) + return builder.SaveAsFile(path.Join(tmpDir, "package.cnb"), windows, map[string]string{}) }}, } { // always use copies to avoid stale refs @@ -412,7 +415,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) builder.AddDependency(dependency2) - img, err := builder.SaveAsImage("some/package", false, expectedImageOS, map[string]string{}) + img, err := builder.SaveAsImage("some/package", false, dist.Target{OS: expectedImageOS}, map[string]string{}) h.AssertNil(t, err) metadata := buildpack.Metadata{} @@ -474,7 +477,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) builder.AddDependency(dependency2) - img, err := builder.SaveAsImage("some/package", false, expectedImageOS, map[string]string{}) + img, err := builder.SaveAsImage("some/package", false, dist.Target{OS: expectedImageOS}, map[string]string{}) h.AssertNil(t, err) metadata := buildpack.Metadata{} @@ -539,7 +542,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { builder.AddDependency(dependencyNestedNested) - img, err := builder.SaveAsImage("some/package", false, expectedImageOS, map[string]string{}) + img, err := builder.SaveAsImage("some/package", false, dist.Target{OS: expectedImageOS}, map[string]string{}) h.AssertNil(t, err) metadata := buildpack.Metadata{} @@ -586,7 +589,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { var customLabels = map[string]string{"test.label.one": "1", "test.label.two": "2"} - packageImage, err := builder.SaveAsImage("some/package", false, "linux", customLabels) + packageImage, err := builder.SaveAsImage("some/package", false, dist.Target{OS: "linux"}, customLabels) h.AssertNil(t, err) labelData, err := packageImage.Label("io.buildpacks.buildpackage.metadata") @@ -637,7 +640,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) builder := buildpack.NewBuilder(mockImageFactory("linux")) builder.SetExtension(extension1) - packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{}) + packageImage, err := builder.SaveAsImage("some/package", false, dist.Target{OS: "linux"}, map[string]string{}) h.AssertNil(t, err) labelData, err := packageImage.Label("io.buildpacks.buildpackage.metadata") h.AssertNil(t, err) @@ -670,7 +673,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { builder := buildpack.NewBuilder(mockImageFactory("linux")) builder.SetBuildpack(buildpack1) - packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{}) + packageImage, err := builder.SaveAsImage("some/package", false, dist.Target{OS: "linux"}, map[string]string{}) h.AssertNil(t, err) var bpLayers dist.ModuleLayers @@ -694,7 +697,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { builder := buildpack.NewBuilder(mockImageFactory("linux")) builder.SetBuildpack(buildpack1) - packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{}) + packageImage, err := builder.SaveAsImage("some/package", false, dist.Target{OS: "linux"}, map[string]string{}) h.AssertNil(t, err) buildpackExists := func(name, version string) { @@ -741,7 +744,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { builder := buildpack.NewBuilder(mockImageFactory("windows")) builder.SetBuildpack(buildpack1) - _, err = builder.SaveAsImage("some/package", false, "windows", map[string]string{}) + _, err = builder.SaveAsImage("some/package", false, dist.Target{OS: "windows"}, map[string]string{}) h.AssertNil(t, err) }) @@ -749,7 +752,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { mockImageFactory = func(expectedImageOS string) *testmocks.MockImageFactory { var imageWithLabelError = &imageWithLabelError{Image: fakes.NewImage("some/package", "", nil)} imageFactory := testmocks.NewMockImageFactory(mockController) - imageFactory.EXPECT().NewImage("some/package", true, expectedImageOS).Return(imageWithLabelError, nil).MaxTimes(1) + imageFactory.EXPECT().NewImage("some/package", true, dist.Target{OS: expectedImageOS}).Return(imageWithLabelError, nil).MaxTimes(1) return imageFactory } @@ -782,7 +785,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { var customLabels = map[string]string{"test.label.fail": "true"} - _, err = builder.SaveAsImage("some/package", false, "linux", customLabels) + _, err = builder.SaveAsImage("some/package", false, dist.Target{OS: "linux"}, customLabels) h.AssertError(t, err, "adding label test.label.fail=true") }) @@ -910,7 +913,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { builder.AddDependencies(bp1, nil) builder.AddDependencies(compositeBP2, []buildpack.BuildModule{bp21, bp22, compositeBP3, bp31}) - packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{}) + packageImage, err := builder.SaveAsImage("some/package", false, dist.Target{OS: "linux"}, map[string]string{}) h.AssertNil(t, err) fakePackageImage := packageImage.(*fakes.Image) @@ -933,7 +936,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { builder.AddDependencies(bp1, nil) builder.AddDependencies(compositeBP2, []buildpack.BuildModule{bp21, bp22, compositeBP3, bp31}) - packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{}) + packageImage, err := builder.SaveAsImage("some/package", false, dist.Target{OS: "linux"}, map[string]string{}) h.AssertNil(t, err) fakePackageImage := packageImage.(*fakes.Image) @@ -960,7 +963,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { var customLabels = map[string]string{"test.label.one": "1", "test.label.two": "2"} outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10))) - h.AssertNil(t, builder.SaveAsFile(outputFile, "linux", customLabels)) + h.AssertNil(t, builder.SaveAsFile(outputFile, dist.Target{OS: "linux"}, customLabels)) withContents := func(fn func(data []byte)) h.TarEntryAssertion { return func(t *testing.T, header *tar.Header, data []byte) { @@ -1020,7 +1023,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { builder.SetBuildpack(buildpack1) outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10))) - h.AssertNil(t, builder.SaveAsFile(outputFile, "linux", map[string]string{})) + h.AssertNil(t, builder.SaveAsFile(outputFile, dist.Target{OS: "linux"}, map[string]string{})) h.AssertOnTarEntry(t, outputFile, "/blobs", h.IsDirectory(), @@ -1070,7 +1073,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { builder.SetBuildpack(buildpack1) outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10))) - h.AssertNil(t, builder.SaveAsFile(outputFile, "windows", map[string]string{})) + h.AssertNil(t, builder.SaveAsFile(outputFile, dist.Target{OS: "windows"}, map[string]string{})) // Windows baselayer content is constant expectedBaseLayerReader, err := layer.WindowsBaseLayer() diff --git a/pkg/buildpack/downloader.go b/pkg/buildpack/downloader.go index 0a8a0d64cf..a127effb9e 100644 --- a/pkg/buildpack/downloader.go +++ b/pkg/buildpack/downloader.go @@ -65,12 +65,9 @@ type DownloadOptions struct { // The base directory to use to resolve relative assets RelativeBaseDir string - // The OS of the builder image + // Deprecated: the older alternative to specify the OS to download; use Target instead ImageOS string - // The OS/Architecture to download - Platform string - // Deprecated: the older alternative to buildpack URI ImageName string @@ -80,6 +77,9 @@ type DownloadOptions struct { Daemon bool PullPolicy image.PullPolicy + + // The OS/Architecture/Variant to download. + Target *dist.Target } func (c *buildpackDownloader) Download(ctx context.Context, moduleURI string, opts DownloadOptions) (BuildModule, []BuildModule, error) { @@ -109,7 +109,7 @@ func (c *buildpackDownloader) Download(ctx context.Context, moduleURI string, op mainBP, depBPs, err = extractPackaged(ctx, kind, imageName, c.imageFetcher, image.FetchOptions{ Daemon: opts.Daemon, PullPolicy: opts.PullPolicy, - Platform: opts.Platform, + Target: opts.Target, }) if err != nil { return nil, nil, errors.Wrapf(err, "extracting from registry %s", style.Symbol(moduleURI)) @@ -124,7 +124,7 @@ func (c *buildpackDownloader) Download(ctx context.Context, moduleURI string, op mainBP, depBPs, err = extractPackaged(ctx, kind, address, c.imageFetcher, image.FetchOptions{ Daemon: opts.Daemon, PullPolicy: opts.PullPolicy, - Platform: opts.Platform, + Target: opts.Target, }) if err != nil { return nil, nil, errors.Wrapf(err, "extracting from registry %s", style.Symbol(moduleURI)) @@ -142,7 +142,11 @@ func (c *buildpackDownloader) Download(ctx context.Context, moduleURI string, op return nil, nil, errors.Wrapf(err, "downloading %s from %s", kind, style.Symbol(moduleURI)) } - mainBP, depBPs, err = decomposeBlob(blob, kind, opts.ImageOS, c.logger) + imageOS := opts.ImageOS + if opts.Target != nil { + imageOS = opts.Target.OS + } + mainBP, depBPs, err = decomposeBlob(blob, kind, imageOS, c.logger) if err != nil { return nil, nil, errors.Wrapf(err, "extracting from %s", style.Symbol(moduleURI)) } diff --git a/pkg/buildpack/downloader_test.go b/pkg/buildpack/downloader_test.go index 171b61f10b..03ce73b0e5 100644 --- a/pkg/buildpack/downloader_test.go +++ b/pkg/buildpack/downloader_test.go @@ -60,7 +60,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { var createPackage = func(imageName string) *fakes.Image { packageImage := fakes.NewImage(imageName, "", nil) - mockImageFactory.EXPECT().NewImage(packageImage.Name(), false, "linux").Return(packageImage, nil) + mockImageFactory.EXPECT().NewImage(packageImage.Name(), false, dist.Target{OS: "linux"}).Return(packageImage, nil) pack, err := client.NewClient( client.WithLogger(logger), @@ -124,14 +124,16 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { when("#Download", func() { var ( packageImage *fakes.Image - downloadOptions = buildpack.DownloadOptions{ImageOS: "linux"} + downloadOptions = buildpack.DownloadOptions{Target: &dist.Target{ + OS: "linux", + }} ) - shouldFetchPackageImageWith := func(demon bool, pull image.PullPolicy, platform string) { + shouldFetchPackageImageWith := func(demon bool, pull image.PullPolicy, target *dist.Target) { mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), image.FetchOptions{ Daemon: demon, PullPolicy: pull, - Platform: platform, + Target: target, }).Return(packageImage, nil) } @@ -144,13 +146,12 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { it("should pull and use local package image", func() { downloadOptions = buildpack.DownloadOptions{ RegistryName: "some-registry", - ImageOS: "linux", - Platform: "linux/amd64", + Target: &dist.Target{OS: "linux", Arch: "amd64"}, Daemon: true, PullPolicy: image.PullAlways, } - shouldFetchPackageImageWith(true, image.PullAlways, "linux/amd64") + shouldFetchPackageImageWith(true, image.PullAlways, &dist.Target{OS: "linux", Arch: "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,12 +162,12 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { it("should find package in registry", func() { downloadOptions = buildpack.DownloadOptions{ RegistryName: "some-registry", - ImageOS: "linux", + Target: &dist.Target{OS: "linux"}, Daemon: true, PullPolicy: image.PullAlways, } - shouldFetchPackageImageWith(true, image.PullAlways, "") + shouldFetchPackageImageWith(true, image.PullAlways, &dist.Target{OS: "linux"}) 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") @@ -189,12 +190,11 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { downloadOptions = buildpack.DownloadOptions{ Daemon: true, PullPolicy: image.PullAlways, - ImageOS: "linux", - Platform: "linux/amd64", + Target: &dist.Target{OS: "linux", Arch: "amd64"}, ImageName: "some/package:tag", } - shouldFetchPackageImageWith(true, image.PullAlways, "linux/amd64") + shouldFetchPackageImageWith(true, image.PullAlways, &dist.Target{OS: "linux", Arch: "amd64"}) mainBP, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions) h.AssertNil(t, err) h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo") @@ -204,13 +204,13 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { when("daemon=true and pull-policy=always", func() { it("should pull and use local package image", func() { downloadOptions = buildpack.DownloadOptions{ - ImageOS: "linux", + Target: &dist.Target{OS: "linux"}, ImageName: packageImage.Name(), Daemon: true, PullPolicy: image.PullAlways, } - shouldFetchPackageImageWith(true, image.PullAlways, "") + shouldFetchPackageImageWith(true, image.PullAlways, &dist.Target{OS: "linux"}) mainBP, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions) h.AssertNil(t, err) h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo") @@ -220,13 +220,13 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { when("daemon=false and pull-policy=always", func() { it("should use remote package image", func() { downloadOptions = buildpack.DownloadOptions{ - ImageOS: "linux", + Target: &dist.Target{OS: "linux"}, ImageName: packageImage.Name(), Daemon: false, PullPolicy: image.PullAlways, } - shouldFetchPackageImageWith(false, image.PullAlways, "") + shouldFetchPackageImageWith(false, image.PullAlways, &dist.Target{OS: "linux"}) mainBP, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions) h.AssertNil(t, err) h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo") @@ -236,11 +236,11 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { when("daemon=false and pull-policy=always", func() { it("should use remote package URI", func() { downloadOptions = buildpack.DownloadOptions{ - ImageOS: "linux", + Target: &dist.Target{OS: "linux"}, Daemon: false, PullPolicy: image.PullAlways, } - shouldFetchPackageImageWith(false, image.PullAlways, "") + shouldFetchPackageImageWith(false, image.PullAlways, &dist.Target{OS: "linux"}) mainBP, _, err := buildpackDownloader.Download(context.TODO(), packageImage.Name(), downloadOptions) h.AssertNil(t, err) h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo") @@ -250,13 +250,13 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { when("publish=true and pull-policy=never", func() { it("should push to registry and not pull package image", func() { downloadOptions = buildpack.DownloadOptions{ - ImageOS: "linux", + Target: &dist.Target{OS: "linux"}, ImageName: packageImage.Name(), Daemon: false, PullPolicy: image.PullNever, } - shouldFetchPackageImageWith(false, image.PullNever, "") + shouldFetchPackageImageWith(false, image.PullNever, &dist.Target{OS: "linux"}) mainBP, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions) h.AssertNil(t, err) h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo") @@ -266,7 +266,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { when("daemon=true pull-policy=never and there is no local package image", func() { it("should fail without trying to retrieve package image from registry", func() { downloadOptions = buildpack.DownloadOptions{ - ImageOS: "linux", + Target: &dist.Target{OS: "linux"}, ImageName: packageImage.Name(), Daemon: true, PullPolicy: image.PullNever, @@ -293,7 +293,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { buildpackURI, _ := paths.FilePathToURI(buildpackPath, "") mockDownloader.EXPECT().Download(gomock.Any(), buildpackURI).Return(blob.NewBlob(buildpackPath), nil).AnyTimes() downloadOptions = buildpack.DownloadOptions{ - ImageOS: "linux", + Target: &dist.Target{OS: "linux"}, RelativeBaseDir: "testdata", } mainBP, _, err := buildpackDownloader.Download(context.TODO(), "buildpack", downloadOptions) @@ -307,7 +307,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { extensionURI, _ := paths.FilePathToURI(extensionPath, "") mockDownloader.EXPECT().Download(gomock.Any(), extensionURI).Return(blob.NewBlob(extensionPath), nil).AnyTimes() downloadOptions = buildpack.DownloadOptions{ - ImageOS: "linux", + Target: &dist.Target{OS: "linux"}, ModuleKind: "extension", RelativeBaseDir: "testdata", } @@ -323,7 +323,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { packagedExtensionURI, _ := paths.FilePathToURI(packagedExtensionPath, "") mockDownloader.EXPECT().Download(gomock.Any(), packagedExtensionURI).Return(blob.NewBlob(packagedExtensionPath), nil).AnyTimes() downloadOptions = buildpack.DownloadOptions{ - ImageOS: "linux", + Target: &dist.Target{OS: "linux"}, ModuleKind: "extension", RelativeBaseDir: "testdata", Daemon: true, @@ -380,7 +380,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { when("can't download image from registry", func() { it("errors", func() { packageImage := fakes.NewImage("example.com/some/package@sha256:74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7", "", nil) - mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways}).Return(nil, errors.New("failed to pull")) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways, Target: &dist.Target{OS: "linux"}}).Return(nil, errors.New("failed to pull")) downloadOptions.RegistryName = "some-registry" _, _, err := buildpackDownloader.Download(context.TODO(), "urn:cnb:registry:example/foo@1.1.0", downloadOptions) diff --git a/pkg/buildpack/multi_architecture_helper.go b/pkg/buildpack/multi_architecture_helper.go new file mode 100644 index 0000000000..6443f5b44a --- /dev/null +++ b/pkg/buildpack/multi_architecture_helper.go @@ -0,0 +1,148 @@ +package buildpack + +import ( + "io" + "os" + "path/filepath" + + "github.com/buildpacks/pack/internal/paths" + "github.com/buildpacks/pack/pkg/dist" + "github.com/buildpacks/pack/pkg/logging" +) + +// MultiArchConfig targets can be defined in .toml files or can be overridden by end-users via the command line; this structure offers +// utility methods to determine the expected final targets configuration. +type MultiArchConfig struct { + // Targets defined in .toml files + buildpackTargets []dist.Target + + // Targets defined by end-users to override configuration files + expectedTargets []dist.Target + logger logging.Logger +} + +func NewMultiArchConfig(targets []dist.Target, expected []dist.Target, logger logging.Logger) (*MultiArchConfig, error) { + return &MultiArchConfig{ + buildpackTargets: targets, + expectedTargets: expected, + logger: logger, + }, nil +} + +func (m *MultiArchConfig) Targets() []dist.Target { + if len(m.expectedTargets) == 0 { + return m.buildpackTargets + } + return m.expectedTargets +} + +// CopyConfigFiles will, given a base directory (which is expected to be the root folder of a single buildpack), +// copy the buildpack.toml file from the base directory into the corresponding platform root folder for each target. +// It will return an array with all the platform root folders where the buildpack.toml file was copied. +func (m *MultiArchConfig) CopyConfigFiles(baseDir string) ([]string, error) { + var filesToClean []string + targets := dist.ExpandTargetsDistributions(m.Targets()...) + for _, target := range targets { + path, err := CopyConfigFile(baseDir, target) + if err != nil { + return nil, err + } + if path != "" { + filesToClean = append(filesToClean, path) + } + } + return filesToClean, nil +} + +// CopyConfigFile will copy the buildpack.toml file from the base directory into the corresponding platform folder +// for the specified target and desired distribution version. +func CopyConfigFile(baseDir string, target dist.Target) (string, error) { + if ok, platformRootFolder := PlatformRootFolder(baseDir, target); ok { + path, err := copyBuildpackTOML(baseDir, platformRootFolder) + if err != nil { + return "", err + } + return path, nil + } + return "", nil +} + +// PlatformRootFolder finds the top-most directory that identifies a target in a given buildpack folder. +// Let's define a target with the following format: [os][/arch][/variant]:[name@version], and consider the following examples: +// - Given a target linux/amd64 the platform root folder will be /linux/amd64 if the folder exists +// - Given a target windows/amd64:windows@10.0.20348.1970 the platform root folder will be /windows/amd64/windows@10.0.20348.1970 if the folder exists +// - When no target folder exists, the root folder will be equal to folder +// +// Note: If the given target has more than 1 distribution, it is recommended to use `ExpandTargetsDistributions` before +// calling this method. +func PlatformRootFolder(bpPathURI string, target dist.Target) (bool, string) { + var ( + pRootFolder string + err error + ) + + if paths.IsURI(bpPathURI) { + if pRootFolder, err = paths.URIToFilePath(bpPathURI); err != nil { + return false, "" + } + } else { + pRootFolder = bpPathURI + } + + targets := target.ValuesAsSlice() + found := false + current := false + for _, t := range targets { + current, pRootFolder = targetExists(pRootFolder, t) + if current { + found = current + } else { + // No need to keep looking + break + } + } + // We will return the last matching folder + return found, pRootFolder +} + +func targetExists(root, expected string) (bool, string) { + if expected == "" { + return false, root + } + path := filepath.Join(root, expected) + if exists, _ := paths.IsDir(path); exists { + return true, path + } + return false, root +} + +func copyBuildpackTOML(src string, dest string) (string, error) { + return copyFile(src, dest, "buildpack.toml") +} + +func copyFile(src, dest, fileName string) (string, error) { + filePath := filepath.Join(dest, fileName) + fileToCopy, err := os.Create(filePath) + if err != nil { + return "", err + } + defer fileToCopy.Close() + + fileCopyFrom, err := os.Open(filepath.Join(src, fileName)) + if err != nil { + return "", err + } + defer fileCopyFrom.Close() + + _, err = io.Copy(fileToCopy, fileCopyFrom) + if err != nil { + return "", err + } + + fileToCopy.Sync() + if err != nil { + return "", err + } + + return filePath, nil +} diff --git a/pkg/buildpack/multi_architecture_helper_test.go b/pkg/buildpack/multi_architecture_helper_test.go new file mode 100644 index 0000000000..1d86ce00ba --- /dev/null +++ b/pkg/buildpack/multi_architecture_helper_test.go @@ -0,0 +1,224 @@ +package buildpack_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/pack/internal/paths" + "github.com/buildpacks/pack/pkg/buildpack" + "github.com/buildpacks/pack/pkg/dist" + "github.com/buildpacks/pack/pkg/logging" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestMultiArchConfig(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + spec.Run(t, "testMultiArchConfig", testMultiArchConfig, spec.Parallel(), spec.Report(report.Terminal{})) +} + +func testMultiArchConfig(t *testing.T, when spec.G, it spec.S) { + var ( + err error + outBuf bytes.Buffer + logger *logging.LogWithWriters + multiArchConfig *buildpack.MultiArchConfig + targetsFromBuildpack []dist.Target + targetsFromFlags []dist.Target + tmpDir string + ) + + it.Before(func() { + targetsFromBuildpack = []dist.Target{{OS: "linux", Arch: "amd64"}} + targetsFromFlags = []dist.Target{{OS: "linux", Arch: "arm64", ArchVariant: "v6"}} + logger = logging.NewLogWithWriters(&outBuf, &outBuf) + + tmpDir, err = os.MkdirTemp("", "test-multi-arch") + h.AssertNil(t, err) + }) + + it.After(func() { + os.RemoveAll(tmpDir) + }) + + when("#Targets", func() { + when("buildpack targets are defined", func() { + it.Before(func() { + multiArchConfig, err = buildpack.NewMultiArchConfig(targetsFromBuildpack, []dist.Target{}, logger) + h.AssertNil(t, err) + }) + + it("returns buildpack targets", func() { + h.AssertEq(t, len(multiArchConfig.Targets()), 1) + h.AssertEq(t, multiArchConfig.Targets()[0].OS, "linux") + h.AssertEq(t, multiArchConfig.Targets()[0].Arch, "amd64") + }) + }) + + when("buildpack targets are not defined, but flags are provided", func() { + it.Before(func() { + multiArchConfig, err = buildpack.NewMultiArchConfig([]dist.Target{}, targetsFromFlags, logger) + h.AssertNil(t, err) + }) + + it("returns targets from flags", func() { + h.AssertEq(t, len(multiArchConfig.Targets()), 1) + h.AssertEq(t, multiArchConfig.Targets()[0].OS, "linux") + h.AssertEq(t, multiArchConfig.Targets()[0].Arch, "arm64") + h.AssertEq(t, multiArchConfig.Targets()[0].ArchVariant, "v6") + }) + }) + + when("buildpack targets are defined and flags are provided", func() { + it.Before(func() { + multiArchConfig, err = buildpack.NewMultiArchConfig(targetsFromBuildpack, targetsFromFlags, logger) + h.AssertNil(t, err) + }) + + it("returns targets from flags", func() { + // flags overrides the targets in the configuration files + h.AssertEq(t, len(multiArchConfig.Targets()), 1) + h.AssertEq(t, multiArchConfig.Targets()[0].OS, "linux") + h.AssertEq(t, multiArchConfig.Targets()[0].Arch, "arm64") + h.AssertEq(t, multiArchConfig.Targets()[0].ArchVariant, "v6") + }) + }) + }) + + when("#CopyConfigFiles", func() { + when("buildpack root folder exists", func() { + var rootFolder string + + it.Before(func() { + rootFolder = filepath.Join(tmpDir, "some-buildpack") + targetsFromBuildpack = []dist.Target{{OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm64", ArchVariant: "v8"}} + multiArchConfig, err = buildpack.NewMultiArchConfig(targetsFromBuildpack, []dist.Target{}, logger) + h.AssertNil(t, err) + + // dummy multi-platform buildpack structure + os.MkdirAll(filepath.Join(rootFolder, "linux", "amd64"), 0755) + os.MkdirAll(filepath.Join(rootFolder, "linux", "arm64", "v8"), 0755) + _, err = os.Create(filepath.Join(rootFolder, "buildpack.toml")) + h.AssertNil(t, err) + }) + + it("copies the buildpack.toml to each target platform folder", func() { + paths, err := multiArchConfig.CopyConfigFiles(rootFolder) + h.AssertNil(t, err) + h.AssertEq(t, len(paths), 2) + h.AssertPathExists(t, filepath.Join(rootFolder, "linux", "amd64", "buildpack.toml")) + h.AssertPathExists(t, filepath.Join(rootFolder, "linux", "arm64", "v8", "buildpack.toml")) + }) + }) + }) + + when("#PlatformRootFolder", func() { + var target dist.Target + + when("root folder exists", func() { + var bpURI string + + it.Before(func() { + os.MkdirAll(filepath.Join(tmpDir, "linux", "arm64", "v8"), 0755) + os.MkdirAll(filepath.Join(tmpDir, "windows", "amd64", "v2", "windows@10.0.20348.1970"), 0755) + bpURI, err = paths.FilePathToURI(tmpDir, "") + h.AssertNil(t, err) + }) + + when("target has 'os'", func() { + when("'os' directory exists", func() { + it.Before(func() { + target = dist.Target{OS: "linux"} + }) + + it("returns /", func() { + found, path := buildpack.PlatformRootFolder(bpURI, target) + h.AssertTrue(t, found) + h.AssertEq(t, path, filepath.Join(tmpDir, "linux")) + }) + }) + + when("'os' directory doesn't exist", func() { + it.Before(func() { + target = dist.Target{OS: "darwin"} + }) + + it("returns not found", func() { + found, _ := buildpack.PlatformRootFolder(bpURI, target) + h.AssertFalse(t, found) + }) + }) + }) + + when("target has 'os' and 'arch'", func() { + when("'arch' directory exists", func() { + it.Before(func() { + target = dist.Target{OS: "linux", Arch: "arm64"} + }) + + it("returns //", func() { + found, path := buildpack.PlatformRootFolder(bpURI, target) + h.AssertTrue(t, found) + h.AssertEq(t, path, filepath.Join(tmpDir, "linux", "arm64")) + }) + }) + + when("'arch' directory doesn't exist", func() { + it.Before(func() { + target = dist.Target{OS: "linux", Arch: "amd64"} + }) + + it("returns /", func() { + found, path := buildpack.PlatformRootFolder(bpURI, target) + h.AssertTrue(t, found) + h.AssertEq(t, path, filepath.Join(tmpDir, "linux")) + }) + }) + }) + + when("target has 'os', 'arch' and 'variant'", func() { + it.Before(func() { + target = dist.Target{OS: "linux", Arch: "arm64", ArchVariant: "v8"} + }) + + it("returns ///", func() { + found, path := buildpack.PlatformRootFolder(bpURI, target) + h.AssertTrue(t, found) + h.AssertEq(t, path, filepath.Join(tmpDir, "linux", "arm64", "v8")) + }) + }) + + when("target has 'os', 'arch', 'variant' and name@version", func() { + when("all directories exist", func() { + it.Before(func() { + target = dist.Target{OS: "windows", Arch: "amd64", ArchVariant: "v2", Distributions: []dist.Distribution{{Name: "windows", Version: "10.0.20348.1970"}}} + }) + + it("returns ////@", func() { + found, path := buildpack.PlatformRootFolder(bpURI, target) + h.AssertTrue(t, found) + h.AssertEq(t, path, filepath.Join(tmpDir, "windows", "amd64", "v2", "windows@10.0.20348.1970")) + }) + }) + + when("version doesn't exist", func() { + it.Before(func() { + target = dist.Target{OS: "windows", Arch: "amd64", ArchVariant: "v2", Distributions: []dist.Distribution{{Name: "windows", Version: "foo"}}} + }) + + it("returns the most specific matching directory (///)", func() { + found, path := buildpack.PlatformRootFolder(bpURI, target) + h.AssertTrue(t, found) + h.AssertEq(t, path, filepath.Join(tmpDir, "windows", "amd64", "v2")) + }) + }) + }) + }) + }) +} diff --git a/pkg/client/build.go b/pkg/client/build.go index 68547d910f..4491aa95ac 100644 --- a/pkg/client/build.go +++ b/pkg/client/build.go @@ -340,10 +340,12 @@ 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, - Platform: fmt.Sprintf("%s/%s", builderOS, builderArch), + Target: target, } runImageName := c.resolveRunImage(opts.RunImage, imgRegistry, builderRef.Context().RegistryStr(), bldr.DefaultRunImage(), opts.AdditionalMirrors, opts.Publish, fetchOptions) @@ -418,7 +420,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { image.FetchOptions{ Daemon: true, PullPolicy: opts.PullPolicy, - Platform: fmt.Sprintf("%s/%s", builderOS, builderArch), + Target: target, }, ) if err != nil { @@ -1213,10 +1215,10 @@ func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir if err != nil { return nil, nil, errors.Wrapf(err, "getting builder architecture") } + downloadOptions := buildpack.DownloadOptions{ RegistryName: registry, - ImageOS: builderOS, - Platform: fmt.Sprintf("%s/%s", builderOS, builderArch), + Target: &dist.Target{OS: builderOS, Arch: builderArch}, RelativeBaseDir: relativeBaseDir, Daemon: !publish, PullPolicy: pullPolicy, @@ -1253,8 +1255,7 @@ func (c *Client) fetchBuildpackDependencies(ctx context.Context, bp string, pack for _, dep := range packageCfg.Dependencies { mainBP, deps, err := c.buildpackDownloader.Download(ctx, dep.URI, buildpack.DownloadOptions{ RegistryName: downloadOptions.RegistryName, - ImageOS: downloadOptions.ImageOS, - Platform: downloadOptions.Platform, + Target: downloadOptions.Target, 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 e378fbcb15..4d2b76c323 100644 --- a/pkg/client/build_test.go +++ b/pkg/client/build_test.go @@ -1239,7 +1239,7 @@ api = "0.2" }, }) args := fakeImageFetcher.FetchCalls[fakePackage.Name()] - h.AssertEq(t, args.Platform, "linux/amd64") + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/amd64") }) it("fails when no metadata label on package", func() { @@ -2090,7 +2090,7 @@ api = "0.2" args = fakeImageFetcher.FetchCalls["default/run"] h.AssertEq(t, args.Daemon, false) - h.AssertEq(t, args.Platform, "linux/amd64") + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/amd64") }) when("builder is untrusted", func() { @@ -2110,7 +2110,7 @@ api = "0.2" h.AssertNotNil(t, args) h.AssertEq(t, args.Daemon, true) h.AssertEq(t, args.PullPolicy, image.PullAlways) - h.AssertEq(t, args.Platform, "linux/amd64") + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/amd64") }) it("uses the api versions of the lifecycle image", func() { h.AssertTrue(t, true) @@ -2209,7 +2209,7 @@ api = "0.2" h.AssertNotNil(t, args) h.AssertEq(t, args.Daemon, true) h.AssertEq(t, args.PullPolicy, image.PullAlways) - h.AssertEq(t, args.Platform, "linux/amd64") + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/amd64") }) }) @@ -2281,7 +2281,7 @@ api = "0.2" args = fakeImageFetcher.FetchCalls[fmt.Sprintf("%s:%s", cfg.DefaultLifecycleImageRepo, builder.DefaultLifecycleVersion)] h.AssertEq(t, args.Daemon, true) h.AssertEq(t, args.PullPolicy, image.PullNever) - h.AssertEq(t, args.Platform, "linux/amd64") + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/amd64") }) }) diff --git a/pkg/client/client.go b/pkg/client/client.go index c7e11613e2..2f2bc9806a 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -32,6 +32,7 @@ import ( "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/pkg/blob" "github.com/buildpacks/pack/pkg/buildpack" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/image" "github.com/buildpacks/pack/pkg/index" "github.com/buildpacks/pack/pkg/logging" @@ -83,7 +84,7 @@ type BlobDownloader interface { type ImageFactory interface { // NewImage initializes an image object with required settings so that it // can be written either locally or to a registry. - NewImage(repoName string, local bool, imageOS string) (imgutil.Image, error) + NewImage(repoName string, local bool, target dist.Target) (imgutil.Image, error) } //go:generate mockgen -package testmocks -destination ../testmocks/mock_index_factory.go github.com/buildpacks/pack/pkg/client IndexFactory @@ -269,7 +270,6 @@ func NewClient(opts ...Option) (*Client, error) { return nil, errors.Wrap(err, "getting pack home") } indexRootStoragePath := filepath.Join(packHome, "manifests") - if xdgPath, ok := os.LookupEnv(xdgRuntimePath); ok { indexRootStoragePath = xdgPath } @@ -315,8 +315,14 @@ type imageFactory struct { keychain authn.Keychain } -func (f *imageFactory) NewImage(repoName string, daemon bool, imageOS string) (imgutil.Image, error) { - platform := imgutil.Platform{OS: imageOS} +func (f *imageFactory) NewImage(repoName string, daemon bool, target dist.Target) (imgutil.Image, error) { + platform := imgutil.Platform{OS: target.OS, Architecture: target.Arch, Variant: target.ArchVariant} + + if len(target.Distributions) > 0 { + // We need to set platform distribution information so that it will be reflected in the image config. + // We assume the given target's distributions were already expanded, we should be dealing with just 1 distribution name and version. + platform.OSVersion = target.Distributions[0].Version + } if daemon { return local.NewImage(repoName, f.dockerClient, local.WithDefaultPlatform(platform)) diff --git a/pkg/client/create_builder.go b/pkg/client/create_builder.go index ee1062bc4f..ca99c99210 100644 --- a/pkg/client/create_builder.go +++ b/pkg/client/create_builder.go @@ -17,6 +17,7 @@ import ( "github.com/buildpacks/pack/internal/paths" "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/pkg/buildpack" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/image" ) @@ -50,26 +51,64 @@ type CreateBuilderOptions struct { // List of modules to be flattened Flatten buildpack.FlattenModuleInfos + + // Target platforms to build builder images for + Targets []dist.Target } // CreateBuilder creates and saves a builder image to a registry with the provided options. // If any configuration is invalid, it will error and exit without creating any images. func (c *Client) CreateBuilder(ctx context.Context, opts CreateBuilderOptions) error { - if err := c.validateConfig(ctx, opts); err != nil { + targets, err := c.processBuilderCreateTargets(ctx, opts) + if err != nil { return err } - bldr, err := c.createBaseBuilder(ctx, opts) + if len(targets) == 0 { + _, err = c.createBuilderTarget(ctx, opts, nil, false) + if err != nil { + return err + } + } else { + var digests []string + multiArch := len(targets) > 1 && opts.Publish + + for _, target := range targets { + digest, err := c.createBuilderTarget(ctx, opts, &target, multiArch) + if err != nil { + return err + } + digests = append(digests, digest) + } + + if multiArch && len(digests) > 1 { + return c.CreateManifest(ctx, CreateManifestOptions{ + IndexRepoName: opts.BuilderName, + RepoNames: digests, + Publish: true, + }) + } + } + + return nil +} + +func (c *Client) createBuilderTarget(ctx context.Context, opts CreateBuilderOptions, target *dist.Target, multiArch bool) (string, error) { + if err := c.validateConfig(ctx, opts, target); err != nil { + return "", err + } + + bldr, err := c.createBaseBuilder(ctx, opts, target) if err != nil { - return errors.Wrap(err, "failed to create builder") + return "", errors.Wrap(err, "failed to create builder") } if err := c.addBuildpacksToBuilder(ctx, opts, bldr); err != nil { - return errors.Wrap(err, "failed to add buildpacks to builder") + return "", errors.Wrap(err, "failed to add buildpacks to builder") } if err := c.addExtensionsToBuilder(ctx, opts, bldr); err != nil { - return errors.Wrap(err, "failed to add extensions to builder") + return "", errors.Wrap(err, "failed to add extensions to builder") } bldr.SetOrder(opts.Config.Order) @@ -81,27 +120,40 @@ func (c *Client) CreateBuilder(ctx context.Context, opts CreateBuilderOptions) e bldr.SetRunImage(opts.Config.Run) bldr.SetBuildConfigEnv(opts.BuildConfigEnv) - return bldr.Save(c.logger, builder.CreatorMetadata{Version: c.version}) + err = bldr.Save(c.logger, builder.CreatorMetadata{Version: c.version}) + if err != nil { + return "", err + } + + if multiArch { + // We need to keep the identifier to create the image index + id, err := bldr.Image().Identifier() + if err != nil { + return "", errors.Wrapf(err, "determining image manifest digest") + } + return id.String(), nil + } + return "", nil } -func (c *Client) validateConfig(ctx context.Context, opts CreateBuilderOptions) error { +func (c *Client) validateConfig(ctx context.Context, opts CreateBuilderOptions, target *dist.Target) error { if err := pubbldr.ValidateConfig(opts.Config); err != nil { return errors.Wrap(err, "invalid builder config") } - if err := c.validateRunImageConfig(ctx, opts); err != nil { + if err := c.validateRunImageConfig(ctx, opts, target); err != nil { return errors.Wrap(err, "invalid run image config") } return nil } -func (c *Client) validateRunImageConfig(ctx context.Context, opts CreateBuilderOptions) error { +func (c *Client) validateRunImageConfig(ctx context.Context, opts CreateBuilderOptions, target *dist.Target) error { var runImages []imgutil.Image for _, r := range opts.Config.Run.Images { for _, i := range append([]string{r.Image}, r.Mirrors...) { if !opts.Publish { - img, err := c.imageFetcher.Fetch(ctx, i, image.FetchOptions{Daemon: true, PullPolicy: opts.PullPolicy}) + img, err := c.imageFetcher.Fetch(ctx, i, image.FetchOptions{Daemon: true, PullPolicy: opts.PullPolicy, Target: target}) if err != nil { if errors.Cause(err) != image.ErrNotFound { return errors.Wrap(err, "failed to fetch image") @@ -112,7 +164,7 @@ func (c *Client) validateRunImageConfig(ctx context.Context, opts CreateBuilderO } } - img, err := c.imageFetcher.Fetch(ctx, i, image.FetchOptions{Daemon: false, PullPolicy: opts.PullPolicy}) + img, err := c.imageFetcher.Fetch(ctx, i, image.FetchOptions{Daemon: false, PullPolicy: opts.PullPolicy, Target: target}) if err != nil { if errors.Cause(err) != image.ErrNotFound { return errors.Wrap(err, "failed to fetch image") @@ -145,8 +197,8 @@ func (c *Client) validateRunImageConfig(ctx context.Context, opts CreateBuilderO return nil } -func (c *Client) createBaseBuilder(ctx context.Context, opts CreateBuilderOptions) (*builder.Builder, error) { - baseImage, err := c.imageFetcher.Fetch(ctx, opts.Config.Build.Image, image.FetchOptions{Daemon: !opts.Publish, PullPolicy: opts.PullPolicy}) +func (c *Client) createBaseBuilder(ctx context.Context, opts CreateBuilderOptions, target *dist.Target) (*builder.Builder, error) { + baseImage, err := c.imageFetcher.Fetch(ctx, opts.Config.Build.Image, image.FetchOptions{Daemon: !opts.Publish, PullPolicy: opts.PullPolicy, Target: target}) if err != nil { return nil, errors.Wrap(err, "fetch build image") } @@ -218,14 +270,14 @@ func (c *Client) fetchLifecycle(ctx context.Context, config pubbldr.LifecycleCon return nil, errors.Wrapf(err, "%s must be a valid semver", style.Symbol("lifecycle.version")) } - uri = uriFromLifecycleVersion(*v, os, architecture) + uri = c.uriFromLifecycleVersion(*v, os, architecture) case config.URI != "": uri, err = paths.FilePathToURI(config.URI, relativeBaseDir) if err != nil { return nil, err } default: - uri = uriFromLifecycleVersion(*semver.MustParse(builder.DefaultLifecycleVersion), os, architecture) + uri = c.uriFromLifecycleVersion(*semver.MustParse(builder.DefaultLifecycleVersion), os, architecture) } blob, err := c.downloader.Download(ctx, uri) @@ -271,15 +323,17 @@ func (c *Client) addConfig(ctx context.Context, kind string, config pubbldr.Modu return errors.Wrapf(err, "getting builder architecture") } + target := &dist.Target{OS: builderOS, Arch: builderArch} + c.logger.Debugf("Downloading buildpack for platform: %s", target.ValuesAsPlatform()) + mainBP, depBPs, err := c.buildpackDownloader.Download(ctx, config.URI, buildpack.DownloadOptions{ Daemon: !opts.Publish, ImageName: config.ImageName, - ImageOS: builderOS, - Platform: fmt.Sprintf("%s/%s", builderOS, builderArch), ModuleKind: kind, PullPolicy: opts.PullPolicy, RegistryName: opts.Registry, RelativeBaseDir: opts.RelativeBaseDir, + Target: target, }) if err != nil { return errors.Wrapf(err, "downloading %s", kind) @@ -323,6 +377,24 @@ func (c *Client) addConfig(ctx context.Context, kind string, config pubbldr.Modu return nil } +func (c *Client) processBuilderCreateTargets(ctx context.Context, opts CreateBuilderOptions) ([]dist.Target, error) { + var targets []dist.Target + + if len(opts.Targets) > 0 { + if opts.Publish { + targets = opts.Targets + } else { + // find a target that matches the daemon + daemonTarget, err := c.daemonTarget(ctx, opts.Targets) + if err != nil { + return targets, err + } + targets = append(targets, daemonTarget) + } + } + return targets, nil +} + func validateModule(kind string, module buildpack.BuildModule, source, expectedID, expectedVersion string) error { info := module.Descriptor().Info() if expectedID != "" && info.ID != expectedID { @@ -348,15 +420,18 @@ func validateModule(kind string, module buildpack.BuildModule, source, expectedI return nil } -func uriFromLifecycleVersion(version semver.Version, os string, architecture string) string { +func (c *Client) uriFromLifecycleVersion(version semver.Version, os string, architecture string) string { arch := "x86-64" if os == "windows" { return fmt.Sprintf("https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+windows.%s.tgz", version.String(), version.String(), arch) } - if architecture == "arm64" { + if builder.SupportedLinuxArchitecture(architecture) { arch = architecture + } else { + // FIXME: this should probably be an error case in the future, see https://github.com/buildpacks/pack/issues/2163 + c.logger.Warnf("failed to find a lifecycle binary for requested architecture %s, defaulting to %s", style.Symbol(architecture), style.Symbol(arch)) } return fmt.Sprintf("https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+linux.%s.tgz", version.String(), version.String(), arch) diff --git a/pkg/client/create_builder_test.go b/pkg/client/create_builder_test.go index 88e888d9c6..4fa47d6474 100644 --- a/pkg/client/create_builder_test.go +++ b/pkg/client/create_builder_test.go @@ -218,8 +218,8 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { }) it("should fail when the stack ID from the builder config does not match the stack ID from the build image", func() { - mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(fakeBuildImage, nil) h.AssertNil(t, fakeBuildImage.SetLabel("io.buildpacks.stack.id", "other.stack.id")) + prepareFetcherWithBuildImage() prepareFetcherWithRunImages() err := subject.CreateBuilder(context.TODO(), opts) @@ -471,7 +471,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { prepareFetcherWithRunImages() h.AssertNil(t, fakeBuildImage.SetOS("windows")) - mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(fakeBuildImage, nil) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", gomock.Any()).Return(fakeBuildImage, nil) err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "failed to create builder: Windows containers support is currently experimental.") @@ -860,7 +860,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { 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") + h.AssertEq(t, opts.Target.ValuesAsPlatform(), "linux/amd64") return bp, bpDependencies, nil }) @@ -870,7 +870,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { 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") + h.AssertEq(t, opts.Target.ValuesAsPlatform(), "linux/amd64") return extension, nil, nil }) diff --git a/pkg/client/package_buildpack.go b/pkg/client/package_buildpack.go index 2cdcddb9ec..ed31a3203c 100644 --- a/pkg/client/package_buildpack.go +++ b/pkg/client/package_buildpack.go @@ -2,6 +2,8 @@ package client import ( "context" + "fmt" + "path/filepath" "github.com/pkg/errors" @@ -11,6 +13,7 @@ import ( "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/pkg/blob" "github.com/buildpacks/pack/pkg/buildpack" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/image" ) @@ -54,11 +57,14 @@ type PackageBuildpackOptions struct { // Flatten layers Flatten bool - // List of buildpack images to exclude from the package been flatten. + // List of buildpack images to exclude from being flattened. FlattenExclude []string // Map of labels to add to the Buildpack Labels map[string]string + + // Target platforms to build packages for + Targets []dist.Target } // PackageBuildpack packages buildpack(s) into either an image or file. @@ -67,18 +73,48 @@ func (c *Client) PackageBuildpack(ctx context.Context, opts PackageBuildpackOpti opts.Format = FormatImage } - if opts.Config.Platform.OS == "windows" && !c.experimental { - return NewExperimentError("Windows buildpackage support is currently experimental.") + targets, err := c.processPackageBuildpackTargets(ctx, opts) + if err != nil { + return err + } + multiArch := len(targets) > 1 && (opts.Publish || opts.Format == FormatFile) + + var digests []string + targets = dist.ExpandTargetsDistributions(targets...) + for _, target := range targets { + digest, err := c.packageBuildpackTarget(ctx, opts, target, multiArch) + if err != nil { + return err + } + digests = append(digests, digest) + } + + if opts.Publish && len(digests) > 1 { + // Image Index must be created only when we pushed to registry + return c.CreateManifest(ctx, CreateManifestOptions{ + IndexRepoName: opts.Name, + RepoNames: digests, + Publish: true, + }) + } + + return nil +} + +func (c *Client) packageBuildpackTarget(ctx context.Context, opts PackageBuildpackOptions, target dist.Target, multiArch bool) (string, error) { + var digest string + if target.OS == "windows" && !c.experimental { + return "", NewExperimentError("Windows buildpackage support is currently experimental.") } - err := c.validateOSPlatform(ctx, opts.Config.Platform.OS, opts.Publish, opts.Format) + err := c.validateOSPlatform(ctx, target.OS, opts.Publish, opts.Format) if err != nil { - return err + return digest, err } - writerFactory, err := layer.NewWriterFactory(opts.Config.Platform.OS) + writerFactory, err := layer.NewWriterFactory(target.OS) if err != nil { - return errors.Wrap(err, "creating layer writer factory") + return digest, errors.Wrap(err, "creating layer writer factory") } var packageBuilderOpts []buildpack.PackageBuilderOption @@ -90,33 +126,50 @@ func (c *Client) PackageBuildpack(ctx context.Context, opts PackageBuildpackOpti bpURI := opts.Config.Buildpack.URI if bpURI == "" { - return errors.New("buildpack URI must be provided") + return digest, errors.New("buildpack URI must be provided") + } + + if ok, platformRootFolder := buildpack.PlatformRootFolder(bpURI, target); ok { + bpURI = platformRootFolder } mainBlob, err := c.downloadBuildpackFromURI(ctx, bpURI, opts.RelativeBaseDir) if err != nil { - return err + return digest, err } bp, err := buildpack.FromBuildpackRootBlob(mainBlob, writerFactory, c.logger) if err != nil { - return errors.Wrapf(err, "creating buildpack from %s", style.Symbol(bpURI)) + return digest, errors.Wrapf(err, "creating buildpack from %s", style.Symbol(bpURI)) } packageBuilder.SetBuildpack(bp) + platform := target.ValuesAsPlatform() + for _, dep := range opts.Config.Dependencies { + if multiArch { + locatorType, err := buildpack.GetLocatorType(dep.URI, opts.RelativeBaseDir, []dist.ModuleInfo{}) + if err != nil { + return digest, err + } + if locatorType == buildpack.URILocator { + // When building a composite multi-platform buildpack all the dependencies must be pushed to a registry + return digest, errors.New(fmt.Sprintf("uri %s is not allowed when creating a composite multi-platform buildpack; push your dependencies to a registry and use 'docker://' instead", style.Symbol(dep.URI))) + } + } + + c.logger.Debugf("Downloading buildpack dependency for platform %s", platform) mainBP, deps, err := c.buildpackDownloader.Download(ctx, dep.URI, buildpack.DownloadOptions{ RegistryName: opts.Registry, RelativeBaseDir: opts.RelativeBaseDir, - ImageOS: opts.Config.Platform.OS, ImageName: dep.ImageName, Daemon: !opts.Publish, PullPolicy: opts.PullPolicy, + Target: &target, }) - if err != nil { - return errors.Wrapf(err, "packaging dependencies (uri=%s,image=%s)", style.Symbol(dep.URI), style.Symbol(dep.ImageName)) + return digest, errors.Wrapf(err, "packaging dependencies (uri=%s,image=%s)", style.Symbol(dep.URI), style.Symbol(dep.ImageName)) } packageBuilder.AddDependencies(mainBP, deps) @@ -124,13 +177,37 @@ func (c *Client) PackageBuildpack(ctx context.Context, opts PackageBuildpackOpti switch opts.Format { case FormatFile: - return packageBuilder.SaveAsFile(opts.Name, opts.Config.Platform.OS, opts.Labels) + name := opts.Name + if multiArch { + extension := filepath.Ext(name) + origFileName := name[:len(name)-len(filepath.Ext(name))] + if target.Arch != "" { + name = fmt.Sprintf("%s-%s-%s%s", origFileName, target.OS, target.Arch, extension) + } else { + name = fmt.Sprintf("%s-%s%s", origFileName, target.OS, extension) + } + } + err = packageBuilder.SaveAsFile(name, target, opts.Labels) + if err != nil { + return digest, err + } case FormatImage: - _, err = packageBuilder.SaveAsImage(opts.Name, opts.Publish, opts.Config.Platform.OS, opts.Labels) - return errors.Wrapf(err, "saving image") + img, err := packageBuilder.SaveAsImage(opts.Name, opts.Publish, target, opts.Labels) + if err != nil { + return digest, errors.Wrapf(err, "saving image") + } + if multiArch { + // We need to keep the identifier to create the image index + id, err := img.Identifier() + if err != nil { + return digest, errors.Wrapf(err, "determining image manifest digest") + } + digest = id.String() + } default: - return errors.Errorf("unknown format: %s", style.Symbol(opts.Format)) + return digest, errors.Errorf("unknown format: %s", style.Symbol(opts.Format)) } + return digest, nil } func (c *Client) downloadBuildpackFromURI(ctx context.Context, uri, relativeBaseDir string) (blob.Blob, error) { @@ -149,6 +226,25 @@ func (c *Client) downloadBuildpackFromURI(ctx context.Context, uri, relativeBase return blob, nil } +func (c *Client) processPackageBuildpackTargets(ctx context.Context, opts PackageBuildpackOptions) ([]dist.Target, error) { + var targets []dist.Target + if len(opts.Targets) > 0 { + // when exporting to the daemon, we need to select just one target + if !opts.Publish && opts.Format == FormatImage { + daemonTarget, err := c.daemonTarget(ctx, opts.Targets) + if err != nil { + return targets, err + } + targets = append(targets, daemonTarget) + } else { + targets = opts.Targets + } + } else { + targets = append(targets, dist.Target{OS: opts.Config.Platform.OS}) + } + return targets, nil +} + func (c *Client) validateOSPlatform(ctx context.Context, os string, publish bool, format string) error { if publish || format == FormatFile { return nil @@ -165,3 +261,19 @@ func (c *Client) validateOSPlatform(ctx context.Context, os string, publish bool return nil } + +// daemonTarget returns a target that matches with the given daemon os/arch +func (c *Client) daemonTarget(ctx context.Context, targets []dist.Target) (dist.Target, error) { + info, err := c.docker.ServerVersion(ctx) + if err != nil { + return dist.Target{}, err + } + for _, t := range targets { + if t.Arch != "" && t.OS == info.Os && t.Arch == info.Arch { + return t, nil + } else if t.OS == info.Os { + return t, nil + } + } + return dist.Target{}, errors.Errorf("could not find a target that matches daemon os=%s and architecture=%s", info.Os, info.Arch) +} diff --git a/pkg/client/package_buildpack_test.go b/pkg/client/package_buildpack_test.go index 1810eb418a..7d92ef5890 100644 --- a/pkg/client/package_buildpack_test.go +++ b/pkg/client/package_buildpack_test.go @@ -13,6 +13,7 @@ import ( "github.com/buildpacks/lifecycle/api" "github.com/docker/docker/api/types/system" "github.com/golang/mock/gomock" + "github.com/google/go-containerregistry/pkg/name" "github.com/heroku/color" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -47,6 +48,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { mockImageFactory *testmocks.MockImageFactory mockImageFetcher *testmocks.MockImageFetcher mockDockerClient *testmocks.MockCommonAPIClient + mockIndexFactory *testmocks.MockIndexFactory out bytes.Buffer ) @@ -56,6 +58,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { mockImageFactory = testmocks.NewMockImageFactory(mockController) mockImageFetcher = testmocks.NewMockImageFetcher(mockController) mockDockerClient = testmocks.NewMockCommonAPIClient(mockController) + mockIndexFactory = testmocks.NewMockIndexFactory(mockController) var err error subject, err = client.NewClient( @@ -64,6 +67,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { client.WithImageFactory(mockImageFactory), client.WithFetcher(mockImageFetcher), client.WithDockerClient(mockDockerClient), + client.WithIndexFactory(mockIndexFactory), ) h.AssertNil(t, err) }) @@ -182,7 +186,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) fakeImage := fakes.NewImage("basic/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(fakeImage.Name(), true, daemonOS).Return(fakeImage, nil) + mockImageFactory.EXPECT().NewImage(fakeImage.Name(), true, dist.Target{OS: daemonOS}).Return(fakeImage, nil) fakeBlob := blob.NewBlob(filepath.Join("testdata", "empty-file")) bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12)) @@ -250,7 +254,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { it.Before(func() { nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, "linux").Return(nestedPackage, nil) + mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, dist.Target{OS: "linux"}).Return(nestedPackage, nil) mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes() @@ -270,22 +274,22 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { }) shouldFetchNestedPackage := func(demon bool, pull image.PullPolicy) { - mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), image.FetchOptions{Daemon: demon, PullPolicy: pull}).Return(nestedPackage, nil) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), image.FetchOptions{Daemon: demon, PullPolicy: pull, Target: &dist.Target{OS: "linux"}}).Return(nestedPackage, nil) } shouldNotFindNestedPackageWhenCallingImageFetcherWith := func(demon bool, pull image.PullPolicy) { - mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), image.FetchOptions{Daemon: demon, PullPolicy: pull}).Return(nil, image.ErrNotFound) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), image.FetchOptions{Daemon: demon, PullPolicy: pull, Target: &dist.Target{OS: "linux"}}).Return(nil, image.ErrNotFound) } shouldCreateLocalPackage := func() imgutil.Image { img := fakes.NewImage("some/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(img.Name(), true, "linux").Return(img, nil) + mockImageFactory.EXPECT().NewImage(img.Name(), true, dist.Target{OS: "linux"}).Return(img, nil) return img } shouldCreateRemotePackage := func() *fakes.Image { img := fakes.NewImage("some/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(img.Name(), false, "linux").Return(img, nil) + mockImageFactory.EXPECT().NewImage(img.Name(), false, dist.Target{OS: "linux"}).Return(img, nil) return img } @@ -395,7 +399,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { when("nested package is not a valid package", func() { it("should error", func() { notPackageImage := fakes.NewImage("not/package", "", nil) - mockImageFetcher.EXPECT().Fetch(gomock.Any(), notPackageImage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(notPackageImage, nil) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), notPackageImage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Target: &dist.Target{OS: "linux"}}).Return(notPackageImage, nil) mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes() @@ -457,7 +461,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { name := "basic/package-" + h.RandString(12) fakeImage := fakes.NewImage(name, "", nil) fakeLayerImage = &h.FakeAddedLayerImage{Image: fakeImage} - mockImageFactory.EXPECT().NewImage(fakeLayerImage.Name(), true, "linux").Return(fakeLayerImage, nil) + mockImageFactory.EXPECT().NewImage(fakeLayerImage.Name(), true, dist.Target{OS: "linux"}).Return(fakeLayerImage, nil) mockImageFetcher.EXPECT().Fetch(gomock.Any(), name, gomock.Any()).Return(fakeLayerImage, nil).AnyTimes() blob1 := blob.NewBlob(filepath.Join("testdata", "buildpack-flatten", "buildpack-1")) @@ -513,6 +517,313 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { // TODO add test case for flatten all with --flatten-exclude }) }) + + when("multi-platform", func() { + var ( + index *h.MockImageIndex + indexLocalPath string + targets []dist.Target + bpPathURI string + repoName string + tmpDir string + err error + ) + + it.Before(func() { + tmpDir, err = os.MkdirTemp("", "package-buildpack-multi-platform") + h.AssertNil(t, err) + h.AssertNil(t, os.Setenv("XDG_RUNTIME_DIR", tmpDir)) + + repoName = "basic/multi-platform-package-" + h.RandString(12) + indexLocalPath = filepath.Join(tmpDir, imgutil.MakeFileSafeName(repoName)) + }) + + it.After(func() { + os.Remove(tmpDir) + }) + + when("simple buildpack", func() { + it.Before(func() { + // index stub returned to check if push operation was called + index = h.NewMockImageIndex(t, repoName, 0, 0) + + // We need to mock the index factory to inject a stub index to be pushed. + mockIndexFactory.EXPECT().Exists(gomock.Eq(repoName)).Return(false) + mockIndexFactory.EXPECT().CreateIndex(gomock.Eq(repoName), gomock.Any()).Return(index, nil) + }) + + when("folder structure doesn't follow multi-platform convention", func() { + it.Before(func() { + destBpPath := filepath.Join("testdata", "buildpack-multi-platform", "buildpack-old-format") + bpPathURI, err = paths.FilePathToURI(destBpPath, "") + + prepareDownloadedBuildpackBlobAtURI(t, mockDownloader, destBpPath) + prepareExpectedMultiPlaformImages(t, mockImageFactory, mockImageFetcher, repoName, dist.Target{OS: "linux", Arch: "amd64"}, + expectedMultiPlatformImage{digest: newDigest(t, repoName, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34")}) + prepareExpectedMultiPlaformImages(t, mockImageFactory, mockImageFetcher, repoName, dist.Target{OS: "linux", Arch: "arm"}, + expectedMultiPlatformImage{digest: newDigest(t, repoName, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda35")}) + }) + + it("creates a multi-platform buildpack and pushes it to a registry", func() { + // Define targets we want to package + targets = []dist.Target{{OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm"}} + + h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ + Format: client.FormatImage, + Publish: true, + RelativeBaseDir: "", + Name: repoName, + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: bpPathURI}, + Targets: []dist.Target{}, + }, + Targets: targets, + PullPolicy: image.PullNever, + })) + + // index is not saved locally + h.AssertPathDoesNotExists(t, indexLocalPath) + + // Push operation was done + h.AssertTrue(t, index.PushCalled) + h.AssertTrue(t, index.PurgeOption) + + // index has the two expected manifests amd64 and arm + indexManifest, err := index.IndexManifest() + h.AssertNil(t, err) + h.AssertEq(t, len(indexManifest.Manifests), 2) + }) + }) + + when("folder structure follows multi-platform convention", func() { + when("os/arch is used", func() { + it.Before(func() { + destBpPath := filepath.Join("testdata", "buildpack-multi-platform", "buildpack-new-format") + + bpPathURI, err = paths.FilePathToURI(destBpPath, "") + h.AssertNil(t, err) + + prepareDownloadedBuildpackBlobAtURI(t, mockDownloader, filepath.Join(destBpPath, "linux", "amd64")) + prepareDownloadedBuildpackBlobAtURI(t, mockDownloader, filepath.Join(destBpPath, "linux", "arm")) + + prepareExpectedMultiPlaformImages(t, mockImageFactory, mockImageFetcher, repoName, dist.Target{OS: "linux", Arch: "amd64"}, + expectedMultiPlatformImage{digest: newDigest(t, repoName, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34")}) + + prepareExpectedMultiPlaformImages(t, mockImageFactory, mockImageFetcher, repoName, dist.Target{OS: "linux", Arch: "arm"}, + expectedMultiPlatformImage{digest: newDigest(t, repoName, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda35")}) + }) + + it("creates a multi-platform buildpack and pushes it to a registry", func() { + // Define targets we want to package + targets = []dist.Target{{OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm"}} + + h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ + Format: client.FormatImage, + Publish: true, + RelativeBaseDir: "", + Name: repoName, + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: bpPathURI}, + Targets: []dist.Target{}, + }, + Targets: targets, + PullPolicy: image.PullNever, + })) + + // index is not saved locally + h.AssertPathDoesNotExists(t, indexLocalPath) + + // Push operation was done + h.AssertTrue(t, index.PushCalled) + h.AssertTrue(t, index.PurgeOption) + + // index has the two expected manifests amd64 and arm + indexManifest, err := index.IndexManifest() + h.AssertNil(t, err) + h.AssertEq(t, len(indexManifest.Manifests), 2) + }) + }) + + when("os/arch/variant/name@version is used", func() { + it.Before(func() { + destBpPath := filepath.Join("testdata", "buildpack-multi-platform", "buildpack-new-format-with-versions") + + bpPathURI, err = paths.FilePathToURI(destBpPath, "") + h.AssertNil(t, err) + + prepareDownloadedBuildpackBlobAtURI(t, mockDownloader, filepath.Join(destBpPath, "linux", "amd64", "v5", "ubuntu@18.01")) + prepareDownloadedBuildpackBlobAtURI(t, mockDownloader, filepath.Join(destBpPath, "linux", "amd64", "v5", "ubuntu@21.01")) + prepareDownloadedBuildpackBlobAtURI(t, mockDownloader, filepath.Join(destBpPath, "linux", "arm", "v6", "ubuntu@18.01")) + prepareDownloadedBuildpackBlobAtURI(t, mockDownloader, filepath.Join(destBpPath, "linux", "arm", "v6", "ubuntu@21.01")) + + prepareExpectedMultiPlaformImages(t, mockImageFactory, mockImageFetcher, repoName, dist.Target{OS: "linux", Arch: "amd64", ArchVariant: "v5", Distributions: []dist.Distribution{ + {Name: "ubuntu", Version: "21.01"}}}, expectedMultiPlatformImage{digest: newDigest(t, repoName, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34")}) + + prepareExpectedMultiPlaformImages(t, mockImageFactory, mockImageFetcher, repoName, dist.Target{OS: "linux", Arch: "amd64", ArchVariant: "v5", Distributions: []dist.Distribution{ + {Name: "ubuntu", Version: "18.01"}}}, expectedMultiPlatformImage{digest: newDigest(t, repoName, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda35")}) + + prepareExpectedMultiPlaformImages(t, mockImageFactory, mockImageFetcher, repoName, dist.Target{OS: "linux", Arch: "arm", ArchVariant: "v6", Distributions: []dist.Distribution{ + {Name: "ubuntu", Version: "18.01"}}}, expectedMultiPlatformImage{digest: newDigest(t, repoName, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda36")}) + + prepareExpectedMultiPlaformImages(t, mockImageFactory, mockImageFetcher, repoName, dist.Target{OS: "linux", Arch: "arm", ArchVariant: "v6", Distributions: []dist.Distribution{ + {Name: "ubuntu", Version: "21.01"}}}, expectedMultiPlatformImage{digest: newDigest(t, repoName, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda36")}) + }) + + it("creates a multi-platform buildpack and pushes it to a registry", func() { + // Define targets we want to package + targets = []dist.Target{{OS: "linux", Arch: "amd64", ArchVariant: "v5", + Distributions: []dist.Distribution{{Name: "ubuntu", Version: "18.01"}, {Name: "ubuntu", Version: "21.01"}}}, + {OS: "linux", Arch: "arm", ArchVariant: "v6", Distributions: []dist.Distribution{{Name: "ubuntu", Version: "18.01"}, {Name: "ubuntu", Version: "21.01"}}}} + + h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ + Format: client.FormatImage, + Publish: true, + RelativeBaseDir: "", + Name: repoName, + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: bpPathURI}, + Targets: []dist.Target{}, + }, + Targets: targets, + PullPolicy: image.PullNever, + })) + + // index is not saved locally + h.AssertPathDoesNotExists(t, indexLocalPath) + + // Push operation was done + h.AssertTrue(t, index.PushCalled) + h.AssertTrue(t, index.PurgeOption) + + // index has the four expected manifests two for each architecture + indexManifest, err := index.IndexManifest() + h.AssertNil(t, err) + h.AssertEq(t, len(indexManifest.Manifests), 4) + }) + }) + }) + }) + + when("composite buildpack", func() { + var ( + target1 dist.Target + bp1URI string + target2 dist.Target + bp2URI string + ) + + it.Before(func() { + bp1URI = "localhost:3333/bp-1" + target1 = dist.Target{OS: "linux", Arch: "amd64"} + + bp2URI = "localhost:3333/bp-2" + target2 = dist.Target{OS: "linux", Arch: "arm"} + }) + + when("dependencies are saved on a registry", func() { + it.Before(func() { + // Check testdata/buildpack-multi-platform/buildpack-composite for configuration details + destBpPath := filepath.Join("testdata", "buildpack-multi-platform", "buildpack-composite") + + bpPathURI, err = paths.FilePathToURI(destBpPath, "") + h.AssertNil(t, err) + + prepareDownloadedBuildpackBlobAtURI(t, mockDownloader, destBpPath) + + indexAMD64Digest := newDigest(t, repoName, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda40") + prepareRemoteMultiPlatformBuildpackPackage(t, mockImageFactory, mockImageFetcher, repoName, indexAMD64Digest, target1, []expectedMultiPlatformImage{ + {digest: newDigest(t, bp1URI, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34"), id: "samples/bp-1", version: "0.0.1", bpURI: bp1URI}, + {digest: newDigest(t, bp2URI, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda35"), id: "samples/bp-2", version: "0.0.1", bpURI: bp2URI}, + }) + + indexARMDigest := newDigest(t, repoName, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda41") + prepareRemoteMultiPlatformBuildpackPackage(t, mockImageFactory, mockImageFetcher, repoName, indexARMDigest, target2, []expectedMultiPlatformImage{ + {digest: newDigest(t, bp1URI, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda36"), id: "samples/bp-1", version: "0.0.1", bpURI: bp1URI}, + {digest: newDigest(t, bp2URI, "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda37"), id: "samples/bp-2", version: "0.0.1", bpURI: bp2URI}, + }) + + // Define expected targets to package + targets = []dist.Target{target1, target2} + + // index stub returned to check if push operation was called + index = h.NewMockImageIndex(t, repoName, 0, 0) + + // We need to mock the index factory to inject a stub index to be pushed. + mockIndexFactory.EXPECT().Exists(gomock.Eq(repoName)).Return(false) + mockIndexFactory.EXPECT().CreateIndex(gomock.Eq(repoName), gomock.Any()).Return(index, nil) + }) + + it("creates a multi-platform buildpack and pushes it to a registry", func() { + h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ + Format: client.FormatImage, + Publish: true, + RelativeBaseDir: "", + Name: repoName, + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: bpPathURI}, + Dependencies: []dist.ImageOrURI{ + {BuildpackURI: dist.BuildpackURI{URI: bp1URI}}, + {BuildpackURI: dist.BuildpackURI{URI: bp2URI}}, + }, + Targets: []dist.Target{}, + }, + Targets: targets, + })) + + // index is not saved locally + h.AssertPathDoesNotExists(t, indexLocalPath) + + // Push operation was done + h.AssertTrue(t, index.PushCalled) + h.AssertTrue(t, index.PurgeOption) + + // index has the two expected manifests amd64 and arm + indexManifest, err := index.IndexManifest() + h.AssertNil(t, err) + h.AssertEq(t, len(indexManifest.Manifests), 2) + }) + }) + + when("dependencies are on disk", func() { + it.Before(func() { + // Check testdata/buildpack-multi-platform/buildpack-composite for configuration details + destBpPath := filepath.Join("testdata", "buildpack-multi-platform", "buildpack-composite-with-dependencies-on-disk") + + bpPathURI, err = paths.FilePathToURI(destBpPath, "") + h.AssertNil(t, err) + + prepareDownloadedBuildpackBlobAtURI(t, mockDownloader, destBpPath) + + bp1URI = filepath.Join("testdata", "buildpack-multi-platform", "buildpack-new-format") + + // Define expected targets to package + targets = []dist.Target{target1, target2} + }) + + it("errors with a message", func() { + // If dependencies point to a file or a URL like https://example.com/buildpack.tgz + // we will need to define some conventions to fetch by target + // The OCI registry already solved the problem, that's why we do not allow this path for now + err = subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ + Format: client.FormatImage, + Publish: true, + RelativeBaseDir: "", + Name: repoName, + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: bpPathURI}, + Dependencies: []dist.ImageOrURI{ + {BuildpackURI: dist.BuildpackURI{URI: bp1URI}}, + }, + Targets: []dist.Target{}, + }, + Targets: targets, + }) + h.AssertNotNil(t, err) + h.AssertError(t, err, "is not allowed when creating a composite multi-platform buildpack; push your dependencies to a registry and use 'docker://' instead") + }) + }) + }) + }) }) when("FormatFile", func() { @@ -594,7 +905,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { when("dependencies are packaged buildpack image", func() { it.Before(func() { nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, "linux").Return(nestedPackage, nil) + mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, dist.Target{OS: "linux"}).Return(nestedPackage, nil) h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ Name: nestedPackage.Name(), @@ -606,7 +917,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { PullPolicy: image.PullAlways, })) - mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(nestedPackage, nil) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Target: &dist.Target{OS: "linux"}}).Return(nestedPackage, nil) }) it("should pull and use local nested package image", func() { @@ -709,7 +1020,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { }}}) nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, "linux").Return(nestedPackage, nil) + mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, dist.Target{OS: "linux"}).Return(nestedPackage, nil) h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ Name: nestedPackage.Name(), @@ -721,7 +1032,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { PullPolicy: image.PullAlways, })) - mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(nestedPackage, nil) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Target: &dist.Target{OS: "linux"}}).Return(nestedPackage, nil) }) it("should include both of them", func() { @@ -829,7 +1140,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) err = packageImage.SetLabel("io.buildpacks.buildpack.layers", `{"example/foo":{"1.1.0":{"api": "0.2", "layerDiffID":"sha256:xxx", "stacks":[{"id":"some.stack.id"}]}}}`) h.AssertNil(t, err) - mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(packageImage, nil) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Target: &dist.Target{OS: "linux"}}).Return(packageImage, nil) packHome := filepath.Join(tmpDir, "packHome") h.AssertNil(t, os.Setenv("PACK_HOME", packHome)) @@ -902,3 +1213,62 @@ func assertPackageBPFileHasBuildpacks(t *testing.T, path string, descriptors []d h.AssertNil(t, err) h.AssertBuildpacksHaveDescriptors(t, append([]buildpack.BuildModule{mainBP}, depBPs...), descriptors) } + +func prepareDownloadedBuildpackBlobAtURI(t *testing.T, mockDownloader *testmocks.MockBlobDownloader, path string) { + blob := blob.NewBlob(path) + uri, err := paths.FilePathToURI(path, "") + h.AssertNil(t, err) + mockDownloader.EXPECT().Download(gomock.Any(), uri).Return(blob, nil).AnyTimes() +} + +// prepareExpectedMultiPlaformImages creates a fake CNBImage that will be fetched from a registry +func prepareExpectedMultiPlaformImages(t *testing.T, mockImageFactory *testmocks.MockImageFactory, mockImageFetcher *testmocks.MockImageFetcher, repoName string, target dist.Target, expected expectedMultiPlatformImage) { + fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, repoName, expected.digest) + mockImageFactory.EXPECT().NewImage(repoName, false, gomock.Eq(target)).Return(fakeImage, nil) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), expected.digest.Name(), gomock.Any()).Return(fakeImage, nil) +} + +// prepareRemoteMultiPlatformBuildpackPackage creates remotes buildpack packages required to create a composite buildapck +// repoName: image index reference name +// digest: manifest digest for the given target +// target: os/arch for the given manifest +func prepareRemoteMultiPlatformBuildpackPackage(t *testing.T, mockImageFactory *testmocks.MockImageFactory, mockImageFetcher *testmocks.MockImageFetcher, repoName string, digest name.Digest, target dist.Target, expected []expectedMultiPlatformImage) { + // crates each remote buildpack package for the given target + for _, v := range expected { + // it must already exist in a registry, pack will pull it from a registry and write its content on disk to create a .tar + fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, v.bpURI, v.digest) + // Each buildpack package is expected to have some labels + h.AssertNil(t, fakeImage.SetLabel("io.buildpacks.buildpackage.metadata", fmt.Sprintf(`{"id":"%s","version":"%s","stacks":[{"id":"*"}]}`, v.id, v.version))) + layers, err := fakeImage.UnderlyingImage().Layers() + h.AssertNil(t, err) + diffID, err := layers[0].DiffID() + h.AssertNil(t, err) + h.AssertNil(t, fakeImage.SetLabel("io.buildpacks.buildpack.layers", fmt.Sprintf(`{"%s":{"%s":{"api":"0.10","stacks":[{"id":"*"}],"layerDiffID":"%s"}}}`, v.id, v.version, diffID))) + + // pack will fetch the buildpack package from the registry by target + mockImageFetcher.EXPECT().Fetch(gomock.Any(), v.bpURI, gomock.Eq(image.FetchOptions{Daemon: false, Target: &target})).Return(fakeImage, nil) + } + + // Once all the buildpacks were written to disk as .tar giles + // pack will create a new OCI image adding all the .tar files as layers + compositeBuildpackImage := h.NewFakeWithRandomUnderlyingV1Image(t, repoName, digest) + mockImageFactory.EXPECT().NewImage(repoName, false, gomock.Eq(target)).Return(compositeBuildpackImage, nil) + + // Once the composite buildpack image was pushed to the registry, pack will create an Image Index adding + // each manifest by digest + mockImageFetcher.EXPECT().Fetch(gomock.Any(), digest.Name(), gomock.Any()).Return(compositeBuildpackImage, nil) +} + +func newDigest(t *testing.T, repoName, sha string) name.Digest { + digest, err := name.NewDigest(fmt.Sprintf("%s@%s", repoName, sha)) + h.AssertNil(t, err) + return digest +} + +// expectedMultiPlatformImage is a helper struct with the data needed to prepare a mock remote buildpack package +type expectedMultiPlatformImage struct { + id string + version string + bpURI string + digest name.Digest +} diff --git a/pkg/client/package_extension.go b/pkg/client/package_extension.go index 690d12afba..85a8c4aa07 100644 --- a/pkg/client/package_extension.go +++ b/pkg/client/package_extension.go @@ -8,6 +8,7 @@ import ( "github.com/buildpacks/pack/internal/layer" "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/pkg/buildpack" + "github.com/buildpacks/pack/pkg/dist" ) // PackageExtension packages extension(s) into either an image or file. @@ -49,11 +50,12 @@ func (c *Client) PackageExtension(ctx context.Context, opts PackageBuildpackOpti packageBuilder.SetExtension(ex) + target := dist.Target{OS: opts.Config.Platform.OS} switch opts.Format { case FormatFile: - return packageBuilder.SaveAsFile(opts.Name, opts.Config.Platform.OS, map[string]string{}) + return packageBuilder.SaveAsFile(opts.Name, target, map[string]string{}) case FormatImage: - _, err = packageBuilder.SaveAsImage(opts.Name, opts.Publish, opts.Config.Platform.OS, map[string]string{}) + _, err = packageBuilder.SaveAsImage(opts.Name, opts.Publish, target, map[string]string{}) return errors.Wrapf(err, "saving image") default: return errors.Errorf("unknown format: %s", style.Symbol(opts.Format)) diff --git a/pkg/client/package_extension_test.go b/pkg/client/package_extension_test.go index bb24d95736..31368b10cf 100644 --- a/pkg/client/package_extension_test.go +++ b/pkg/client/package_extension_test.go @@ -141,7 +141,7 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) fakeImage := fakes.NewImage("basic/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(fakeImage.Name(), true, daemonOS).Return(fakeImage, nil) + mockImageFactory.EXPECT().NewImage(fakeImage.Name(), true, dist.Target{OS: daemonOS}).Return(fakeImage, nil) fakeBlob := blob.NewBlob(filepath.Join("testdata", "empty-file")) exURL := fmt.Sprintf("https://example.com/ex.%s.tgz", h.RandString(12)) diff --git a/pkg/client/rebase.go b/pkg/client/rebase.go index 3d7e6532da..797c6e2ff1 100644 --- a/pkg/client/rebase.go +++ b/pkg/client/rebase.go @@ -2,7 +2,6 @@ package client import ( "context" - "fmt" "os" "path/filepath" @@ -99,10 +98,11 @@ func (c *Client) Rebase(ctx context.Context, opts RebaseOptions) error { } } + target := &dist.Target{OS: appOS, Arch: appArch} fetchOptions := image.FetchOptions{ Daemon: !opts.Publish, PullPolicy: opts.PullPolicy, - Platform: fmt.Sprintf("%s/%s", appOS, appArch), + Target: target, } runImageName := c.resolveRunImage( diff --git a/pkg/client/rebase_test.go b/pkg/client/rebase_test.go index 7ceff08df2..601999607d 100644 --- a/pkg/client/rebase_test.go +++ b/pkg/client/rebase_test.go @@ -264,7 +264,7 @@ func testRebase(t *testing.T, when spec.G, it spec.S) { 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") + h.AssertEq(t, args.Target.ValuesAsPlatform(), "linux/amd64") }) }) }) diff --git a/pkg/client/testdata/buildpack-multi-platform/README.md b/pkg/client/testdata/buildpack-multi-platform/README.md new file mode 100644 index 0000000000..3f7f242b8f --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/README.md @@ -0,0 +1,6 @@ +When creating multi-platform buildpacks, the root buildpack.toml file must be copied into each +plaform root folder; this operation must be done by the caller of the method: + +`PackageBuildpack(ctx context.Context, opts PackageBuildpackOptions) error` + +To simplify the tests, the buildpack.toml is already copied in each buildpack folder. diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-composite-with-dependencies-on-disk/buildpack.toml b/pkg/client/testdata/buildpack-multi-platform/buildpack-composite-with-dependencies-on-disk/buildpack.toml new file mode 100644 index 0000000000..3dfd7a5909 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-composite-with-dependencies-on-disk/buildpack.toml @@ -0,0 +1,12 @@ +api = "0.10" + +[buildpack] +id = "samples/composite-buildpack" +version = "0.0.1" + +# Order used for detection +[[order]] +[[order.group]] +id = "samples/bp-1" +version = "0.0.1" + diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-composite-with-dependencies-on-disk/package.toml b/pkg/client/testdata/buildpack-multi-platform/buildpack-composite-with-dependencies-on-disk/package.toml new file mode 100644 index 0000000000..d174b516fd --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-composite-with-dependencies-on-disk/package.toml @@ -0,0 +1,14 @@ +[buildpack] +uri = "." + +[[targets]] +os = "linux" +arch = "amd64" + +[[targets]] +os = "linux" +arch = "arm64" + +[[dependencies]] +uri = "../samples/bp-1" + diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-composite/buildpack.toml b/pkg/client/testdata/buildpack-multi-platform/buildpack-composite/buildpack.toml new file mode 100644 index 0000000000..236a11aaf6 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-composite/buildpack.toml @@ -0,0 +1,15 @@ +api = "0.10" + +[buildpack] +id = "samples/composite-buildpack" +version = "0.0.1" + +# Order used for detection +[[order]] +[[order.group]] +id = "samples/bp-1" +version = "0.0.1" + +[[order.group]] +id = "samples/bp-2" +version = "0.0.1" diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-composite/package.toml b/pkg/client/testdata/buildpack-multi-platform/buildpack-composite/package.toml new file mode 100644 index 0000000000..67bb4ee9a8 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-composite/package.toml @@ -0,0 +1,16 @@ +[buildpack] +uri = "." + +[[targets]] +os = "linux" +arch = "amd64" + +[[targets]] +os = "linux" +arch = "arm64" + +[[dependencies]] +uri = "localhost:3333/bp-1" + +[[dependencies]] +uri = "localhost:3333/bp-2" diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@18.01/bin/build b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@18.01/bin/build new file mode 100644 index 0000000000..fb0d852a61 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@18.01/bin/build @@ -0,0 +1 @@ +build-amd64-contents diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@18.01/bin/detect b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@18.01/bin/detect new file mode 100644 index 0000000000..27f6569700 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@18.01/bin/detect @@ -0,0 +1 @@ +detect-amd64-contents diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@18.01/buildpack.toml b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@18.01/buildpack.toml new file mode 100644 index 0000000000..eda6e1e65e --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@18.01/buildpack.toml @@ -0,0 +1,23 @@ +api = "0.10" + +[buildpack] +id = "samples/multi-platform" +version = "0.0.1" + +[[targets]] +os = "linux" +arch = "amd64" +[[targets.distributions]] +name = "ubuntu" +versions = ["18.01", "21.01"] + +[[targets]] +os = "linux" +arch = "arm64" +variant = "v6" +[[targets.distributions]] +name = "ubuntu" +versions = ["18.01", "21.01"] + +[[stacks]] +id = "*" diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@21.01/bin/build b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@21.01/bin/build new file mode 100644 index 0000000000..fb0d852a61 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@21.01/bin/build @@ -0,0 +1 @@ +build-amd64-contents diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@21.01/bin/detect b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@21.01/bin/detect new file mode 100644 index 0000000000..27f6569700 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@21.01/bin/detect @@ -0,0 +1 @@ +detect-amd64-contents diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@21.01/buildpack.toml b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@21.01/buildpack.toml new file mode 100644 index 0000000000..802d22f733 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/amd64/v5/ubuntu@21.01/buildpack.toml @@ -0,0 +1,16 @@ +api = "0.10" + +[buildpack] +id = "samples/multi-platform" +version = "0.0.1" + +[[targets]] +os = "linux" +arch = "amd64" + +[[targets]] +os = "linux" +arch = "arm64" + +[[stacks]] +id = "*" diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@18.01/bin/build b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@18.01/bin/build new file mode 100644 index 0000000000..d8744bb41c --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@18.01/bin/build @@ -0,0 +1 @@ +build-arm-contents diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@18.01/bin/detect b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@18.01/bin/detect new file mode 100644 index 0000000000..3788406fd2 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@18.01/bin/detect @@ -0,0 +1 @@ +detect-arm-contents diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@18.01/buildpack.toml b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@18.01/buildpack.toml new file mode 100644 index 0000000000..802d22f733 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@18.01/buildpack.toml @@ -0,0 +1,16 @@ +api = "0.10" + +[buildpack] +id = "samples/multi-platform" +version = "0.0.1" + +[[targets]] +os = "linux" +arch = "amd64" + +[[targets]] +os = "linux" +arch = "arm64" + +[[stacks]] +id = "*" diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@21.01/bin/build b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@21.01/bin/build new file mode 100644 index 0000000000..d8744bb41c --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@21.01/bin/build @@ -0,0 +1 @@ +build-arm-contents diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@21.01/bin/detect b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@21.01/bin/detect new file mode 100644 index 0000000000..3788406fd2 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@21.01/bin/detect @@ -0,0 +1 @@ +detect-arm-contents diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@21.01/buildpack.toml b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@21.01/buildpack.toml new file mode 100644 index 0000000000..802d22f733 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format-with-versions/linux/arm/v6/ubuntu@21.01/buildpack.toml @@ -0,0 +1,16 @@ +api = "0.10" + +[buildpack] +id = "samples/multi-platform" +version = "0.0.1" + +[[targets]] +os = "linux" +arch = "amd64" + +[[targets]] +os = "linux" +arch = "arm64" + +[[stacks]] +id = "*" diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/amd64/bin/build b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/amd64/bin/build new file mode 100644 index 0000000000..fb0d852a61 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/amd64/bin/build @@ -0,0 +1 @@ +build-amd64-contents diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/amd64/bin/detect b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/amd64/bin/detect new file mode 100644 index 0000000000..27f6569700 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/amd64/bin/detect @@ -0,0 +1 @@ +detect-amd64-contents diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/amd64/buildpack.toml b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/amd64/buildpack.toml new file mode 100644 index 0000000000..802d22f733 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/amd64/buildpack.toml @@ -0,0 +1,16 @@ +api = "0.10" + +[buildpack] +id = "samples/multi-platform" +version = "0.0.1" + +[[targets]] +os = "linux" +arch = "amd64" + +[[targets]] +os = "linux" +arch = "arm64" + +[[stacks]] +id = "*" diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/arm/bin/build b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/arm/bin/build new file mode 100644 index 0000000000..d8744bb41c --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/arm/bin/build @@ -0,0 +1 @@ +build-arm-contents diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/arm/bin/detect b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/arm/bin/detect new file mode 100644 index 0000000000..3788406fd2 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/arm/bin/detect @@ -0,0 +1 @@ +detect-arm-contents diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/arm/buildpack.toml b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/arm/buildpack.toml new file mode 100644 index 0000000000..802d22f733 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-new-format/linux/arm/buildpack.toml @@ -0,0 +1,16 @@ +api = "0.10" + +[buildpack] +id = "samples/multi-platform" +version = "0.0.1" + +[[targets]] +os = "linux" +arch = "amd64" + +[[targets]] +os = "linux" +arch = "arm64" + +[[stacks]] +id = "*" diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-old-format/bin/build b/pkg/client/testdata/buildpack-multi-platform/buildpack-old-format/bin/build new file mode 100644 index 0000000000..c76df1a291 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-old-format/bin/build @@ -0,0 +1 @@ +build-contents \ No newline at end of file diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-old-format/bin/detect b/pkg/client/testdata/buildpack-multi-platform/buildpack-old-format/bin/detect new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pkg/client/testdata/buildpack-multi-platform/buildpack-old-format/buildpack.toml b/pkg/client/testdata/buildpack-multi-platform/buildpack-old-format/buildpack.toml new file mode 100644 index 0000000000..131cb045f6 --- /dev/null +++ b/pkg/client/testdata/buildpack-multi-platform/buildpack-old-format/buildpack.toml @@ -0,0 +1,10 @@ +api = "0.3" + +[buildpack] +id = "bp.one" +version = "1.2.3" +homepage = "http://one.buildpack" + +[[stacks]] +id = "some.stack.id" +mixins = ["mixinX", "build:mixinY", "run:mixinZ"] diff --git a/pkg/dist/buildmodule.go b/pkg/dist/buildmodule.go index ea34cb68ca..01bd8e7957 100644 --- a/pkg/dist/buildmodule.go +++ b/pkg/dist/buildmodule.go @@ -1,6 +1,9 @@ package dist import ( + "fmt" + "strings" + "github.com/pkg/errors" "github.com/buildpacks/pack/internal/style" @@ -59,6 +62,66 @@ type Target struct { Distributions []Distribution `json:"distros,omitempty" toml:"distros,omitempty"` } +// ValuesAsSlice converts the internal representation of a target (os, arch, variant, etc.) into a string slice, +// where each value included in the final array must be not empty. +func (t *Target) ValuesAsSlice() []string { + var targets []string + if t.OS != "" { + targets = append(targets, t.OS) + } + if t.Arch != "" { + targets = append(targets, t.Arch) + } + if t.ArchVariant != "" { + targets = append(targets, t.ArchVariant) + } + + for _, d := range t.Distributions { + targets = append(targets, fmt.Sprintf("%s@%s", d.Name, d.Version)) + } + return targets +} + +func (t *Target) ValuesAsPlatform() string { + return strings.Join(t.ValuesAsSlice(), "/") +} + +// ExpandTargetsDistributions expands each provided target (with multiple distribution versions) to multiple targets (each with a single distribution version). +// For example, given an array with ONE target with the format: +// +// [ +// {OS:"linux", Distributions: []dist.Distribution{{Name: "ubuntu", Version: "18.01"},{Name: "ubuntu", Version: "21.01"}}} +// ] +// +// it returns an array with TWO targets each with the format: +// +// [ +// {OS:"linux",Distributions: []dist.Distribution{{Name: "ubuntu", Version: "18.01"}}}, +// {OS:"linux",Distributions: []dist.Distribution{{Name: "ubuntu", Version: "21.01"}}} +// ] +func ExpandTargetsDistributions(targets ...Target) []Target { + var expandedTargets []Target + for _, target := range targets { + expandedTargets = append(expandedTargets, expandTargetDistributions(target)...) + } + return expandedTargets +} + +func expandTargetDistributions(target Target) []Target { + var expandedTargets []Target + if (len(target.Distributions)) > 1 { + originalDistros := target.Distributions + for _, distro := range originalDistros { + copyTarget := target + copyTarget.Distributions = []Distribution{distro} + expandedTargets = append(expandedTargets, copyTarget) + } + } else { + expandedTargets = append(expandedTargets, target) + } + return expandedTargets +} + type Distribution struct { Name string `json:"name,omitempty" toml:"name,omitempty"` Version string `json:"version,omitempty" toml:"version,omitempty"` diff --git a/pkg/image/fetcher.go b/pkg/image/fetcher.go index 3d34d1d02e..5b61a3aba5 100644 --- a/pkg/image/fetcher.go +++ b/pkg/image/fetcher.go @@ -23,6 +23,7 @@ import ( pname "github.com/buildpacks/pack/internal/name" "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/internal/term" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/logging" ) @@ -62,7 +63,7 @@ type Fetcher struct { type FetchOptions struct { Daemon bool - Platform string + Target *dist.Target PullPolicy PullPolicy LayoutOption LayoutOption } @@ -94,7 +95,7 @@ func (f *Fetcher) Fetch(ctx context.Context, name string, options FetchOptions) } if !options.Daemon { - return f.fetchRemoteImage(name) + return f.fetchRemoteImage(name, options.Target) } switch options.PullPolicy { @@ -109,7 +110,13 @@ func (f *Fetcher) Fetch(ctx context.Context, name string, options FetchOptions) } f.logger.Debugf("Pulling image %s", style.Symbol(name)) - if err = f.pullImage(ctx, name, options.Platform); err != nil { + + platform := "" + if options.Target != nil { + platform = options.Target.ValuesAsPlatform() + } + + 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") { @@ -171,8 +178,19 @@ func (f *Fetcher) fetchDaemonImage(name string) (imgutil.Image, error) { return image, nil } -func (f *Fetcher) fetchRemoteImage(name string) (imgutil.Image, error) { - image, err := remote.NewImage(name, f.keychain, remote.FromBaseImage(name)) +func (f *Fetcher) fetchRemoteImage(name string, target *dist.Target) (imgutil.Image, error) { + var ( + image imgutil.Image + err error + ) + + if target == nil { + image, err = remote.NewImage(name, f.keychain, remote.FromBaseImage(name)) + } else { + platform := imgutil.Platform{OS: target.OS, Architecture: target.Arch, Variant: target.ArchVariant} + image, err = remote.NewImage(name, f.keychain, remote.FromBaseImage(name), remote.WithDefaultPlatform(platform)) + } + if err != nil { return nil, err } diff --git a/pkg/image/fetcher_test.go b/pkg/image/fetcher_test.go index b9491b24c2..85f5d6aa60 100644 --- a/pkg/image/fetcher_test.go +++ b/pkg/image/fetcher_test.go @@ -21,6 +21,7 @@ import ( "github.com/sclevine/spec" "github.com/sclevine/spec/report" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/image" "github.com/buildpacks/pack/pkg/logging" "github.com/buildpacks/pack/pkg/testmocks" @@ -71,16 +72,56 @@ func testFetcher(t *testing.T, when spec.G, it spec.S) { when("daemon is false", func() { when("PullAlways", func() { when("there is a remote image", func() { - it.Before(func() { - img, err := remote.NewImage(repoName, authn.DefaultKeychain) - h.AssertNil(t, err) + when("default platform", func() { + // default is linux/runtime.GOARCH + it.Before(func() { + img, err := remote.NewImage(repoName, authn.DefaultKeychain) + h.AssertNil(t, err) - h.AssertNil(t, img.Save()) + h.AssertNil(t, img.Save()) + }) + + it("returns the remote image", func() { + _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways}) + h.AssertNil(t, err) + }) }) - it("returns the remote image", func() { - _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways}) - h.AssertNil(t, err) + when("platform with variant and version", func() { + var target dist.Target + + // default is linux/runtime.GOARCH + it.Before(func() { + img, err := remote.NewImage(repoName, authn.DefaultKeychain, remote.WithDefaultPlatform(imgutil.Platform{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + Variant: "v1", + OSVersion: "my-version", + })) + h.AssertNil(t, err) + h.AssertNil(t, img.Save()) + }) + + it("returns the remote image", func() { + target = dist.Target{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + ArchVariant: "v1", + Distributions: []dist.Distribution{ + {Name: "some-name", Version: "my-version"}, + }, + } + + img, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways, Target: &target}) + h.AssertNil(t, err) + variant, err := img.Variant() + h.AssertNil(t, err) + h.AssertEq(t, variant, "v1") + + osVersion, err := img.OSVersion() + h.AssertNil(t, err) + h.AssertEq(t, osVersion, "my-version") + }) }) }) @@ -221,7 +262,7 @@ func testFetcher(t *testing.T, when spec.G, it spec.S) { when("image platform is specified", func() { it("passes the platform argument to the daemon", func() { - _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Platform: "some-unsupported-platform"}) + _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Target: &dist.Target{OS: "some-unsupported-platform"}}) h.AssertError(t, err, "unknown operating system or architecture") }) @@ -233,7 +274,7 @@ func testFetcher(t *testing.T, when spec.G, it spec.S) { }) it("retry without setting platform", func() { - _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Platform: fmt.Sprintf("%s/%s", osType, runtime.GOARCH)}) + _, 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) }) }) @@ -340,7 +381,7 @@ func testFetcher(t *testing.T, when spec.G, it spec.S) { when("image platform is specified", func() { it("passes the platform argument to the daemon", func() { - _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullIfNotPresent, Platform: "some-unsupported-platform"}) + _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullIfNotPresent, Target: &dist.Target{OS: "some-unsupported-platform"}}) h.AssertError(t, err, "unknown operating system or architecture") }) }) diff --git a/pkg/testmocks/mock_access_checker.go b/pkg/testmocks/mock_access_checker.go new file mode 100644 index 0000000000..558b85a580 --- /dev/null +++ b/pkg/testmocks/mock_access_checker.go @@ -0,0 +1,48 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/buildpacks/pack/pkg/client (interfaces: AccessChecker) + +// Package testmocks is a generated GoMock package. +package testmocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockAccessChecker is a mock of AccessChecker interface. +type MockAccessChecker struct { + ctrl *gomock.Controller + recorder *MockAccessCheckerMockRecorder +} + +// MockAccessCheckerMockRecorder is the mock recorder for MockAccessChecker. +type MockAccessCheckerMockRecorder struct { + mock *MockAccessChecker +} + +// NewMockAccessChecker creates a new mock instance. +func NewMockAccessChecker(ctrl *gomock.Controller) *MockAccessChecker { + mock := &MockAccessChecker{ctrl: ctrl} + mock.recorder = &MockAccessCheckerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAccessChecker) EXPECT() *MockAccessCheckerMockRecorder { + return m.recorder +} + +// Check mocks base method. +func (m *MockAccessChecker) Check(arg0 string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Check", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// Check indicates an expected call of Check. +func (mr *MockAccessCheckerMockRecorder) Check(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Check", reflect.TypeOf((*MockAccessChecker)(nil).Check), arg0) +} diff --git a/pkg/testmocks/mock_image_factory.go b/pkg/testmocks/mock_image_factory.go index 42e4ec6b42..3c25c82b8a 100644 --- a/pkg/testmocks/mock_image_factory.go +++ b/pkg/testmocks/mock_image_factory.go @@ -9,6 +9,8 @@ import ( imgutil "github.com/buildpacks/imgutil" gomock "github.com/golang/mock/gomock" + + dist "github.com/buildpacks/pack/pkg/dist" ) // MockImageFactory is a mock of ImageFactory interface. @@ -35,7 +37,7 @@ func (m *MockImageFactory) EXPECT() *MockImageFactoryMockRecorder { } // NewImage mocks base method. -func (m *MockImageFactory) NewImage(arg0 string, arg1 bool, arg2 string) (imgutil.Image, error) { +func (m *MockImageFactory) NewImage(arg0 string, arg1 bool, arg2 dist.Target) (imgutil.Image, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NewImage", arg0, arg1, arg2) ret0, _ := ret[0].(imgutil.Image) diff --git a/testhelpers/image_index.go b/testhelpers/image_index.go index 29b8d155e1..4b80421f22 100644 --- a/testhelpers/image_index.go +++ b/testhelpers/image_index.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "os" "path/filepath" @@ -189,3 +190,16 @@ type FakeWithRandomUnderlyingImage struct { func (t *FakeWithRandomUnderlyingImage) UnderlyingImage() v1.Image { return t.underlyingImage } + +func (t *FakeWithRandomUnderlyingImage) GetLayer(sha string) (io.ReadCloser, error) { + hash, err := v1.NewHash(sha) + if err != nil { + return nil, err + } + + layer, err := t.UnderlyingImage().LayerByDiffID(hash) + if err != nil { + return nil, err + } + return layer.Uncompressed() +}