diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index d16cffbfe0..2605007a1b 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -125,6 +125,7 @@ func testWithoutSpecificBuilderRequirement( it.Before(func() { pack = invoke.NewPackInvoker(t, assert, packConfig, registryConfig.DockerConfigDir) + pack.EnableExperimental() buildpackManager = buildpacks.NewBuildpackManager(t, assert) }) @@ -240,8 +241,6 @@ func testWithoutSpecificBuilderRequirement( "pack does not support 'package-buildpack'", ) - h.SkipIf(t, dockerHostOS() == "windows", "These tests are not yet compatible with Windows-based containers") - var err error tmpDir, err = ioutil.TempDir("", "package-buildpack-tests") assert.Nil(err) @@ -327,6 +326,8 @@ func testWithoutSpecificBuilderRequirement( when("--publish", func() { it("publishes image to registry", func() { + h.SkipIf(t, !pack.Supports("package-buildpack --os"), "os not supported") + nestedPackageName := registryConfig.RepoName("test/package-" + h.RandString(10)) nestedPackage := buildpacks.NewPackageImage( @@ -334,8 +335,9 @@ func testWithoutSpecificBuilderRequirement( pack, nestedPackageName, simplePackageConfigPath, - buildpacks.WithPublish(), buildpacks.WithRequiredBuildpacks(buildpacks.SimpleLayers), + buildpacks.WithPublish(), + buildpacks.WithOS(dockerHostOS()), ) buildpackManager.PrepareBuildpacks(tmpDir, nestedPackage) defer h.DockerRmi(dockerCli, nestedPackageName) @@ -346,6 +348,7 @@ func testWithoutSpecificBuilderRequirement( "package-buildpack", packageName, "-c", aggregatePackageToml, "--publish", + "--os", dockerHostOS(), ) defer h.DockerRmi(dockerCli, packageName) assertions.NewOutputAssertionManager(t, output).ReportsPackagePublished(packageName) @@ -537,8 +540,11 @@ func testWithoutSpecificBuilderRequirement( pack, packageFileLocation, pack.FixtureManager().FixtureLocation("package_for_build_cmd.toml"), - buildpacks.FolderSimpleLayersParent, - buildpacks.FolderSimpleLayers, + buildpacks.WithRequiredBuildpacks( + buildpacks.FolderSimpleLayersParent, + buildpacks.FolderSimpleLayers, + ), + buildpacks.WithOS(dockerHostOS()), ) buildpackManager.PrepareBuildpacks(tmpDir, packageFile) @@ -559,10 +565,6 @@ func testWithoutSpecificBuilderRequirement( }) when("buildpack image", func() { - it.Before(func() { - h.SkipIf(t, dockerHostOS() == "windows", "These tests are not yet compatible with Windows-based containers") - }) - when("inspect-buildpack", func() { it("succeeds", func() { packageImageName := registryConfig.RepoName("buildpack-" + h.RandString(8)) @@ -1244,12 +1246,8 @@ func testAcceptance( it.Before(func() { h.SkipUnless(t, - pack.Supports("package-buildpack"), - "--buildpack does not accept buildpackage unless package-buildpack is supported", - ) - h.SkipIf(t, - dockerHostOS() == "windows", - "These tests are not yet compatible with Windows-based containers", + pack.Supports("package-buildpack --os"), + "--buildpack does not accept buildpackage unless package-buildpack --os is supported", ) }) @@ -1297,11 +1295,10 @@ func testAcceptance( var tmpDir string it.Before(func() { - h.SkipIf(t, - !pack.Supports("package-buildpack --format"), - "--buildpack does not accept buildpackage file unless package-buildpack with --format is supported", + h.SkipUnless(t, + pack.Supports("package-buildpack --os"), + "--buildpack does not accept buildpackage unless package-buildpack --os is supported", ) - h.SkipIf(t, dockerHostOS() == "windows", "These tests are not yet compatible with Windows-based containers") var err error tmpDir, err = ioutil.TempDir("", "package-file") @@ -1323,8 +1320,11 @@ func testAcceptance( pack, packageFileLocation, pack.FixtureManager().FixtureLocation("package_for_build_cmd.toml"), - buildpacks.FolderSimpleLayersParent, - buildpacks.FolderSimpleLayers, + buildpacks.WithRequiredBuildpacks( + buildpacks.FolderSimpleLayersParent, + buildpacks.FolderSimpleLayers, + ), + buildpacks.WithOS(dockerHostOS()), ) buildpackManager.PrepareBuildpacks(tmpDir, packageFile) @@ -2123,70 +2123,68 @@ func createComplexBuilder(t *testing.T, "run_image_mirror": runImageMirror, } - if dockerHostOS() != "windows" { - packageImageName := registryConfig.RepoName("nested-level-1-buildpack-" + h.RandString(8)) - nestedLevelTwoBuildpackName := registryConfig.RepoName("nested-level-2-buildpack-" + h.RandString(8)) - simpleLayersBuildpackName := registryConfig.RepoName("simple-layers-buildpack-" + h.RandString(8)) - - templateMapping["package_id"] = "simple/nested-level-1" - templateMapping["package_image_name"] = packageImageName - templateMapping["nested_level_1_buildpack"] = packageImageName - templateMapping["nested_level_2_buildpack"] = nestedLevelTwoBuildpackName - templateMapping["simple_layers_buildpack"] = simpleLayersBuildpackName - - fixtureManager := pack.FixtureManager() - - nestedLevelOneConfigFile, err := ioutil.TempFile(tmpDir, "nested-level-1-package.toml") - assert.Nil(err) - fixtureManager.TemplateFixtureToFile( - "nested-level-1-buildpack_package.toml", - nestedLevelOneConfigFile, - templateMapping, - ) - err = nestedLevelOneConfigFile.Close() - assert.Nil(err) - - nestedLevelTwoConfigFile, err := ioutil.TempFile(tmpDir, "nested-level-2-package.toml") - assert.Nil(err) - fixtureManager.TemplateFixtureToFile( - "nested-level-2-buildpack_package.toml", - nestedLevelTwoConfigFile, - templateMapping, - ) - err = nestedLevelTwoConfigFile.Close() - assert.Nil(err) + packageImageName := registryConfig.RepoName("nested-level-1-buildpack-" + h.RandString(8)) + nestedLevelTwoBuildpackName := registryConfig.RepoName("nested-level-2-buildpack-" + h.RandString(8)) + simpleLayersBuildpackName := registryConfig.RepoName("simple-layers-buildpack-" + h.RandString(8)) - packageImageBuildpack := buildpacks.NewPackageImage( - t, - pack, - packageImageName, - nestedLevelOneConfigFile.Name(), - buildpacks.WithRequiredBuildpacks( - buildpacks.NestedLevelOne, - buildpacks.NewPackageImage( - t, - pack, - nestedLevelTwoBuildpackName, - nestedLevelTwoConfigFile.Name(), - buildpacks.WithRequiredBuildpacks( - buildpacks.NestedLevelTwo, - buildpacks.NewPackageImage( - t, - pack, - simpleLayersBuildpackName, - fixtureManager.FixtureLocation("simple-layers-buildpack_package.toml"), - buildpacks.WithRequiredBuildpacks(buildpacks.SimpleLayers), - ), + templateMapping["package_id"] = "simple/nested-level-1" + templateMapping["package_image_name"] = packageImageName + templateMapping["nested_level_1_buildpack"] = packageImageName + templateMapping["nested_level_2_buildpack"] = nestedLevelTwoBuildpackName + templateMapping["simple_layers_buildpack"] = simpleLayersBuildpackName + + fixtureManager := pack.FixtureManager() + + nestedLevelOneConfigFile, err := ioutil.TempFile(tmpDir, "nested-level-1-package.toml") + assert.Nil(err) + fixtureManager.TemplateFixtureToFile( + "nested-level-1-buildpack_package.toml", + nestedLevelOneConfigFile, + templateMapping, + ) + err = nestedLevelOneConfigFile.Close() + assert.Nil(err) + + nestedLevelTwoConfigFile, err := ioutil.TempFile(tmpDir, "nested-level-2-package.toml") + assert.Nil(err) + fixtureManager.TemplateFixtureToFile( + "nested-level-2-buildpack_package.toml", + nestedLevelTwoConfigFile, + templateMapping, + ) + err = nestedLevelTwoConfigFile.Close() + assert.Nil(err) + + packageImageBuildpack := buildpacks.NewPackageImage( + t, + pack, + packageImageName, + nestedLevelOneConfigFile.Name(), + buildpacks.WithRequiredBuildpacks( + buildpacks.NestedLevelOne, + buildpacks.NewPackageImage( + t, + pack, + nestedLevelTwoBuildpackName, + nestedLevelTwoConfigFile.Name(), + buildpacks.WithRequiredBuildpacks( + buildpacks.NestedLevelTwo, + buildpacks.NewPackageImage( + t, + pack, + simpleLayersBuildpackName, + fixtureManager.FixtureLocation("simple-layers-buildpack_package.toml"), + buildpacks.WithRequiredBuildpacks(buildpacks.SimpleLayers), ), ), ), - ) + ), + ) - builderBuildpacks = append( - builderBuildpacks, - packageImageBuildpack, - ) - } + builderBuildpacks = append( + builderBuildpacks, + packageImageBuildpack, + ) buildpackManager.PrepareBuildpacks(tmpDir, builderBuildpacks...) @@ -2257,30 +2255,20 @@ func createBuilder( buildpacks.ReadEnv, } - // NOTE: Windows-based packages are not yet supported, so we'll add this buildpack in the usual way for now. - // Remove this block once Windows-based packages are supported. - if dockerHostOS() == "windows" { - builderBuildpacks = append(builderBuildpacks, buildpacks.SimpleLayers) - } - - // NOTE: Windows-based packages are not yet supported, so we'll add this buildpack in the usual way for now (see above). - // Remove this guard once Windows-based packages are supported. - if dockerHostOS() != "windows" { - packageImageName := registryConfig.RepoName("simple-layers-package-image-buildpack-" + h.RandString(8)) + packageImageName := registryConfig.RepoName("simple-layers-package-image-buildpack-" + h.RandString(8)) - packageImageBuildpack := buildpacks.NewPackageImage( - t, - pack, - packageImageName, - pack.FixtureManager().FixtureLocation("package.toml"), - buildpacks.WithRequiredBuildpacks(buildpacks.SimpleLayers), - ) + packageImageBuildpack := buildpacks.NewPackageImage( + t, + pack, + packageImageName, + pack.FixtureManager().FixtureLocation("package.toml"), + buildpacks.WithRequiredBuildpacks(buildpacks.SimpleLayers), + ) - builderBuildpacks = append(builderBuildpacks, packageImageBuildpack) + builderBuildpacks = append(builderBuildpacks, packageImageBuildpack) - templateMapping["package_image_name"] = packageImageName - templateMapping["package_id"] = "simple/layers" - } + templateMapping["package_image_name"] = packageImageName + templateMapping["package_id"] = "simple/layers" buildpackManager.PrepareBuildpacks(tmpDir, builderBuildpacks...) @@ -2300,11 +2288,6 @@ func createBuilder( // RENDER builder.toml configFileName := "builder.toml" - // NOTE: Remove when Windows-based packages are supported (can use same toml at that point) - if dockerHostOS() == "windows" { - configFileName = "builder-windows.toml" - } - builderConfigFile, err := ioutil.TempFile(tmpDir, "builder.toml") assert.Nil(err) diff --git a/acceptance/buildpacks/manager.go b/acceptance/buildpacks/manager.go index 58f46f0734..f3777bd42e 100644 --- a/acceptance/buildpacks/manager.go +++ b/acceptance/buildpacks/manager.go @@ -51,3 +51,28 @@ func (b BuildpackManager) PrepareBuildpacks(destination string, buildpacks ...Te b.assert.Nil(err) } } + +type Modifiable interface { + SetOS(string) + SetPublish() + SetBuildpacks([]TestBuildpack) +} +type PackageModifier func(p Modifiable) + +func WithRequiredBuildpacks(buildpacks ...TestBuildpack) PackageModifier { + return func(p Modifiable) { + p.SetBuildpacks(buildpacks) + } +} + +func WithPublish() PackageModifier { + return func(p Modifiable) { + p.SetPublish() + } +} + +func WithOS(osVal string) PackageModifier { + return func(p Modifiable) { + p.SetOS(osVal) + } +} diff --git a/acceptance/buildpacks/package_file_buildpack.go b/acceptance/buildpacks/package_file_buildpack.go index 21c478a15e..3b6d5cb616 100644 --- a/acceptance/buildpacks/package_file_buildpack.go +++ b/acceptance/buildpacks/package_file_buildpack.go @@ -21,22 +21,37 @@ type PackageFile struct { destination string sourceConfigLocation string buildpacks []TestBuildpack + os string } +func (p *PackageFile) SetOS(os string) { + p.os = os +} + +func (p *PackageFile) SetBuildpacks(buildpacks []TestBuildpack) { + p.buildpacks = buildpacks +} + +func (p *PackageFile) SetPublish() {} + func NewPackageFile( t *testing.T, pack *invoke.PackInvoker, destination, configLocation string, - buildpacks ...TestBuildpack, + modifiers ...PackageModifier, ) PackageFile { - return PackageFile{ + p := PackageFile{ testObject: t, pack: pack, destination: destination, sourceConfigLocation: configLocation, - buildpacks: buildpacks, } + for _, mod := range modifiers { + mod(&p) + } + + return p } func (p PackageFile) Prepare(sourceDir, _ string) error { @@ -59,13 +74,18 @@ func (p PackageFile) Prepare(sourceDir, _ string) error { configLocation := filepath.Join(tmpDir, "package.toml") h.CopyFile(p.testObject, p.sourceConfigLocation, configLocation) - output := p.pack.RunSuccessfully( - "package-buildpack", + packArgs := []string{ p.destination, "--no-color", "-c", configLocation, "--format", "file", - ) + } + + if p.os != "" { + packArgs = append(packArgs, "--os", p.os) + } + + output := p.pack.RunSuccessfully("package-buildpack", packArgs...) if !strings.Contains(output, fmt.Sprintf("Successfully created package '%s'", p.destination)) { return errors.New("failed to create package") diff --git a/acceptance/buildpacks/package_image_buildpack.go b/acceptance/buildpacks/package_image_buildpack.go index 81719abc9d..fc616176a9 100644 --- a/acceptance/buildpacks/package_image_buildpack.go +++ b/acceptance/buildpacks/package_image_buildpack.go @@ -23,27 +23,26 @@ type PackageImage struct { sourceConfigLocation string buildpacks []TestBuildpack publish bool + os string } -type PackageImageModifier func(p *PackageImage) +func (p *PackageImage) SetOS(os string) { + p.os = os +} -func WithRequiredBuildpacks(buildpacks ...TestBuildpack) PackageImageModifier { - return func(p *PackageImage) { - p.buildpacks = buildpacks - } +func (p *PackageImage) SetBuildpacks(buildpacks []TestBuildpack) { + p.buildpacks = buildpacks } -func WithPublish() PackageImageModifier { - return func(p *PackageImage) { - p.publish = true - } +func (p *PackageImage) SetPublish() { + p.publish = true } func NewPackageImage( t *testing.T, pack *invoke.PackInvoker, name, configLocation string, - modifiers ...PackageImageModifier, + modifiers ...PackageModifier, ) PackageImage { p := PackageImage{ testObject: t, @@ -56,7 +55,6 @@ func NewPackageImage( for _, mod := range modifiers { mod(&p) } - return p } @@ -90,6 +88,10 @@ func (p PackageImage) Prepare(sourceDir, _ string) error { packArgs = append(packArgs, "--publish") } + if p.os != "" { + packArgs = append(packArgs, "--os", p.os) + } + output := p.pack.RunSuccessfully("package-buildpack", packArgs...) assertOutput := assertions.NewOutputAssertionManager(p.testObject, output) diff --git a/acceptance/testdata/pack_fixtures/builder-windows.toml b/acceptance/testdata/pack_fixtures/builder-windows.toml deleted file mode 100644 index 94a9919811..0000000000 --- a/acceptance/testdata/pack_fixtures/builder-windows.toml +++ /dev/null @@ -1,40 +0,0 @@ -[[buildpacks]] - id = "read/env" - version = "read-env-version" - uri = "read-env-buildpack.tgz" - -[[buildpacks]] - # intentionally missing id/version as they are optional - uri = "noop-buildpack.tgz" - -[[buildpacks]] - # noop-buildpack-2 has the same id but a different version compared to noop-buildpack - uri = "noop-buildpack-2.tgz" - -[[buildpacks]] - uri = "simple-layers-buildpack.tgz" - -[[order]] - -[[order.group]] - id = "simple/layers" - # intentionlly missing version to test support - -[[order.group]] - id = "read/env" - version = "read-env-version" - optional = true - -[stack] - id = "pack.test.stack" - build-image = "pack-test/build" - run-image = "pack-test/run" - run-image-mirrors = ["{{.run_image_mirror}}"] - -[lifecycle] -{{- if .lifecycle_uri}} - uri = "{{.lifecycle_uri}}" -{{- end}} -{{- if .lifecycle_version}} - version = "{{.lifecycle_version}}" -{{- end}} diff --git a/acceptance/testdata/pack_fixtures/report_output.txt b/acceptance/testdata/pack_fixtures/report_output.txt index 66cf032d80..d432ae706c 100644 --- a/acceptance/testdata/pack_fixtures/report_output.txt +++ b/acceptance/testdata/pack_fixtures/report_output.txt @@ -8,3 +8,4 @@ Supported Platform APIs: 0.3, 0.4 Config: default-builder-image = "{{ .DefaultBuilder }}" + experimental = true diff --git a/build.go b/build.go index 944f7a0613..91cfd7bfc1 100644 --- a/build.go +++ b/build.go @@ -630,7 +630,11 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima return fetchedBPs, order, errors.Wrapf(err, "extracting buildpacks from %s", style.Symbol(bp)) } } else { - layerWriterFactory, err := layer.NewWriterFactory(builderImage) + imageOS, err := builderImage.OS() + if err != nil { + return fetchedBPs, order, errors.Wrap(err, "getting image OS") + } + layerWriterFactory, err := layer.NewWriterFactory(imageOS) if err != nil { return fetchedBPs, order, errors.Wrapf(err, "get tar writer factory for image %s", style.Symbol(builderImage.Name())) } diff --git a/build_test.go b/build_test.go index b716028282..25afcd4e0e 100644 --- a/build_test.go +++ b/build_test.go @@ -2253,7 +2253,9 @@ func newWindowsImage(name, topLayerSha string, identifier imgutil.Identifier) *f result := fakes.NewImage(name, topLayerSha, identifier) arch, _ := result.Architecture() osVersion, _ := result.OSVersion() - result.SetPlatform("windows", osVersion, arch) + result.SetOS("windows") + result.SetOSVersion(osVersion) + result.SetArchitecture(arch) return result } diff --git a/cmd/cmd.go b/cmd/cmd.go index e9dedb4f0c..6106c77094 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -76,7 +76,7 @@ func NewPackCommand(logger ConfigurableLogger) (*cobra.Command, error) { rootCmd.AddCommand(commands.ListTrustedBuilders(logger, cfg)) rootCmd.AddCommand(commands.CreateBuilder(logger, cfg, &packClient)) - rootCmd.AddCommand(commands.PackageBuildpack(logger, &packClient, buildpackage.NewConfigReader())) + rootCmd.AddCommand(commands.PackageBuildpack(logger, cfg, &packClient, buildpackage.NewConfigReader())) rootCmd.AddCommand(commands.SuggestStacks(logger)) diff --git a/create_builder.go b/create_builder.go index 3d2cb786ae..ea4a636535 100644 --- a/create_builder.go +++ b/create_builder.go @@ -261,7 +261,11 @@ func (c *Client) addBuildpacksToBuilder(ctx context.Context, opts CreateBuilderO return errors.Wrapf(err, "extracting buildpacks from %s", style.Symbol(b.ID)) } } else { - layerWriterFactory, err := layer.NewWriterFactory(bldr.Image()) + imageOS, err := bldr.Image().OS() + if err != nil { + return errors.Wrap(err, "getting image OS") + } + layerWriterFactory, err := layer.NewWriterFactory(imageOS) if err != nil { return errors.Wrapf(err, "get tar writer factory for image %s", style.Symbol(bldr.Name())) } diff --git a/create_builder_test.go b/create_builder_test.go index 99241c2852..46f002de41 100644 --- a/create_builder_test.go +++ b/create_builder_test.go @@ -10,10 +10,9 @@ import ( "runtime" "testing" - "github.com/buildpacks/pack/config" - "github.com/buildpacks/imgutil/fakes" "github.com/buildpacks/lifecycle/api" + "github.com/docker/docker/api/types" "github.com/golang/mock/gomock" "github.com/heroku/color" "github.com/pkg/errors" @@ -23,6 +22,7 @@ import ( "github.com/buildpacks/pack" pubbldr "github.com/buildpacks/pack/builder" pubbldpkg "github.com/buildpacks/pack/buildpackage" + "github.com/buildpacks/pack/config" "github.com/buildpacks/pack/internal/blob" "github.com/buildpacks/pack/internal/builder" cfg "github.com/buildpacks/pack/internal/config" @@ -48,6 +48,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { mockDownloader *testmocks.MockDownloader mockImageFactory *testmocks.MockImageFactory mockImageFetcher *testmocks.MockImageFetcher + mockDockerClient *testmocks.MockCommonAPIClient fakeBuildImage *fakes.Image fakeRunImage *fakes.Image fakeRunImageMirror *fakes.Image @@ -64,6 +65,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { mockDownloader = testmocks.NewMockDownloader(mockController) mockImageFetcher = testmocks.NewMockImageFetcher(mockController) mockImageFactory = testmocks.NewMockImageFactory(mockController) + mockDockerClient = testmocks.NewMockCommonAPIClient(mockController) fakeBuildImage = fakes.NewImage("some/build-image", "", nil) h.AssertNil(t, fakeBuildImage.SetLabel("io.buildpacks.stack.id", "some.stack.id")) @@ -88,9 +90,12 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { pack.WithDownloader(mockDownloader), pack.WithImageFactory(mockImageFactory), pack.WithFetcher(mockImageFetcher), + pack.WithDockerClient(mockDockerClient), ) h.AssertNil(t, err) + mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() + opts = pack.CreateBuilderOptions{ BuilderName: "some/builder", Config: pubbldr.Config{ @@ -345,7 +350,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { prepareFetcherWithRunImages() - fakeBuildImage.SetPlatform("windows", "0123", "amd64") + h.AssertNil(t, fakeBuildImage.SetOS("windows")) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", true, config.PullAlways).Return(fakeBuildImage, nil) err = packClientWithExperimental.CreateBuilder(context.TODO(), opts) @@ -357,7 +362,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { it("fails", func() { prepareFetcherWithRunImages() - fakeBuildImage.SetPlatform("windows", "0123", "amd64") + h.AssertNil(t, fakeBuildImage.SetOS("windows")) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", true, config.PullAlways).Return(fakeBuildImage, nil) err := subject.CreateBuilder(context.TODO(), opts) @@ -424,7 +429,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "3.4.5" - fakeBuildImage.SetPlatform("windows", "0123", "amd64") + h.AssertNil(t, fakeBuildImage.SetOS("windows")) mockDownloader.EXPECT().Download( gomock.Any(), @@ -476,7 +481,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "" - fakeBuildImage.SetPlatform("windows", "0123", "amd64") + h.AssertNil(t, fakeBuildImage.SetOS("windows")) mockDownloader.EXPECT().Download( gomock.Any(), diff --git a/go.mod b/go.mod index 976de07147..252dec09e2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/Microsoft/hcsshim v0.8.7 // indirect github.com/apex/log v1.9.0 - github.com/buildpacks/imgutil v0.0.0-20200805143852-1844b230530d + github.com/buildpacks/imgutil v0.0.0-20201015202701-6dd3ca364074 github.com/buildpacks/lifecycle v0.7.2 github.com/containerd/containerd v1.3.3 // indirect github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41 // indirect diff --git a/go.sum b/go.sum index 7c0d5491c1..30707cb5ab 100644 --- a/go.sum +++ b/go.sum @@ -45,11 +45,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/apex/log v1.1.2-0.20190827100214-baa5455d1012/go.mod h1:Ls949n1HFtXfbDcjiTTFQqkVUrte0puoIBfO3SVgwOA= -github.com/apex/log v1.1.2 h1:bnDuVoi+o98wOdVqfEzNDlY0tcmBia7r4YkjS9EqGYk= -github.com/apex/log v1.1.2/go.mod h1:SyfRweFO+TlkIJ3DVizTSeI1xk7jOIIqOnUPZQTTsww= github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= -github.com/apex/logs v0.0.3/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= @@ -66,8 +63,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/buildpacks/imgutil v0.0.0-20200313170640-a02052f47d62/go.mod h1:TjPmM78urjQIiMal4T7en6iBekAPv6z1yVMZobc4Kd8= -github.com/buildpacks/imgutil v0.0.0-20200805143852-1844b230530d h1:uVTFIiev3f7fi5BSv1RtM5W5lcqQodqLRBG5AdoDe2U= -github.com/buildpacks/imgutil v0.0.0-20200805143852-1844b230530d/go.mod h1:uVv0fNwOEBNgyM9ZvwV0g2Dvzk31q5TtSeoC1GGmW+k= +github.com/buildpacks/imgutil v0.0.0-20201015202701-6dd3ca364074 h1:uqgt8gd/oekjLqyk8oXRZdWaHv9Sc85ssjpmUcfn/cg= +github.com/buildpacks/imgutil v0.0.0-20201015202701-6dd3ca364074/go.mod h1:Oj9x40zkDyafaKpvgPuLGFjzzrdQQ+w9DNi1JzqJV1I= github.com/buildpacks/lifecycle v0.7.2 h1:FO7i2cokLNc7lcuThq/LYt1jmkB8HBrjpK+2GWWsaLI= github.com/buildpacks/lifecycle v0.7.2/go.mod h1:k41tT3XOt7ufaMGAvOpEsXyuJpUPoo4F686Z652lU3E= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -177,8 +174,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.1 h1:ocYkMQY5RrXTYgXl7ICpV0IXwlEQGwKIsery4gyXa1U= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -192,8 +187,6 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-containerregistry v0.0.0-20200311163244-4b1985e5ea21/go.mod h1:m8YvHwSOuBCq25yrj1DaX/fIMrv6ec3CNg8jY8+5PEA= @@ -390,8 +383,7 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= -github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU= github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -412,9 +404,11 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= +github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= @@ -475,8 +469,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -543,7 +535,6 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -643,6 +634,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= @@ -683,8 +675,6 @@ modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/internal/build/phase_config_provider_test.go b/internal/build/phase_config_provider_test.go index 2cf09aaac3..105f65af22 100644 --- a/internal/build/phase_config_provider_test.go +++ b/internal/build/phase_config_provider_test.go @@ -64,7 +64,7 @@ func testPhaseConfigProvider(t *testing.T, when spec.G, it spec.S) { when("building for Windows", func() { it("sets process isolation", func() { fakeBuilderImage := ifakes.NewImage("fake-builder", "", nil) - fakeBuilderImage.SetPlatform("windows", "", "") + h.AssertNil(t, fakeBuilderImage.SetOS("windows")) fakeBuilder, err := fakes.NewFakeBuilder(fakes.WithImage(fakeBuilderImage)) h.AssertNil(t, err) lifecycle := newTestLifecycleExec(t, false, fakes.WithBuilder(fakeBuilder)) @@ -141,7 +141,7 @@ func testPhaseConfigProvider(t *testing.T, when spec.G, it spec.S) { when("building for Windows", func() { it("sets daemon access on the config", func() { fakeBuilderImage := ifakes.NewImage("fake-builder", "", nil) - fakeBuilderImage.SetPlatform("windows", "", "") + h.AssertNil(t, fakeBuilderImage.SetOS("windows")) fakeBuilder, err := fakes.NewFakeBuilder(fakes.WithImage(fakeBuilderImage)) h.AssertNil(t, err) lifecycle := newTestLifecycleExec(t, false, fakes.WithBuilder(fakeBuilder)) @@ -242,7 +242,7 @@ func testPhaseConfigProvider(t *testing.T, when spec.G, it spec.S) { when("building for Windows", func() { it("sets root user on the config", func() { fakeBuilderImage := ifakes.NewImage("fake-builder", "", nil) - fakeBuilderImage.SetPlatform("windows", "", "") + h.AssertNil(t, fakeBuilderImage.SetOS("windows")) fakeBuilder, err := fakes.NewFakeBuilder(fakes.WithImage(fakeBuilderImage)) h.AssertNil(t, err) lifecycle := newTestLifecycleExec(t, false, fakes.WithBuilder(fakeBuilder)) diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 2afbbc863b..c73bd7628a 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -89,7 +89,11 @@ func New(baseImage imgutil.Image, name string) (*Builder, error) { } func constructBuilder(img imgutil.Image, newName string, metadata Metadata) (*Builder, error) { - layerWriterFactory, err := layer.NewWriterFactory(img) + imageOS, err := img.OS() + if err != nil { + return nil, errors.Wrap(err, "getting image OS") + } + layerWriterFactory, err := layer.NewWriterFactory(imageOS) if err != nil { return nil, err } diff --git a/internal/buildpackage/builder.go b/internal/buildpackage/builder.go index 43bddb19e1..1680ca61ea 100644 --- a/internal/buildpackage/builder.go +++ b/internal/buildpackage/builder.go @@ -3,9 +3,12 @@ package buildpackage import ( "archive/tar" "compress/gzip" + "io" "io/ioutil" "os" + "github.com/buildpacks/imgutil/layer" + "github.com/buildpacks/imgutil" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" @@ -25,6 +28,7 @@ type ImageFactory interface { } type WorkableImage interface { + SetOS(string) error SetLabel(string, string) error AddLayerWithDiffID(path, diffID string) error } @@ -47,12 +51,22 @@ func (i *layoutImage) SetLabel(key string, val string) error { return err } +func (i *layoutImage) SetOS(osVal string) error { + configFile, err := i.ConfigFile() + if err != nil { + return err + } + configFile.OS = osVal + i.Image, err = mutate.ConfigFile(i.Image, configFile) + return err +} + func (i *layoutImage) AddLayerWithDiffID(path, _ string) error { - layer, err := tarball.LayerFromFile(path, tarball.WithCompressionLevel(gzip.DefaultCompression)) + tarLayer, err := tarball.LayerFromFile(path, tarball.WithCompressionLevel(gzip.DefaultCompression)) if err != nil { return err } - i.Image, err = mutate.AppendLayers(i.Image, layer) + i.Image, err = mutate.AppendLayers(i.Image, tarLayer) if err != nil { return errors.Wrap(err, "add layer") } @@ -79,7 +93,7 @@ func (b *PackageBuilder) AddDependency(buildpack dist.Buildpack) { b.dependencies = append(b.dependencies, buildpack) } -func (b *PackageBuilder) finalizeImage(tmpDir string, image WorkableImage) error { +func (b *PackageBuilder) finalizeImage(image WorkableImage, imageOS, tmpDir string) error { if err := dist.SetLabel(image, MetadataLabel, &Metadata{ BuildpackInfo: b.buildpack.Descriptor().Info, Stacks: b.resolvedStacks(), @@ -87,6 +101,16 @@ func (b *PackageBuilder) finalizeImage(tmpDir string, image WorkableImage) error return err } + if err := image.SetOS(imageOS); err != nil { + return err + } + + if imageOS == "windows" { + if err := addWindowsShimBaseLayer(image, tmpDir); err != nil { + return err + } + } + bpLayers := dist.BuildpackLayers{} for _, bp := range append(b.dependencies, b.buildpack) { bpLayerTar, err := dist.BuildpackToLayerTar(tmpDir, bp) @@ -116,6 +140,39 @@ func (b *PackageBuilder) finalizeImage(tmpDir string, image WorkableImage) error return nil } +func addWindowsShimBaseLayer(image WorkableImage, tmpDir string) error { + baseLayerFile, err := ioutil.TempFile(tmpDir, "windows-baselayer") + if err != nil { + return err + } + defer baseLayerFile.Close() + + baseLayer, err := layer.WindowsBaseLayer() + if err != nil { + return err + } + + if _, err := io.Copy(baseLayerFile, baseLayer); err != nil { + return err + } + + if err := baseLayerFile.Close(); err != nil { + return err + } + + baseLayerPath := baseLayerFile.Name() + diffID, err := dist.LayerDiffID(baseLayerPath) + if err != nil { + return err + } + + if err := image.AddLayerWithDiffID(baseLayerPath, diffID.String()); err != nil { + return err + } + + return nil +} + func (b *PackageBuilder) validate() error { if b.buildpack == nil { return errors.New("buildpack must be set") @@ -147,7 +204,7 @@ func (b *PackageBuilder) resolvedStacks() []dist.Stack { return stacks } -func (b *PackageBuilder) SaveAsFile(path string) error { +func (b *PackageBuilder) SaveAsFile(path, imageOS string) error { if err := b.validate(); err != nil { return err } @@ -162,7 +219,7 @@ func (b *PackageBuilder) SaveAsFile(path string) error { } defer os.RemoveAll(tmpDir) - if err := b.finalizeImage(tmpDir, layoutImage); err != nil { + if err := b.finalizeImage(layoutImage, imageOS, tmpDir); err != nil { return err } @@ -192,7 +249,7 @@ func (b *PackageBuilder) SaveAsFile(path string) error { return archive.WriteDirToTar(tw, layoutDir, "/", 0, 0, 0755, true, nil) } -func (b *PackageBuilder) SaveAsImage(repoName string, publish bool) (imgutil.Image, error) { +func (b *PackageBuilder) SaveAsImage(repoName string, publish bool, imageOS string) (imgutil.Image, error) { if err := b.validate(); err != nil { return nil, err } @@ -208,7 +265,7 @@ func (b *PackageBuilder) SaveAsImage(repoName string, publish bool) (imgutil.Ima } defer os.RemoveAll(tmpDir) - if err := b.finalizeImage(tmpDir, image); err != nil { + if err := b.finalizeImage(image, imageOS, tmpDir); err != nil { return nil, err } diff --git a/internal/buildpackage/builder_test.go b/internal/buildpackage/builder_test.go index a7b5dbe4c2..0076b71561 100644 --- a/internal/buildpackage/builder_test.go +++ b/internal/buildpackage/builder_test.go @@ -12,6 +12,8 @@ import ( "path/filepath" "testing" + "github.com/buildpacks/imgutil/layer" + "github.com/buildpacks/imgutil/fakes" "github.com/buildpacks/lifecycle/api" "github.com/golang/mock/gomock" @@ -36,7 +38,6 @@ func TestPackageBuilder(t *testing.T) { func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { var ( - fakePackageImage *fakes.Image mockController *gomock.Controller mockImageFactory *testmocks.MockImageFactory subject *buildpackage.PackageBuilder @@ -47,7 +48,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { mockController = gomock.NewController(t) mockImageFactory = testmocks.NewMockImageFactory(mockController) - fakePackageImage = fakes.NewImage("some/package", "", nil) + fakePackageImage := fakes.NewImage("some/package", "", nil) mockImageFactory.EXPECT().NewImage("some/package", true).Return(fakePackageImage, nil).AnyTimes() subject = buildpackage.NewBuilder(mockImageFactory) @@ -68,11 +69,18 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { fn func() error }{ {name: "SaveAsImage", fn: func() error { - _, err := subject.SaveAsImage(fakePackageImage.Name(), false) + _, err := subject.SaveAsImage("some/package", false, "linux") + return err + }}, + {name: "SaveAsImage", fn: func() error { + _, err := subject.SaveAsImage("some/package", false, "windows") return err }}, {name: "SaveAsFile", fn: func() error { - return subject.SaveAsFile(path.Join(tmpDir, "package.cnb")) + return subject.SaveAsFile(path.Join(tmpDir, "package.cnb"), "windows") + }}, + {name: "SaveAsFile", fn: func() error { + return subject.SaveAsFile(path.Join(tmpDir, "package.cnb"), "linux") }}, } { testFn := test.fn @@ -269,7 +277,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) subject.AddDependency(dependency2) - _, err = subject.SaveAsImage("some/package", false) + _, err = subject.SaveAsImage("some/package", false, "linux") h.AssertError(t, err, "no compatible stacks among provided buildpacks") }) }) @@ -324,7 +332,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) subject.AddDependency(dependency2) - img, err := subject.SaveAsImage("some/package", false) + img, err := subject.SaveAsImage("some/package", false, "linux") h.AssertNil(t, err) metadata := buildpackage.Metadata{} @@ -388,7 +396,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { subject.AddDependency(dependencyNestedNested) - img, err := subject.SaveAsImage("some/package", false) + img, err := subject.SaveAsImage("some/package", false, "linux") h.AssertNil(t, err) metadata := buildpackage.Metadata{} @@ -421,7 +429,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { subject.SetBuildpack(buildpack1) - packageImage, err := subject.SaveAsImage(fakePackageImage.Name(), false) + packageImage, err := subject.SaveAsImage("some/package", false, "linux") h.AssertNil(t, err) labelData, err := packageImage.Label("io.buildpacks.buildpackage.metadata") @@ -434,6 +442,10 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, len(md.Stacks), 2) h.AssertEq(t, md.Stacks[0].ID, "stack.id.1") h.AssertEq(t, md.Stacks[1].ID, "stack.id.2") + + osVal, err := packageImage.OS() + h.AssertNil(t, err) + h.AssertEq(t, osVal, "linux") }) it("sets buildpack layers label", func() { @@ -446,11 +458,11 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) subject.SetBuildpack(buildpack1) - _, err = subject.SaveAsImage(fakePackageImage.Name(), false) + packageImage, err := subject.SaveAsImage("some/package", false, "linux") h.AssertNil(t, err) var bpLayers dist.BuildpackLayers - _, err = dist.GetLabel(fakePackageImage, "io.buildpacks.buildpack.layers", &bpLayers) + _, err = dist.GetLabel(packageImage, "io.buildpacks.buildpack.layers", &bpLayers) h.AssertNil(t, err) bp1Info, ok1 := bpLayers["bp.1.id"]["bp.1.version"] @@ -458,7 +470,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, bp1Info.Stacks, []dist.Stack{{ID: "stack.id.1"}, {ID: "stack.id.2"}}) }) - it("adds buildpack layers", func() { + it("adds buildpack layers for linux", func() { buildpack1, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ API: api.MustParse("0.2"), Info: dist.BuildpackInfo{ID: "bp.1.id", Version: "bp.1.version"}, @@ -468,12 +480,13 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) subject.SetBuildpack(buildpack1) - _, err = subject.SaveAsImage(fakePackageImage.Name(), false) + packageImage, err := subject.SaveAsImage("some/package", false, "linux") h.AssertNil(t, err) buildpackExists := func(name, version string) { t.Helper() dirPath := fmt.Sprintf("/cnb/buildpacks/%s/%s", name, version) + fakePackageImage := packageImage.(*fakes.Image) layerTar, err := fakePackageImage.FindLayerWithPath(dirPath) h.AssertNil(t, err) @@ -495,6 +508,31 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { } buildpackExists("bp.1.id", "bp.1.version") + + fakePackageImage := packageImage.(*fakes.Image) + h.AssertEq(t, fakePackageImage.NumberOfAddedLayers(), 1) + }) + + it("adds baselayer + buildpack layers for windows", func() { + buildpack1, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ + API: api.MustParse("0.2"), + Info: dist.BuildpackInfo{ID: "bp.1.id", Version: "bp.1.version"}, + Stacks: []dist.Stack{{ID: "stack.id.1"}, {ID: "stack.id.2"}}, + Order: nil, + }, 0644) + h.AssertNil(t, err) + subject.SetBuildpack(buildpack1) + + packageImage, err := subject.SaveAsImage("some/package", false, "windows") + h.AssertNil(t, err) + + fakePackageImage := packageImage.(*fakes.Image) + + osVal, err := fakePackageImage.OS() + h.AssertNil(t, err) + h.AssertEq(t, osVal, "windows") + + h.AssertEq(t, fakePackageImage.NumberOfAddedLayers(), 2) }) }) @@ -510,7 +548,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { subject.SetBuildpack(buildpack1) outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10))) - h.AssertNil(t, subject.SaveAsFile(outputFile)) + h.AssertNil(t, subject.SaveAsFile(outputFile, "linux")) withContents := func(fn func(data []byte)) h.TarEntryAssertion { return func(t *testing.T, header *tar.Header, data []byte) { @@ -532,6 +570,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { "/blobs/sha256/"+index.Manifests[0].Digest.Hex(), h.HasOwnerAndGroup(0, 0), h.IsJSON(), + withContents(func(data []byte) { manifest := v1.Manifest{} err := json.Unmarshal(data, &manifest) @@ -546,12 +585,14 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.ContentContains(`"io.buildpacks.buildpackage.metadata":"{\"id\":\"bp.1.id\",\"version\":\"bp.1.version\",\"stacks\":[{\"id\":\"stack.id.1\"},{\"id\":\"stack.id.2\"}]}"`), // buildpack layers metadata h.ContentContains(`"io.buildpacks.buildpack.layers":"{\"bp.1.id\":{\"bp.1.version\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"stack.id.1\"},{\"id\":\"stack.id.2\"}],\"layerDiffID\":\"sha256:a10862daec7a8a62fd04cc5d4520fdb80d4d5c07a3c146fb604a9c23c22fd5b0\"}}}"`), + // image os + h.ContentContains(`"os":"linux"`), ) })) })) }) - it("adds buildpack layers", func() { + it("adds buildpack layers for linux", func() { buildpack1, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ API: api.MustParse("0.2"), Info: dist.BuildpackInfo{ID: "bp.1.id", Version: "bp.1.version"}, @@ -562,7 +603,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { subject.SetBuildpack(buildpack1) outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10))) - h.AssertNil(t, subject.SaveAsFile(outputFile)) + h.AssertNil(t, subject.SaveAsFile(outputFile, "linux")) h.AssertOnTarEntry(t, outputFile, "/blobs", h.IsDirectory(), @@ -573,8 +614,12 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.HasOwnerAndGroup(0, 0), h.HasFileMode(0755)) + bpReader, err := buildpack1.Open() + h.AssertNil(t, err) + defer bpReader.Close() + // layer: application/vnd.docker.image.rootfs.diff.tar.gzip - buildpackLayerSHA, err := computeBuildpackLayerSHA(buildpack1) + buildpackLayerSHA, err := computeLayerSHA(bpReader) h.AssertNil(t, err) h.AssertOnTarEntry(t, outputFile, "/blobs/sha256/"+buildpackLayerSHA, @@ -594,18 +639,53 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.HasOwnerAndGroup(0, 0), h.HasFileMode(0644))) }) + + it("adds baselayer + buildpack layers for windows", func() { + buildpack1, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ + API: api.MustParse("0.2"), + Info: dist.BuildpackInfo{ID: "bp.1.id", Version: "bp.1.version"}, + Stacks: []dist.Stack{{ID: "stack.id.1"}, {ID: "stack.id.2"}}, + Order: nil, + }, 0644) + h.AssertNil(t, err) + subject.SetBuildpack(buildpack1) + + outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10))) + h.AssertNil(t, subject.SaveAsFile(outputFile, "windows")) + + // Windows baselayer content is constant + expectedBaseLayerReader, err := layer.WindowsBaseLayer() + h.AssertNil(t, err) + + // layer: application/vnd.docker.image.rootfs.diff.tar.gzip + expectedBaseLayerSHA, err := computeLayerSHA(ioutil.NopCloser(expectedBaseLayerReader)) + h.AssertNil(t, err) + h.AssertOnTarEntry(t, outputFile, + "/blobs/sha256/"+expectedBaseLayerSHA, + h.HasOwnerAndGroup(0, 0), + h.HasFileMode(0755), + h.IsGzipped(), + ) + + bpReader, err := buildpack1.Open() + h.AssertNil(t, err) + defer bpReader.Close() + + buildpackLayerSHA, err := computeLayerSHA(bpReader) + h.AssertNil(t, err) + h.AssertOnTarEntry(t, outputFile, + "/blobs/sha256/"+buildpackLayerSHA, + h.HasOwnerAndGroup(0, 0), + h.HasFileMode(0755), + h.IsGzipped(), + ) + }) }) } -func computeBuildpackLayerSHA(buildpack dist.Buildpack) (string, error) { - reader, err := buildpack.Open() - if err != nil { - return "", err - } - defer reader.Close() - - layer := stream.NewLayer(reader, stream.WithCompressionLevel(gzip.DefaultCompression)) - compressed, err := layer.Compressed() +func computeLayerSHA(reader io.ReadCloser) (string, error) { + bpLayer := stream.NewLayer(reader, stream.WithCompressionLevel(gzip.DefaultCompression)) + compressed, err := bpLayer.Compressed() if err != nil { return "", err } @@ -615,7 +695,7 @@ func computeBuildpackLayerSHA(buildpack dist.Buildpack) (string, error) { return "", err } - digest, err := layer.Digest() + digest, err := bpLayer.Digest() if err != nil { return "", err } diff --git a/internal/commands/package_buildpack.go b/internal/commands/package_buildpack.go index c1c6457091..6a00ca4bbd 100644 --- a/internal/commands/package_buildpack.go +++ b/internal/commands/package_buildpack.go @@ -3,12 +3,14 @@ package commands import ( "context" - "github.com/buildpacks/pack/config" + pubcfg "github.com/buildpacks/pack/config" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/buildpacks/pack" + "github.com/buildpacks/pack/internal/config" + pubbldpkg "github.com/buildpacks/pack/buildpackage" "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/logging" @@ -18,6 +20,7 @@ import ( type PackageBuildpackFlags struct { PackageTomlPath string Format string + OS string Publish bool NoPull bool Policy string @@ -34,7 +37,7 @@ type PackageConfigReader interface { } // PackageBuildpack packages (a) buildpack(s) into OCI format, based on a package config -func PackageBuildpack(logger logging.Logger, client BuildpackPackager, packageConfigReader PackageConfigReader) *cobra.Command { +func PackageBuildpack(logger logging.Logger, cfg config.Config, client BuildpackPackager, packageConfigReader PackageConfigReader) *cobra.Command { var flags PackageBuildpackFlags cmd := &cobra.Command{ @@ -42,12 +45,12 @@ func PackageBuildpack(logger logging.Logger, client BuildpackPackager, packageCo Short: "Package buildpack in OCI format.", Args: cobra.ExactValidArgs(1), RunE: logError(logger, func(cmd *cobra.Command, args []string) error { - if err := validatePackageBuildpackFlags(&flags, logger); err != nil { + if err := validatePackageBuildpackFlags(&flags, cfg, logger); err != nil { return err } var err error - pullPolicy, err := config.ParsePullPolicy(flags.Policy) + pullPolicy, err := pubcfg.ParsePullPolicy(flags.Policy) if err != nil { return errors.Wrap(err, "parsing pull policy") } @@ -56,7 +59,7 @@ func PackageBuildpack(logger logging.Logger, client BuildpackPackager, packageCo logger.Warn("Flag --package-config has been deprecated, please use --config instead") } - config, err := packageConfigReader.Read(flags.PackageTomlPath) + cfg, err := packageConfigReader.Read(flags.PackageTomlPath) if err != nil { return errors.Wrap(err, "reading config") } @@ -65,7 +68,8 @@ func PackageBuildpack(logger logging.Logger, client BuildpackPackager, packageCo if err := client.PackageBuildpack(cmd.Context(), pack.PackageBuildpackOptions{ Name: name, Format: flags.Format, - Config: config, + OS: flags.OS, + Config: cfg, Publish: flags.Publish, PullPolicy: pullPolicy, }); err != nil { @@ -84,7 +88,11 @@ func PackageBuildpack(logger logging.Logger, client BuildpackPackager, packageCo cmd.Flags().StringVarP(&flags.PackageTomlPath, "config", "c", "", "Path to package TOML config (required)") cmd.Flags().StringVarP(&flags.Format, "format", "f", "", `Format to save package as ("image" or "file")`) - cmd.Flags().BoolVar(&flags.Publish, "publish", false, `Publish to registry (applies to "--image" only)`) + cmd.Flags().BoolVar(&flags.Publish, "publish", false, `Publish to registry (applies to "--format=image" only)`) + cmd.Flags().StringVar(&flags.OS, "os", "", `Operating system format of the package OCI image: "linux" or "windows" (defaults to "linux", except local images which use the daemon OS)`) + if !cfg.Experimental { + cmd.Flags().MarkHidden("os") + } cmd.Flags().StringVar(&flags.Policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, and if-not-present. The default is always") // TODO: Remove --no-pull flag after v0.13.0 released. See https://github.com/buildpacks/pack/issues/775 cmd.Flags().BoolVar(&flags.NoPull, "no-pull", false, "Skip pulling packages before use") @@ -94,8 +102,8 @@ func PackageBuildpack(logger logging.Logger, client BuildpackPackager, packageCo return cmd } -func validatePackageBuildpackFlags(p *PackageBuildpackFlags, logger logging.Logger) error { - if p.Publish && p.Policy == config.PullNever.String() { +func validatePackageBuildpackFlags(p *PackageBuildpackFlags, cfg config.Config, logger logging.Logger) error { + if p.Publish && p.Policy == pubcfg.PullNever.String() { return errors.Errorf("--publish and --pull-policy never cannot be used together. The --publish flag requires the use of remote images.") } @@ -113,9 +121,13 @@ func validatePackageBuildpackFlags(p *PackageBuildpackFlags, logger logging.Logg if p.Policy != "" { logger.Warn("Flag --no-pull ignored in favor of --pull-policy") } else { - p.Policy = config.PullNever.String() + p.Policy = pubcfg.PullNever.String() } } + if p.OS != "" && !cfg.Experimental { + return pack.NewExperimentError("Support for OS flag is currently experimental.") + } + return nil } diff --git a/internal/commands/package_buildpack_test.go b/internal/commands/package_buildpack_test.go index d4880d8656..0d470164c4 100644 --- a/internal/commands/package_buildpack_test.go +++ b/internal/commands/package_buildpack_test.go @@ -5,7 +5,9 @@ import ( "fmt" "testing" - "github.com/buildpacks/pack/config" + "github.com/buildpacks/pack/internal/config" + + pubcfg "github.com/buildpacks/pack/config" "github.com/heroku/color" "github.com/pkg/errors" @@ -32,17 +34,17 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { when("valid package config", func() { it("reads package config from the configured path", func() { fakePackageConfigReader := fakes.NewFakePackageConfigReader() - expectedConfigPath := "/path/to/some/file" + expectedPackageConfigPath := "/path/to/some/file" packageBuildpackCommand := packageBuildpackCommand( - withConfigReader(fakePackageConfigReader), - withConfigPath(expectedConfigPath), + withPackageConfigReader(fakePackageConfigReader), + withPackageConfigPath(expectedPackageConfigPath), ) err := packageBuildpackCommand.Execute() h.AssertNil(t, err) - h.AssertEq(t, fakePackageConfigReader.ReadCalledWithArg, expectedConfigPath) + h.AssertEq(t, fakePackageConfigReader.ReadCalledWithArg, expectedPackageConfigPath) }) it("creates package with correct image name", func() { @@ -70,7 +72,7 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { packageBuildpackCommand := packageBuildpackCommand( withBuildpackPackager(fakeBuildpackPackager), - withConfigReader(fakes.NewFakePackageConfigReader(whereReadReturns(myConfig, nil))), + withPackageConfigReader(fakes.NewFakePackageConfigReader(whereReadReturns(myConfig, nil))), ) err := packageBuildpackCommand.Execute() @@ -109,7 +111,7 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions - h.AssertEq(t, receivedOptions.PullPolicy, config.PullNever) + h.AssertEq(t, receivedOptions.PullPolicy, pubcfg.PullNever) }) when("used together with --pull-policy always", func() { @@ -125,7 +127,7 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { h.AssertContains(t, output, "Flag --no-pull ignored in favor of --pull-policy") receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions - h.AssertEq(t, receivedOptions.PullPolicy, config.PullAlways) + h.AssertEq(t, receivedOptions.PullPolicy, pubcfg.PullAlways) }) }) @@ -142,7 +144,7 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { h.AssertContains(t, output, "Flag --no-pull ignored in favor of --pull-policy") receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions - h.AssertEq(t, receivedOptions.PullPolicy, config.PullNever) + h.AssertEq(t, receivedOptions.PullPolicy, pubcfg.PullNever) }) }) }) @@ -174,7 +176,7 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions - h.AssertEq(t, receivedOptions.PullPolicy, config.PullNever) + h.AssertEq(t, receivedOptions.PullPolicy, pubcfg.PullNever) }) it("pull-policy=always sets policy", func() { @@ -185,7 +187,35 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions - h.AssertEq(t, receivedOptions.PullPolicy, config.PullAlways) + h.AssertEq(t, receivedOptions.PullPolicy, pubcfg.PullAlways) + }) + }) + + when("--os", func() { + when("experimental enabled", func() { + it("creates package with correct image name and os", func() { + fakeBuildpackPackager := &fakes.FakeBuildpackPackager{} + + packageBuildpackCommand := packageBuildpackCommand( + withBuildpackPackager(fakeBuildpackPackager), + withExperimental(), + ) + + packageBuildpackCommand.SetArgs( + []string{ + "some-image-name", + "--config", "/path/to/some/file", + "--os", "windows", + }, + ) + + err := packageBuildpackCommand.Execute() + h.AssertNil(t, err) + + receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions + + h.AssertEq(t, receivedOptions.OS, "windows") + }) }) }) }) @@ -197,8 +227,9 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { logger := logging.NewLogWithWriters(&bytes.Buffer{}, &bytes.Buffer{}) configReader := fakes.NewFakePackageConfigReader() buildpackPackager := &fakes.FakeBuildpackPackager{} + clientConfig := config.Config{} - command := commands.PackageBuildpack(logger, buildpackPackager, configReader) + command := commands.PackageBuildpack(logger, clientConfig, buildpackPackager, configReader) command.SetArgs([]string{ "some-image-name", "--config", "/path/to/some/file", @@ -217,8 +248,9 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { logger := logging.NewLogWithWriters(&bytes.Buffer{}, &bytes.Buffer{}) configReader := fakes.NewFakePackageConfigReader() buildpackPackager := &fakes.FakeBuildpackPackager{} + clientConfig := config.Config{} - command := commands.PackageBuildpack(logger, buildpackPackager, configReader) + command := commands.PackageBuildpack(logger, clientConfig, buildpackPackager, configReader) command.SetArgs([]string{ "some-image-name", "--config", "/path/to/some/file", @@ -239,7 +271,7 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { packageBuildpackCommand := packageBuildpackCommand( withLogger(logging.NewLogWithWriters(outBuf, outBuf)), - withConfigReader( + withPackageConfigReader( fakes.NewFakePackageConfigReader(whereReadReturns(pubbldpkg.Config{}, expectedErr)), ), ) @@ -255,15 +287,15 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { outBuf := &bytes.Buffer{} config := &packageCommandConfig{ - logger: logging.NewLogWithWriters(outBuf, outBuf), - configReader: fakes.NewFakePackageConfigReader(), - buildpackPackager: &fakes.FakeBuildpackPackager{}, + logger: logging.NewLogWithWriters(outBuf, outBuf), + packageConfigReader: fakes.NewFakePackageConfigReader(), + buildpackPackager: &fakes.FakeBuildpackPackager{}, imageName: "some-image-name", configPath: "/path/to/some/file", } - cmd := commands.PackageBuildpack(config.logger, config.buildpackPackager, config.configReader) + cmd := commands.PackageBuildpack(config.logger, config.clientConfig, config.buildpackPackager, config.packageConfigReader) cmd.SetArgs([]string{config.imageName, "--package-config", config.configPath}) err := cmd.Execute() @@ -274,14 +306,14 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { when("no config path is specified", func() { it("errors with a descriptive message", func() { config := &packageCommandConfig{ - logger: logging.NewLogWithWriters(&bytes.Buffer{}, &bytes.Buffer{}), - configReader: fakes.NewFakePackageConfigReader(), - buildpackPackager: &fakes.FakeBuildpackPackager{}, + logger: logging.NewLogWithWriters(&bytes.Buffer{}, &bytes.Buffer{}), + packageConfigReader: fakes.NewFakePackageConfigReader(), + buildpackPackager: &fakes.FakeBuildpackPackager{}, imageName: "some-image-name", } - cmd := commands.PackageBuildpack(config.logger, config.buildpackPackager, config.configReader) + cmd := commands.PackageBuildpack(config.logger, config.clientConfig, config.buildpackPackager, config.packageConfigReader) cmd.SetArgs([]string{config.imageName}) err := cmd.Execute() @@ -294,8 +326,9 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { logger := logging.NewLogWithWriters(&bytes.Buffer{}, &bytes.Buffer{}) configReader := fakes.NewFakePackageConfigReader() buildpackPackager := &fakes.FakeBuildpackPackager{} + clientConfig := config.Config{} - command := commands.PackageBuildpack(logger, buildpackPackager, configReader) + command := commands.PackageBuildpack(logger, clientConfig, buildpackPackager, configReader) command.SetArgs([]string{ "some-image-name", "--config", "/path/to/some/file", @@ -306,13 +339,36 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) { h.AssertError(t, command.Execute(), "parsing pull policy") }) }) + + when("--os flag is specified but experimental isn't set in the config", func() { + it("errors with a descriptive message", func() { + fakeBuildpackPackager := &fakes.FakeBuildpackPackager{} + + packageBuildpackCommand := packageBuildpackCommand( + withBuildpackPackager(fakeBuildpackPackager), + ) + + packageBuildpackCommand.SetArgs( + []string{ + "some-image-name", + "--config", "/path/to/some/file", + "--os", "windows", + }, + ) + + err := packageBuildpackCommand.Execute() + h.AssertNotNil(t, err) + h.AssertError(t, err, "Support for OS flag is currently experimental") + }) + }) }) } type packageCommandConfig struct { - logger *logging.LogWithWriters - configReader *fakes.FakePackageConfigReader - buildpackPackager *fakes.FakeBuildpackPackager + logger *logging.LogWithWriters + packageConfigReader *fakes.FakePackageConfigReader + buildpackPackager *fakes.FakeBuildpackPackager + clientConfig config.Config imageName string configPath string @@ -322,9 +378,10 @@ type packageCommandOption func(config *packageCommandConfig) func packageBuildpackCommand(ops ...packageCommandOption) *cobra.Command { config := &packageCommandConfig{ - logger: logging.NewLogWithWriters(&bytes.Buffer{}, &bytes.Buffer{}), - configReader: fakes.NewFakePackageConfigReader(), - buildpackPackager: &fakes.FakeBuildpackPackager{}, + logger: logging.NewLogWithWriters(&bytes.Buffer{}, &bytes.Buffer{}), + packageConfigReader: fakes.NewFakePackageConfigReader(), + buildpackPackager: &fakes.FakeBuildpackPackager{}, + clientConfig: config.Config{}, imageName: "some-image-name", configPath: "/path/to/some/file", @@ -334,7 +391,7 @@ func packageBuildpackCommand(ops ...packageCommandOption) *cobra.Command { op(config) } - cmd := commands.PackageBuildpack(config.logger, config.buildpackPackager, config.configReader) + cmd := commands.PackageBuildpack(config.logger, config.clientConfig, config.buildpackPackager, config.packageConfigReader) cmd.SetArgs([]string{config.imageName, "--config", config.configPath}) return cmd @@ -346,9 +403,9 @@ func withLogger(logger *logging.LogWithWriters) packageCommandOption { } } -func withConfigReader(reader *fakes.FakePackageConfigReader) packageCommandOption { +func withPackageConfigReader(reader *fakes.FakePackageConfigReader) packageCommandOption { return func(config *packageCommandConfig) { - config.configReader = reader + config.packageConfigReader = reader } } @@ -364,12 +421,18 @@ func withImageName(name string) packageCommandOption { } } -func withConfigPath(path string) packageCommandOption { +func withPackageConfigPath(path string) packageCommandOption { return func(config *packageCommandConfig) { config.configPath = path } } +func withExperimental() packageCommandOption { + return func(config *packageCommandConfig) { + config.clientConfig.Experimental = true + } +} + func whereReadReturns(config pubbldpkg.Config, err error) func(*fakes.FakePackageConfigReader) { return func(r *fakes.FakePackageConfigReader) { r.ReadReturnConfig = config diff --git a/internal/layer/writer_factory.go b/internal/layer/writer_factory.go index 4faa116531..72d9c86948 100644 --- a/internal/layer/writer_factory.go +++ b/internal/layer/writer_factory.go @@ -2,9 +2,9 @@ package layer import ( "archive/tar" + "fmt" "io" - "github.com/buildpacks/imgutil" ilayer "github.com/buildpacks/imgutil/layer" "github.com/buildpacks/pack/internal/archive" @@ -14,13 +14,12 @@ type WriterFactory struct { os string } -func NewWriterFactory(image imgutil.Image) (*WriterFactory, error) { - os, err := image.OS() - if err != nil { - return nil, err +func NewWriterFactory(imageOS string) (*WriterFactory, error) { + if imageOS != "linux" && imageOS != "windows" { + return nil, fmt.Errorf("provided image OS '%s' must be either 'linux' or 'windows'", imageOS) } - return &WriterFactory{os: os}, nil + return &WriterFactory{os: imageOS}, nil } func (f *WriterFactory) NewWriter(fileWriter io.Writer) archive.TarWriter { diff --git a/internal/layer/writer_factory_test.go b/internal/layer/writer_factory_test.go index df1074702a..af9c5cfc59 100644 --- a/internal/layer/writer_factory_test.go +++ b/internal/layer/writer_factory_test.go @@ -4,7 +4,6 @@ import ( "archive/tar" "testing" - "github.com/buildpacks/imgutil/fakes" ilayer "github.com/buildpacks/imgutil/layer" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -18,11 +17,16 @@ func TestTarWriterFactory(t *testing.T) { } func testWriterFactory(t *testing.T, when spec.G, it spec.S) { + when("#NewWriterFactory", func() { + it("returns an error for invalid image OS", func() { + _, err := layer.NewWriterFactory("not-an-os") + h.AssertError(t, err, "provided image OS 'not-an-os' must be either 'linux' or 'windows'") + }) + }) + when("#NewWriter", func() { - it("returns a regular tar writer for posix-based images", func() { - image := fakes.NewImage("fake-image", "", nil) - image.SetPlatform("linux", "", "") - factory, err := layer.NewWriterFactory(image) + it("returns a regular tar writer for Linux", func() { + factory, err := layer.NewWriterFactory("linux") h.AssertNil(t, err) _, ok := factory.NewWriter(nil).(*tar.Writer) @@ -31,10 +35,8 @@ func testWriterFactory(t *testing.T, when spec.G, it spec.S) { } }) - it("returns a Windows layer writer for Windows-based images", func() { - image := fakes.NewImage("fake-image", "", nil) - image.SetPlatform("windows", "", "") - factory, err := layer.NewWriterFactory(image) + it("returns a Windows layer writer for Windows", func() { + factory, err := layer.NewWriterFactory("windows") h.AssertNil(t, err) _, ok := factory.NewWriter(nil).(*ilayer.WindowsWriter) diff --git a/package_buildpack.go b/package_buildpack.go index f475c0ff58..1bd9a0a704 100644 --- a/package_buildpack.go +++ b/package_buildpack.go @@ -3,12 +3,12 @@ package pack import ( "context" - "github.com/buildpacks/pack/config" - "github.com/pkg/errors" + "github.com/buildpacks/pack/config" + "github.com/buildpacks/pack/internal/layer" + pubbldpkg "github.com/buildpacks/pack/buildpackage" - "github.com/buildpacks/pack/internal/archive" "github.com/buildpacks/pack/internal/buildpackage" "github.com/buildpacks/pack/internal/dist" "github.com/buildpacks/pack/internal/style" @@ -31,6 +31,9 @@ type PackageBuildpackOptions struct { // Type of output format, The options are the either the const FormatImage, or FormatFile. Format string + // Type of OCI image to generate in the image or file. The options are "windows" or "linux" + OS string + // Defines the Buildpacks configuration. Config pubbldpkg.Config @@ -44,12 +47,30 @@ type PackageBuildpackOptions struct { // PackageBuildpack packages buildpack(s) into either an image or file. func (c *Client) PackageBuildpack(ctx context.Context, opts PackageBuildpackOptions) error { - packageBuilder := buildpackage.NewBuilder(c.imageFactory) - if opts.Format == "" { opts.Format = FormatImage } + if opts.OS == "" { + osType, err := c.defaultOSType(ctx, opts.Publish, opts.Format) + if err != nil { + return errors.Wrap(err, "daemon OS cannot be detected") + } + + opts.OS = osType + } + + if opts.OS == "windows" && !c.experimental { + return NewExperimentError("Windows buildpackage support is currently experimental.") + } + + writerFactory, err := layer.NewWriterFactory(opts.OS) + if err != nil { + return errors.Wrap(err, "creating layer writer factory") + } + + packageBuilder := buildpackage.NewBuilder(c.imageFactory) + bpURI := opts.Config.Buildpack.URI if bpURI == "" { return errors.New("buildpack URI must be provided") @@ -60,7 +81,7 @@ func (c *Client) PackageBuildpack(ctx context.Context, opts PackageBuildpackOpti return errors.Wrapf(err, "downloading buildpack from %s", style.Symbol(bpURI)) } - bp, err := dist.BuildpackFromRootBlob(blob, archive.DefaultTarWriterFactory()) + bp, err := dist.BuildpackFromRootBlob(blob, writerFactory) if err != nil { return errors.Wrapf(err, "creating buildpack from %s", style.Symbol(bpURI)) } @@ -89,7 +110,7 @@ func (c *Client) PackageBuildpack(ctx context.Context, opts PackageBuildpackOpti depBPs = append([]dist.Buildpack{mainBP}, deps...) } else { - depBP, err := dist.BuildpackFromRootBlob(blob, archive.DefaultTarWriterFactory()) + depBP, err := dist.BuildpackFromRootBlob(blob, writerFactory) if err != nil { return errors.Wrapf(err, "creating buildpack from %s", style.Symbol(dep.URI)) } @@ -111,11 +132,26 @@ func (c *Client) PackageBuildpack(ctx context.Context, opts PackageBuildpackOpti switch opts.Format { case FormatFile: - return packageBuilder.SaveAsFile(opts.Name) + return packageBuilder.SaveAsFile(opts.Name, opts.OS) case FormatImage: - _, err = packageBuilder.SaveAsImage(opts.Name, opts.Publish) + _, err = packageBuilder.SaveAsImage(opts.Name, opts.Publish, opts.OS) return errors.Wrapf(err, "saving image") default: return errors.Errorf("unknown format: %s", style.Symbol(opts.Format)) } } + +func (c *Client) defaultOSType(ctx context.Context, publish bool, format string) (string, error) { + if publish || format == FormatFile { + c.logger.Warnf(`buildpackage OS unspecified - defaulting to "linux"`) + + return "linux", nil + } + + info, err := c.docker.Info(ctx) + if err != nil { + return "", err + } + + return info.OSType, nil +} diff --git a/package_buildpack_test.go b/package_buildpack_test.go index 99b8726e62..79e46ff454 100644 --- a/package_buildpack_test.go +++ b/package_buildpack_test.go @@ -9,7 +9,9 @@ import ( "path/filepath" "testing" - "github.com/buildpacks/pack/config" + "github.com/docker/docker/api/types" + + pubcfg "github.com/buildpacks/pack/config" "github.com/buildpacks/imgutil" "github.com/buildpacks/imgutil/fakes" @@ -44,6 +46,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { mockDownloader *testmocks.MockDownloader mockImageFactory *testmocks.MockImageFactory mockImageFetcher *testmocks.MockImageFetcher + mockDockerClient *testmocks.MockCommonAPIClient out bytes.Buffer ) @@ -52,6 +55,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { mockDownloader = testmocks.NewMockDownloader(mockController) mockImageFactory = testmocks.NewMockImageFactory(mockController) mockImageFetcher = testmocks.NewMockImageFetcher(mockController) + mockDockerClient = testmocks.NewMockCommonAPIClient(mockController) var err error subject, err = pack.NewClient( @@ -59,6 +63,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { pack.WithDownloader(mockDownloader), pack.WithImageFactory(mockImageFactory), pack.WithFetcher(mockImageFetcher), + pack.WithDockerClient(mockDockerClient), ) h.AssertNil(t, err) }) @@ -129,6 +134,8 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { dependencyPath := "fakePath.file" mockDownloader.EXPECT().Download(gomock.Any(), dependencyPath).Return(blob.NewBlob("no-file.txt"), nil).AnyTimes() + mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() + packageDescriptor := dist.BuildpackDescriptor{ API: api.MustParse("0.2"), Info: dist.BuildpackInfo{ID: "bp.1", Version: "1.2.3"}, @@ -147,7 +154,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: dependencyPath}}}, }, Publish: false, - PullPolicy: config.PullAlways, + PullPolicy: pubcfg.PullAlways, }) h.AssertError(t, err, "inspecting buildpack blob") @@ -156,6 +163,61 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { }) when("FormatImage", func() { + when("simple package for both OS formats (experimental only)", func() { + it("creates package image based on daemon OS", func() { + for _, daemonOS := range []string{"linux", "windows"} { + localMockDockerClient := testmocks.NewMockCommonAPIClient(mockController) + localMockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: daemonOS}, nil).AnyTimes() + + packClientWithExperimental, err := pack.NewClient( + pack.WithDockerClient(localMockDockerClient), + pack.WithDownloader(mockDownloader), + pack.WithImageFactory(mockImageFactory), + pack.WithExperimental(true), + ) + h.AssertNil(t, err) + + fakeImage := fakes.NewImage("basic/package-"+h.RandString(12), "", nil) + mockImageFactory.EXPECT().NewImage(fakeImage.Name(), true).Return(fakeImage, nil) + + fakeBlob := blob.NewBlob(filepath.Join("testdata", "empty-file")) + bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12)) + mockDownloader.EXPECT().Download(gomock.Any(), bpURL).Return(fakeBlob, nil).AnyTimes() + + h.AssertNil(t, packClientWithExperimental.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ + Format: pack.FormatImage, + Name: fakeImage.Name(), + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: createBuildpack(dist.BuildpackDescriptor{ + API: api.MustParse("0.2"), + Info: dist.BuildpackInfo{ID: "bp.basic", Version: "2.3.4"}, + Stacks: []dist.Stack{{ID: "some.stack.id"}}, + })}, + }, + PullPolicy: pubcfg.PullNever, + })) + + actualImageOS, err := fakeImage.OS() + h.AssertNil(t, err) + h.AssertEq(t, actualImageOS, daemonOS) + } + }) + + it("fails without experimental on Windows daemons", func() { + windowsMockDockerClient := testmocks.NewMockCommonAPIClient(mockController) + windowsMockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "windows"}, nil).AnyTimes() + + packClientWithoutExperimental, err := pack.NewClient( + pack.WithDockerClient(windowsMockDockerClient), + pack.WithExperimental(false), + ) + h.AssertNil(t, err) + + err = packClientWithoutExperimental.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{}) + h.AssertError(t, err, "Windows buildpackage support is currently experimental.") + }) + }) + when("nested package lives in registry", func() { var nestedPackage *fakes.Image @@ -163,6 +225,8 @@ 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).Return(nestedPackage, nil) + mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() + h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ Name: nestedPackage.Name(), Config: pubbldpkg.Config{ @@ -173,15 +237,15 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { })}, }, Publish: true, - PullPolicy: config.PullAlways, + PullPolicy: pubcfg.PullAlways, })) }) - shouldFetchNestedPackage := func(demon bool, pull config.PullPolicy) { + shouldFetchNestedPackage := func(demon bool, pull pubcfg.PullPolicy) { mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), demon, pull).Return(nestedPackage, nil) } - shouldNotFindNestedPackageWhenCallingImageFetcherWith := func(demon bool, pull config.PullPolicy) { + shouldNotFindNestedPackageWhenCallingImageFetcherWith := func(demon bool, pull pubcfg.PullPolicy) { mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), demon, pull).Return(nil, image.ErrNotFound) } @@ -199,7 +263,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { when("publish=false and no-pull=false", func() { it("should pull and use local nested package image", func() { - shouldFetchNestedPackage(true, config.PullAlways) + shouldFetchNestedPackage(true, pubcfg.PullAlways) packageImage := shouldCreateLocalPackage() h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ @@ -218,14 +282,14 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}}, }, Publish: false, - PullPolicy: config.PullAlways, + PullPolicy: pubcfg.PullAlways, })) }) }) when("publish=true and no-pull=false", func() { it("should use remote nested package image", func() { - shouldFetchNestedPackage(false, config.PullAlways) + shouldFetchNestedPackage(false, pubcfg.PullAlways) packageImage := shouldCreateRemotePackage() h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ @@ -244,14 +308,14 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}}, }, Publish: true, - PullPolicy: config.PullAlways, + PullPolicy: pubcfg.PullAlways, })) }) }) when("publish=true and pull-policy=never", func() { it("should push to registry and not pull nested package image", func() { - shouldFetchNestedPackage(false, config.PullNever) + shouldFetchNestedPackage(false, pubcfg.PullNever) packageImage := shouldCreateRemotePackage() h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ @@ -270,14 +334,14 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}}, }, Publish: true, - PullPolicy: config.PullNever, + PullPolicy: pubcfg.PullNever, })) }) }) when("publish=false pull-policy=never and there is no local image", func() { it("should fail without trying to retrieve nested image from registry", func() { - shouldNotFindNestedPackageWhenCallingImageFetcherWith(true, config.PullNever) + shouldNotFindNestedPackageWhenCallingImageFetcherWith(true, pubcfg.PullNever) h.AssertError(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ Name: "some/package", @@ -290,7 +354,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}}, }, Publish: false, - PullPolicy: config.PullNever, + PullPolicy: pubcfg.PullNever, }), "not found") }) }) @@ -299,7 +363,9 @@ 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(), true, config.PullAlways).Return(notPackageImage, nil) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), notPackageImage.Name(), true, pubcfg.PullAlways).Return(notPackageImage, nil) + + mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() h.AssertError(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ Name: "some/package", @@ -312,233 +378,277 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: notPackageImage.Name()}}}, }, Publish: false, - PullPolicy: config.PullAlways, + PullPolicy: pubcfg.PullAlways, }), "extracting buildpacks from 'not/package': could not find label 'io.buildpacks.buildpackage.metadata'") }) }) }) when("FormatFile", func() { - var ( - nestedPackage *fakes.Image - childDescriptor dist.BuildpackDescriptor - packageDescriptor dist.BuildpackDescriptor - tmpDir string - err error - ) + when("simple package for both OS formats (experimental only)", func() { + it("creates package image in either OS format", func() { + tmpDir, err := ioutil.TempDir("", "package-buildpack") + h.AssertNil(t, err) + defer os.Remove(tmpDir) + + for _, imageOS := range []string{"linux", "windows"} { + localMockDockerClient := testmocks.NewMockCommonAPIClient(mockController) + localMockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: imageOS}, nil).AnyTimes() + + packClientWithExperimental, err := pack.NewClient( + pack.WithDockerClient(localMockDockerClient), + pack.WithLogger(logging.NewLogWithWriters(&out, &out)), + pack.WithDownloader(mockDownloader), + pack.WithExperimental(true), + ) + h.AssertNil(t, err) - it.Before(func() { - childDescriptor = dist.BuildpackDescriptor{ - API: api.MustParse("0.2"), - Info: dist.BuildpackInfo{ID: "bp.nested", Version: "2.3.4"}, - Stacks: []dist.Stack{{ID: "some.stack.id"}}, - } - - packageDescriptor = dist.BuildpackDescriptor{ - API: api.MustParse("0.2"), - Info: dist.BuildpackInfo{ID: "bp.1", Version: "1.2.3"}, - Order: dist.Order{{ - Group: []dist.BuildpackRef{{ - BuildpackInfo: dist.BuildpackInfo{ID: "bp.nested", Version: "2.3.4"}, - Optional: false, - }}, - }}, - } + fakeBlob := blob.NewBlob(filepath.Join("testdata", "empty-file")) + bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12)) + mockDownloader.EXPECT().Download(gomock.Any(), bpURL).Return(fakeBlob, nil).AnyTimes() - tmpDir, err = ioutil.TempDir("", "package-buildpack") - h.AssertNil(t, err) + packagePath := filepath.Join(tmpDir, h.RandString(12)+"-test.cnb") + h.AssertNil(t, packClientWithExperimental.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ + Format: pack.FormatFile, + Name: packagePath, + OS: imageOS, + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: createBuildpack(dist.BuildpackDescriptor{ + API: api.MustParse("0.2"), + Info: dist.BuildpackInfo{ID: "bp.basic", Version: "2.3.4"}, + Stacks: []dist.Stack{{ID: "some.stack.id"}}, + })}, + }, + PullPolicy: pubcfg.PullNever, + })) + } + }) }) - it.After(func() { - h.AssertNil(t, os.RemoveAll(tmpDir)) - }) + when("nested package", func() { + var ( + nestedPackage *fakes.Image + childDescriptor dist.BuildpackDescriptor + packageDescriptor dist.BuildpackDescriptor + tmpDir string + err error + ) - 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).Return(nestedPackage, nil) + childDescriptor = dist.BuildpackDescriptor{ + API: api.MustParse("0.2"), + Info: dist.BuildpackInfo{ID: "bp.nested", Version: "2.3.4"}, + Stacks: []dist.Stack{{ID: "some.stack.id"}}, + } - h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ - Name: nestedPackage.Name(), - Config: pubbldpkg.Config{ - Buildpack: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}, - }, - Publish: true, - PullPolicy: config.PullAlways, - })) + packageDescriptor = dist.BuildpackDescriptor{ + API: api.MustParse("0.2"), + Info: dist.BuildpackInfo{ID: "bp.1", Version: "1.2.3"}, + Order: dist.Order{{ + Group: []dist.BuildpackRef{{ + BuildpackInfo: dist.BuildpackInfo{ID: "bp.nested", Version: "2.3.4"}, + Optional: false, + }}, + }}, + } - mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), true, config.PullAlways).Return(nestedPackage, nil) + tmpDir, err = ioutil.TempDir("", "package-buildpack") + h.AssertNil(t, err) }) - it("should pull and use local nested package image", func() { - packagePath := filepath.Join(tmpDir, "test.cnb") - - h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ - Name: packagePath, - Config: pubbldpkg.Config{ - Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, - Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}}, - }, - Publish: false, - PullPolicy: config.PullAlways, - Format: pack.FormatFile, - })) - - assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor}) + it.After(func() { + h.AssertNil(t, os.RemoveAll(tmpDir)) }) - }) - - when("dependencies are unpackaged buildpack", func() { - it("should work", func() { - packagePath := filepath.Join(tmpDir, "test.cnb") - h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ - Name: packagePath, - Config: pubbldpkg.Config{ - Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, - Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}}}, - }, - Publish: false, - PullPolicy: config.PullAlways, - Format: pack.FormatFile, - })) + 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).Return(nestedPackage, nil) - assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor}) - }) + h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ + Name: nestedPackage.Name(), + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}, + }, + Publish: true, + PullPolicy: pubcfg.PullAlways, + })) - when("dependency download fails", func() { - it("should error", func() { - bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12)) - mockDownloader.EXPECT().Download(gomock.Any(), bpURL).Return(nil, image.ErrNotFound).AnyTimes() + mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), true, pubcfg.PullAlways).Return(nestedPackage, nil) + }) + it("should pull and use local nested package image", func() { packagePath := filepath.Join(tmpDir, "test.cnb") - err = subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ + h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ Name: packagePath, Config: pubbldpkg.Config{ Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, - Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: bpURL}}}, + Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}}, }, Publish: false, - PullPolicy: config.PullAlways, + PullPolicy: pubcfg.PullAlways, Format: pack.FormatFile, - }) - h.AssertError(t, err, "downloading buildpack") + })) + + assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor}) }) }) - when("dependency isn't a valid buildpack", func() { - it("should error", func() { - fakeBlob := blob.NewBlob(filepath.Join("testdata", "empty-file")) - bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12)) - mockDownloader.EXPECT().Download(gomock.Any(), bpURL).Return(fakeBlob, nil).AnyTimes() - + when("dependencies are unpackaged buildpack", func() { + it("should work", func() { packagePath := filepath.Join(tmpDir, "test.cnb") - err = subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ + h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ Name: packagePath, Config: pubbldpkg.Config{ Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, - Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: bpURL}}}, + Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}}}, }, Publish: false, - PullPolicy: config.PullAlways, + PullPolicy: pubcfg.PullAlways, Format: pack.FormatFile, + })) + + assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor}) + }) + + when("dependency download fails", func() { + it("should error", func() { + bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12)) + mockDownloader.EXPECT().Download(gomock.Any(), bpURL).Return(nil, image.ErrNotFound).AnyTimes() + + packagePath := filepath.Join(tmpDir, "test.cnb") + + err = subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ + Name: packagePath, + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, + Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: bpURL}}}, + }, + Publish: false, + PullPolicy: pubcfg.PullAlways, + Format: pack.FormatFile, + }) + h.AssertError(t, err, "downloading buildpack") + }) + }) + + when("dependency isn't a valid buildpack", func() { + it("should error", func() { + fakeBlob := blob.NewBlob(filepath.Join("testdata", "empty-file")) + bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12)) + mockDownloader.EXPECT().Download(gomock.Any(), bpURL).Return(fakeBlob, nil).AnyTimes() + + packagePath := filepath.Join(tmpDir, "test.cnb") + + err = subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ + Name: packagePath, + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, + Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: bpURL}}}, + }, + Publish: false, + PullPolicy: pubcfg.PullAlways, + Format: pack.FormatFile, + }) + h.AssertError(t, err, "creating buildpack") }) - h.AssertError(t, err, "creating buildpack") }) }) - }) - when("dependencies include packaged buildpack image and unpacked buildpack", func() { - var secondChildDescriptor dist.BuildpackDescriptor + when("dependencies include packaged buildpack image and unpacked buildpack", func() { + var secondChildDescriptor dist.BuildpackDescriptor - it.Before(func() { - secondChildDescriptor = dist.BuildpackDescriptor{ - API: api.MustParse("0.2"), - Info: dist.BuildpackInfo{ID: "bp.nested1", Version: "2.3.4"}, - Stacks: []dist.Stack{{ID: "some.stack.id"}}, - } + it.Before(func() { + secondChildDescriptor = dist.BuildpackDescriptor{ + API: api.MustParse("0.2"), + Info: dist.BuildpackInfo{ID: "bp.nested1", Version: "2.3.4"}, + Stacks: []dist.Stack{{ID: "some.stack.id"}}, + } - packageDescriptor.Order = append(packageDescriptor.Order, dist.OrderEntry{Group: []dist.BuildpackRef{{ - BuildpackInfo: dist.BuildpackInfo{ID: secondChildDescriptor.Info.ID, Version: secondChildDescriptor.Info.Version}, - Optional: false, - }}}) + packageDescriptor.Order = append(packageDescriptor.Order, dist.OrderEntry{Group: []dist.BuildpackRef{{ + BuildpackInfo: dist.BuildpackInfo{ID: secondChildDescriptor.Info.ID, Version: secondChildDescriptor.Info.Version}, + Optional: false, + }}}) - nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false).Return(nestedPackage, nil) + nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", nil) + mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false).Return(nestedPackage, nil) - h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ - Name: nestedPackage.Name(), - Config: pubbldpkg.Config{ - Buildpack: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}, - }, - Publish: true, - PullPolicy: config.PullAlways, - })) + h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ + Name: nestedPackage.Name(), + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}, + }, + Publish: true, + PullPolicy: pubcfg.PullAlways, + })) - mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), true, config.PullAlways).Return(nestedPackage, nil) - }) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), true, pubcfg.PullAlways).Return(nestedPackage, nil) + }) - it("should include both of them", func() { - packagePath := filepath.Join(tmpDir, "test.cnb") + it("should include both of them", func() { + packagePath := filepath.Join(tmpDir, "test.cnb") - h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ - Name: packagePath, - Config: pubbldpkg.Config{ - Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, - Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}, - {BuildpackURI: dist.BuildpackURI{URI: createBuildpack(secondChildDescriptor)}}}, - }, - Publish: false, - PullPolicy: config.PullAlways, - Format: pack.FormatFile, - })) + h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ + Name: packagePath, + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, + Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}, + {BuildpackURI: dist.BuildpackURI{URI: createBuildpack(secondChildDescriptor)}}}, + }, + Publish: false, + PullPolicy: pubcfg.PullAlways, + Format: pack.FormatFile, + })) - assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor, secondChildDescriptor}) + assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor, secondChildDescriptor}) + }) }) - }) - when("dependencies include a packaged buildpack file", func() { - var ( - dependencyPackagePath string - ) - it.Before(func() { - dependencyPackagePath = filepath.Join(tmpDir, "dep.cnb") + when("dependencies include a packaged buildpack file", func() { + var ( + dependencyPackagePath string + ) + it.Before(func() { + dependencyPackagePath = filepath.Join(tmpDir, "dep.cnb") - h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ - Name: dependencyPackagePath, - Config: pubbldpkg.Config{ - Buildpack: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}, - }, - PullPolicy: config.PullAlways, - Format: pack.FormatFile, - })) + h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ + Name: dependencyPackagePath, + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}, + }, + PullPolicy: pubcfg.PullAlways, + Format: pack.FormatFile, + })) - mockDownloader.EXPECT().Download(gomock.Any(), dependencyPackagePath).Return(blob.NewBlob(dependencyPackagePath), nil).AnyTimes() - }) + mockDownloader.EXPECT().Download(gomock.Any(), dependencyPackagePath).Return(blob.NewBlob(dependencyPackagePath), nil).AnyTimes() + }) - it("should open file and correctly add buildpacks", func() { - packagePath := filepath.Join(tmpDir, "test.cnb") + it("should open file and correctly add buildpacks", func() { + packagePath := filepath.Join(tmpDir, "test.cnb") - h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ - Name: packagePath, - Config: pubbldpkg.Config{ - Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, - Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: dependencyPackagePath}}}, - }, - Publish: false, - PullPolicy: config.PullAlways, - Format: pack.FormatFile, - })) + h.AssertNil(t, subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ + Name: packagePath, + Config: pubbldpkg.Config{ + Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, + Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: dependencyPackagePath}}}, + }, + Publish: false, + PullPolicy: pubcfg.PullAlways, + Format: pack.FormatFile, + })) - assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor}) + assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor}) + }) }) }) }) when("unknown format is provided", func() { it("should error", func() { + mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() + err := subject.PackageBuildpack(context.TODO(), pack.PackageBuildpackOptions{ Name: "some-buildpack", Format: "invalid-format", @@ -550,7 +660,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { })}, }, Publish: false, - PullPolicy: config.PullAlways, + PullPolicy: pubcfg.PullAlways, }) h.AssertError(t, err, "unknown format: 'invalid-format'") })