Skip to content

Commit

Permalink
Merge pull request #2162 from buildpacks/build-with-platform
Browse files Browse the repository at this point in the history
Support `pack build --platform`
  • Loading branch information
jjbustamante authored May 23, 2024
2 parents 7306444 + 89cb617 commit ebea3ae
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 117 deletions.
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 @@ type BuildOptions struct {
// 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 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
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]}
case 2:
return &dist.Target{OS: parts[0], Arch: parts[1]}
default:
return &dist.Target{OS: parts[0], Arch: parts[1], ArchVariant: parts[2]}
}
}()

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
}
}

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 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
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 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
image.FetchOptions{
Daemon: true,
PullPolicy: opts.PullPolicy,
Target: target,
Target: targetToUse,
},
)
if err != nil {
Expand Down Expand Up @@ -492,7 +517,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
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 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
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 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
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)
}
builderArch, err := builderImage.Architecture()
if err != nil {
return nil, fmt.Errorf("failed to get builder architecture: %w", err)
}
builderArchVariant, err := builderImage.Variant()
if err != nil {
return nil, fmt.Errorf("failed to get builder architecture variant: %w", err)
}
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 @@ func (c *Client) processProxyConfig(config *ProxyConfig) ProxyConfig {
// ----------
// - 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 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima
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 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima
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 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima
}

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 @@ func (c *Client) processBuildpacks(ctx context.Context, builderImage imgutil.Ima
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 @@ func (c *Client) fetchBuildpack(ctx context.Context, bp string, relativeBaseDir
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 @@ func prependBuildpackToOrder(order dist.Order, bpInfo dist.ModuleInfo) (newOrder
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 @@ func (c *Client) processExtensions(ctx context.Context, builderImage imgutil.Ima
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

1 comment on commit ebea3ae

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Go Benchmark'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 2.

Benchmark suite Current: ebea3ae Previous: 7306444 Ratio
BenchmarkBuild/with_Trusted_Builder 2991523315 ns/op 920402144 ns/op 3.25

This comment was automatically generated by workflow using github-action-benchmark.

CC: @buildpacks/platform-maintainers

Please sign in to comment.