diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 935f41a196..237e5c685f 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -127,6 +127,7 @@ func testWithoutSpecificBuilderRequirement( it.Before(func() { pack = invoke.NewPackInvoker(t, assert, packConfig, registryConfig.DockerConfigDir) + pack.EnableExperimental() buildpackManager = buildpacks.NewBuildpackManager(t, assert) }) @@ -242,8 +243,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) @@ -329,6 +328,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( @@ -336,8 +337,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) @@ -348,6 +350,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) @@ -2119,70 +2119,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...) @@ -2253,30 +2251,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...) @@ -2296,11 +2284,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 46b75a6109..8c61dbcc6c 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 58af0ca10e..2a45774e82 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 9bb35b3aa9..c7d97f5308 100644 --- a/create_builder.go +++ b/create_builder.go @@ -262,7 +262,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 c62a44b774..2c243bd7d2 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 de4d15ac87..3f426c8562 100644 --- a/go.sum +++ b/go.sum @@ -63,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= @@ -163,7 +163,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= @@ -172,7 +171,6 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 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.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= @@ -194,8 +192,6 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -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= @@ -234,7 +230,6 @@ github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/heroku/color v0.0.6 h1:UTFFMrmMLFcL3OweqP1lAdp8i1y/9oHqkeHjQ/b/Ny0= github.com/heroku/color v0.0.6/go.mod h1:ZBvOcx7cTF2QKOv4LbmoBtNl5uB17qWxGuzZrsi1wLU= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= @@ -414,7 +409,6 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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= @@ -479,7 +473,6 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 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.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -552,13 +545,11 @@ golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/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= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -586,7 +577,6 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200210192313-1ace956b0e17/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -634,7 +624,6 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= @@ -657,7 +646,6 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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 d3a3d68f60..c1319278e5 100644 --- a/internal/commands/package_buildpack.go +++ b/internal/commands/package_buildpack.go @@ -3,13 +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" pubbldpkg "github.com/buildpacks/pack/buildpackage" + "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/logging" ) @@ -18,6 +19,7 @@ import ( type PackageBuildpackFlags struct { PackageTomlPath string Format string + OS string Publish bool Policy string } @@ -33,7 +35,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{ @@ -41,12 +43,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); err != nil { + if err := validatePackageBuildpackFlags(&flags, cfg); 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") } @@ -55,7 +57,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") } @@ -64,7 +66,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 { @@ -83,15 +86,19 @@ 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") AddHelpFlag(cmd, "package-buildpack") return cmd } -func validatePackageBuildpackFlags(p *PackageBuildpackFlags) error { - if p.Publish && p.Policy == config.PullNever.String() { +func validatePackageBuildpackFlags(p *PackageBuildpackFlags, cfg config.Config) 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.") } @@ -99,5 +106,9 @@ func validatePackageBuildpackFlags(p *PackageBuildpackFlags) error { return errors.Errorf("Please provide a package config path, using --config") } + 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 a39b6e7bf1..206ff18dc4 100644 --- a/internal/commands/package_buildpack_test.go +++ b/internal/commands/package_buildpack_test.go @@ -5,8 +5,6 @@ import ( "fmt" "testing" - "github.com/buildpacks/pack/config" - "github.com/heroku/color" "github.com/pkg/errors" "github.com/sclevine/spec" @@ -14,8 +12,10 @@ import ( "github.com/spf13/cobra" pubbldpkg "github.com/buildpacks/pack/buildpackage" + pubcfg "github.com/buildpacks/pack/config" "github.com/buildpacks/pack/internal/commands" "github.com/buildpacks/pack/internal/commands/fakes" + "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/internal/dist" "github.com/buildpacks/pack/internal/logging" h "github.com/buildpacks/pack/testhelpers" @@ -32,17 +32,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 +70,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() @@ -108,7 +108,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() { @@ -119,7 +119,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") + }) }) }) }) @@ -131,8 +159,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", @@ -153,7 +182,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)), ), ) @@ -169,15 +198,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() @@ -188,14 +217,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() @@ -208,8 +237,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", @@ -220,13 +250,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 @@ -236,9 +289,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", @@ -248,7 +302,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 @@ -260,9 +314,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 } } @@ -278,12 +332,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 dcf2d54f3e..6d4e1bd399 100644 --- a/package_buildpack.go +++ b/package_buildpack.go @@ -7,10 +7,10 @@ import ( pubbldpkg "github.com/buildpacks/pack/buildpackage" "github.com/buildpacks/pack/config" - "github.com/buildpacks/pack/internal/archive" "github.com/buildpacks/pack/internal/buildpack" "github.com/buildpacks/pack/internal/buildpackage" "github.com/buildpacks/pack/internal/dist" + "github.com/buildpacks/pack/internal/layer" "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)) } @@ -99,7 +120,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)) } @@ -123,11 +144,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 95feee0fa6..ef9a48c441 100644 --- a/package_buildpack_test.go +++ b/package_buildpack_test.go @@ -9,11 +9,10 @@ import ( "path/filepath" "testing" - "github.com/buildpacks/pack/config" - "github.com/buildpacks/imgutil" "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/sclevine/spec" @@ -21,6 +20,7 @@ import ( "github.com/buildpacks/pack" pubbldpkg "github.com/buildpacks/pack/buildpackage" + pubcfg "github.com/buildpacks/pack/config" "github.com/buildpacks/pack/internal/blob" "github.com/buildpacks/pack/internal/buildpackage" "github.com/buildpacks/pack/internal/dist" @@ -44,6 +44,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 +53,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 +61,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 +132,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 +152,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 +161,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 +223,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 +235,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 +261,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { when("publish=false and pull-policy=always", 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 +280,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 pull-policy=always", 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 +306,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 +332,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 +352,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 +361,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 +376,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 +658,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'") })