deploy

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: AGPL-3.0 Imports: 28 Imported by: 0

Documentation

Overview

Package deploy is part of the Redoubt control plane. See CLAUDE.md for its role.

Index

Constants

View Source
const (
	StreamBuild  = "build"
	StreamRun    = "run"
	StreamSystem = "system"
)

Log streams.

View Source
const (
	PermAppsManage   = "apps.manage"
	PermAppsDeploy   = "apps.deploy"
	PermAppsView     = "apps.view"
	PermSecretsWrite = "secrets.write"
	PermLogsView     = "logs.view"
)

Permissions checked by this service (mirrors internal/auth permissions by name so that the auth package stays the single source of truth; the strings must match auth.Perm* values).

View Source
const DefaultKeepReleases = 3

DefaultKeepReleases is how many non-active releases keep their images for rollback.

Variables

View Source
var ErrForbidden = errors.New("forbidden")

ErrForbidden is returned when the actor lacks a permission (HTTP 403).

View Source
var ErrInvalid = errors.New("invalid request")

ErrInvalid wraps request validation failures (HTTP 400).

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound is returned for unknown apps/deployments (HTTP 404).

Functions

func CanTransition

func CanTransition(from, to State) bool

CanTransition reports whether from -> to is legal.

func IsActive

func IsActive(s State) bool

IsActive reports whether the deployment is still being worked on.

func IsTerminal

func IsTerminal(s State) bool

IsTerminal reports whether no further transitions are possible.

Types

type Actor

type Actor struct {
	ID    string
	Email string
	Role  string
	// Can reports whether the actor holds a permission; nil means "bootstrap: allow all".
	Can func(perm string) bool
}

Actor identifies who performs an operation (from the session/API token/bootstrap).

type AppStatus

type AppStatus struct {
	App           string   `json:"app"`
	Release       string   `json:"release"`
	ContainerID   string   `json:"container_id"`
	ContainerName string   `json:"container_name"`
	Image         string   `json:"image"`
	State         string   `json:"state"`
	Hardened      bool     `json:"hardened"`
	Problems      []string `json:"problems,omitempty"`
}

AppStatus summarises one managed container for listings and the self-audit.

type AppView

type AppView struct {
	ID          string           `json:"id"`
	Name        string           `json:"name"`
	Hostname    string           `json:"hostname"`
	URL         string           `json:"url,omitempty"`
	Port        int64            `json:"port"`
	MemoryBytes int64            `json:"memory_bytes"`
	NanoCPUs    int64            `json:"nano_cpus"`
	PidsLimit   int64            `json:"pids_limit"`
	Overrides   docker.Overrides `json:"overrides"`
	ScanPolicy  string           `json:"scan_policy"`
	CreatedAt   string           `json:"created_at"`
	UpdatedAt   string           `json:"updated_at"`
	GitSource   *GitSourceView   `json:"git_source,omitempty"`
}

AppView is the API representation of an app (never includes secret values).

type Broker

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

Broker fans live log lines out to SSE subscribers per deployment.

func NewBroker

func NewBroker() *Broker

NewBroker creates an empty broker.

func (*Broker) Subscribe

func (b *Broker) Subscribe(deploymentID string) (<-chan LogLine, func())

Subscribe returns a channel of live lines for a deployment and an unsubscribe func. Slow subscribers drop lines rather than block the pipeline.

type CreateAppRequest

type CreateAppRequest struct {
	Name        string           `json:"name"`
	Hostname    string           `json:"hostname"`
	Port        int              `json:"port"`
	MemoryBytes int64            `json:"memory_bytes"`
	NanoCPUs    int64            `json:"nano_cpus"`
	PidsLimit   int64            `json:"pids_limit"`
	Overrides   docker.Overrides `json:"overrides"`
	// ScanPolicy is off | warn | block (default warn).
	ScanPolicy string `json:"scan_policy,omitempty"`
}

CreateAppRequest describes a new app.

type DeployRequest

type DeployRequest struct {
	Trigger       string `json:"trigger"` // manual | webhook | poll | rollback
	CommitSHA     string `json:"commit_sha,omitempty"`
	CommitMessage string `json:"commit_message,omitempty"`
	// ImageRef deploys a pre-built image for apps without a git source.
	ImageRef string `json:"image_ref,omitempty"`
}

DeployRequest triggers a deployment.

type DeploymentView

type DeploymentView struct {
	ID            string `json:"id"`
	App           string `json:"app"`
	Trigger       string `json:"trigger"`
	Actor         string `json:"actor"`
	CommitSHA     string `json:"commit_sha"`
	CommitMessage string `json:"commit_message"`
	ImageRef      string `json:"image_ref"`
	State         string `json:"state"`
	Error         string `json:"error,omitempty"`
	ContainerName string `json:"container_name,omitempty"`
	ReleaseID     string `json:"release_id,omitempty"`
	CreatedAt     string `json:"created_at"`
	StartedAt     string `json:"started_at,omitempty"`
	FinishedAt    string `json:"finished_at,omitempty"`
}

DeploymentView is the API representation of a deployment.

type ErrIllegalTransition

type ErrIllegalTransition struct{ From, To State }

ErrIllegalTransition is returned for transitions outside the graph.

func (ErrIllegalTransition) Error

func (e ErrIllegalTransition) Error() string

type GitSourceResult

type GitSourceResult struct {
	GitSourceView
	WebhookSecret string `json:"webhook_secret"`
}

GitSourceResult is returned once: it contains the webhook secret in plaintext, which is shown to the operator exactly one time (write-only thereafter).

type GitSourceView

type GitSourceView struct {
	Provider        string `json:"provider"`
	RepoURL         string `json:"repo_url"`
	Branch          string `json:"branch"`
	BaseDir         string `json:"base_dir"`
	Dockerfile      string `json:"dockerfile"`
	DeployKeyPublic string `json:"deploy_key_public"`
	WebhookURL      string `json:"webhook_url"`
	PollInterval    int64  `json:"poll_interval_seconds"`
	LastSeenCommit  string `json:"last_seen_commit"`
}

GitSourceView is the API representation of a git source (secrets omitted).

type LogLine

type LogLine struct {
	Seq    int64  `json:"seq"`
	TS     string `json:"ts"`
	Stream string `json:"stream"`
	Line   string `json:"line"`
}

LogLine is one persisted, already-redacted line of build/deploy output.

type Logs

type Logs struct {
	Store  *store.Store
	Broker *Broker
	// Redact is applied to every line before persistence (nil = identity).
	Redact func(string) string
	// MaxLines caps persisted lines per deployment (oldest trimmed). Default 5000.
	MaxLines int64
	Logger   *slog.Logger
	Now      func() time.Time
	// contains filtered or unexported fields
}

Logs persists deployment output (size-capped) and publishes it live. Callers MUST wrap writers with the secrets redactor before handing them to build/run steps; Logs itself applies an additional Redact func when set (defense in depth).

func (*Logs) Append

func (l *Logs) Append(ctx context.Context, deploymentID, stream, line string)

Append stores one line (redacted) and publishes it.

func (*Logs) Since

func (l *Logs) Since(ctx context.Context, deploymentID string, after, limit int64) ([]LogLine, error)

Since returns persisted lines with seq > after (up to limit).

func (*Logs) Writer

func (l *Logs) Writer(ctx context.Context, deploymentID, stream string) io.WriteCloser

Writer returns an io.WriteCloser that splits writes into lines and appends them.

type Pipeline

type Pipeline struct {
	Store   *store.Store
	Docker  *docker.Client
	Builder build.Builder
	Routes  *proxy.Writer
	Keeper  *secrets.Keeper
	// Redactor is the process-wide redactor: every secret value the pipeline decrypts is
	// registered here before it can appear anywhere (golden rule 3).
	Redactor *secrets.Redactor
	Logs     *Logs
	Audit    audit.Sink
	Cfg      config.Config
	Logger   *slog.Logger
	Now      func() time.Time
	// TestContainers labels containers redoubt.test=1 (test-suite only).
	TestContainers bool
	HealthTimeout  time.Duration
	CloneMaxBytes  int64
	// AllowLocalRepos permits file:// / path repositories (dev mode and tests only).
	AllowLocalRepos bool
	// KeepReleases is how many non-active release images are retained for rollback (default 3).
	KeepReleases int
	// Scanner runs the Trivy/Syft gate before a container starts (nil = scanning disabled).
	Scanner scan.Scanner
}

Pipeline executes deployments: clone → build → hardened run → health-gated cutover → route. It is the River worker for queue.DeployArgs and the only code path that runs app containers.

func (*Pipeline) Run

func (p *Pipeline) Run(ctx context.Context, deploymentID string) (err error)

Run executes the deployment with the given ID. Any error marks it failed; the previously healthy release keeps serving traffic (health-gated cutover).

type Poller

type Poller struct {
	Service *Service
	Logger  *slog.Logger
	// Tick is the scheduler resolution (default 30s); each source is checked when its own
	// interval has elapsed since its last check.
	Tick time.Duration
	// Resolve is overridable for tests; defaults to git.ResolveBranchHead.
	Resolve func(ctx context.Context, src git.Source) (string, error)
	// contains filtered or unexported fields
}

Poller is the webhook fallback for firewalled hosts: for every git source with a poll interval it periodically resolves the branch head (a pure-Go ls-remote, no clone) and triggers a deployment when it changes. It shares the Service's deploy path, so polling and webhooks are audited and queued identically.

func (*Poller) Once

func (p *Poller) Once(ctx context.Context)

Once checks every due source a single time (exported for tests).

func (*Poller) Run

func (p *Poller) Run(ctx context.Context)

Run blocks until ctx is cancelled.

type ReleaseView

type ReleaseView struct {
	ID           string `json:"id"`
	App          string `json:"app"`
	DeploymentID string `json:"deployment_id"`
	ImageRef     string `json:"image_ref"`
	ImageDigest  string `json:"image_digest,omitempty"`
	Active       bool   `json:"active"`
	SBOMPath     string `json:"sbom_path,omitempty"`
	ScanSummary  string `json:"scan_summary,omitempty"`
	CreatedAt    string `json:"created_at"`
}

ReleaseView is the API representation of a release (a deployable image reference).

type RollbackRequest

type RollbackRequest struct {
	ReleaseID string `json:"release_id,omitempty"`
}

RollbackRequest selects the release to return to; empty means "the one before the active release".

type SecretMeta

type SecretMeta struct {
	Name      string `json:"name"`
	Version   int64  `json:"version"`
	UpdatedAt string `json:"updated_at"`
}

SecretMeta is what the API returns about a secret: never the value.

type Service

type Service struct {
	Store    *store.Store
	Queue    *queue.Client
	Keeper   *secrets.Keeper
	Redactor *secrets.Redactor
	Routes   *proxy.Writer
	Docker   *docker.Client
	Logs     *Logs
	Audit    audit.Sink
	Cfg      config.Config
	Logger   *slog.Logger
	Now      func() time.Time
	// AllowLocalRepos permits file:// repositories (dev/tests).
	AllowLocalRepos bool
	// PublicBaseURL is used to render webhook URLs (e.g. https://redoubt.example.com).
	PublicBaseURL string
}

Service is the application service layer shared by the JSON API, the HTMX UI and the CLI. It owns app records, git sources, write-only secrets and deployment triggers. RBAC checks live here (golden rule 6): every method takes the acting principal.

func (*Service) CreateApp

func (s *Service) CreateApp(ctx context.Context, actor Actor, req CreateAppRequest) (AppView, error)

CreateApp registers a new app.

func (*Service) DeleteApp

func (s *Service) DeleteApp(ctx context.Context, actor Actor, name string) error

DeleteApp removes the app, its containers, route and network.

func (*Service) DeleteSecret

func (s *Service) DeleteSecret(ctx context.Context, actor Actor, appName, name string) error

DeleteSecret removes a secret.

func (*Service) Deploy

func (s *Service) Deploy(ctx context.Context, actor Actor, appName string, req DeployRequest) (DeploymentView, error)

Deploy creates a deployment row and enqueues the job in the same transaction.

func (*Service) DeploymentLogs

func (s *Service) DeploymentLogs(ctx context.Context, actor Actor, id string, after, limit int64) ([]LogLine, error)

DeploymentLogs returns persisted (redacted) log lines after seq.

func (*Service) GetApp

func (s *Service) GetApp(ctx context.Context, actor Actor, name string) (AppView, error)

GetApp returns an app by name.

func (*Service) GetDeployment

func (s *Service) GetDeployment(ctx context.Context, actor Actor, id string) (DeploymentView, error)

GetDeployment returns one deployment (scoped to its app name for authorization clarity).

func (*Service) GetRelease

func (s *Service) GetRelease(ctx context.Context, actor Actor, id string) (ReleaseView, error)

GetRelease returns one release by id.

func (*Service) ListApps

func (s *Service) ListApps(ctx context.Context, actor Actor) ([]AppView, error)

ListApps lists all apps.

func (*Service) ListDeployments

func (s *Service) ListDeployments(ctx context.Context, actor Actor, appName string, limit int64) ([]DeploymentView, error)

ListDeployments lists an app's deployments, newest first.

func (*Service) ListReleases

func (s *Service) ListReleases(ctx context.Context, actor Actor, appName string, limit int64) ([]ReleaseView, error)

ListReleases returns an app's releases, newest first.

func (*Service) ListSecrets

func (s *Service) ListSecrets(ctx context.Context, actor Actor, appName string) ([]SecretMeta, error)

ListSecrets returns names and metadata only.

func (*Service) Rollback

func (s *Service) Rollback(ctx context.Context, actor Actor, appName string, req RollbackRequest) (DeploymentView, error)

Rollback re-deploys a previous release's image through the full health-gated deploy path (no clone, no build). Deployer and above.

func (*Service) SBOMPath

func (s *Service) SBOMPath(ctx context.Context, actor Actor, releaseID, format string) (string, error)

SBOMPath returns the on-disk SBOM for a release in the given format. The path is derived from the release's deployment id and a fixed file name, then verified to lie inside <DataDir>/sboms — no user-controlled path component is ever used.

func (*Service) SetGitSource

func (s *Service) SetGitSource(ctx context.Context, actor Actor, appName string, req SetGitSourceRequest) (GitSourceResult, error)

SetGitSource creates/replaces the app's git source, generating a webhook secret and an ed25519 deploy key (private half age-encrypted at rest).

func (*Service) SetScanPolicy

func (s *Service) SetScanPolicy(ctx context.Context, actor Actor, appName, policy string) (AppView, error)

SetScanPolicy changes an app's vulnerability scan policy (off | warn | block).

func (*Service) SetSecret

func (s *Service) SetSecret(ctx context.Context, actor Actor, appName, name, value string) (SecretMeta, error)

SetSecret creates or rotates a secret. The value is never returned or logged.

func (*Service) WebhookLookup

func (s *Service) WebhookLookup(ctx context.Context, appName string) ([]byte, string, webhooks.Provider, error)

WebhookLookup implements webhooks.Handler.Lookup: returns the decrypted secret, branch and provider. Not-found conditions (invalid name, unknown app, app without a git source) are wrapped with webhooks.ErrUnknownApp so the handler answers 404 rather than treating them as a 500.

func (*Service) WebhookTrigger

func (s *Service) WebhookTrigger(ctx context.Context, appName string, ev webhooks.Event) error

WebhookTrigger implements webhooks.Handler.Trigger.

type SetGitSourceRequest

type SetGitSourceRequest struct {
	Provider     string `json:"provider"`
	RepoURL      string `json:"repo_url"`
	Branch       string `json:"branch"`
	BaseDir      string `json:"base_dir"`
	Dockerfile   string `json:"dockerfile"`
	PollInterval int64  `json:"poll_interval_seconds"`
	// Token is an optional HTTPS access token (write-only; stored as the reserved secret
	// REDOUBT_GIT_TOKEN, never returned).
	Token string `json:"token,omitempty"`
}

SetGitSourceRequest connects a repository to an app.

type State

type State string

State is a deployment's lifecycle state. Every transition is persisted and audited.

const (
	StateQueued     State = "queued"
	StateCloning    State = "cloning"
	StateBuilding   State = "building"
	StateStarting   State = "starting"
	StateRouting    State = "routing"
	StateHealthy    State = "healthy"
	StateFailed     State = "failed"
	StateSuperseded State = "superseded"
)

Deployment states (also the CHECK constraint in migrations/00001_init.sql).

type Static

type Static struct {
	Docker *docker.Client
	Routes *proxy.Writer
	Audit  audit.Sink
	Cfg    config.Config
	Logger *slog.Logger
	// Now is overridable for tests.
	Now func() time.Time
	// TestContainers marks every container with redoubt.test=1 (set only by the test-suite).
	TestContainers bool
	// HealthTimeout bounds how long Deploy waits for the container to become healthy.
	HealthTimeout time.Duration
}

Static deploys pre-built images as hardened, routed app containers. It is the Phase 0 deploy path and the tail end of the Phase 1 git → build → run pipeline (which produces an image and then calls Deploy).

func (*Static) Deploy

func (s *Static) Deploy(ctx context.Context, actor string, req StaticRequest) (StaticResult, error)

Deploy pulls (if needed), runs the image as a hardened container on its per-app network, attaches Traefik, writes the route, and retires the previous release.

func (*Static) List

func (s *Static) List(ctx context.Context) ([]AppStatus, error)

List returns every managed app container with its hardening verdict.

func (*Static) Remove

func (s *Static) Remove(ctx context.Context, actor, app string) error

Remove tears an app down: route, containers, ingress attachment, network.

type StaticRequest

type StaticRequest struct {
	App   string `json:"app"`
	Image string `json:"image"`
	Host  string `json:"host"`
	Port  int    `json:"port"`
	// Overrides are the audited escape hatches (see docker.Overrides).
	Overrides docker.Overrides `json:"overrides"`
}

StaticRequest describes one deployment.

type StaticResult

type StaticResult struct {
	App           string           `json:"app"`
	Release       string           `json:"release"`
	ContainerID   string           `json:"container_id"`
	ContainerName string           `json:"container_name"`
	Network       string           `json:"network"`
	Host          string           `json:"host"`
	URL           string           `json:"url"`
	Warnings      []docker.Warning `json:"warnings,omitempty"`
}

StaticResult is what Deploy returns.

type Worker

type Worker struct {
	river.WorkerDefaults[queue.DeployArgs]
	P *Pipeline
}

Worker adapts Pipeline to River.

func (*Worker) Timeout

func (w *Worker) Timeout(*river.Job[queue.DeployArgs]) time.Duration

Timeout bounds one deployment.

func (*Worker) Work

func (w *Worker) Work(ctx context.Context, job *river.Job[queue.DeployArgs]) error

Work runs one deployment job.

Jump to

Keyboard shortcuts

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