build

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 21, 2026 License: Apache-2.0 Imports: 44 Imported by: 0

Documentation

Overview

Package build drives container image builds through BuildKit's Go client (github.com/moby/buildkit/client), never by shelling out to `docker build` or the `docker` CLI, matching the same "no CLI shelling" rule the node communication layer follows. It accepts plain Dockerfiles via the dockerfile.v0 frontend, per the declarative app spec's build config, and Railpack-detected sources via their own solve path (railpack.go).

A build runs against the BuildKit instance embedded in whichever Docker Engine is local to the process running it, and Router (router.go) decides which process that is: this one, or a node an operator marked build-capable, reached over the agent transport.

Index

Constants

View Source
const (
	CacheModeMin = "min"
	CacheModeMax = "max"
)

Cache export modes accepted by BuildKit's s3 backend.

View Source
const (
	EnvStaticMaxMB     = "APP_STATIC_BUILD_MAX_MB"
	DefaultStaticMaxMB = 2048
)

Environment override and default for the size cap on an extracted static build.

View Source
const (
	EnvBuildTimeout       = "APP_BUILD_TIMEOUT"
	EnvBuildMaxConcurrent = "APP_BUILD_MAX_CONCURRENT"
	EnvBuildMemoryMB      = "APP_BUILD_MEMORY_MB"
	EnvBuildCPUMillis     = "APP_BUILD_CPU_MILLIS"
	EnvBuildCgroupRoot    = "APP_BUILD_CGROUP_ROOT"

	DefaultBuildTimeout       = 30 * time.Minute
	DefaultBuildMaxConcurrent = 2
)

Environment overrides and defaults for build limits. Zero disables a limit.

View Source
const (
	SpeedCacheDisabled     = "cache_disabled"
	SpeedCacheUnconfigured = "cache_unconfigured"
	SpeedLowCacheHit       = "low_cache_hit"
	SpeedSlowerRecently    = "slower_recently"
)

Speed finding codes. The dashboard translates these.

View Source
const (
	SpeedFixAuto   = "auto"
	SpeedFixGuided = "guided"
	SpeedFixManual = "manual"
)

Fix kinds: auto is applied by the control plane, guided needs one action elsewhere, manual is a repo change.

View Source
const EnvAttest = "APP_BUILD_ATTEST"

EnvAttest turns BuildKit SBOM and provenance attestations on for Dockerfile builds.

Variables

View Source
var (
	ErrRailpackSourceDirRequired = errors.New("build: railpack: source dir is required")
	ErrRailpackTagRequired       = errors.New("build: railpack: tag is required")
)

ErrRailpackSourceDirRequired and ErrRailpackTagRequired are returned by Validate for the two required fields, matching Request.Validate's own "distinguishable sentinel error" shape for the Dockerfile path.

View Source
var (
	ErrContextDirRequired = errors.New("build: context dir is required")
	ErrTagRequired        = errors.New("build: tag is required")
)

ErrContextDirRequired and ErrTagRequired are returned by Validate for the two required fields, so callers can match on them if they need to distinguish the failure reason without parsing the error string.

View Source
var ErrBuildMemoryLimit = errors.New("build exceeded its memory limit")

ErrBuildMemoryLimit marks a build killed for exceeding its memory limit.

View Source
var ErrBuildTimeout = errors.New("build timed out")

ErrBuildTimeout is the cause of a build stopped by its time limit.

View Source
var ErrCacheRegistryRefRequired = errors.New("build: cache registry insecure flag set without a registry ref")

ErrCacheRegistryRefRequired is returned by CacheConfig.entries when RegistryInsecure is set but RegistryRef is empty: an insecure flag with nothing to apply it to is almost certainly a caller mistake (e.g. a typo'd env var name that leaves RegistryRef unset), not a meaningful configuration, so this fails loudly at construction time rather than silently doing nothing with the flag.

View Source
var ErrExtractTooLarge = errors.New("build: extracted output exceeds the size limit")

ErrExtractTooLarge is returned when an image directory exceeds the size cap.

View Source
var ErrNoBuildNodeAvailable = errors.New("build: no build-capable node is currently online")

ErrNoBuildNodeAvailable is no longer returned by SelectBuildNode: the build-node-routing initiative made falling back to the local node the default instead (SelectBuildNode's own doc comment). Kept as an exported sentinel in case anything outside this package still references it.

View Source
var ErrRemoteDockerfileOutsideContext = errors.New("build: a dispatched build's dockerfile must live inside its build context")

ErrRemoteDockerfileOutsideContext is returned when a build's Dockerfile sits outside its own build context. A local build can read it anyway, since both are paths on one filesystem; a dispatched one cannot, because only the context is shipped to the build node.

View Source
var ErrRemoteSecretsUnsupported = errors.New("build: secret mounts (private package registries) are not supported on remote build nodes yet; build on the control plane or remove the package registry")

ErrRemoteSecretsUnsupported is returned when a build with secret mounts would run on a remote build node, which does not receive secrets yet.

View Source
var ErrRepoTooLarge = errors.New("build: detect: repository is too large to inspect")

ErrRepoTooLarge is returned by Detect when the cloned tree exceeds detectMaxRepoBytes.

View Source
var ErrRepoURLSchemeNotAllowed = errors.New("repo_url must use http or https")

ErrRepoURLSchemeNotAllowed is returned by ValidatePublicRepoURL for a repoURL whose scheme is not http/https.

View Source
var ErrUnsafeContextPath = errors.New("build: build context entry escapes the destination directory")

ErrUnsafeContextPath is returned by UntarContext for an entry whose name escapes the destination directory. A build context arrives over the agent transport from an authenticated control plane, but an extractor that trusts entry names is a file-overwrite primitive regardless of who is on the other end.

Functions

func AppFromTag

func AppFromTag(tag string) string

AppFromTag maps an image tag like "reg.io/org/web:abc" to the app key "web".

func EmitCacheWarning added in v0.2.0

func EmitCacheWarning(progress func(ProgressEvent), msg string)

EmitCacheWarning reports that the remote build cache was skipped.

func FrameworkLabel added in v0.2.0

func FrameworkLabel(provider string) (string, bool)

FrameworkLabel returns provider's human-readable name and true, or ("", false) if provider is empty or outside supportedRailpackProviders.

func HostPlatform

func HostPlatform() string

HostPlatform is the OS/arch BuildKit builds for: the daemon's own platform, since no cross-platform target is requested.

func SelectBuildNode

func SelectBuildNode(nodes []NodeInfo) string

SelectBuildNode picks which node a build should run on, given every known node. AcceptsBuildWorkloads is a routing preference, not a hard requirement: a build is never failed just because the node an operator would rather use is unavailable right now.

  • No node in nodes has AcceptsBuildWorkloads set: returns "" (this control plane's own local node), matching the "" == local convention migrations/0009_node_placement.sql established for service and database placement. This is also what a single-node deployment sees unchanged.
  • At least one node has AcceptsBuildWorkloads set and is Online: the Online one with the lexicographically smallest ID is selected, a deterministic, easy-to-reason-about tie-break rather than anything load-aware; real load-based scheduling is explicitly out of scope, this project's stated non-goal against building a scheduler with bin-packing, affinity rules, or autoscaling in v1, and this is the build-node equivalent of that same non-goal.
  • At least one node has AcceptsBuildWorkloads set but none is Online: falls back to "" (local) rather than failing the build, since a build routing preference blipping offline should never be the reason a deploy fails.

func SlogProgress

func SlogProgress(log *slog.Logger) func(ProgressEvent)

SlogProgress adapts a *slog.Logger into a progress func, for callers that just want build progress logged rather than consumed some other way (an SSE stream, a test assertion). log defaults to slog.Default() if nil.

func StaticMaxBytesFromEnv

func StaticMaxBytesFromEnv(lookup func(string) (string, bool)) int64

StaticMaxBytesFromEnv reads the extraction cap in bytes; zero means unlimited.

func TarContext added in v0.2.0

func TarContext(ctx context.Context, dir string, w io.Writer) error

TarContext writes dir's contents to w as a tar stream: regular files, directories, and symlinks, with their permission bits. Anything else (sockets, devices, named pipes) is skipped, since no build context meaningfully needs one and Docker's own context upload skips them too.

func UntarConfined

func UntarConfined(r io.Reader, destDir string, maxBytes int64) error

UntarConfined unpacks regular files and directories of r under destDir, skipping links and special files, and fails past maxBytes (0 is unlimited).

func UntarContext added in v0.2.0

func UntarContext(ctx context.Context, r io.Reader, dir string) error

UntarContext extracts a TarContext stream into dir, which must already exist. Entry names that escape dir fail the whole extraction with ErrUnsafeContextPath rather than being skipped: a context that cannot be reproduced faithfully must not be built. Every write goes through an os.Root, so even a symlink the checks below miss cannot redirect a write outside dir.

func ValidatePublicRepoURL added in v0.2.0

func ValidatePublicRepoURL(repoURL string) error

ValidatePublicRepoURL rejects any scheme other than http/https, the same restriction internal/api/git_branches.go always enforced for a caller-supplied repo_url (that file's own doc comment on errRepoURLSchemeNotAllowed explains why: go-git registers a "file" transport unconditionally, and an unrestricted URL would let a caller read local files on this host or probe internal network reachability). Detect and internal/api/build_detect.go share this exact check since both accept the same caller-controlled repo_url shape.

Types

type Attestations added in v0.2.0

type Attestations struct {
	// SBOM is the bare SPDX or CycloneDX document, empty when none was produced.
	SBOM []byte
	// Provenance is the SLSA provenance predicate, empty when none was produced.
	Provenance []byte
	// Incomplete is true when scanning the image stream failed part way, so a
	// missing SBOM or provenance may be a read error rather than "none produced".
	Incomplete bool
	// ScanError says why the stream scan stopped, empty when it finished.
	ScanError string
}

Attestations are the in-toto predicates BuildKit attached to a build.

type Builder

type Builder interface {
	Build(ctx context.Context, req Request, progress func(ProgressEvent)) (*Result, error)
	BuildRailpack(ctx context.Context, req RailpackRequest, progress func(ProgressEvent)) (*Result, error)
}

Builder is the build surface the Limiter wraps and itself provides.

type CacheConfig

type CacheConfig struct {
	// Dir enables BuildKit's "local" cache backend: a directory on this
	// process's own disk. Only ever useful to the one build node that
	// owns that disk, which is exactly Phase 1's scoped-down cache
	// (client.go's WithCacheDir).
	Dir string

	// RegistryRef enables BuildKit's "registry" cache backend: an image
	// reference (e.g. "registry.example.com/levelrail/build-cache:app")
	// that cache blobs are pushed to and pulled from using the same
	// registry credentials Docker itself already has configured. This
	// is the actual "remote cache... shared across dedicated build
	// nodes" the build design calls for: any build
	// node with network access to the registry gets the same cache,
	// unlike Dir.
	RegistryRef string

	// RegistryInsecure allows the registry backend to talk plain HTTP
	// or accept a self-signed certificate, matching BuildKit's own
	// "insecure" cache attribute. Only meaningful when RegistryRef is
	// set; ignored otherwise.
	RegistryInsecure bool

	// S3 enables BuildKit's s3 cache backend for one build.
	S3 *S3Cache
}

CacheConfig describes every BuildKit cache import/export backend a Client should use. The zero value disables caching entirely, matching WithCacheDir's pre-existing "empty disables cache import/export entirely" behavior.

func (CacheConfig) String

func (c CacheConfig) String() string

String renders c for logging, never including credentials (this struct never holds any: registry auth comes from Docker's own configured credential store, the same as every other registry operation in this codebase, per ensureImage in internal/docker).

type CgroupProvisioner

type CgroupProvisioner interface {
	Provision(id string, memoryMB, cpuMillis int) (parent string, cleanup func(), err error)
}

CgroupProvisioner creates a resource-capped cgroup for one build. parent is the value BuildKit takes as its cgroup parent; cleanup removes the cgroup.

func NewCgroupProvisioner

func NewCgroupProvisioner(root string) CgroupProvisioner

NewCgroupProvisioner returns a cgroup v2 provisioner rooted at root, or at /sys/fs/cgroup when root is empty.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client drives builds against the BuildKit instance embedded in a local Docker Engine daemon.

Docker Desktop (and any dockerd using the default "docker" buildx driver, as opposed to the "docker-container" driver) does not expose a bare buildkitd gRPC endpoint of its own. BuildKit runs inside dockerd and is only reachable by hijacking the daemon's HTTP connection at the /grpc endpoint, exactly the way `docker buildx build` reaches it when using the default driver. That is what dockerbuildkit.ClientOpts wires up below, using the same *dockerclient.Client already used to talk to the Engine API elsewhere in this codebase (see internal/docker).

This hijacking approach is the one that actually works in this environment; a standalone buildkitd endpoint is not an option here.

func NewClient

func NewClient(ctx context.Context, docker *dockerclient.Client, opts ...Option) (*Client, error)

NewClient builds a Client that reaches BuildKit through docker's hijacked connection, and reuses the same Docker Engine API client to load the finished image back into the local image store.

docker must already be a working *dockerclient.Client (e.g. from dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())). NewClient does not create one itself so callers control the daemon connection lifecycle.

func (*Client) Build

func (c *Client) Build(ctx context.Context, req Request, progress func(ProgressEvent)) (*Result, error)

Build runs req end to end: solves the Dockerfile with BuildKit, streams the resulting image as a tar into the local Docker Engine's /images/load endpoint, and waits for both to finish. It never shells out to the docker CLI.

progress, if non-nil, receives every build progress update as it happens, including the docker-image-load phase after BuildKit's own solve finishes (that phase isn't part of BuildKit's SolveStatus stream, but is relayed through the same callback for one unified progress feed). Pass SlogProgress(logger) to just log them, or nil to discard them entirely.

func (*Client) BuildRailpack

func (c *Client) BuildRailpack(ctx context.Context, req RailpackRequest, progress func(ProgressEvent)) (*Result, error)

BuildRailpack runs req end to end: generates a Railpack build plan for req.SourceDir, rejects it unless Railpack detected a supported provider (supportedRailpackProviders), converts the plan to a BuildKit LLB definition, solves it, and streams the resulting image into the local Docker Engine's /images/load endpoint, exactly like Build does for a Dockerfile. It never shells out to the railpack or docker CLI.

progress behaves exactly as it does for Build: pass SlogProgress(logger) to log every update, or nil to discard them.

func (*Client) Close

func (c *Client) Close() error

Close releases the underlying BuildKit connection. It does not close the Docker Engine API client passed to NewClient, since the caller owns that client's lifecycle.

func (*Client) SolveRemote added in v0.2.0

func (c *Client) SolveRemote(ctx context.Context, req RemoteRequest, out io.Writer, progress func(ProgressEvent)) (*Result, error)

SolveRemote runs a build dispatched from a control plane against this node's own BuildKit and writes the resulting docker-save tar to out, rather than loading it into this node's image store: the image is wanted by whoever asked for the build, not by the node that ran it.

req.ContextDir must already hold the unpacked build context.

type Clock

type Clock interface {
	Now() time.Time
	After(d time.Duration) <-chan time.Time
}

Clock lets tests drive the timeout and queue wait without sleeping.

type DetectRequest added in v0.2.0

type DetectRequest struct {
	// RepoURL is the git remote to shallow-clone. Required, and must pass
	// ValidatePublicRepoURL.
	RepoURL string
	// Ref is the branch to check out. Empty means the remote's default
	// branch (HEAD).
	Ref string
}

DetectRequest is Detect's input: a git source to inspect, never built or pushed anywhere.

type DetectResult added in v0.2.0

type DetectResult struct {
	// Provider is Railpack's own raw provider id (e.g. "node", "golang"),
	// empty if Railpack detected nothing buildable at all.
	Provider string
	// FrameworkName is FrameworkLabel(Provider)'s human-readable name,
	// empty if Provider is empty or not in supportedRailpackProviders:
	// either way, the caller's own contract is "fall back to manual build
	// type selection" for an empty FrameworkName, not an error.
	FrameworkName string
	// Port is the detected container port the app listens on, or 0 if
	// detection failed or no port flag was found. For Node, parsed from
	// scripts.start (or preview/serve); for Dockerfile, from EXPOSE.
	Port int
	// PortSource describes where Port came from (e.g. "from scripts.start
	// --port 4173"), empty if Port is 0.
	PortSource string
	// FrameworkID is the matched internal/frameworks preset id, empty when
	// no preset matched.
	FrameworkID string
}

DetectResult is Detect's output.

func Detect added in v0.2.0

func Detect(ctx context.Context, req DetectRequest) (*DetectResult, error)

Detect shallow-clones req.RepoURL at req.Ref into a temporary directory and runs Railpack's own provider detection against it, no build, no image, no BuildKit connection: the same pure core.GenerateBuildPlan call generateRailpackPlan (railpack.go) already uses for a real build, which only ever reads files under the checkout (package.json, go.mod, pom.xml, and the like) to decide a provider, so this never executes anything the checkout itself contains.

Bounded against an untrusted repoURL three ways: a hard timeout (detectCloneTimeout), a shallow, single-branch clone (Depth: 1), and a post-clone size check (detectMaxRepoBytes). None of these are a build's own real safety net (the reconciler and BuildKit's own resource limits still apply once a build is actually triggered); they exist only because this pre-flight check runs synchronously inside an HTTP request the wizard is waiting on.

type ImageExtractor

type ImageExtractor struct {
	// contains filtered or unexported fields
}

ImageExtractor copies a directory out of a built image into a host directory, then deletes the image. It never starts the image.

func NewImageExtractor

func NewImageExtractor(docker *dockerclient.Client, maxBytes int64) *ImageExtractor

NewImageExtractor wraps docker; maxBytes caps the extracted size (0 is unlimited).

func (*ImageExtractor) ExtractDir

func (e *ImageExtractor) ExtractDir(ctx context.Context, tag, srcPath, destDir string) error

ExtractDir copies srcPath of image tag into destDir and removes the image.

type ImageSizer

type ImageSizer func(ctx context.Context, tag string) int64

ImageSizer reports a built image's size in bytes, 0 when unknown.

type Limiter

type Limiter struct {
	// contains filtered or unexported fields
}

Limiter queues builds past the per-node concurrency limit, stops builds past the time limit, and records each outcome with its cache hit counts.

func NewLimiter

func NewLimiter(inner Builder, limits LimitsSource, opts ...LimiterOption) *Limiter

NewLimiter wraps inner. limits is read once per build.

func (*Limiter) Build

func (l *Limiter) Build(ctx context.Context, req Request, progress func(ProgressEvent)) (*Result, error)

Build implements the deploy pipeline's ImageBuilder.

func (*Limiter) BuildRailpack

func (l *Limiter) BuildRailpack(ctx context.Context, req RailpackRequest, progress func(ProgressEvent)) (*Result, error)

BuildRailpack implements the deploy pipeline's ImageBuilder.

func (*Limiter) Load

func (l *Limiter) Load() []NodeLoad

Load reports slot usage per node that has seen a build, sorted by node ID.

func (*Limiter) Queued

func (l *Limiter) Queued() (waiting, running int)

Queued reports builds waiting for a slot and builds running, across all nodes.

type LimiterOption

type LimiterOption func(*Limiter)

LimiterOption configures a Limiter.

func WithCgroupProvisioner

func WithCgroupProvisioner(c CgroupProvisioner) LimiterOption

WithCgroupProvisioner enforces the CPU and memory limits through a cgroup per build.

func WithImageSizer

func WithImageSizer(s ImageSizer) LimiterOption

WithImageSizer records the size of every built image.

func WithLimiterClock

func WithLimiterClock(c Clock) LimiterOption

WithLimiterClock overrides the clock, for tests.

func WithLimiterLogger

func WithLimiterLogger(lg *slog.Logger) LimiterOption

WithLimiterLogger overrides the logger.

func WithNodeResolver

func WithNodeResolver(n NodeResolver) LimiterOption

WithNodeResolver makes the concurrency limit apply per node instead of globally.

func WithRunRecorder

func WithRunRecorder(r RunRecorder) LimiterOption

WithRunRecorder records every finished build.

type Limits

type Limits struct {
	MaxConcurrent int
	Timeout       time.Duration
	MemoryMB      int
	CPUMillis     int
}

Limits caps builds. A zero field means unlimited. MemoryMB and CPUMillis (1000 = one core) are per build; the rest are control-plane wide.

func LimitsFromEnv

func LimitsFromEnv(lookup func(string) (string, bool)) Limits

LimitsFromEnv reads the env defaults; a malformed value falls back to the default.

type LimitsSource

type LimitsSource func(ctx context.Context, app string) Limits

LimitsSource resolves the limits for the next build of app, so a settings change applies without a restart.

type NodeBuilder added in v0.2.0

type NodeBuilder interface {
	BuildOnNode(ctx context.Context, nodeID string, req RemoteRequest, image io.Writer, progress func(ProgressEvent)) (*Result, error)
}

NodeBuilder runs a build on one specific node and streams the result back: progress as it happens, and the docker-save image tar into image. internal/agent's BuildDispatcher satisfies this over the agent transport; nothing in this package knows how a node is reached.

type NodeInfo

type NodeInfo struct {
	// ID is the node's identifier, matching store.Node.ID.
	ID string
	// AcceptsBuildWorkloads mirrors store.Node.AcceptsBuildWorkloads
	// (migrations/0010_node_workloads.sql): whether this node has opted
	// in to running build work at all.
	AcceptsBuildWorkloads bool
	// Online reports whether this node is currently reachable through
	// the agent transport, i.e. whether
	// agent.Registry.Get for this node's ID currently succeeds. A
	// build-capable node the control plane cannot currently reach is
	// not a usable candidate, the same reasoning resolveNodeTransport
	// (cmd/levelrail/main.go) already applies to service/database
	// placement.
	Online bool
}

NodeInfo is the minimal node shape SelectBuildNode needs. Deliberately not store.Node: internal/build has no dependency on internal/store today, and build routing only ever cares about three of a node's fields, the same narrow, consumer-defined interface convention every other package boundary in this codebase already follows (e.g. internal/deploy's ImageBuilder/ServiceStore/SecretChecker).

type NodeLoad

type NodeLoad struct {
	Node    string
	Running int
	Waiting int
	Limit   int
}

NodeLoad is one node's build slot usage.

type NodeResolver

type NodeResolver func(ctx context.Context) string

NodeResolver names the node the next build will run on, "" for the local node.

type NodeSource added in v0.2.0

type NodeSource func(ctx context.Context) ([]NodeInfo, error)

NodeSource reports every node the control plane currently knows about, with whichever of them are reachable right now marked Online. Consulted per build rather than once at startup, so marking a node build-capable, or a build node dropping offline, takes effect on the next build instead of the next control-plane restart.

type Option

type Option func(*Client)

Option configures optional Client behavior.

func WithCacheDir

func WithCacheDir(dir string) Option

WithCacheDir enables BuildKit's local cache backend, importing from and exporting to dir on every build. Empty (the default) disables cache import/export entirely, not an empty-but-present cache.

This was the build design's "remote cache" goal scoped honestly for Phase 1's single-node target, before WithCacheRegistry was added: a cache genuinely shared across dedicated build nodes needs a registry or object-store backend and dedicated build nodes to share it with. WithCacheDir is still useful on its own (fast incremental rebuilds on one node with no registry round trip) and composes with WithCacheRegistry rather than being replaced by it: both may be set on the same Client, see CacheConfig.

func WithCacheRegistry

func WithCacheRegistry(ref string) Option

WithCacheRegistry enables BuildKit's registry cache backend (CacheConfig.RegistryRef): this is the actual remote cache the build design asks for, shared by every build node with network access to ref's registry, unlike WithCacheDir's per-machine directory. Empty ref disables the registry backend, the same "empty disables this backend" convention WithCacheDir already establishes.

func WithCacheRegistryInsecure

func WithCacheRegistryInsecure() Option

WithCacheRegistryInsecure allows the registry cache backend (WithCacheRegistry) to talk plain HTTP or accept a self-signed certificate. Only meaningful combined with WithCacheRegistry; NewClient returns ErrCacheRegistryRefRequired if this is set without a registry ref configured, rather than silently ignoring it.

type ProgressEvent

type ProgressEvent struct {
	// Step is the build step's name, e.g. "[2/4] RUN go build".
	Step string
	// Cached is true when this step was skipped because BuildKit's cache
	// (see WithCacheDir) already had the result.
	Cached bool
	// Completed is true once Step finished, successfully or not.
	Completed bool
	// Error is non-empty when Step failed.
	Error string
	// Log is a raw output line from the step (stdout/stderr from the
	// build), empty for step-lifecycle events.
	Log string
	// Stream is "stdout" or "stderr" when Log is non-empty, and empty
	// for a step-lifecycle event (Log == ""), since those never came
	// from either stream. Added for internal/deploylog.Recorder, so a
	// persisted/live-streamed build log line can carry the same
	// stdout/stderr distinction web/src/hooks/useDeployLogStream.ts's
	// contract already expects (its own doc comment: "distinguishing
	// stdout/stderr is useful for highlighting failed build steps").
	// Populated from BuildKit's own VertexLog.Stream (see
	// relayProgress), 1/2 being BuildKit's stdout/stderr convention; the
	// docker-image-load phase (loadImage) has no equivalent stream
	// signal from the Engine API, so it always reports "stdout".
	Stream string
	// CacheWarning is set when the remote build cache was skipped or failed and
	// the build carried on without it. Log carries the same text.
	CacheWarning string
}

ProgressEvent is one structured build progress update, deliberately decoupled from BuildKit's own SolveStatus wire type so callers, a future SSE handler in particular (the build design's build log streaming requirement), don't need to import moby/buildkit/client just to format a build log line for a browser.

type RailpackRequest

type RailpackRequest struct {
	// SourceDir is the app's source root Railpack inspects to detect its
	// provider and generate a build plan, and the build context BuildKit
	// reads from. Required.
	SourceDir string

	// Tag is the image reference given to the built image, e.g.
	// "levelrail/thesvg:abc1234". Required.
	Tag string

	// CgroupParent places every build step in a pre-sized cgroup. Set by the Limiter; local builds only.
	CgroupParent string
}

RailpackRequest describes a single Railpack-detected build: no Dockerfile, no user-authored build steps, just a source directory Railpack inspects itself to determine how to build it.

func (RailpackRequest) Validate

func (r RailpackRequest) Validate() error

Validate checks the request is well-formed enough to attempt a build. Like Request.Validate, it does not check the filesystem: a missing source directory surfaces later, from Railpack's own app.NewApp call, which already produces a clear error for that case.

type RemoteKind added in v0.2.0

type RemoteKind string

RemoteKind selects which solve path a dispatched build runs, since a Dockerfile build and a Railpack build are genuinely different solves (see railpack.go) rather than one solve with a flag.

const (
	RemoteKindDockerfile RemoteKind = "dockerfile"
	RemoteKindRailpack   RemoteKind = "railpack"
)

RemoteKindDockerfile and RemoteKindRailpack are the two dispatchable build kinds, matching internal/deploy's own two building build types.

type RemoteRequest added in v0.2.0

type RemoteRequest struct {
	Kind RemoteKind

	// ContextDir is the build context root on whichever side currently
	// holds it: the control plane's checkout when dispatching, the build
	// node's unpacked copy when running.
	ContextDir string

	// DockerfilePath is relative to ContextDir, since the control plane's
	// own absolute paths mean nothing on the build node. Empty means the
	// context root's own Dockerfile. Ignored for RemoteKindRailpack.
	DockerfilePath string

	Tag       string
	Target    string
	BuildArgs map[string]string
	NoCache   bool

	// Cache is the registry cache backend the build node should import
	// from and export to. CacheConfig.Dir is never carried here: it names
	// a directory on the dispatching side's own disk.
	Cache CacheConfig
}

RemoteRequest is one build dispatched to another node: everything the build node needs that is not the context's bytes.

func NewRemoteRailpackRequest added in v0.2.0

func NewRemoteRailpackRequest(req RailpackRequest, cache CacheConfig) (RemoteRequest, error)

NewRemoteRailpackRequest turns a local Railpack build into a dispatchable one.

func NewRemoteRequest added in v0.2.0

func NewRemoteRequest(req Request, cache CacheConfig) (RemoteRequest, error)

NewRemoteRequest turns a local Dockerfile build into a dispatchable one, re-rooting DockerfilePath relative to the context.

type Request

type Request struct {
	// ContextDir is the build context root, matching `docker build <dir>`.
	// Required.
	ContextDir string

	// DockerfilePath is the path to the Dockerfile. It may live outside
	// ContextDir. Defaults to "<ContextDir>/Dockerfile" when empty.
	DockerfilePath string

	// Tag is the image reference given to the built image, e.g.
	// "levelrail-spike:latest". Required.
	Tag string

	// Target is an optional multi-stage build target name.
	Target string

	// BuildArgs are passed through as build-time --build-arg equivalents.
	BuildArgs map[string]string

	// NoCache disables BuildKit's cache for this build.
	NoCache bool

	// Attest asks BuildKit for an SBOM and minimal provenance attestation.
	Attest bool

	// S3Cache adds BuildKit's s3 cache backend for this build. Nil means none.
	S3Cache *S3Cache

	// Secrets are BuildKit secret mounts by id, readable in a Dockerfile
	// with RUN --mount=type=secret,id=<id>. They never enter an image layer
	// or the build cache key. Local builds only: a remote build node does
	// not receive them.
	Secrets map[string][]byte

	// CgroupParent places the build's processes in a pre-sized cgroup so
	// CPU and memory limits hold. Set by the Limiter; local builds only.
	CgroupParent string
}

Request describes a single Dockerfile build. It is deliberately the pure, unit-testable input to newSolveOpt: nothing in this file touches a filesystem, a network, or a daemon, so Validate can be exercised with plain table-driven tests that need no live BuildKit connection.

func (Request) Validate

func (r Request) Validate() error

Validate checks the request is well-formed enough to attempt a build. It does not check the filesystem; newSolveOpt does that, because a missing directory is an environment problem, not a malformed request.

type Result

type Result struct {
	Tag      string
	Duration time.Duration

	// ExporterResponse is BuildKit's raw exporter metadata (e.g. the
	// resulting image ID), passed through for callers that need it.
	ExporterResponse map[string]string

	// Attestations holds the SBOM and provenance of a build run with Request.Attest.
	Attestations Attestations
}

Result is what a successful build produced.

type Router added in v0.2.0

type Router struct {
	// contains filtered or unexported fields
}

Router is the build entry point the deploy pipeline calls: it picks a build node (SelectBuildNode, node.go) and either builds locally or dispatches, with the same method set and the same results either way.

func NewRouter added in v0.2.0

func NewRouter(local *Client, nodes NodeSource, remote NodeBuilder, opts ...RouterOption) *Router

NewRouter builds a Router over local. nodes and remote may both be nil, which pins every build to local: that is the single-node default, not a degraded mode.

func (*Router) Build added in v0.2.0

func (r *Router) Build(ctx context.Context, req Request, progress func(ProgressEvent)) (*Result, error)

Build implements internal/deploy.ImageBuilder.

func (*Router) BuildRailpack added in v0.2.0

func (r *Router) BuildRailpack(ctx context.Context, req RailpackRequest, progress func(ProgressEvent)) (*Result, error)

BuildRailpack implements internal/deploy.ImageBuilder.

func (*Router) ImageSize

func (r *Router) ImageSize(ctx context.Context, tag string) int64

ImageSize reports a built image's size in bytes from the local image store, 0 when unknown.

func (*Router) SelectedNode

func (r *Router) SelectedNode(ctx context.Context) string

SelectedNode names the node the next build would run on, "" for the local one.

type RouterOption added in v0.2.0

type RouterOption func(*Router)

RouterOption configures optional Router behavior.

func WithRouterLogger added in v0.2.0

func WithRouterLogger(logger *slog.Logger) RouterOption

WithRouterLogger overrides the logger a dispatch decision is reported on. Defaults to slog.Default().

type Run

type Run struct {
	App         string        `json:"app"`
	Tag         string        `json:"tag"`
	StartedAt   time.Time     `json:"started_at"`
	Duration    time.Duration `json:"-"`
	QueueWait   time.Duration `json:"-"`
	Status      RunStatus     `json:"status"`
	CachedSteps int           `json:"cached_steps"`
	TotalSteps  int           `json:"total_steps"`
	Error       string        `json:"error,omitempty"`
	Node        string        `json:"node,omitempty"`
	Platform    string        `json:"platform,omitempty"`
	ImageBytes  int64         `json:"image_bytes,omitempty"`
}

Run is one finished build as the Limiter saw it.

type RunRecorder

type RunRecorder interface {
	RecordBuildRun(ctx context.Context, r Run) error
}

RunRecorder persists one finished build.

type RunStatus

type RunStatus string

RunStatus is how one build ended.

const (
	RunSuccess  RunStatus = "success"
	RunFailed   RunStatus = "failed"
	RunTimeout  RunStatus = "timeout"
	RunCanceled RunStatus = "canceled"
	// RunMemoryLimit is a build killed for exceeding its memory limit.
	RunMemoryLimit RunStatus = "memory_limit"
)

Build run outcomes recorded by the Limiter.

type S3Cache added in v0.2.0

type S3Cache struct {
	Region          string
	Bucket          string
	Prefix          string
	Endpoint        string
	PathStyle       bool
	AccessKeyID     string
	SecretAccessKey string
	Mode            string
}

S3Cache configures BuildKit's s3 cache backend. It carries credentials, so it must never be logged or serialised; String is safe.

func (S3Cache) String added in v0.2.0

func (s S3Cache) String() string

String describes the cache without any credential.

type SpeedCacheState

type SpeedCacheState struct {
	Configured bool
	Enabled    bool
	TargetID   string
}

SpeedCacheState is what the control plane knows about the app's remote cache setting.

type SpeedFinding

type SpeedFinding struct {
	Code     string `json:"code"`
	Severity string `json:"severity"`
	FixKind  string `json:"fix_kind"`
	TargetID string `json:"target_id,omitempty"`
}

SpeedFinding is one suggestion to make builds faster.

type SpeedReport

type SpeedReport struct {
	Builds           int            `json:"builds"`
	WarmP50Seconds   float64        `json:"warm_p50_seconds"`
	ColdP50Seconds   float64        `json:"cold_p50_seconds"`
	WarmKnown        bool           `json:"warm_known"`
	TimeSavedSeconds float64        `json:"time_saved_seconds"`
	Findings         []SpeedFinding `json:"findings"`
}

SpeedReport is the build speed summary and advice for one app.

func AdviseSpeed

func AdviseSpeed(runs []Run, cache SpeedCacheState) SpeedReport

AdviseSpeed analyses runs (newest first) and returns time saved by cache and fix suggestions.

type Trend

type Trend struct {
	Builds     int     `json:"builds"`
	Succeeded  int     `json:"succeeded"`
	Failed     int     `json:"failed"`
	TimedOut   int     `json:"timed_out"`
	Canceled   int     `json:"canceled"`
	Skewed     int     `json:"skewed"`
	P50Seconds float64 `json:"p50_seconds"`
	P95Seconds float64 `json:"p95_seconds"`
	MinSeconds float64 `json:"min_seconds"`
	MaxSeconds float64 `json:"max_seconds"`
	// CacheHitRatio is cached steps over total steps across successful builds,
	// valid only when CacheKnown is true.
	CacheHitRatio float64 `json:"cache_hit_ratio"`
	CacheKnown    bool    `json:"cache_known"`
}

Trend summarises the last N builds of one app. Percentiles cover successful builds only; failed, timed out and canceled builds are counted, not timed.

func ComputeTrend

func ComputeTrend(runs []Run) Trend

ComputeTrend folds runs into a Trend. A successful run with a negative duration (clock skew) is counted in Skewed and left out of the timings.

type UnsupportedProviderError

type UnsupportedProviderError struct {
	// Provider is the provider name Railpack detected, or "" if it
	// detected none at all.
	Provider string
}

UnsupportedProviderError is returned when Railpack's own detection (core.BuildResult.DetectedProviders) resolved to a provider outside this slice's scope (supportedRailpackProviders), including the empty string for "no provider matched at all". It is a distinct type, not a plain fmt.Errorf, so callers (internal/deploy in particular) can errors.As it to produce a caller-appropriate message rather than a generic build failure, the same "fail loudly, not silently" pattern deploy.go's validateEnv already establishes for unsupported env resolution.

func (*UnsupportedProviderError) Detail

func (e *UnsupportedProviderError) Detail() string

Detail is the message without the package prefix, for callers that add their own (internal/deploy).

func (*UnsupportedProviderError) Error

func (e *UnsupportedProviderError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL