spec.go (238 lines of code) (raw):
//go:generate go run ./cmd/gen-jsonschema docs/spec.schema.json
package dalec
import (
"io/fs"
"strings"
"time"
"github.com/goccy/go-yaml"
"github.com/goccy/go-yaml/parser"
"github.com/moby/buildkit/client/llb"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
// Spec is the specification for a package build.
type Spec struct {
// Name is the name of the package.
Name string `yaml:"name" json:"name" jsonschema:"required"`
// Description is a short description of the package.
Description string `yaml:"description" json:"description" jsonschema:"required"`
// Website is the URL to store in the metadata of the package.
Website string `yaml:"website" json:"website"`
// Version sets the version of the package.
Version string `yaml:"version" json:"version" jsonschema:"required"`
// Revision sets the package revision.
// This will generally get merged into the package version when generating the package.
Revision string `yaml:"revision" json:"revision" jsonschema:"required,oneof_type=string;integer"`
// Marks the package as architecture independent.
// It is up to the package author to ensure that the package is actually architecture independent.
// This is metadata only.
NoArch bool `yaml:"noarch,omitempty" json:"noarch,omitempty"`
// Conflicts is the list of packages that conflict with the generated package.
// This will prevent the package from being installed if any of these packages are already installed or vice versa.
Conflicts map[string]PackageConstraints `yaml:"conflicts,omitempty" json:"conflicts,omitempty"`
// Replaces is the list of packages that are replaced by the generated package.
Replaces map[string]PackageConstraints `yaml:"replaces,omitempty" json:"replaces,omitempty"`
// Provides is the list of things that the generated package provides.
// This can be used to satisfy dependencies of other packages.
// As an example, the moby-runc package provides "runc", other packages could depend on "runc" and be satisfied by moby-runc.
// This is an advanced use case and consideration should be taken to ensure that the package actually provides the thing it claims to provide.
Provides map[string]PackageConstraints `yaml:"provides,omitempty" json:"provides,omitempty"`
// Sources is the list of sources to use to build the artifact(s).
// The map key is the name of the source and the value is the source configuration.
// The source configuration is used to fetch the source and filter the files to include/exclude.
// This can be mounted into the build using the "Mounts" field in the StepGroup.
//
// Sources can be embedded in the main spec as here or overridden in a build request.
Sources map[string]Source `yaml:"sources,omitempty" json:"sources,omitempty"`
// Patches is the list of patches to apply to the sources.
// The map key is the name of the source to apply the patches to.
// The value is the list of patches to apply to the source.
// The patch must be present in the `Sources` map.
// Each patch is applied in order and the result is used as the source for the build.
Patches map[string][]PatchSpec `yaml:"patches,omitempty" json:"patches,omitempty"`
// Build is the configuration for building the artifacts in the package.
Build ArtifactBuild `yaml:"build,omitempty" json:"build,omitempty"`
// Args is the list of arguments that can be used for shell-style expansion in (certain fields of) the spec.
// Any arg supplied in the build request which does not appear in this list will cause an error.
// Attempts to use an arg in the spec which is not specified here will assume to be a literal string.
// The map value is the default value to use if the arg is not supplied in the build request.
Args map[string]string `yaml:"args,omitempty" json:"args,omitempty"`
// License is the license of the package.
License string `yaml:"license" json:"license"`
// Vendor is the vendor of the package.
Vendor string `yaml:"vendor,omitempty" json:"vendor,omitempty"`
// Packager is the name of the person,team,company that packaged the package.
Packager string `yaml:"packager,omitempty" json:"packager,omitempty"`
// Artifacts is the list of artifacts to include in the package.
Artifacts Artifacts `yaml:"artifacts,omitempty" json:"artifacts,omitempty"`
// The list of distro targets to build the package for.
Targets map[string]Target `yaml:"targets,omitempty" json:"targets,omitempty"`
// Dependencies are the different dependencies that need to be specified in the package.
// Dependencies are overwritten if specified in the target map for the requested distro.
Dependencies *PackageDependencies `yaml:"dependencies,omitempty" json:"dependencies,omitempty"`
// PackageConfig is the configuration to use for artifact targets, such as
// rpms, debs, or zip files containing Windows binaries
PackageConfig *PackageConfig `yaml:"package_config,omitempty" json:"package_config,omitempty"`
// Image is the image configuration when the target output is a container image.
// This is overwritten if specified in the target map for the requested distro.
Image *ImageConfig `yaml:"image,omitempty" json:"image,omitempty"`
// Changelog is the list of changes to the package.
Changelog []ChangelogEntry `yaml:"changelog,omitempty" json:"changelog,omitempty"`
// Tests are the list of tests to run for the package that should work regardless of target OS
// Each item in this list is run with a separate rootfs and cannot interact with other tests.
// Each [TestSpec] is run with a separate rootfs, asynchronously from other [TestSpec].
Tests []*TestSpec `yaml:"tests,omitempty" json:"tests,omitempty"`
extensions extensionFields `yaml:"-" json:"-"`
}
type extensionFields map[string]rawYAML
// PatchSpec is used to apply a patch to a source with a given set of options.
// This is used in [Spec.Patches]
type PatchSpec struct {
// Source is the name of the source that contains the patch to apply.
Source string `yaml:"source" json:"source" jsonschema:"required"`
// Strip is the number of leading path components to strip from the patch.
// The default is 1 which is typical of a git diff.
Strip *int `yaml:"strip,omitempty" json:"strip,omitempty"`
// Optional subpath to the patch file inside the source
// This is only useful for directory-backed sources.
Path string `yaml:"path,omitempty" json:"path,omitempty"`
}
// ChangelogEntry is an entry in the changelog.
// This is used to generate the changelog for the package.
type ChangelogEntry struct {
// Date is the date of the changelog entry.
Date time.Time `yaml:"date" json:"date" jsonschema:"oneof_required=date"`
// Author is the author of the changelog entry. e.g. `John Smith <john.smith@example.com>`
Author string `yaml:"author" json:"author"`
// Changes is the list of changes in the changelog entry.
Changes []string `yaml:"changes" json:"changes"`
}
// PostInstall is the post install configuration for the image.
type PostInstall struct {
// Symlinks is the list of symlinks to create in the container rootfs after the package(s) are installed.
// The key is the path the symlink should point to.
Symlinks map[string]SymlinkTarget `yaml:"symlinks,omitempty" json:"symlinks,omitempty"`
}
// SymlinkTarget specifies the properties of a symlink
type SymlinkTarget struct {
// Path is the path where the symlink should be placed
//
// Deprecated: This is here for backward compatibility. Use `Paths` instead.
Path string `yaml:"path" json:"path" jsonschema:"oneof_required=path"`
// Path is a list of `newpath`s that will all point to the same `oldpath`.
Paths []string `yaml:"paths" json:"paths" jsonschema:"oneof_required=paths"`
}
type SourceDockerImage struct {
Ref string `yaml:"ref" json:"ref"`
Cmd *Command `yaml:"cmd,omitempty" json:"cmd,omitempty"`
}
type SourceGit struct {
URL string `yaml:"url" json:"url"`
Commit string `yaml:"commit" json:"commit"`
KeepGitDir bool `yaml:"keepGitDir,omitempty" json:"keepGitDir,omitempty"`
Auth GitAuth `yaml:"auth,omitempty" json:"auth,omitempty"`
}
type GitAuth struct {
// Header is the name of the secret which contains the git auth header.
// when using git auth header based authentication.
// Note: This should not have the *actual* secret value, just the name of
// the secret which was specified as a build secret.
Header string `yaml:"header,omitempty" json:"header,omitempty"`
// Token is the name of the secret which contains a git auth token when using
// token based authentication.
// Note: This should not have the *actual* secret value, just the name of
// the secret which was specified as a build secret.
Token string `yaml:"token,omitempty" json:"token,omitempty"`
// SSH is the name of the secret which contains the ssh auth into when using
// ssh based auth.
// Note: This should not have the *actual* secret value, just the name of
// the secret which was specified as a build secret.
SSH string `yaml:"ssh,omitempty" json:"ssh,omitempty"`
}
// LLBOpt returns an [llb.GitOption] which sets the auth header and token secret
// values in LLB if they are set.
func (a *GitAuth) LLBOpt() llb.GitOption {
return gitOptionFunc(func(gi *llb.GitInfo) {
if a == nil {
return
}
if a.Header != "" {
gi.AuthHeaderSecret = a.Header
}
if a.Token != "" {
gi.AuthTokenSecret = a.Token
}
if a.SSH != "" {
gi.MountSSHSock = a.SSH
}
})
}
// SourceHTTP is used to download a file from an HTTP(s) URL.
type SourceHTTP struct {
// URL is the URL to download the file from.
URL string `yaml:"url" json:"url"`
// Digest is the digest of the file to download.
// This is used to verify the integrity of the file.
// Form: <algorithm>:<digest>
Digest digest.Digest `yaml:"digest,omitempty" json:"digest,omitempty"`
// Permissions is the octal file permissions to set on the file.
Permissions fs.FileMode `yaml:"permissions,omitempty" json:"permissions,omitempty"`
}
// SourceContext is used to generate a source from a build context. The path to
// the build context is provided to the `Path` field of the owning `Source`.
type SourceContext struct {
// Name is the name of the build context. By default, it is the magic name
// `context`, recognized by Docker as the default context.
Name string `yaml:"name,omitempty" json:"name,omitempty"`
}
// SourceInlineFile is used to specify the content of an inline source.
type SourceInlineFile struct {
// Contents is the content.
Contents string `yaml:"contents,omitempty" json:"contents,omitempty"`
// Permissions is the octal file permissions to set on the file.
Permissions fs.FileMode `yaml:"permissions,omitempty" json:"permissions,omitempty"`
// UID is the user ID to set on the directory and all files and directories within it.
// UID must be greater than or equal to 0
UID int `yaml:"uid,omitempty" json:"uid,omitempty"`
// GID is the group ID to set on the directory and all files and directories within it.
// UID must be greater than or equal to 0
GID int `yaml:"gid,omitempty" json:"gid,omitempty"`
}
// SourceInlineDir is used by by [SourceInline] to represent a filesystem directory.
type SourceInlineDir struct {
// Files is the list of files to include in the directory.
// The map key is the name of the file.
//
// Files with path separators in the key will be rejected.
Files map[string]*SourceInlineFile `yaml:"files,omitempty" json:"files,omitempty"`
// Permissions is the octal permissions to set on the directory.
Permissions fs.FileMode `yaml:"permissions,omitempty" json:"permissions,omitempty"`
// UID is the user ID to set on the directory and all files and directories within it.
// UID must be greater than or equal to 0
UID int `yaml:"uid,omitempty" json:"uid,omitempty"`
// GID is the group ID to set on the directory and all files and directories within it.
// UID must be greater than or equal to 0
GID int `yaml:"gid,omitempty" json:"gid,omitempty"`
}
// SourceInline is used to generate a source from inline content.
type SourceInline struct {
// File is the inline file to generate.
// File is treated as a literal single file.
// [SourceIsDir] will return false when this is set.
// This is mutually exclusive with [Dir]
File *SourceInlineFile `yaml:"file,omitempty" json:"file,omitempty"`
// Dir creates a directory with the given files and directories.
// [SourceIsDir] will return true when this is set.
// This is mutually exclusive with [File]
Dir *SourceInlineDir `yaml:"dir,omitempty" json:"dir,omitempty"`
}
// Command is used to execute a command to generate a source from a docker image.
type Command struct {
// Dir is the working directory to run the command in.
Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`
// Mounts is the list of sources to mount into the build steps.
Mounts []SourceMount `yaml:"mounts,omitempty" json:"mounts,omitempty"`
// List of CacheDirs which will be used across all Steps
CacheDirs map[string]CacheDirConfig `yaml:"cache_dirs,omitempty" json:"cache_dirs,omitempty"`
// Env is the list of environment variables to set for all commands in this step group.
Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`
// Steps is the list of commands to run to generate the source.
// Steps are run sequentially and results of each step should be cached.
Steps []*BuildStep `yaml:"steps" json:"steps" jsonschema:"required"`
}
// Source defines a source to be used in the build.
// A source can be a local directory, a git repositoryt, http(s) URL, etc.
type Source struct {
// This is an embedded union representing all of the possible source types.
// Exactly one must be non-nil, with all other cases being errors.
//
// === Begin Source Variants ===
DockerImage *SourceDockerImage `yaml:"image,omitempty" json:"image,omitempty"`
Git *SourceGit `yaml:"git,omitempty" json:"git,omitempty"`
HTTP *SourceHTTP `yaml:"http,omitempty" json:"http,omitempty"`
Context *SourceContext `yaml:"context,omitempty" json:"context,omitempty"`
Build *SourceBuild `yaml:"build,omitempty" json:"build,omitempty"`
Inline *SourceInline `yaml:"inline,omitempty" json:"inline,omitempty"`
// === End Source Variants ===
// Path is the path to the source after fetching it based on the identifier.
Path string `yaml:"path,omitempty" json:"path,omitempty"`
// Includes is a list of paths underneath `Path` to include, everything else is execluded
// If empty, everything is included (minus the excludes)
Includes []string `yaml:"includes,omitempty" json:"includes,omitempty"`
// Excludes is a list of paths underneath `Path` to exclude, everything else is included
Excludes []string `yaml:"excludes,omitempty" json:"excludes,omitempty"`
// Generate is the list generators to run on the source.
//
// Generators are used to generate additional sources from this source.
// As an example the `gomod` generator can be used to generate a go module cache from a go source.
// How a generator operates is dependent on the actual generator.
// Generators may also cauuse modifications to the build environment.
//
// Currently only two generators are supported: "gomod" and "cargohome".
// The "gomod" generator will generate a go module cache from the source.
// The "cargohome" generator will generate a cargo home from the source.
Generate []*SourceGenerator `yaml:"generate,omitempty" json:"generate,omitempty"`
}
// GeneratorGomod is used to generate a go module cache from go module sources
type GeneratorGomod struct {
// Paths is the list of paths to run the generator on. Used to generate multi-module in a single source.
Paths []string `yaml:"paths,omitempty" json:"paths,omitempty"`
}
// GeneratorCargohome is used to generate a cargo home from cargo sources
type GeneratorCargohome struct {
// Paths is the list of paths to run the generator on. Used to generate multi-module in a single source.
Paths []string `yaml:"paths,omitempty" json:"paths,omitempty"`
}
// SourceGenerator holds the configuration for a source generator.
// This can be used inside of a [Source] to generate additional sources from the given source.
type SourceGenerator struct {
// Subpath is the path inside a source to run the generator from.
Subpath string `yaml:"subpath,omitempty" json:"subpath,omitempty"`
// Gomod is the go module generator.
Gomod *GeneratorGomod `yaml:"gomod" json:"gomod"`
// Cargohome is the cargo home generator.
Cargohome *GeneratorCargohome `yaml:"cargohome" json:"cargohome"`
}
// ArtifactBuild configures a group of steps that are run sequentially along with their outputs to build the artifact(s).
type ArtifactBuild struct {
// Steps is the list of commands to run to build the artifact(s).
// Each step is run sequentially and will be cached accordingly depending on the frontend implementation.
Steps []BuildStep `yaml:"steps" json:"steps" jsonschema:"required"`
// Env is the list of environment variables to set for all commands in this step group.
Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`
// NetworkMode sets the network mode to use during the build phase.
// Accepted values: none, sandbox
// Default: none
NetworkMode string `yaml:"network_mode,omitempty" json:"network_mode,omitempty" jsonschema:"enum=none,enum=sandbox"`
}
// BuildStep is used to execute a command to build the artifact(s).
type BuildStep struct {
// Command is the command to run to build the artifact(s).
// This will always be wrapped as /bin/sh -c "<command>", or whatever the equivalent is for the target distro.
Command string `yaml:"command" json:"command" jsonschema:"required"`
// Env is the list of environment variables to set for the command.
Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"`
}
// SourceMount wraps a [Source] with a target mount point.
type SourceMount struct {
// Dest is the destination directory to mount to
Dest string `yaml:"dest" json:"dest" jsonschema:"required"`
// Spec specifies the source to mount
Spec Source `yaml:"spec" json:"spec" jsonschema:"required"`
}
// CacheDirConfig configures a persistent cache to be used across builds.
type CacheDirConfig struct {
// Mode is the locking mode to set on the cache directory
// values: shared, private, locked
// default: shared
Mode string `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"enum=shared,enum=private,enum=locked"`
// Key is the cache key to use to cache the directory
// default: Value of `Path`
Key string `yaml:"key,omitempty" json:"key,omitempty"`
// IncludeDistroKey is used to include the distro key as part of the cache key
// What this key is depends on the frontend implementation
// Example for Debian Buster may be "buster"
//
// An example use for this is with a Go(lang) build cache when CGO is included.
// Go is unable to invalidate cgo and re-using the same cache across different distros may cause issues.
IncludeDistroKey bool `yaml:"include_distro_key,omitempty" json:"include_distro_key,omitempty"`
// IncludeArchKey is used to include the architecture key as part of the cache key
// What this key is depends on the frontend implementation
// Frontends SHOULD use the buildkit platform arch
//
// As with [IncludeDistroKey], this is useful for Go(lang) builds with CGO.
IncludeArchKey bool `yaml:"include_arch_key,omitempty" json:"include_arch_key,omitempty"`
}
// Frontend encapsulates the configuration for a frontend to forward a build target to.
type Frontend struct {
// Image specifies the frontend image to forward the build to.
// This can be left unspecified *if* the original frontend has builtin support for the distro.
//
// If the original frontend does not have builtin support for the distro, this must be specified or the build will fail.
// If this is specified then it MUST be used.
Image string `yaml:"image,omitempty" json:"image,omitempty" jsonschema:"required,example=docker.io/my/frontend:latest"`
// CmdLine is the command line to use to forward the build to the frontend.
// By default the frontend image's entrypoint/cmd is used.
CmdLine string `yaml:"cmdline,omitempty" json:"cmdline,omitempty"`
}
// PackageSigner is the configuration for defining how to sign a package
type PackageSigner struct {
*Frontend `yaml:",inline" json:",inline"`
// Args are passed along to the signer frontend as build args
Args map[string]string `yaml:"args,omitempty" json:"args,omitempty"`
}
// PackageConfig encapsulates the configuration for artifact targets
type PackageConfig struct {
// Signer is the configuration to use for signing packages
Signer *PackageSigner `yaml:"signer,omitempty" json:"signer,omitempty"`
}
func (s *SystemdConfiguration) IsEmpty() bool {
if s == nil {
return true
}
if len(s.Units) == 0 {
return true
}
return false
}
func (s *SystemdConfiguration) EnabledUnits() map[string]SystemdUnitConfig {
if len(s.Units) == 0 {
return nil
}
units := make(map[string]SystemdUnitConfig)
for path, unit := range s.Units {
if unit.Enable {
units[path] = unit
}
}
return units
}
type ExtDecodeConfig struct {
AllowUnknownFields bool
}
var (
ErrNodeNotFound = errors.New("node not found")
ErrInvalidExtKey = errors.New("extension keys must start with \"x-\"")
)
// Ext reads the extension field from the spec and unmarshals it into the target
// value.
func (s *Spec) Ext(key string, target interface{}, opts ...func(*ExtDecodeConfig)) error {
v, ok := s.extensions[key]
if !ok {
return errors.Wrapf(ErrNodeNotFound, "extension field not found %q", key)
}
var yamlOpts []yaml.DecodeOption
if len(opts) > 0 {
var cfg ExtDecodeConfig
for _, opt := range opts {
opt(&cfg)
}
if !cfg.AllowUnknownFields {
yamlOpts = append(yamlOpts, yaml.Strict())
}
}
return yaml.UnmarshalWithOptions(v, target, yamlOpts...)
}
// WithExtension adds an extension field to the spec.
// If the value is set to a []byte, it is used as-is and is expected to already
// be in YAML format.
func (s *Spec) WithExtension(key string, value interface{}) error {
if !strings.HasPrefix(key, "x-") && !strings.HasPrefix(key, "X-") {
return errors.Wrap(ErrInvalidExtKey, key)
}
if s.extensions == nil {
s.extensions = make(extensionFields)
}
dt, ok := value.([]byte)
if ok {
_, err := parser.ParseBytes(dt, parseModeIgnoreComments)
if err != nil {
return errors.Wrap(err, "extension value provided is a []byte but is not valid YAML")
}
s.extensions[key] = dt
return nil
}
dt, err := yaml.Marshal(value)
if err != nil {
return errors.Wrapf(err, "failed to marshal extension field %q", key)
}
s.extensions[key] = dt
return nil
}