diff --git a/docs/spec.schema.json b/docs/spec.schema.json index 66e12a151..08c4b7b3c 100644 --- a/docs/spec.schema.json +++ b/docs/spec.schema.json @@ -160,6 +160,19 @@ "type": "object", "description": "Artifacts describes all the artifacts to include in the package." }, + "BaseImage": { + "properties": { + "rootfs": { + "$ref": "#/$defs/Source", + "description": "Rootfs represents an image rootfs." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "rootfs" + ] + }, "BuildStep": { "properties": { "command": { @@ -487,7 +500,14 @@ }, "base": { "type": "string", - "description": "Base is the base image to use for the output image.\nThis only affects the output image, not the intermediate build image." + "description": "Deprecated: Use [Bases] instead." + }, + "Bases": { + "items": { + "$ref": "#/$defs/BaseImage" + }, + "type": "array", + "description": "Bases is used to specify a list of base images to build images for. The\nintent of allowing multiple bases is for cases, such as Windows, where you\nmay want to publish multiple versions of a base image in one image.\n\nWindows is the example here because of the way Windows works, the image\nthat the base is based off of must match the OS version of the host machine.\nTherefore it is common to have multiple Windows images in one with a\ndifferent value for the os version field of the platform.\n\nFor the most part implementations are not expected to support multiple base\nimages and may error out if multiple are specified.\n\nThis should not be set if [Base] is also set." }, "post": { "$ref": "#/$defs/PostInstall", @@ -500,6 +520,9 @@ }, "additionalProperties": false, "type": "object", + "required": [ + "Bases" + ], "description": "ImageConfig is the configuration for the output image." }, "PackageConfig": { diff --git a/frontend/azlinux/handle_container.go b/frontend/azlinux/handle_container.go index 969577708..d6f6bf2ab 100644 --- a/frontend/azlinux/handle_container.go +++ b/frontend/azlinux/handle_container.go @@ -30,7 +30,7 @@ func handleContainer(w worker) gwclient.BuildFunc { return nil, nil, fmt.Errorf("error creating rpm: %w", err) } - img, err := resolveBaseConfig(ctx, w, client, platform, spec, targetKey) + img, err := resolveBaseConfig(ctx, w, sOpt, platform, spec, targetKey) if err != nil { return nil, nil, errors.Wrap(err, "could not resolve base image config") } @@ -50,9 +50,13 @@ func specToContainerLLB(w worker, spec *dalec.Spec, targetKey string, rpmDir llb return llb.Scratch(), err } - rootfs := llb.Scratch() - if ref := dalec.GetBaseOutputImage(spec, targetKey); ref != "" { - rootfs = llb.Image(ref, llb.WithMetaResolver(sOpt.Resolver), dalec.WithConstraints(opts...)) + bi, err := spec.GetSingleBase(targetKey) + if err != nil { + return llb.Scratch(), err + } + rootfs, err := bi.ToState(sOpt, opts...) + if err != nil { + return llb.Scratch(), err } installTimeRepos := spec.GetInstallRepos(targetKey) @@ -84,13 +88,20 @@ func specToContainerLLB(w worker, spec *dalec.Spec, targetKey string, rpmDir llb return rootfs, nil } -func resolveBaseConfig(ctx context.Context, w worker, resolver llb.ImageMetaResolver, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (*dalec.DockerImageSpec, error) { +func resolveBaseConfig(ctx context.Context, w worker, sOpt dalec.SourceOpts, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (*dalec.DockerImageSpec, error) { var img *dalec.DockerImageSpec - if ref := dalec.GetBaseOutputImage(spec, targetKey); ref != "" { - _, _, dt, err := resolver.ResolveImageConfig(ctx, ref, sourceresolver.Opt{Platform: platform}) + bi, err := spec.GetSingleBase(targetKey) + if err != nil { + return nil, errors.Wrap(err, "error resolving base image config") + } + + if bi != nil { + dt, err := bi.ResolveImageConfig(ctx, sOpt, sourceresolver.Opt{ + Platform: platform, + }) if err != nil { - return nil, errors.Wrap(err, "error resolving base image config") + return nil, err } var i dalec.DockerImageSpec @@ -100,7 +111,7 @@ func resolveBaseConfig(ctx context.Context, w worker, resolver llb.ImageMetaReso img = &i } else { var err error - img, err = w.DefaultImageConfig(ctx, resolver, platform) + img, err = w.DefaultImageConfig(ctx, sOpt.Resolver, platform) if err != nil { return nil, errors.Wrap(err, "error resolving default image config") } diff --git a/frontend/azlinux/handle_depsonly.go b/frontend/azlinux/handle_depsonly.go index ca3f18963..eae82a248 100644 --- a/frontend/azlinux/handle_depsonly.go +++ b/frontend/azlinux/handle_depsonly.go @@ -49,7 +49,7 @@ func handleDepsOnly(w worker) gwclient.BuildFunc { return nil, nil, err } - img, err := resolveBaseConfig(ctx, w, client, platform, spec, targetKey) + img, err := resolveBaseConfig(ctx, w, sOpt, platform, spec, targetKey) if err != nil { return nil, nil, err } diff --git a/frontend/azlinux/handle_rpm.go b/frontend/azlinux/handle_rpm.go index 03545329d..3fe3f94be 100644 --- a/frontend/azlinux/handle_rpm.go +++ b/frontend/azlinux/handle_rpm.go @@ -56,7 +56,7 @@ func handleRPM(w worker) gwclient.BuildFunc { if imgRef, err := runTests(ctx, client, w, spec, sOpt, st, targetKey, pg); err != nil { // return the container ref in case of error so it can be used to debug // the installed package state. - cfg, _ := resolveBaseConfig(ctx, w, client, platform, spec, targetKey) + cfg, _ := resolveBaseConfig(ctx, w, sOpt, platform, spec, targetKey) return imgRef, cfg, err } @@ -94,7 +94,7 @@ func runTests(ctx context.Context, client gwclient.Client, w worker, spec *dalec } err = frontend.RunTests(ctx, client, spec, ref, withDeps, targetKey) - return ref, errors.Wrap(err, "TESTS FAILED") + return ref, err } var azlinuxRepoPlatform = dalec.RepoPlatformConfig{ diff --git a/frontend/build.go b/frontend/build.go index 86a2e1f83..8c8a4c79f 100644 --- a/frontend/build.go +++ b/frontend/build.go @@ -105,14 +105,18 @@ 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. +// It takes care of looping through each target 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) +} +// Like [BuildWithPlatform] but with a pre-initialized dockerui.Client +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 { diff --git a/frontend/deb/distro/container.go b/frontend/deb/distro/container.go index d3f74ee98..b332f33d6 100644 --- a/frontend/deb/distro/container.go +++ b/frontend/deb/distro/container.go @@ -12,13 +12,23 @@ import ( ) func (c *Config) BuildContainer(worker llb.State, sOpt dalec.SourceOpts, client gwclient.Client, spec *dalec.Spec, targetKey string, debSt llb.State, opts ...llb.ConstraintsOpt) (llb.State, error) { - base := dalec.GetBaseOutputImage(spec, targetKey) - if base == "" { - base = c.DefaultOutputImage + bi, err := spec.GetSingleBase(targetKey) + if err != nil { + return llb.Scratch(), err } - if base == "" { - return llb.Scratch(), fmt.Errorf("no output image ref specified, cannot build from scratch") + var baseImg llb.State + if bi != nil { + img, err := bi.ToState(sOpt, opts...) + if err != nil { + return llb.Scratch(), err + } + baseImg = img + } else { + if c.DefaultOutputImage == "" { + return llb.Scratch(), fmt.Errorf("no output image ref specified, cannot build from scratch") + } + baseImg = llb.Image(c.DefaultOutputImage, llb.WithMetaResolver(sOpt.Resolver), dalec.WithConstraints(opts...)) } opts = append(opts, dalec.ProgressGroup("Build Container Image")) @@ -31,8 +41,6 @@ func (c *Config) BuildContainer(worker llb.State, sOpt dalec.SourceOpts, client return llb.Scratch(), err } - baseImg := llb.Image(base, llb.WithMetaResolver(sOpt.Resolver), dalec.WithConstraints(opts...)) - debug := llb.Scratch().File(llb.Mkfile("debug", 0o644, []byte(`debug=2`)), opts...) opts = append(opts, dalec.ProgressGroup("Install spec package")) @@ -93,7 +101,7 @@ func (c *Config) HandleContainer(ctx context.Context, client gwclient.Client) (* return nil, nil, err } - img, err := c.BuildImageConfig(ctx, client, spec, platform, targetKey) + img, err := c.BuildImageConfig(ctx, sOpt, spec, platform, targetKey) if err != nil { return nil, nil, err } diff --git a/frontend/deb/distro/distro.go b/frontend/deb/distro/distro.go index 42ab0be30..b1ea9febc 100644 --- a/frontend/deb/distro/distro.go +++ b/frontend/deb/distro/distro.go @@ -39,8 +39,8 @@ type Config struct { ExtraRepos []dalec.PackageRepositoryConfig } -func (cfg *Config) BuildImageConfig(ctx context.Context, resolver llb.ImageMetaResolver, spec *dalec.Spec, platform *ocispecs.Platform, targetKey string) (*dalec.DockerImageSpec, error) { - img, err := resolveConfig(ctx, resolver, spec, platform, targetKey) +func (cfg *Config) BuildImageConfig(ctx context.Context, sOpt dalec.SourceOpts, spec *dalec.Spec, platform *ocispecs.Platform, targetKey string) (*dalec.DockerImageSpec, error) { + img, err := resolveConfig(ctx, sOpt, spec, platform, targetKey) if err != nil { return nil, err } @@ -52,17 +52,21 @@ func (cfg *Config) BuildImageConfig(ctx context.Context, resolver llb.ImageMetaR return img, nil } -func resolveConfig(ctx context.Context, resolver llb.ImageMetaResolver, spec *dalec.Spec, platform *ocispecs.Platform, targetKey string) (*dalec.DockerImageSpec, error) { - ref := dalec.GetBaseOutputImage(spec, targetKey) - if ref == "" { +func resolveConfig(ctx context.Context, sOpt dalec.SourceOpts, spec *dalec.Spec, platform *ocispecs.Platform, targetKey string) (*dalec.DockerImageSpec, error) { + bi, err := spec.GetSingleBase(targetKey) + if err != nil { + return nil, err + } + + if bi == nil { return dalec.BaseImageConfig(platform), nil } - _, _, dt, err := resolver.ResolveImageConfig(ctx, ref, sourceresolver.Opt{ + dt, err := bi.ResolveImageConfig(ctx, sOpt, sourceresolver.Opt{ Platform: platform, }) if err != nil { - return nil, err + return nil, errors.Wrap(err, "error resolving base image config") } var img dalec.DockerImageSpec diff --git a/frontend/deb/distro/pkg.go b/frontend/deb/distro/pkg.go index 87d529819..15659ad33 100644 --- a/frontend/deb/distro/pkg.go +++ b/frontend/deb/distro/pkg.go @@ -178,7 +178,7 @@ func (cfg *Config) HandleDeb(ctx context.Context, client gwclient.Client) (*gwcl } if ref, err := cfg.runTests(ctx, client, spec, sOpt, targetKey, ctr, pg); err != nil { - cfg, _ := cfg.BuildImageConfig(ctx, client, spec, platform, targetKey) + cfg, _ := cfg.BuildImageConfig(ctx, sOpt, spec, platform, targetKey) return ref, cfg, err } diff --git a/frontend/gateway.go b/frontend/gateway.go index 862afb67e..415e4a8c5 100644 --- a/frontend/gateway.go +++ b/frontend/gateway.go @@ -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), @@ -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 ( diff --git a/frontend/windows/dockerui.go b/frontend/windows/dockerui.go new file mode 100644 index 000000000..51f023df7 --- /dev/null +++ b/frontend/windows/dockerui.go @@ -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 OSVersions. +// 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 +} diff --git a/frontend/windows/handle_container.go b/frontend/windows/handle_container.go index 8d58207f2..283a8496a 100644 --- a/frontend/windows/handle_container.go +++ b/frontend/windows/handle_container.go @@ -6,15 +6,18 @@ import ( "fmt" "path" "runtime" + "sync" "github.com/Azure/dalec" "github.com/Azure/dalec/frontend" + "github.com/containerd/platforms" "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/client/llb/sourceresolver" "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" ) const ( @@ -34,80 +37,159 @@ var ( ) func handleContainer(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) { - return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) { - sOpt, err := frontend.SourceOptFromClient(ctx, client) - if err != nil { - return nil, nil, err - } + dc, err := dockerui.NewClient(client) + if err != nil { + return nil, err + } - if err := validateRuntimeDeps(spec, targetKey); err != nil { - return nil, nil, fmt.Errorf("error validating windows spec: %w", err) + if len(dc.TargetPlatforms) > 1 { + return nil, fmt.Errorf("multi-platform output is not supported") + } + + sOpt := frontend.SourceOptFromUIClient(ctx, client, dc) + + spec, err := frontend.LoadSpec(ctx, dc, nil) + if err != nil { + return nil, err + } + + targetKey := frontend.GetTargetKey(client) + bases := spec.GetImageBases(targetKey) + + if len(bases) == 0 { + bases = append(bases, dalec.BaseImage{ + Rootfs: dalec.Source{ + DockerImage: &dalec.SourceDockerImage{Ref: defaultBaseImage}, + }, + }) + } + + eg, grpCtx := errgroup.WithContext(ctx) + var mu sync.Mutex + cfgs := make([][]byte, len(bases)) + targets := make([]ocispecs.Platform, len(cfgs)) + + basePlatform := defaultPlatform + if len(dc.TargetPlatforms) > 0 { + basePlatform = dc.TargetPlatforms[0] + } + + for idx, bi := range bases { + idx := idx + bi := bi + eg.Go(func() error { + dt, err := bi.ResolveImageConfig(grpCtx, sOpt, sourceresolver.Opt{ + Platform: &basePlatform, + ImageOpt: &sourceresolver.ResolveImageOpt{ + ResolveMode: dc.ImageResolveMode.String(), + }, + }) + if err != nil { + return err + } + + var cfg dalec.DockerImageSpec + if err := json.Unmarshal(dt, &cfg); err != nil { + return errors.Wrapf(err, "error unmarshalling base image config for base image at index %d", idx) + } + + mu.Lock() + cfgs[idx] = dt + targets[idx] = cfg.Platform + mu.Unlock() + + return nil + }) + } + + if err := eg.Wait(); err != nil { + return nil, err + } + + seen := make(map[string]struct{}) + for _, p := range targets { + s := platforms.FormatAll(p) + if _, ok := seen[s]; ok { + return nil, fmt.Errorf("mutiple base images provided with the same platform value") } + seen[s] = struct{}{} + } - bc, err := dockerui.NewClient(client) + dc.TargetPlatforms = targets + if len(targets) > 1 { + dc.MultiPlatformRequested = true + } + + rb, err := dcBuild(ctx, dc, func(ctx context.Context, platform *ocispecs.Platform, idx int) (ref gwclient.Reference, retCfg, retBaseCfg *dalec.DockerImageSpec, retErr error) { + spec, err := frontend.LoadSpec(ctx, dc, platform) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - targetPlatform, err := getTargetPlatform(bc) - if err != nil { - return nil, nil, err + if err := validateRuntimeDeps(spec, targetKey); err != nil { + return nil, nil, nil, fmt.Errorf("error validating windows spec: %w", err) } pg := dalec.ProgressGroup("Build windows container: " + spec.Name) worker, err := distroConfig.Worker(sOpt, pg) if err != nil { - return nil, nil, err + return nil, nil, nil, err } bin, err := buildBinaries(ctx, spec, worker, client, sOpt, targetKey) if err != nil { - return nil, nil, fmt.Errorf("unable to build binary %w", err) + return nil, nil, nil, fmt.Errorf("unable to build binary %w", err) } - baseImgName := getBaseOutputImage(spec, targetKey, defaultBaseImage) - baseImage := llb.Image(baseImgName, llb.Platform(targetPlatform)) + bi := bases[idx] + if platform == nil { + platform = &defaultPlatform + } + baseImage, err := bi.ToState(sOpt, pg, llb.Platform(*platform)) + if err != nil { + return nil, nil, nil, err + } out := baseImage. File(llb.Copy(bin, "/", windowsSystemDir)). With(copySymlinks(spec.GetImagePost(targetKey))) def, err := out.Marshal(ctx) if err != nil { - return nil, nil, err + return nil, nil, nil, err } res, err := client.Solve(ctx, gwclient.SolveRequest{ Definition: def.ToPB(), }) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - imgRef := dalec.GetBaseOutputImage(spec, targetKey) - if imgRef == "" { - imgRef = defaultBaseImage - } + dt := cfgs[idx] - _, _, dt, err := client.ResolveImageConfig(ctx, imgRef, sourceresolver.Opt{ - Platform: &targetPlatform, - }) - if err != nil { - return nil, nil, errors.Wrap(err, "could not resolve base image config") + var baseCfg dalec.DockerImageSpec + if err := json.Unmarshal(cfgs[idx], &baseCfg); err != nil { + return nil, nil, nil, errors.Wrap(err, "error unmarshalling base image config") } var img dalec.DockerImageSpec if err := json.Unmarshal(dt, &img); err != nil { - return nil, nil, errors.Wrap(err, "error unmarshalling base image config") + return nil, nil, nil, errors.Wrap(err, "error unmarshalling base image config") } if err := dalec.BuildImageConfig(spec, targetKey, &img); err != nil { - return nil, nil, errors.Wrap(err, "error creating image config") + return nil, nil, nil, errors.Wrap(err, "error creating image config") } - ref, err := res.SingleRef() - return ref, &img, err + ref, err = res.SingleRef() + return ref, &img, &baseCfg, err }) + if err != nil { + return nil, err + } + + return rb.Finalize() } func copySymlinks(post *dalec.PostInstall) llb.StateOption { @@ -130,28 +212,4 @@ func copySymlinks(post *dalec.PostInstall) llb.StateOption { return s } - -} - -func getTargetPlatform(bc *dockerui.Client) (ocispecs.Platform, error) { - platform := defaultPlatform - - switch len(bc.TargetPlatforms) { - case 0: - case 1: - platform = bc.TargetPlatforms[0] - default: - return ocispecs.Platform{}, - fmt.Errorf("multiple target supplied for build: %v. note: only amd64 is supported for windows outputs", bc.TargetPlatforms) - } - - return platform, nil -} - -func getBaseOutputImage(spec *dalec.Spec, target, defaultBase string) string { - baseRef := defaultBase - if spec.Targets[target].Image != nil && spec.Targets[target].Image.Base != "" { - baseRef = spec.Targets[target].Image.Base - } - return baseRef } diff --git a/image.go b/image.go index 7fcc4b5fe..f2e483fea 100644 --- a/image.go +++ b/image.go @@ -1,7 +1,12 @@ package dalec import ( + "context" + goerrors "errors" + "github.com/google/shlex" + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/client/llb/sourceresolver" dockerspec "github.com/moby/docker-image-spec/specs-go/v1" "github.com/pkg/errors" ) @@ -34,8 +39,25 @@ type ImageConfig struct { StopSignal string `yaml:"stop_signal,omitempty" json:"stop_signal,omitempty" jsonschema:"example=SIGTERM"` // Base is the base image to use for the output image. // This only affects the output image, not the intermediate build image. + + // Deprecated: Use [Bases] instead. Base string `yaml:"base,omitempty" json:"base,omitempty"` + // Bases is used to specify a list of base images to build images for. The + // intent of allowing multiple bases is for cases, such as Windows, where you + // may want to publish multiple versions of a base image in one image. + // + // Windows is the example here because of the way Windows works, the image + // that the base is based off of must match the OS version of the host machine. + // Therefore it is common to have multiple Windows images in one with a + // different value for the os version field of the platform. + // + // For the most part implementations are not expected to support multiple base + // images and may error out if multiple are specified. + // + // This should not be set if [Base] is also set. + Bases []BaseImage `yaml:"bases,omitempty json:bases,omitempty"` + // Post is the post install configuration for the image. // This allows making additional modifications to the container rootfs after the package(s) are installed. // @@ -47,6 +69,11 @@ type ImageConfig struct { User string `yaml:"user,omitempty" json:"user,omitempty"` } +type BaseImage struct { + // Rootfs represents an image rootfs. + Rootfs Source `yaml:"rootfs" json:"rootfs"` +} + // MergeImageConfig copies the fields from the source [ImageConfig] into the destination [image.Image]. // If a field is not set in the source, it is not modified in the destination. // Envs from [ImageConfig] are merged into the destination [image.Image] and take precedence. @@ -117,3 +144,107 @@ func MergeImageConfig(dst *DockerImageConfig, src *ImageConfig) error { return nil } + +func (s *ImageConfig) validate() error { + if s == nil { + return nil + } + + var errs []error + + if s.Base != "" && len(s.Bases) > 0 { + errs = append(errs, errors.New("cannot specify both image.base and image.bases")) + } + + for i, base := range s.Bases { + if err := base.validate(); err != nil { + errs = append(errs, errors.Wrapf(err, "bases[%d]", i)) + } + } + return goerrors.Join(errs...) +} + +func (s *ImageConfig) fillDefaults() { + if s == nil { + return + } + + // s.Bases is a superset of s.Base, so migrate s.Base to s.Bases + if s.Base != "" { + s.Bases = append(s.Bases, BaseImage{ + Rootfs: Source{ + DockerImage: &SourceDockerImage{ + Ref: s.Base, + }, + }, + }) + + s.Base = "" + } + + for _, bi := range s.Bases { + bi.fillDefaults() + } +} + +func (s *BaseImage) validate() error { + if s.Rootfs.DockerImage == nil { + // In the future we may support other source types but this adds a lot of complexity + // that is currently unecessary. + return errors.New("rootfs currently only supports image source types") + } + if err := s.Rootfs.validate(); err != nil { + return errors.Wrap(err, "rootfs") + } + return nil +} + +func (s *BaseImage) fillDefaults() { + fillDefaults(&s.Rootfs) +} + +func (bi *BaseImage) ResolveImageConfig(ctx context.Context, sOpt SourceOpts, opt sourceresolver.Opt) ([]byte, error) { + // In the future, *BaseImage may support other source types, but for now it only supports Docker images. + // + // Likewise we may support passing in a config separate from the requested image rootfs, + // e.g. through a new field in *BaseImage, but for now we only support resolving the image config from the provided image reference. + _, _, dt, err := sOpt.Resolver.ResolveImageConfig(ctx, bi.Rootfs.DockerImage.Ref, opt) + return dt, err +} + +func (bi *BaseImage) ToState(sOpt SourceOpts, opts ...llb.ConstraintsOpt) (llb.State, error) { + if bi == nil { + return llb.Scratch(), nil + } + return bi.Rootfs.AsState("rootfs", sOpt, opts...) +} + +func (s *Spec) GetImageBases(targetKey string) []BaseImage { + if t, ok := s.Targets[targetKey]; ok && t.Image != nil { + // note: this is intentionally only doing a nil check and *not* a length check + // so that an empty list of bases can be used to override the default bases + if t.Image.Bases != nil { + return t.Image.Bases + } + } + + if s.Image == nil { + return nil + } + return s.Image.Bases +} + +// GetSingleBase looks up the base images to use for the targetKey and returns +// only the first entry. +// If there is more than 1 entry an error is returned. +// If there are no entries then both return values are nil. +func (s *Spec) GetSingleBase(targetKey string) (*BaseImage, error) { + bases := s.GetImageBases(targetKey) + if len(bases) > 1 { + return nil, errors.New("multiple image bases, expected only one") + } + if len(bases) == 0 { + return nil, nil + } + return &bases[0], nil +} diff --git a/imgconfig.go b/imgconfig.go index 056d25c4c..c3228c575 100644 --- a/imgconfig.go +++ b/imgconfig.go @@ -10,14 +10,6 @@ func BuildImageConfig(spec *Spec, targetKey string, img *DockerImageSpec) error return nil } -func GetBaseOutputImage(spec *Spec, target string) string { - i := spec.Targets[target].Image - if i == nil || i.Base == "" { - return "" - } - return i.Base -} - func MergeSpecImage(spec *Spec, targetKey string) *ImageConfig { var cfg ImageConfig diff --git a/load.go b/load.go index f6dcdd8c1..2be576713 100644 --- a/load.go +++ b/load.go @@ -317,6 +317,7 @@ func (s *Spec) FillDefaults() { } s.Dependencies.fillDefaults() + s.Image.fillDefaults() for k := range s.Targets { t := s.Targets[k] @@ -375,6 +376,10 @@ func (s Spec) Validate() error { errs = append(errs, errors.Wrap(err, "dependencies")) } + if err := s.Image.validate(); err != nil { + errs = append(errs, errors.Wrap(err, "image")) + } + for k, t := range s.Targets { if err := t.validate(); err != nil { errs = append(errs, errors.Wrapf(err, "target %s", k)) diff --git a/load_test.go b/load_test.go index 0a4027b13..4b8104586 100644 --- a/load_test.go +++ b/load_test.go @@ -1073,3 +1073,90 @@ func Test_validatePatch(t *testing.T) { }) } } + +func TestImage_fillDefaults(t *testing.T) { + t.Run("image.base is migrated to image.bases", func(t *testing.T) { + dt := []byte(` +image: + base: busybox:latest + +targets: + foo: + image: + base: busybox:latest +`) + + spec, err := LoadSpec(dt) + assert.NilError(t, err) + + // image.base should be migrated to image.bases + assert.Check(t, cmp.Equal(spec.Image.Base, "")) + assert.Check(t, cmp.Equal(spec.Targets["foo"].Image.Base, "")) + assert.Check(t, cmp.Len(spec.Image.Bases, 1)) + assert.Check(t, spec.Image.Bases[0].Rootfs.DockerImage != nil) + assert.Check(t, cmp.Equal(spec.Image.Bases[0].Rootfs.DockerImage.Ref, "busybox:latest")) + assert.Check(t, cmp.Len(spec.Targets["foo"].Image.Bases, 1)) + assert.Check(t, spec.Targets["foo"].Image.Bases[0].Rootfs.DockerImage != nil) + assert.Check(t, cmp.Equal(spec.Targets["foo"].Image.Bases[0].Rootfs.DockerImage.Ref, "busybox:latest")) + }) +} + +func TestImage_validate(t *testing.T) { + type testCase struct { + Name string + Image ImageConfig + expectErr string + } + + cases := []testCase{ + { + Name: "No base image", + Image: ImageConfig{}, + }, + { + Name: "image.base set", + Image: ImageConfig{ + Base: "busybox:latest", + }, + }, + { + Name: "image.bases set with valid sources", + Image: ImageConfig{ + Bases: []BaseImage{ + {Rootfs: Source{DockerImage: &SourceDockerImage{Ref: "busybox:latest"}}}, + {Rootfs: Source{DockerImage: &SourceDockerImage{Ref: "alpine:latest"}}}, + }, + }, + }, + { + Name: "both image.bases and image.base set", + expectErr: "cannot specify both", + Image: ImageConfig{ + Base: "busybox:latest", + Bases: []BaseImage{ + {Rootfs: Source{DockerImage: &SourceDockerImage{Ref: "busybox:latest"}}}, + }, + }, + }, + { + Name: "image.bases set to anything other than image source type", + expectErr: "rootfs currently only supports image source types", + Image: ImageConfig{ + Bases: []BaseImage{ + {Rootfs: Source{Context: &SourceContext{}}}, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + err := tc.Image.validate() + if tc.expectErr != "" { + assert.ErrorContains(t, err, tc.expectErr) + return + } + assert.NilError(t, err) + }) + } +} diff --git a/target.go b/target.go index 5f332e7d9..bc4b7df30 100644 --- a/target.go +++ b/target.go @@ -36,6 +36,10 @@ func (t *Target) validate() error { errs = append(errs, errors.Wrap(err, "dependencies")) } + if err := t.Image.validate(); err != nil { + errs = append(errs, errors.Wrap(err, "image")) + } + for _, test := range t.Tests { if err := test.validate(); err != nil { errs = append(errs, errors.Wrapf(err, "test %s", test.Name)) @@ -68,4 +72,5 @@ func (t *Target) processBuildArgs(lex *shell.Lex, args map[string]string, allowA func (t *Target) fillDefaults() { t.Dependencies.fillDefaults() + t.Image.fillDefaults() } diff --git a/test/windows_test.go b/test/windows_test.go index afe50148b..33fd89a62 100644 --- a/test/windows_test.go +++ b/test/windows_test.go @@ -2,6 +2,7 @@ package test import ( "context" + "encoding/json" "errors" "fmt" "testing" @@ -9,11 +10,16 @@ import ( "github.com/Azure/dalec" "github.com/Azure/dalec/frontend/ubuntu" "github.com/Azure/dalec/frontend/windows" + "github.com/containerd/platforms" "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/client/llb/sourceresolver" + "github.com/moby/buildkit/exporter/containerimage/exptypes" gwclient "github.com/moby/buildkit/frontend/gateway/client" moby_buildkit_v1_frontend "github.com/moby/buildkit/frontend/gateway/pb" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "golang.org/x/exp/maps" + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" ) var windowsAmd64 = ocispecs.Platform{OS: "windows", Architecture: "amd64"} @@ -186,37 +192,38 @@ func testWindows(ctx context.Context, t *testing.T, tcfg targetConfig) { t.Run("container", func(t *testing.T) { t.Parallel() - spec := dalec.Spec{ - Name: "test-container-build", - Version: "0.0.1", - Revision: "1", - License: "MIT", - Website: "https://github.com/azure/dalec", - Vendor: "Dalec", - Packager: "Dalec", - Description: "Testing container target", - Sources: map[string]dalec.Source{ - "src1": { - Inline: &dalec.SourceInline{ - File: &dalec.SourceInlineFile{ - Contents: "#!/usr/bin/env bash\necho hello world", - Permissions: 0o700, + newSpec := func() dalec.Spec { + return dalec.Spec{ + Name: "test-container-build", + Version: "0.0.1", + Revision: "1", + License: "MIT", + Website: "https://github.com/azure/dalec", + Vendor: "Dalec", + Packager: "Dalec", + Description: "Testing container target", + Sources: map[string]dalec.Source{ + "src1": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: "#!/usr/bin/env bash\necho hello world", + Permissions: 0o700, + }, }, }, - }, - "src2": { - Inline: &dalec.SourceInline{ - Dir: &dalec.SourceInlineDir{ - Files: map[string]*dalec.SourceInlineFile{ - "file1": {Contents: "file1 contents\n"}, + "src2": { + Inline: &dalec.SourceInline{ + Dir: &dalec.SourceInlineDir{ + Files: map[string]*dalec.SourceInlineFile{ + "file1": {Contents: "file1 contents\n"}, + }, }, }, }, - }, - "src2-patch1": { - Inline: &dalec.SourceInline{ - File: &dalec.SourceInlineFile{ - Contents: ` + "src2-patch1": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: ` diff --git a/file1 b/file1 index 84d55c5..22b9b11 100644 --- a/file1 @@ -225,13 +232,13 @@ index 84d55c5..22b9b11 100644 -file1 contents +file1 contents patched `, + }, }, }, - }, - "src2-patch2": { - Inline: &dalec.SourceInline{ - File: &dalec.SourceInlineFile{ - Contents: ` + "src2-patch2": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: ` diff --git a/file2 b/file2 new file mode 100700 index 0000000..5260cb1 @@ -242,92 +249,84 @@ index 0000000..5260cb1 + +echo "Added a new file" `, + }, }, }, - }, - "src3": { - Inline: &dalec.SourceInline{ - File: &dalec.SourceInlineFile{ - Contents: "#!/usr/bin/env bash\necho goodbye", - Permissions: 0o700, + "src3": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: "#!/usr/bin/env bash\necho goodbye", + Permissions: 0o700, + }, }, }, }, - }, - Patches: map[string][]dalec.PatchSpec{ - "src2": { - {Source: "src2-patch1"}, - {Source: "src2-patch2"}, + Patches: map[string][]dalec.PatchSpec{ + "src2": { + {Source: "src2-patch1"}, + {Source: "src2-patch2"}, + }, }, - }, - Dependencies: &dalec.PackageDependencies{}, + Dependencies: &dalec.PackageDependencies{}, - Build: dalec.ArtifactBuild{ - Steps: []dalec.BuildStep{ - // These are "build" steps where we aren't really building things just verifying - // that sources are in the right place and have the right permissions and content - { - Command: "test -x ./src1", - }, - { - Command: "./src1 | grep 'hello world'", - }, - { - // file added by patch - Command: "test -x ./src2/file2", - }, - { - Command: "grep 'Added a new file' ./src2/file2", - }, - { - // Test that a multiline command works with env vars - Env: map[string]string{ - "FOO": "foo", - "BAR": "bar", + Build: dalec.ArtifactBuild{ + Steps: []dalec.BuildStep{ + // These are "build" steps where we aren't really building things just verifying + // that sources are in the right place and have the right permissions and content + { + Command: "test -x ./src1", + }, + { + Command: "./src1 | grep 'hello world'", }, - Command: ` + { + // file added by patch + Command: "test -x ./src2/file2", + }, + { + Command: "grep 'Added a new file' ./src2/file2", + }, + { + // Test that a multiline command works with env vars + Env: map[string]string{ + "FOO": "foo", + "BAR": "bar", + }, + Command: ` echo "${FOO}_0" > foo0.txt echo "${FOO}_1" > foo1.txt echo "$BAR" > bar.txt `, + }, }, }, - }, - Image: &dalec.ImageConfig{ - Post: &dalec.PostInstall{ - Symlinks: map[string]dalec.SymlinkTarget{ - "/Windows/System32/src1": {Path: "/src1"}, - "/Windows/System32/src3": {Path: "/non/existing/dir/src3"}, + Image: &dalec.ImageConfig{ + Post: &dalec.PostInstall{ + Symlinks: map[string]dalec.SymlinkTarget{ + "/Windows/System32/src1": {Path: "/src1"}, + "/Windows/System32/src3": {Path: "/non/existing/dir/src3"}, + }, }, }, - }, - Artifacts: dalec.Artifacts{ - Binaries: map[string]dalec.ArtifactConfig{ - "src1": {}, - "src2/file2": {}, - "src3": {}, - // These are files we created in the build step - // They aren't really binaries but we want to test that they are created and have the right content - "foo0.txt": {}, - "foo1.txt": {}, - "bar.txt": {}, + Artifacts: dalec.Artifacts{ + Binaries: map[string]dalec.ArtifactConfig{ + "src1": {}, + "src2/file2": {}, + "src3": {}, + // These are files we created in the build step + // They aren't really binaries but we want to test that they are created and have the right content + "foo0.txt": {}, + "foo1.txt": {}, + "bar.txt": {}, + }, }, - }, - } - - testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) { - sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget(tcfg.Container), withWindowsAmd64) - sr.Evaluate = true - res := solveT(ctx, t, gwc, sr) - - ref, err := res.SingleRef() - if err != nil { - t.Fatal(err) } + } + validateSymlinks := func(ctx context.Context, t *testing.T, ref gwclient.Reference, spec dalec.Spec) { post := spec.GetImagePost("windowscross") for srcPath, l := range post.Symlinks { b1, err := ref.ReadFile(ctx, gwclient.ReadRequest{ @@ -354,6 +353,95 @@ echo "$BAR" > bar.txt } } } + } + + t.Run("single-image", func(t *testing.T) { + t.Parallel() + ctx := startTestSpan(ctx, t) + spec := newSpec() + + testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) { + sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget(tcfg.Container), withWindowsAmd64) + sr.Evaluate = true + res := solveT(ctx, t, gwc, sr) + + ref, err := res.SingleRef() + if err != nil { + t.Fatal(err) + } + validateSymlinks(ctx, t, ref, spec) + }) + }) + + t.Run("multi-image", func(t *testing.T) { + t.Parallel() + ctx := startTestSpan(ctx, t) + + testEnv.RunTest(ctx, t, func(ctx context.Context, gwc gwclient.Client) { + spec := newSpec() + + spec.Image.Bases = []dalec.BaseImage{ + {Rootfs: dalec.Source{DockerImage: &dalec.SourceDockerImage{Ref: "mcr.microsoft.com/windows/nanoserver:ltsc2022"}}}, + {Rootfs: dalec.Source{DockerImage: &dalec.SourceDockerImage{Ref: "mcr.microsoft.com/windows/nanoserver:1809"}}}, + } + sr := newSolveRequest( + withSpec(ctx, t, &spec), + withBuildTarget(tcfg.Container), + withWindowsAmd64, + ) + sr.Evaluate = true + res := solveT(ctx, t, gwc, sr) + + var metaPlatforms exptypes.Platforms + err := json.Unmarshal(res.Metadata["refs.platforms"], &metaPlatforms) + assert.NilError(t, err) + assert.Assert(t, cmp.Len(metaPlatforms.Platforms, 2)) + + // Go through each of the base images we requested and resolve + // them so we can get the platform info + // Then validate that the platform for the base image matches the platform + // in the result platforms. + for i, ref := range spec.Image.Bases { + actual := metaPlatforms.Platforms[i] + + _, _, dt, err := gwc.ResolveImageConfig(ctx, ref.Rootfs.DockerImage.Ref, sourceresolver.Opt{ + Platform: &windowsAmd64, + }) + assert.NilError(t, err) + + var cfg dalec.DockerImageSpec + assert.NilError(t, json.Unmarshal(dt, &cfg)) + assert.Check(t, cmp.Equal(cfg.OS, actual.Platform.OS)) + assert.Check(t, cmp.Equal(cfg.Architecture, actual.Platform.Architecture)) + assert.Check(t, cmp.Equal(cfg.OSVersion, actual.Platform.OSVersion)) + } + + // NOTE: we are not using `res.SingleRef` because we requested multiple + // refs which would cause an error in this case. + // Instead we need to look at res.Refs + assert.Assert(t, cmp.Len(res.Refs, len(metaPlatforms.Platforms))) + + for _, p := range metaPlatforms.Platforms { + ref, ok := res.Refs[platforms.FormatAll(p.Platform)] + assert.Assert(t, ok, "unepxected ref keys: %s", maps.Keys(res.Refs)) + validateSymlinks(ctx, t, ref, spec) + } + + // This should fail since the bases have the same platform + spec.Image.Bases = []dalec.BaseImage{ + {Rootfs: dalec.Source{DockerImage: &dalec.SourceDockerImage{Ref: "mcr.microsoft.com/windows/nanoserver:ltsc2022"}}}, + {Rootfs: dalec.Source{DockerImage: &dalec.SourceDockerImage{Ref: "mcr.microsoft.com/windows/nanoserver:ltsc2022-amd64"}}}, + } + + sr = newSolveRequest( + withSpec(ctx, t, &spec), + withBuildTarget(tcfg.Container), + withWindowsAmd64, + ) + sr.Evaluate = true + _, err = gwc.Solve(ctx, sr) + assert.ErrorContains(t, err, "mutiple base images provided with the same") + }) }) }) diff --git a/website/docs/image.md b/website/docs/image.md new file mode 100644 index 000000000..b3de85e81 --- /dev/null +++ b/website/docs/image.md @@ -0,0 +1,210 @@ +--- +title: Image +--- + +Image is a field in the DALEC spec that allows you to customize certain aspects +of the produced image. The image field is an object with the following properties: + +- `base`: The image ref to use as the base for the output container. [Deprecated: use `bases` instead] [base section](#base) +- `bases`: The list of base images to use as the base for the output container(s). [bases section](#bases) +- `post`: The post processing for the image, such as symlinks. [post section](#post) +- `labels`: The labels for the image. This is an optional field. [labels section](#labels) +- `env`: The environment variables for the image. This is an optional field. [env section](#env) +- `entrypoint`: The entrypoint for the image. This is an optional field. [entrypoint section](#entrypoint) +- `cmd`: The command for the image. This is an optional field. [cmd section](#cmd) +- `workdir`: The working directory for the image. This is an optional field. [workdir section](#workdir) +- `user`: The user for the image. This is an optional field. [user section](#user) +- `stop_signal`: The stop signal for the image. This is an optional field. [stop signal section](#stop-signal) +- `volumes`: The volumes for the image. This is an optional field. [volumes section](#volumes) + + +:::note +The `base` field is deprecated. Use the `bases` field instead. + +For `bases`, the requested build target may not support multiple base images. +In this cases the target will produce an error. + +Currently only `windowscross/container` supports multiple base images for the +purpose of building for multiple windows versions. +If multiple bases are provided with the same `os.version` value in the image +platform, this may produce an error or at least unexpected results since images +are keyed on the image platform metadata. +::: + +With the exception of `base`, `bases`, and `post`, these fields are all used to +merge with the configured (or default) base image(s). + +### Base + +The `base` field is used to specify the base image for the output container. + +Example: + +```yaml +image: + base: mcr.microsoft.com/cbl-mariner/distroless/minimal:2.0 +``` + + +### Bases + +As noted above, the `bases` field is used to specify the base image(s) for the +output container(s). +Multiple bases can be specified for the same target, but the target must support +it. +Currently the only built-in target that supports this is `windowscross/container` +where each base image specified is used to build for a different Windows version. + + +Example: + +```yaml +image: + bases: + - rootfs: + image: + ref: mcr.microsoft.com/cbl-mariner/distroless/minimal:2.0 +``` + +The data type allows specifying any kind of [source](sources.md) for the base image, +however currently only the `image` source is supported. Anything else will produce +an error. +Support for other source types may be added in the future. + +### Post + +The `post` field is used to specify post processing for the image. + +The following fields are supported: + +- `symlinks`: A list of symlinks to create in the image. + +Example: + +```yaml +image: + post: + symlinks: + /usr/bin/my-binary: # Where the symlink points to + path: /my-binary # Where to place the symlink +``` + +### Labels + +The `labels` field is used to specify labels for the image. + +Example: + +```yaml +image: + labels: + com.example.label: example +``` + +### Env + +The `env` field is used to specify environment variables for the image. + +Example: + +```yaml +image: + env: + - MY_ENV_VAR=my-value +``` + +### Entrypoint + +The `entrypoint` field is used to specify the entrypoint for the image. + +Example: + +```yaml +image: + entrypoint: /usr/bin/my-binary +``` + +### Cmd + +The `cmd` field is used to specify the command for the image. + +Example: + +```yaml +image: + cmd: /usr/bin/my-binary +``` + +### Workdir + +The `workdir` field is used to specify the working directory for the image. + +Example: + +```yaml +image: + workdir: /my-dir +``` + +### User + +The `user` field is used to specify the user for the image. + +Example: + +```yaml +image: + user: my-user:my-group +``` + +Alterantively + +```yaml +image: + user: my-user +``` + +You may also use uid/gid values: + +```yaml +image: + user: 1000:1000 +``` + +Or just user: + +```yaml +image: + user: 1000 +``` + +User and group names are not automatically created for you, so it must be in +the base OS to a username to work. + +### Stop Signal + +The `stop_signal` field is used to specify the stop signal for the image. +This is used by the container runtime to know what signal to use to gracefully +stop the container. + +Example: + +```yaml +image: + stop_signal: SIGINT +``` + +### Volumes + +The `volumes` field is used to specify volumes for the image. + +Example: + +```yaml +image: + volumes: + /some-path: {} +``` + +This is always a map of the path to create the volume at and an empty object. + diff --git a/website/docs/spec.md b/website/docs/spec.md index 677bee34d..d66ec02f2 100644 --- a/website/docs/spec.md +++ b/website/docs/spec.md @@ -107,8 +107,19 @@ For more information, please see [Targets](targets.md). Image section is used to define the base image and post processing for the image. -- `base`: The base image for the target. -- `post`: The post processing for the image, such as symlinks. +Example: + +```yaml +image: + base: mcr.microsoft.com/cbl-mariner/distroless/minimal:2.0 + post: + symlinks: + /usr/bin/my-binary: + path: /my-binary + entrypoint: /my-binary +``` + +For more information, please see [Images](image.md). ### Package Config section