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 }