Skip to content

Commit

Permalink
windows: support building for multiple base images
Browse files Browse the repository at this point in the history
This allows the builder to specify a file (either in the main build
context or a custom one) which contains a list of base images that the
windowscross/container target should produce images for.

This is useful for when you want to have one image for different
versions of Windows, given that for Windows containers the container OS
must match the host OS.

Signed-off-by: Brian Goff <[email protected]>
  • Loading branch information
cpuguy83 committed Dec 18, 2024
1 parent e863607 commit 17ab9e3
Show file tree
Hide file tree
Showing 7 changed files with 501 additions and 66 deletions.
21 changes: 12 additions & 9 deletions frontend/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,7 @@ func fillPlatformArgs(prefix string, args map[string]string, platform ocispecs.P

type PlatformBuildFunc func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error)

// BuildWithPlatform is a helper function to build a spec with a given platform
// It takes care of looping through each tarrget platform and executing the build with the platform args substituted in the spec.
// This also deals with the docker-style multi-platform output.
func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBuildFunc) (*gwclient.Result, error) {
dc, err := dockerui.NewClient(client)
if err != nil {
return nil, err
}

func BuildWithPlatformFromUIClient(ctx context.Context, client gwclient.Client, dc *dockerui.Client, f PlatformBuildFunc) (*gwclient.Result, error) {
rb, err := dc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (gwclient.Reference, *dalec.DockerImageSpec, *dalec.DockerImageSpec, error) {
spec, err := LoadSpec(ctx, dc, platform)
if err != nil {
Expand All @@ -101,6 +93,17 @@ func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBu
return rb.Finalize()
}

// BuildWithPlatform is a helper function to build a spec with a given platform
// It takes care of looping through each tarrget platform and executing the build with the platform args substituted in the spec.
// This also deals with the docker-style multi-platform output.
func BuildWithPlatform(ctx context.Context, client gwclient.Client, f PlatformBuildFunc) (*gwclient.Result, error) {
dc, err := dockerui.NewClient(client)
if err != nil {
return nil, err
}
return BuildWithPlatformFromUIClient(ctx, client, dc, f)
}

// GetBaseImage returns an image that first checks if the client provided the
// image in the build context matching the image ref.
//
Expand Down
17 changes: 10 additions & 7 deletions frontend/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,7 @@ func GetBuildArg(client gwclient.Client, k string) (string, bool) {
return "", false
}

func SourceOptFromClient(ctx context.Context, c gwclient.Client) (dalec.SourceOpts, error) {
dc, err := dockerui.NewClient(c)
if err != nil {
return dalec.SourceOpts{}, err
}

func SourceOptFromUIClient(ctx context.Context, c gwclient.Client, dc *dockerui.Client) dalec.SourceOpts {
return dalec.SourceOpts{
Resolver: c,
Forward: ForwarderFromClient(ctx, c),
Expand All @@ -125,7 +120,15 @@ func SourceOptFromClient(ctx context.Context, c gwclient.Client) (dalec.SourceOp
}
return st, nil
},
}, nil
}
}

func SourceOptFromClient(ctx context.Context, c gwclient.Client) (dalec.SourceOpts, error) {
dc, err := dockerui.NewClient(c)
if err != nil {
return dalec.SourceOpts{}, err
}
return SourceOptFromUIClient(ctx, c, dc), nil
}

var (
Expand Down
123 changes: 123 additions & 0 deletions frontend/windows/dockerui.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package windows

import (
"context"
"encoding/json"
"fmt"

"github.com/containerd/platforms"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/frontend/dockerui"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)

// This is a copy of dockerui.Client.Build
// It has one modification: Instead of `platforms.Format` it uses `platforms.FormatAll`
// The value returned from this function is used as a map key to store build
// result references.
// When `platforms.Format` is used, the `OSVersion` field is not taken into account
// which means we end up overwriting map keys when there are multiple windows
// platform images being output but with different OSVerions.
// platforms.FormatAll takes OSVersion into account.
func dcBuild(ctx context.Context, bc *dockerui.Client, fn dockerui.BuildFunc) (*resultBuilder, error) {
res := gwclient.NewResult()

targets := make([]*ocispecs.Platform, 0, len(bc.TargetPlatforms))
for _, p := range bc.TargetPlatforms {
p := p
targets = append(targets, &p)
}
if len(targets) == 0 {
targets = append(targets, nil)
}
expPlatforms := &exptypes.Platforms{
Platforms: make([]exptypes.Platform, len(targets)),
}

eg, ctx := errgroup.WithContext(ctx)

for i, tp := range targets {
i, tp := i, tp
eg.Go(func() error {
ref, img, baseImg, err := fn(ctx, tp, i)
if err != nil {
return err
}

config, err := json.Marshal(img)
if err != nil {
return errors.Wrapf(err, "failed to marshal image config")
}

var baseConfig []byte
if baseImg != nil {
baseConfig, err = json.Marshal(baseImg)
if err != nil {
return errors.Wrapf(err, "failed to marshal source image config")
}
}

p := platforms.DefaultSpec()
if tp != nil {
p = *tp
}

// in certain conditions we allow input platform to be extended from base image
if p.OS == "windows" && img.OS == p.OS {
if p.OSVersion == "" && img.OSVersion != "" {
p.OSVersion = img.OSVersion
}
if p.OSFeatures == nil && len(img.OSFeatures) > 0 {
p.OSFeatures = append([]string{}, img.OSFeatures...)
}
}

p = platforms.Normalize(p)
k := platforms.FormatAll(p)

if bc.MultiPlatformRequested {
res.AddRef(k, ref)
res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, k), config)
if len(baseConfig) > 0 {
res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageBaseConfigKey, k), baseConfig)
}
} else {
res.SetRef(ref)
res.AddMeta(exptypes.ExporterImageConfigKey, config)
if len(baseConfig) > 0 {
res.AddMeta(exptypes.ExporterImageBaseConfigKey, baseConfig)
}
}
expPlatforms.Platforms[i] = exptypes.Platform{
ID: k,
Platform: p,
}
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, err
}
return &resultBuilder{
Result: res,
expPlatforms: expPlatforms,
}, nil
}

type resultBuilder struct {
*gwclient.Result
expPlatforms *exptypes.Platforms
}

func (rb *resultBuilder) Finalize() (*gwclient.Result, error) {
dt, err := json.Marshal(rb.expPlatforms)
if err != nil {
return nil, err
}
rb.AddMeta(exptypes.ExporterPlatformsKey, dt)

return rb.Result, nil
}
Loading

0 comments on commit 17ab9e3

Please sign in to comment.