application

package
v0.1.0 Latest Latest
Warning

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

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

Documentation

Overview

Package application implements the declarative app spec's service contract and TASKS.md 1.3's application controller: the reconcile.Controller that converges a real, store-backed desired service to a running container, replacing nginxdemo's hardcoded desired state with the real thing.

Deploy strategy: a container's name is derived deterministically from its image (ContainerName), so the level-triggered check "does the right container exist" needs no memory of past deploys beyond what's queryable from Docker directly. When the desired image changes, Reconcile creates a new, differently-named container alongside whatever's already running, waits for it to pass its readiness probe, then removes every other container for this service. Traffic switching itself (updating Caddy to point at the new container) is deliberately not this controller's job: this codebase's reconciler pattern is a reconcile loop per resource type, and ingress is its own resource type (TASKS.md 1.6, not yet wired in). This controller's contract with that future ingress controller is simple: whichever container currently exists and is running for a service is the one meant to receive traffic.

Index

Constants

This section is empty.

Variables

View Source
var StorageEnvKeys = []string{
	envKeyS3Endpoint,
	envKeyS3Bucket,
	envKeyS3Region,
	envKeyS3AccessKey,
	envKeyS3SecretKey,
}

StorageEnvKeys are every env var name resolveStorageEnv can inject, built from the same named constants that function assigns below so the two can never drift apart. The single source of truth for any caller needing to know these names ahead of time, e.g. internal/api's GET /api/v1/storage-env-keys, which lets the frontend warn an operator before attaching storage collides with one of their own env vars, instead of hardcoding a second copy of this list in TypeScript.

Functions

func ContainerName

func ContainerName(serviceName, image, restartNonce string) string

ContainerName derives a stable, unique-per-image name so the level-triggered check "does the right container exist" needs no memory beyond what Docker itself can answer. Two deploys of the same image produce the same name (a genuine no-op redeploy correctly finds nothing to do); two different images always produce different names (so both can exist side by side during a cutover). Exported so the ingress controller (internal/reconcile/ingress, TASKS.md 1.6) can derive the exact same name to find a service's currently active container, without reimplementing this hash logic a second time and risking the two drifting apart.

restartNonce (store.DesiredService.RestartNonce) is folded into the hash only when non-empty: an empty nonce, the state of every service that has never been restarted, must produce byte-identical output to before this parameter existed, so an upgrade never orphans an already-running container under a name it no longer recognizes. Once non-empty, changing it changes the name, which is what makes a restart (RestartService) indistinguishable from an image change to this controller's existing, already-tested blue-green/recreate cutover logic: no new branch needed, restarting an app is just another kind of desired-state change.

func NetworkName

func NetworkName(prefix, appID string) string

NetworkName derives the per-app Docker network name every service belonging to appID shares: "<prefix>-app-<appID>". Exported so NetworkCleanupController (network_cleanup.go) can derive the exact same name a running app's containers are actually attached to, without reimplementing this naming rule a second time and risking the two drifting apart, the same reasoning ContainerName's own doc comment already gives for internal/reconcile/ingress's identical reuse of that function.

Types

type AppLister

type AppLister interface {
	ListApps(ctx context.Context) ([]store.App, error)
}

AppLister is the narrow surface NetworkCleanupController needs from internal/store: every currently-desired app. *store.DB satisfies this structurally.

type Controller

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

Controller converges one named service's desired state (read fresh from ServiceStore on every Reconcile, never cached) to a running container.

func New

func New(serviceName string, svcStore ServiceStore, runtime docker.Runtime, opts ...Option) *Controller

New builds a Controller for serviceName.

func (*Controller) Name

func (c *Controller) Name() string

Name implements reconcile.Controller.

func (*Controller) Reconcile

func (c *Controller) Reconcile(ctx context.Context) (reconcile.Result, error)

Reconcile implements reconcile.Controller.

type DatabaseAttachmentStore

type DatabaseAttachmentStore interface {
	GetDesiredDatabase(ctx context.Context, name string) (*store.DesiredDatabase, error)
}

DatabaseAttachmentStore is the narrow surface this controller needs to resolve a { from: ... } env var (store.DesiredService.DatabaseEnv, store.DesiredService.DatabaseAttachment) against a real managed database: just the one read, the same shape StorageTargetStore/ RegistryCredentialStore already establish. *store.DB satisfies this structurally.

type DeployRecorder

type DeployRecorder interface {
	RecordDeploy(ctx context.Context, serviceName string, at time.Time) error
}

DeployRecorder is the narrow surface this controller needs to record TASKS.md 2.1's deploy-frequency metric. *telemetry.DB satisfies this structurally; not imported directly, same reasoning ServiceStore/ SecretResolver above already establish. RecordDeploy is only ever called on a real deploy cutover (justDeployed below, the "Deployed" reason), never on a reconcile tick that finds nothing to do, so a series of recorded samples over time IS deploy frequency, no separate rate computation needed downstream.

type EnvironmentEnvLister

type EnvironmentEnvLister interface {
	ListEnvironmentEnvVars(ctx context.Context, environmentID string) (map[string]string, error)
}

EnvironmentEnvLister is the narrow surface this controller needs to resolve store.DesiredService.EnvironmentID's shared env vars, the tier between ProjectEnvStore and this service's own Env (resolveEnv's own doc comment on precedence). Unlike OrganizationEnvStore, no join is needed: EnvironmentID already lives directly on DesiredService. *store.DB satisfies this structurally.

type HookRunRecorder

type HookRunRecorder interface {
	UpsertHookRun(ctx context.Context, run store.HookRun) error
}

HookRunRecorder is the narrow surface this controller needs to persist a pre/post-deploy hook's outcome (store.ServiceHooks, migrations/0083_service_hook_runs.sql), so GET /api/v1/apps/{name}/hook-runs has something to show. *store.DB satisfies this structurally. nil is valid: a hook still executes and still gates the deploy exactly the same way, its outcome (this pass) is just never persisted for later viewing, the same "optional persistence, mandatory behavior" split DeployRecorder above already makes for the deploy-frequency metric.

type NetworkCleanupController

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

NetworkCleanupController reconciles the whole fleet's per-app Docker networks in a single pass, the same "all of them, not one resource at a time" shape internal/reconcile/ingress's own controller already establishes for an analogous whole-fleet concern (that package's own doc comment explains why: there's no per-network incremental update primitive to converge one at a time against). Every network this codebase creates is created by name (NetworkName's own doc comment) only when a service's Controller actually needs one (createAndStart); nothing ever proactively deletes one, so this controller is the only place that ever does: it diffs Docker's own observed networks under prefix against store.App rows that still exist, and removes any network whose app is gone.

Deliberately store-App-driven, not service-count-driven: a zero-service App (every service under it deleted individually, not via DeleteApp) still keeps its network, since a later deploy reusing that same app name is expected to find it again. Only an App row's own absence, from store.DeleteApp (internal/api's handleDeleteApp deletes it once removing a service leaves the App with no other members) or its cascade, means the network is actually orphaned.

func NewNetworkCleanupController

func NewNetworkCleanupController(apps AppLister, runtime docker.Runtime, prefix string) *NetworkCleanupController

NewNetworkCleanupController builds a NetworkCleanupController. prefix is the same value passed to every Controller's WithNetworkPrefix (typically brand.Brand.ShortName): both must agree, or this will never find the networks the per-service controllers actually create.

func (*NetworkCleanupController) Name

func (c *NetworkCleanupController) Name() string

Name implements reconcile.Controller.

func (*NetworkCleanupController) Reconcile

Reconcile implements reconcile.Controller.

type Option

type Option func(*Controller)

Option configures optional Controller behavior.

func WithDatabaseAttachments

func WithDatabaseAttachments(s DatabaseAttachmentStore) Option

WithDatabaseAttachments enables container creation to resolve store.DesiredService.DatabaseEnv/DatabaseAttachment into real connection env vars, the same "fail loudly if declared but unconfigured" shape WithStorageTargets already establishes: a service declaring a { from: ... } env var with no database store configured fails Reconcile rather than silently starting a container missing the value it needs.

func WithDeployRecorder

func WithDeployRecorder(r DeployRecorder) Option

WithDeployRecorder enables recording TASKS.md 2.1's deploy_count metric every time Reconcile actually performs a deploy cutover. Without one configured (the default), Reconcile behaves exactly as before, deploys just aren't measured.

func WithEnvironmentEnv

func WithEnvironmentEnv(s EnvironmentEnvLister) Option

WithEnvironmentEnv enables resolving store.DesiredService.EnvironmentID's shared env vars as resolveEnv's middle layer, applied above WithProjectEnv/WithOrganizationEnv and below the service's own Env. Same "purely an organizational label" reasoning as WithProjectEnv: a service with an EnvironmentID but no EnvironmentEnvLister configured does not fail Reconcile, environment vars are just silently skipped.

func WithHTTPClient

func WithHTTPClient(c *http.Client) Option

WithHTTPClient overrides the client used for readiness probes. Defaults to http.DefaultClient; tests override this to point at a fake transport rather than making real HTTP calls to a real client.

func WithHookRunRecorder

func WithHookRunRecorder(r HookRunRecorder) Option

WithHookRunRecorder enables persisting every pre/post-deploy hook run's outcome. Without one configured (the default), configured hooks still execute and still gate the deploy exactly the same way; only the persisted-for-later-viewing record is skipped.

func WithHookTimeout

func WithHookTimeout(d time.Duration) Option

WithHookTimeout overrides how long a single pre/post-deploy hook command may run before it's treated as failed. Defaults to defaultHookTimeout.

func WithMeshDNSAddr

func WithMeshDNSAddr(addr string) Option

WithMeshDNSAddr points every container this controller creates at addr, a bare nameserver IP (never "ip:port": neither Docker's DNS HostConfig field nor a container's own resolv.conf supports a non-standard nameserver port, confirmed live while building this option, see cmd/levelrail/mesh.go's dockerNameserverPort doc comment), as an additional nameserver ahead of whatever Docker's own resolver would otherwise configure (docker.ContainerSpec.DNS). Without one configured (the default, empty string), Reconcile behaves exactly as before this field existed: no DNS override at all. addr is expected to already be a real, container-reachable address (cmd/levelrail/mesh.go's containerDNSAddr resolves Docker's own bridge gateway IP, only once the mesh DNS server is confirmed bound to port 53); this controller does not validate it.

func WithNetworkPrefix

func WithNetworkPrefix(prefix string) Option

WithNetworkPrefix sets the Docker network naming prefix (NetworkName's own doc comment) every per-app network this Controller creates uses: callers pass brand.Brand.ShortName, following the same "pass the resolved value in, don't import internal/brand here" convention internal/network.WithShortName already establishes, since brand.ShortName is runtime-configurable and this package (like internal/spec.ReservedLabelPrefix) has no business depending on that config directly. Without one configured (the default, empty string), defaultNetworkPrefix is used instead.

func WithOrganizationEnv

func WithOrganizationEnv(s OrganizationEnvStore) Option

WithOrganizationEnv enables resolving the organization that owns store.DesiredService.ProjectID's project into that organization's shared env vars, applied as resolveEnv's base layer beneath WithProjectEnv. Same "purely an organizational label" reasoning as WithProjectEnv: a ProjectID with no WithOrganizationEnv configured does not fail Reconcile, organization vars are just silently skipped.

func WithProjectEnv

func WithProjectEnv(s ProjectEnvStore) Option

WithProjectEnv enables resolving store.DesiredService.ProjectID's shared env vars as resolveEnv's base layer. Unlike WithSecretResolver/ WithStorageTargets, a service with a ProjectID but no ProjectEnvStore configured does not fail Reconcile: a project is purely an organizational label (internal/api/projects.go's own doc comment), so assigning one must never be able to break an otherwise-healthy service's deploys, project vars are just silently skipped.

func WithReadyBudget

func WithReadyBudget(d time.Duration) Option

WithReadyBudget overrides how long Reconcile waits for a freshly started container to pass its readiness probe before giving up. Defaults to 60s.

func WithRegistryCredentials

func WithRegistryCredentials(s RegistryCredentialStore) Option

WithRegistryCredentials enables resolving store.DesiredService.RegistryCredentialID into real pull credentials, the same "fail loudly if declared but unconfigured" shape WithStorageTargets establishes: an unauthenticated pull silently succeeding for a service that explicitly asked for a private-registry credential would be a worse failure mode than Reconcile erroring.

func WithSecretResolver

func WithSecretResolver(r SecretResolver) Option

WithSecretResolver enables container creation to fill in secret-backed env vars (store.DesiredService.SecretEnv). Without one configured (the default), a service that declares any secret-backed env var fails Reconcile loudly rather than silently starting a container missing a variable it needs, the same "fail loudly" choice internal/deploy.Pipeline makes at save time.

func WithStorageTargets

func WithStorageTargets(s StorageTargetStore) Option

WithStorageTargets enables container creation to resolve store.DesiredService.StorageTargetID into S3_* env vars (resolveEnv's own doc comment). Without one configured (the default), a service that declares a StorageTargetID fails Reconcile loudly rather than silently starting a container missing the credentials it needs, the same "fail loudly" shape WithSecretResolver's own absence already produces for SecretEnv.

type OrganizationEnvStore

type OrganizationEnvStore interface {
	ListOrganizationEnvVarsForProject(ctx context.Context, projectID string) (map[string]string, error)
}

OrganizationEnvStore is the narrow surface this controller needs to resolve store.DesiredService.ProjectID's organization's shared env vars, one tier below ProjectEnvStore (resolveEnv's own doc comment on precedence). *store.DB satisfies this structurally: its ListOrganizationEnvVarsForProject hides the projects.org_id join so this package never needs to know a project carries one.

type ProjectEnvStore

type ProjectEnvStore interface {
	ListProjectEnvVars(ctx context.Context, projectID string) (map[string]string, error)
}

ProjectEnvStore is the narrow surface this controller needs to resolve store.DesiredService.ProjectID into that project's shared env vars (resolveEnv's own doc comment on precedence). *store.DB satisfies this structurally, the same type that already backs internal/api's own project env endpoints.

type RegistryCredentialStore

type RegistryCredentialStore interface {
	GetRegistryCredential(ctx context.Context, id string) (store.RegistryCredential, error)
}

RegistryCredentialStore is the narrow surface this controller needs to resolve store.DesiredService.RegistryCredentialID into a real username, the same "just the one read" shape StorageTargetStore establishes just above. *store.DB satisfies this structurally.

type SecretResolver

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

SecretResolver is the narrow surface this controller needs from internal/secrets.Manager (TASKS.md 1.7), so tests can fake it without a real master key or database. *secrets.Manager satisfies this structurally. Resolve's plaintext return value is used exactly once, merged into a container's env map immediately before docker.Runtime.Create, and never persisted anywhere by this controller.

type ServiceStore

type ServiceStore interface {
	GetDesiredService(ctx context.Context, name string) (*store.DesiredService, error)
}

ServiceStore is the narrow surface this controller needs from internal/store, so tests can fake it without a real database. *store.DB satisfies this.

type StorageTargetStore

type StorageTargetStore interface {
	GetBackupTarget(ctx context.Context, id string) (store.BackupTarget, error)
}

StorageTargetStore is the narrow surface this controller needs to resolve store.DesiredService.StorageTargetID into a real bucket connection: just the one read, GetBackupTarget, the same "narrow interface at the boundary" shape ServiceStore/SecretResolver already establish. *store.DB satisfies this structurally, the same type that already backs internal/api's own BackupTargetStore for GET/POST/DELETE /api/v1/backup-targets.

Jump to

Keyboard shortcuts

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