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 ¶
- Variables
- func ContainerName(serviceName, image, restartNonce string) string
- func NetworkName(prefix, appID string) string
- type AppLister
- type Controller
- type DatabaseAttachmentStore
- type DeployRecorder
- type EnvironmentEnvLister
- type HookRunRecorder
- type NetworkCleanupController
- type Option
- func WithDatabaseAttachments(s DatabaseAttachmentStore) Option
- func WithDeployRecorder(r DeployRecorder) Option
- func WithEnvironmentEnv(s EnvironmentEnvLister) Option
- func WithHTTPClient(c *http.Client) Option
- func WithHookRunRecorder(r HookRunRecorder) Option
- func WithHookTimeout(d time.Duration) Option
- func WithMeshDNSAddr(addr string) Option
- func WithNetworkPrefix(prefix string) Option
- func WithOrganizationEnv(s OrganizationEnvStore) Option
- func WithProjectEnv(s ProjectEnvStore) Option
- func WithReadyBudget(d time.Duration) Option
- func WithRegistryCredentials(s RegistryCredentialStore) Option
- func WithSecretResolver(r SecretResolver) Option
- func WithStorageTargets(s StorageTargetStore) Option
- type OrganizationEnvStore
- type ProjectEnvStore
- type RegistryCredentialStore
- type SecretResolver
- type ServiceStore
- type StorageTargetStore
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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.
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 ¶
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 ¶
WithHookTimeout overrides how long a single pre/post-deploy hook command may run before it's treated as failed. Defaults to defaultHookTimeout.
func WithMeshDNSAddr ¶
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 ¶
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 ¶
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.