image.go (187 lines of code) (raw):
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"
)
type DockerImageSpec = dockerspec.DockerOCIImage
type DockerImageConfig = dockerspec.DockerOCIImageConfig
// ImageConfig is the configuration for the output image.
// When the target output is a container image, this is used to configure the image.
type ImageConfig struct {
// Entrypoint sets the image's "entrypoint" field.
// This is used to control the default command to run when the image is run.
Entrypoint string `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty"`
// Cmd sets the image's "cmd" field.
// When entrypoint is set, this is used as the default arguments to the entrypoint.
// When entrypoint is not set, this is used as the default command to run.
Cmd string `yaml:"cmd,omitempty" json:"cmd,omitempty"`
// Env is the list of environment variables to set in the image.
Env []string `yaml:"env,omitempty" json:"env,omitempty"`
// Labels is the list of labels to set in the image metadata.
Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
// Volumes is the list of volumes for the image.
// Volumes instruct the runtime to bypass the any copy-on-write filesystems and mount the volume directly to the container.
Volumes map[string]struct{} `yaml:"volumes,omitempty" json:"volumes,omitempty"`
// WorkingDir is the working directory to set in the image.
// This sets the directory the container will start in.
WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
// StopSignal is the signal to send to the container to stop it.
// This is used to stop the container gracefully.
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.
//
// Use this to perform actions that would otherwise require additional tooling inside the container that is not relevant to
// the resulting container and makes a post-install script as part of the package unnecessary.
Post *PostInstall `yaml:"post,omitempty" json:"post,omitempty"`
// User is the that the image should run as.
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.
func MergeImageConfig(dst *DockerImageConfig, src *ImageConfig) error {
if src == nil {
return nil
}
if src.Entrypoint != "" {
split, err := shlex.Split(src.Entrypoint)
if err != nil {
return errors.Wrap(err, "error splitting entrypoint into args")
}
dst.Entrypoint = split
// Reset cmd as this may be totally invalid now
// This is the same behavior as the Dockerfile frontend
dst.Cmd = nil
}
if src.Cmd != "" {
split, err := shlex.Split(src.Cmd)
if err != nil {
return errors.Wrap(err, "error splitting cmd into args")
}
dst.Cmd = split
}
if len(src.Env) > 0 {
// Env is append only
// If the env var already exists, replace it
envIdx := make(map[string]int)
for i, env := range dst.Env {
envIdx[env] = i
}
for _, env := range src.Env {
if idx, ok := envIdx[env]; ok {
dst.Env[idx] = env
} else {
dst.Env = append(dst.Env, env)
}
}
}
if src.WorkingDir != "" {
dst.WorkingDir = src.WorkingDir
}
if src.StopSignal != "" {
dst.StopSignal = src.StopSignal
}
if src.User != "" {
dst.User = src.User
}
for k, v := range src.Volumes {
if dst.Volumes == nil {
dst.Volumes = make(map[string]struct{}, len(src.Volumes))
}
dst.Volumes[k] = v
}
for k, v := range src.Labels {
if dst.Labels == nil {
dst.Labels = make(map[string]string, len(src.Labels))
}
dst.Labels[k] = v
}
return nil
}
func (i *ImageConfig) validate() error {
if i == nil {
return nil
}
var errs []error
if i.Base != "" && len(i.Bases) > 0 {
errs = append(errs, errors.New("cannot specify both image.base and image.bases"))
}
for i, base := range i.Bases {
if err := base.validate(); err != nil {
errs = append(errs, errors.Wrapf(err, "bases[%d]", i))
}
}
if err := i.Post.validate(); err != nil {
errs = append(errs, errors.Wrap(err, "postinstall"))
}
return goerrors.Join(errs...)
}
func (i *ImageConfig) fillDefaults() {
if i == nil {
return
}
// s.Bases is a superset of s.Base, so migrate s.Base to s.Bases
if i.Base != "" {
i.Bases = append(i.Bases, BaseImage{
Rootfs: Source{
DockerImage: &SourceDockerImage{
Ref: i.Base,
},
},
})
i.Base = ""
}
for _, bi := range i.Bases {
bi.fillDefaults()
}
i.Post.normalizeSymlinks()
}
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 unnecessary.
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 (p *PostInstall) validate() error {
if p == nil {
return nil
}
var errs []error
if err := validateSymlinks(p.Symlinks); err != nil {
errs = append(errs, err)
}
return errors.Wrap(goerrors.Join(errs...), "symlink")
}
func (s *BaseImage) fillDefaults() {
fillDefaults(&s.Rootfs)
}
func (p *PostInstall) normalizeSymlinks() {
if p == nil {
return
}
// validation has already taken place
for oldpath := range p.Symlinks {
cfg := p.Symlinks[oldpath]
if cfg.Path == "" {
continue
}
cfg.Paths = append(cfg.Paths, cfg.Path)
cfg.Path = ""
p.Symlinks[oldpath] = cfg
}
}
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
}