deploy

package
v0.2.0-beta.1 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package deploy is the build integration: it connects internal/spec's build declaration to internal/build's BuildKit client, and a successful build's output to internal/store's desired state, closing the loop the application controller (internal/reconcile/ application) reads from on every reconcile.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoServices = errors.New("deploy: no services to deploy")

ErrNoServices is returned by DeploySpec when req.Services is empty: there is nothing to fan out, and proceeding would create an App row that owns nothing.

Functions

func ToDesiredService

func ToDesiredService(name, image string, svc spec.Service) (store.DesiredService, error)

ToDesiredService exports toDesiredService for internal/webhook's own legacy single-app path (webhook.Handler.beginDeployAttempt), which has a static spec.Service from its Config but no store.DesiredService to read one back from: it needs the identical spec.Service -> desired state translation this package already does for every real deploy, to build that attempt's store.DeployAttemptSnapshot.

Types

type AppStore

type AppStore interface {
	SaveApp(ctx context.Context, a store.App) error
	GetAppByName(ctx context.Context, name string) (store.App, error)
	UpdateServiceApp(ctx context.Context, serviceName, appID string) error
}

AppStore is the narrow surface DeploySpec needs to create-or-reuse a store.App and link each fanned-out service to it. *store.DB satisfies this structurally. Optional: nil (the default) means DeploySpec itself is unavailable (see its own doc comment), not that a caller still expecting single-service Deploy calls needs to change.

type BuildMetricsRecorder

type BuildMetricsRecorder interface {
	RecordBuildDuration(ctx context.Context, serviceName string, d time.Duration, at time.Time) error
}

BuildMetricsRecorder is the narrow surface this package needs to record the build-duration metric. *telemetry.DB satisfies this structurally; not imported directly to avoid a dependency this package doesn't otherwise need, the same reasoning ImageBuilder/ ServiceStore/SecretChecker above already establish.

type ImageBuilder

type ImageBuilder interface {
	Build(ctx context.Context, req build.Request, progress func(build.ProgressEvent)) (*build.Result, error)

	// BuildRailpack builds a service whose build.Type is
	// spec.BuildRailpack: Railpack's own provider detection replaces a
	// user-authored Dockerfile, scoped to node and golang only (see
	// deployRailpack). A separate method, not a build.Request field,
	// because internal/build's own solve path is genuinely different
	// (Definition-based, not dockerfile.v0-frontend-based; see
	// internal/build/railpack.go's package doc comment).
	BuildRailpack(ctx context.Context, req build.RailpackRequest, progress func(build.ProgressEvent)) (*build.Result, error)
}

ImageBuilder is the narrow surface this package needs from internal/build, so tests can fake it without a real daemon or BuildKit connection. *build.Client satisfies this.

type MultiRequest

type MultiRequest struct {
	// AppName identifies the store.App every fanned-out service is
	// linked to (created if it doesn't exist yet, reused if it does: see
	// DeploySpec's own doc comment). Each service's own DesiredService
	// name is "<AppName>-<serviceKey>", the flat naming convention
	// internal/api/apps_group.go's own doc comment establishes.
	AppName string
	// Services is app.yaml's services: map, keyed by service name (e.g.
	// "web", "worker").
	Services map[string]spec.Service
	// SourceDir is the local checkout root, shared by every service's
	// build: one git checkout produces every service's image.
	SourceDir string
	CommitSHA string
	// ImageRepoBase is the image name without a tag or per-service
	// suffix, e.g. "levelrail/myapp"; each service tags as
	// "<ImageRepoBase>-<serviceKey>:<CommitSHA>".
	ImageRepoBase string
}

MultiRequest is one deploy attempt for N services under one app, fanned out from a single app.yaml's Spec.Services map. Mirrors Request's own fields, minus ServiceName/Service/ImageRepo (one per service key instead of one for the whole request) and plus AppName and ImageRepoBase.

type Option

type Option func(*Pipeline)

Option configures optional Pipeline behavior.

func WithAppStore

func WithAppStore(s AppStore) Option

WithAppStore enables DeploySpec, the multi-service fan-out entry point (see that method's own doc comment). Without one configured (the default), DeploySpec fails loudly rather than fanning out services with no way to link them to an app row.

func WithBuildMetricsRecorder

func WithBuildMetricsRecorder(recorder BuildMetricsRecorder) Option

WithBuildMetricsRecorder enables recording build.Result.Duration as the build_duration_seconds metric after a successful build. Without one configured (the default), a deploy still succeeds exactly as before, it just isn't measured: a metrics-store outage must never block a real deploy.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger overrides the logger used to report a non-fatal metrics recording failure. Defaults to slog.Default().

func WithSecretChecker

func WithSecretChecker(checker SecretChecker) Option

WithSecretChecker enables { secret: true } env vars to pass through deployment instead of being rejected outright. Without one configured (the default), a service declaring any secret-backed env var fails to deploy with an explicit error, the same as before envelope-encrypted secret storage existed: an operator running Levelrail without a master key configured should get a clear failure, not a container silently missing a variable it declared as required.

func WithStaticRootDir

func WithStaticRootDir(dir string) Option

WithStaticRootDir sets the local filesystem directory static site output is copied into, under which every static service gets its own "<ServiceName>/<CommitSHA>" subdirectory. Without one configured (the default, empty string), build.type: static fails its deploy loudly: this package deliberately does not invent a fallback location (e.g. os.TempDir()) for content the ingress controller and Caddy need to keep serving after this process's own temp-file lifecycle might have cleaned it up. The caller (cmd/levelrail) is expected to pass a path under the control plane's own data directory, matching every other on-disk state this codebase keeps there (internal/ingress's WithStorageDir doc comment makes the identical point for Caddy's own certificate storage).

func WithStaticSiteStore

func WithStaticSiteStore(s StaticSiteStore) Option

WithStaticSiteStore enables build.type: static deploys. Without one configured (the default), a service declaring build.type: static fails its deploy loudly rather than silently discarding the copied files, the same "fail loudly without configuration" shape WithSecretChecker already establishes for { secret: true } env vars.

type Pipeline

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

Pipeline builds a service (when its build type requires a build) and writes the result as that service's new desired state.

func New

func New(builder ImageBuilder, svcStore ServiceStore, opts ...Option) *Pipeline

New builds a Pipeline.

func (*Pipeline) Deploy

func (p *Pipeline) Deploy(ctx context.Context, req Request, progress func(build.ProgressEvent)) (string, error)

Deploy runs req's build (if its build.Type needs one) and saves the resulting desired state. For every build.Type except spec.BuildStatic, that means the full image reference the application controller converges to on its next reconcile, also what's returned: a freshly built one for spec.BuildDockerfile/spec.BuildRailpack, or, for spec.BuildImage, req.Service.Build.Image passed straight through with no build at all (see deployImage). spec.BuildStatic has no image and no container for the application controller to converge to at all (see static.go's package doc comment): its desired state is a store.StaticSite instead of a store.DesiredService, consumed directly by internal/reconcile/ingress rather than the application controller, and the string this returns is the local directory now being served, not an image reference.

progress, if non-nil, receives build progress as it happens; see build.ProgressEvent and build.SlogProgress. A static deploy never invokes it: there is no build step to report progress on.

func (*Pipeline) DeploySpec

func (p *Pipeline) DeploySpec(ctx context.Context, req MultiRequest, progress func(serviceKey string, ev build.ProgressEvent)) ([]ServiceOutcome, error)

DeploySpec fans out req into N independent Deploy calls, one per service key, under one store.App (created if AppName has none yet, reused otherwise: p.apps.GetAppByName then SaveApp, App.ID == App.Name by this method's own convention, matching migrations/0039_apps.sql's backfill convention for a pre-existing single-service app). The app row is created before any service's build starts, so a partially failed fan-out still leaves every successfully-deployed sibling linked to a real AppID.

Each service key's build failure is captured in that key's own ServiceOutcome.Err and does not block any other key's build or deploy, matching the reconciler's own "one broken resource must not block convergence of everything else" principle (internal/reconcile/engine.go's ReconcileAll doc comment): a caller gets back one outcome per service key, in deterministic (sorted key) order, and must inspect each Err individually rather than treating this call's own returned error as "did it work." The returned error is non-nil only for a failure that blocks every service alike (req.Services empty, or the app row itself couldn't be created or reused); once fan-out begins, DeploySpec itself always returns a nil error and lets each ServiceOutcome carry its own.

progress, if non-nil, is called with each service key's own build.ProgressEvent stream, so a caller can tell which service a given progress line belongs to.

type Request

type Request struct {
	// ServiceName identifies the service, matching the name the
	// application controller (1.3) reconciles under.
	ServiceName string
	// Service is the app.yaml service block: build config, port, env,
	// resources, health.
	Service spec.Service
	// SourceDir is the local checkout root, the build context.
	SourceDir string
	// CommitSHA tags the built image, and is what a future rollback
	// command points desired.Image back at.
	CommitSHA string
	// ImageRepo is the image name without a tag, e.g. "levelrail/thesvg".
	// Naming policy (namespacing, registry prefix) is the caller's
	// decision, not this package's.
	ImageRepo string
}

Request is one deploy attempt for a single service.

type SecretChecker

type SecretChecker interface {
	Exists(ctx context.Context, serviceName, envKey string) (bool, error)
}

SecretChecker is the narrow surface this package needs from internal/secrets.Manager: whether a value has been set for a { secret: true } env var, never the value itself. A deploy never needs to see a secret's plaintext, only confirm one exists (or doesn't, for a required one) before proceeding.

type ServiceOutcome

type ServiceOutcome struct {
	ServiceKey  string
	ServiceName string
	Image       string
	Err         error
}

ServiceOutcome is one service's own fan-out result: Image is set on success, Err is set on failure, never both.

type ServiceStore

type ServiceStore interface {
	SaveDesiredService(ctx context.Context, svc store.DesiredService) error
	// GetRegistryCredentialByName resolves build.type: image's optional
	// registryCredential field (a name, spec.Build.RegistryCredential's
	// own doc comment on why not an ID) at deploy time, in deployImage.
	GetRegistryCredentialByName(ctx context.Context, name string) (store.RegistryCredential, error)
	// GetDesiredDatabase resolves a { from: "<database>.<field>" } env
	// var's database at deploy time (validateEnv), so an app referencing
	// an unknown database fails the deploy itself rather than only
	// surfacing later as a reconcile failure.
	GetDesiredDatabase(ctx context.Context, name string) (*store.DesiredDatabase, error)
}

ServiceStore is the narrow surface this package needs from internal/store. *store.DB satisfies this.

type StaticSiteStore

type StaticSiteStore interface {
	SaveStaticSite(ctx context.Context, site store.StaticSite) error
}

StaticSiteStore is the narrow surface this package needs from internal/store for build.type: static, so tests can fake it without a real database, the same narrow-interface pattern ServiceStore already establishes for container services. *store.DB satisfies this.

Jump to

Keyboard shortcuts

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