Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support pack build --platform #2162

Merged
merged 9 commits into from
May 23, 2024
15 changes: 15 additions & 0 deletions acceptance/acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2699,6 +2699,21 @@ include = [ "*.jar", "media/mountain.jpg", "/media/person.png", ]
})
})
})

when("--platform", func() {
it.Before(func() {
h.SkipIf(t, !pack.SupportsFeature(invoke.PlatformOption), "")
})

it("uses the builder with the desired platform", func() {
output, _ := pack.Run(
"build", repoName,
"-p", filepath.Join("testdata", "mock_app"),
"--platform", "linux/not-exist-arch",
)
h.AssertContainsMatch(t, output, "Pulling image '.*test/builder.*' with platform 'linux/not-exist-arch")
})
})
})

when("build --buildpack <flattened buildpack>", func() {
Expand Down
4 changes: 4 additions & 0 deletions acceptance/invoke/pack.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ const (
FlattenBuilderCreationV2
FixesRunImageMetadata
ManifestCommands
PlatformOption
MultiPlatformBuildersAndBuildPackages
)

Expand Down Expand Up @@ -279,6 +280,9 @@ var featureTests = map[Feature]func(i *PackInvoker) bool{
ManifestCommands: func(i *PackInvoker) bool {
return i.atLeast("v0.34.0")
},
PlatformOption: func(i *PackInvoker) bool {
return i.atLeast("v0.34.0")
},
MultiPlatformBuildersAndBuildPackages: func(i *PackInvoker) bool {
return i.atLeast("v0.34.0")
},
Expand Down
5 changes: 5 additions & 0 deletions internal/commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type BuildFlags struct {
Builder string
Registry string
RunImage string
Platform string
Policy string
Network string
DescriptorPath string
Expand Down Expand Up @@ -132,6 +133,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
if err != nil {
return errors.Wrapf(err, "parsing pull policy %s", flags.Policy)
}

var lifecycleImage string
if flags.LifecycleImage != "" {
ref, err := name.ParseReference(flags.LifecycleImage)
Expand All @@ -140,6 +142,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
}
lifecycleImage = ref.Name()
}

var gid = -1
if cmd.Flags().Changed("gid") {
gid = flags.GID
Expand All @@ -165,6 +168,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
Image: inputImageName.Name(),
Publish: flags.Publish,
DockerHost: flags.DockerHost,
Platform: flags.Platform,
PullPolicy: pullPolicy,
ClearCache: flags.ClearCache,
TrustBuilder: func(string) bool {
Expand Down Expand Up @@ -257,6 +261,7 @@ Special value 'inherit' may be used in which case DOCKER_HOST environment variab
This option may set DOCKER_HOST environment variable for the build container if needed.
`)
cmd.Flags().StringVar(&buildFlags.LifecycleImage, "lifecycle-image", cfg.LifecycleImage, `Custom lifecycle image to use for analysis, restore, and export when builder is untrusted.`)
cmd.Flags().StringVar(&buildFlags.Platform, "platform", "", `Platform to build on (e.g., "linux/amd64").`)
cmd.Flags().StringVar(&buildFlags.Policy, "pull-policy", "", `Pull policy to use. Accepted values are always, never, and if-not-present. (default "always")`)
cmd.Flags().StringVarP(&buildFlags.Registry, "buildpack-registry", "r", cfg.DefaultRegistryName, "Buildpack Registry by name")
cmd.Flags().StringVar(&buildFlags.RunImage, "run-image", "", "Run image (defaults to default stack's run image)")
Expand Down
20 changes: 20 additions & 0 deletions internal/commands/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ func testBuildCommand(t *testing.T, when spec.G, it spec.S) {
})
})

when("--platform", func() {
it("sets platform", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithPlatform("linux/amd64")).
Return(nil)

command.SetArgs([]string{"image", "--builder", "my-builder", "--platform", "linux/amd64"})
h.AssertNil(t, command.Execute())
})
})

when("--pull-policy", func() {
it("sets pull-policy=never", func() {
mockClient.EXPECT().
Expand Down Expand Up @@ -958,6 +969,15 @@ func EqBuildOptionsDefaultProcess(defaultProc string) gomock.Matcher {
}
}

func EqBuildOptionsWithPlatform(platform string) gomock.Matcher {
return buildOptionsMatcher{
description: fmt.Sprintf("Platform=%s", platform),
equals: func(o client.BuildOptions) bool {
return o.Platform == platform
},
}
}

func EqBuildOptionsWithPullPolicy(policy image.PullPolicy) gomock.Matcher {
return buildOptionsMatcher{
description: fmt.Sprintf("PullPolicy=%s", policy),
Expand Down
105 changes: 70 additions & 35 deletions pkg/client/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@
// Process type that will be used when setting container start command.
DefaultProcessType string

// Platform is the desired platform to build on (e.g., linux/amd64)
Platform string

// Strategy for updating local images before a build.
PullPolicy image.PullPolicy

Expand Down Expand Up @@ -320,32 +323,54 @@
return errors.Wrapf(err, "invalid builder '%s'", opts.Builder)
}

rawBuilderImage, err := c.imageFetcher.Fetch(ctx, builderRef.Name(), image.FetchOptions{Daemon: true, PullPolicy: opts.PullPolicy})
requestedTarget := func() *dist.Target {
if opts.Platform == "" {
return nil
}
parts := strings.Split(opts.Platform, "/")
switch len(parts) {
case 0:
return nil
case 1:
return &dist.Target{OS: parts[0]}

Check warning on line 335 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L332-L335

Added lines #L332 - L335 were not covered by tests
case 2:
return &dist.Target{OS: parts[0], Arch: parts[1]}
default:
return &dist.Target{OS: parts[0], Arch: parts[1], ArchVariant: parts[2]}

Check warning on line 339 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L338-L339

Added lines #L338 - L339 were not covered by tests
}
}()

rawBuilderImage, err := c.imageFetcher.Fetch(
ctx,
builderRef.Name(),
image.FetchOptions{
Daemon: true,
Target: requestedTarget,
PullPolicy: opts.PullPolicy},
)
if err != nil {
return errors.Wrapf(err, "failed to fetch builder image '%s'", builderRef.Name())
}

builderOS, err := rawBuilderImage.OS()
if err != nil {
return errors.Wrapf(err, "getting builder OS")
}

builderArch, err := rawBuilderImage.Architecture()
if err != nil {
return errors.Wrapf(err, "getting builder architecture")
var targetToUse *dist.Target
if requestedTarget != nil {
targetToUse = requestedTarget
} else {
targetToUse, err = getTargetFromBuilder(rawBuilderImage)
if err != nil {
return err
}

Check warning on line 362 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L361-L362

Added lines #L361 - L362 were not covered by tests
}

bldr, err := c.getBuilder(rawBuilderImage)
if err != nil {
return errors.Wrapf(err, "invalid builder %s", style.Symbol(opts.Builder))
}

target := &dist.Target{OS: builderOS, Arch: builderArch}

fetchOptions := image.FetchOptions{
Daemon: !opts.Publish,
PullPolicy: opts.PullPolicy,
Target: target,
Target: targetToUse,
}
runImageName := c.resolveRunImage(opts.RunImage, imgRegistry, builderRef.Context().RegistryStr(), bldr.DefaultRunImage(), opts.AdditionalMirrors, opts.Publish, fetchOptions)

Expand Down Expand Up @@ -374,12 +399,12 @@
return err
}

fetchedBPs, order, err := c.processBuildpacks(ctx, bldr.Image(), bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts)
fetchedBPs, order, err := c.processBuildpacks(ctx, bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts, targetToUse)
if err != nil {
return err
}

fetchedExs, orderExtensions, err := c.processExtensions(ctx, bldr.Image(), bldr.Extensions(), bldr.OrderExtensions(), bldr.StackID, opts)
fetchedExs, orderExtensions, err := c.processExtensions(ctx, bldr.Extensions(), opts, targetToUse)
if err != nil {
return err
}
Expand Down Expand Up @@ -420,7 +445,7 @@
image.FetchOptions{
Daemon: true,
PullPolicy: opts.PullPolicy,
Target: target,
Target: targetToUse,
},
)
if err != nil {
Expand Down Expand Up @@ -492,7 +517,7 @@
defer c.docker.ImageRemove(context.Background(), ephemeralBuilder.Name(), types.RemoveOptions{Force: true})

if len(bldr.OrderExtensions()) > 0 || len(ephemeralBuilder.OrderExtensions()) > 0 {
if builderOS == "windows" {
if targetToUse.OS == "windows" {
return fmt.Errorf("builder contains image extensions which are not supported for Windows builds")
}
if !(opts.PullPolicy == image.PullAlways) {
Expand All @@ -504,7 +529,7 @@
opts.ContainerConfig.Volumes = appendLayoutVolumes(opts.ContainerConfig.Volumes, pathsConfig)
}

processedVolumes, warnings, err := processVolumes(builderOS, opts.ContainerConfig.Volumes)
processedVolumes, warnings, err := processVolumes(targetToUse.OS, opts.ContainerConfig.Volumes)
if err != nil {
return err
}
Expand Down Expand Up @@ -735,6 +760,26 @@
return c.logImageNameAndSha(ctx, opts.Publish, imageRef)
}

func getTargetFromBuilder(builderImage imgutil.Image) (*dist.Target, error) {
builderOS, err := builderImage.OS()
if err != nil {
return nil, fmt.Errorf("failed to get builder OS: %w", err)
}

Check warning on line 767 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L766-L767

Added lines #L766 - L767 were not covered by tests
builderArch, err := builderImage.Architecture()
if err != nil {
return nil, fmt.Errorf("failed to get builder architecture: %w", err)
}

Check warning on line 771 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L770-L771

Added lines #L770 - L771 were not covered by tests
builderArchVariant, err := builderImage.Variant()
if err != nil {
return nil, fmt.Errorf("failed to get builder architecture variant: %w", err)
}

Check warning on line 775 in pkg/client/build.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/build.go#L774-L775

Added lines #L774 - L775 were not covered by tests
return &dist.Target{
OS: builderOS,
Arch: builderArch,
ArchVariant: builderArchVariant,
}, nil
}

func extractSupportedLifecycleApis(labels map[string]string) ([]string, error) {
// sample contents of labels:
// {io.buildpacks.builder.metadata:\"{\"lifecycle\":{\"version\":\"0.15.3\"},\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}}",
Expand Down Expand Up @@ -1087,7 +1132,7 @@
// ----------
// - group:
// - A
func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Image, builderBPs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions) (fetchedBPs []buildpack.BuildModule, order dist.Order, err error) {
func (c *Client) processBuildpacks(ctx context.Context, builderBPs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions, targetToUse *dist.Target) (fetchedBPs []buildpack.BuildModule, order dist.Order, err error) {
relativeBaseDir := opts.RelativeBaseDir
declaredBPs := opts.Buildpacks

Expand Down Expand Up @@ -1130,7 +1175,7 @@
order = newOrder
}
default:
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderImage, builderBPs, opts, buildpack.KindBuildpack)
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse)
if err != nil {
return fetchedBPs, order, err
}
Expand Down Expand Up @@ -1164,7 +1209,7 @@
if len(preBuildpacks) > 0 || len(postBuildpacks) > 0 {
order = builderOrder
for _, bp := range preBuildpacks {
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderImage, builderBPs, opts, buildpack.KindBuildpack)
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse)
if err != nil {
return fetchedBPs, order, err
}
Expand All @@ -1173,7 +1218,7 @@
}

for _, bp := range postBuildpacks {
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderImage, builderBPs, opts, buildpack.KindBuildpack)
newFetchedBPs, moduleInfo, err := c.fetchBuildpack(ctx, bp, relativeBaseDir, builderBPs, opts, buildpack.KindBuildpack, targetToUse)
if err != nil {
return fetchedBPs, order, err
}
Expand All @@ -1186,7 +1231,7 @@
return fetchedBPs, order, nil
}

func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir string, builderImage imgutil.Image, builderBPs []dist.ModuleInfo, opts BuildOptions, kind string) ([]buildpack.BuildModule, *dist.ModuleInfo, error) {
func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir string, builderBPs []dist.ModuleInfo, opts BuildOptions, kind string, targetToUse *dist.Target) ([]buildpack.BuildModule, *dist.ModuleInfo, error) {
pullPolicy := opts.PullPolicy
publish := opts.Publish
registry := opts.Registry
Expand All @@ -1206,19 +1251,9 @@
Version: version,
}
default:
builderOS, err := builderImage.OS()
if err != nil {
return nil, nil, errors.Wrapf(err, "getting builder OS")
}

builderArch, err := builderImage.Architecture()
if err != nil {
return nil, nil, errors.Wrapf(err, "getting builder architecture")
}

downloadOptions := buildpack.DownloadOptions{
RegistryName: registry,
Target: &dist.Target{OS: builderOS, Arch: builderArch},
Target: targetToUse,
RelativeBaseDir: relativeBaseDir,
Daemon: !publish,
PullPolicy: pullPolicy,
Expand Down Expand Up @@ -1322,7 +1357,7 @@
return newOrder
}

func (c *Client) processExtensions(ctx context.Context, builderImage imgutil.Image, builderExs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions) (fetchedExs []buildpack.BuildModule, orderExtensions dist.Order, err error) {
func (c *Client) processExtensions(ctx context.Context, builderExs []dist.ModuleInfo, opts BuildOptions, targetToUse *dist.Target) (fetchedExs []buildpack.BuildModule, orderExtensions dist.Order, err error) {
relativeBaseDir := opts.RelativeBaseDir
declaredExs := opts.Extensions

Expand All @@ -1339,7 +1374,7 @@
case buildpack.FromBuilderLocator:
return nil, nil, errors.New("from builder is not supported for extensions")
default:
newFetchedExs, moduleInfo, err := c.fetchBuildpack(ctx, ex, relativeBaseDir, builderImage, builderExs, opts, buildpack.KindExtension)
newFetchedExs, moduleInfo, err := c.fetchBuildpack(ctx, ex, relativeBaseDir, builderExs, opts, buildpack.KindExtension, targetToUse)
if err != nil {
return fetchedExs, orderExtensions, err
}
Expand Down
Loading
Loading