Documentation
¶
Overview ¶
Package edge owns the managed edge (plan §6): Helmsman supervises a child Caddy and is the SINGLE SOURCE OF TRUTH for its config via the admin API. The config is NEVER stored as text — this package RENDERS the whole Caddy JSON document from typed structs (SBD-7), baking in the secure-by-default baseline (§6.1): admin on loopback/unix only, no admin vhost unless explicitly configured (and then IP-allowlist-first), ACME pinned to one CA for only the configured app hostnames, no wildcard/catch-all proxy, and NO upstream may target a control-plane port (struct-validated AND re-checked at render).
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Available ¶
Available reports whether this host can OWN the managed edge. The edge is a supervised child Caddy with a systemd slice + CAP_NET_BIND_SERVICE + an egress firewall (plan §6) — Linux-only. On any other OS, or with no caddy binary, the edge is FAIL-CLOSED unavailable (managed mode degrades to an "edge not owned" banner; Helmsman's control plane still serves).
func Render ¶
func Render(base BaseConfig, routes []Route, certOnly []string) ([]byte, error)
Render builds the whole Caddy JSON document from the base + the enabled routes (Layer 0 protected base ⊕ Layer 1 per-app routes). The edge config is ALWAYS rendered from these typed structs — the operator never authors Caddy config (neither a file nor a portal field); everything originates from helmsman.yaml / the typed route model. It re-validates every route (defense in depth) and FAILS if any is unsafe — a bad route can never become a partially-applied config. certOnly are hostnames Caddy must obtain+renew an ACME cert for WITHOUT a proxy route — a consumer app (e.g. an MQTT broker) terminates TLS itself using the synced cert (spec.cert_bindings). Caddy still answers the ACME challenge on :80/:443.
func ValidateRoute ¶
ValidateRoute enforces every route-level safety rule (SBD-4). Returns the first violation. A wildcard/catch-all hostname is rejected; an upstream targeting a control-plane port or a loopback/link-local literal IP is rejected.
func VerifyDigest ¶
VerifyDigest checks the caddy binary's SHA-256 against a pinned digest (supply chain — refuse on mismatch, plan §6). An empty want skips the check.
Types ¶
type Admin ¶
type Admin struct {
// contains filtered or unexported fields
}
Admin talks to the child Caddy's admin API — the SINGLE source of truth for its config (SBD-2). It is reached ONLY over a unix socket (preferred) or loopback :2019; there is no on-disk config Caddy auto-loads. /load is transactional: Caddy validates + atomically swaps, and REJECTS a bad document while keeping the running config — so a failed apply never takes the edge down (SBD-8 floor).
type BaseConfig ¶
type BaseConfig struct {
AdminListen string // "unix//run/helmsman/caddy-admin.sock" or "127.0.0.1:2019"
ACMEEmail string // pinned ACME contact
ACMECA string // pinned single issuer directory URL
AdminHostname string // "" = NO admin vhost (reach the UI via SSH tunnel)
AdminAllowlist []string // IP-allowlist CIDRs for the admin vhost (typed, mandatory if AdminHostname set)
AdminUpstream string // the ONLY loopback upstream, identity-pinned (e.g. 127.0.0.1:9000)
}
BaseConfig is Layer 0 — the protected base, injected from typed config (never operator text).
type Reconciler ¶
type Reconciler struct {
// contains filtered or unexported fields
}
Reconciler renders the WHOLE edge config from the declarative route set and pushes it via the admin API. It retains the last-known-good document so a caller can revert (SBD-8).
func NewReconciler ¶
func NewReconciler(store *RouteStore, admin *Admin, base BaseConfig, log *slog.Logger) *Reconciler
NewReconciler builds a Reconciler.
func (*Reconciler) Reconcile ¶
func (r *Reconciler) Reconcile(ctx context.Context) error
Reconcile renders the current route set and applies it. On a render error (an unsafe route) it does NOT touch the live config. On an apply error the previous config keeps running (Caddy /load is transactional).
func (*Reconciler) RevertToLastGood ¶
func (r *Reconciler) RevertToLastGood(ctx context.Context) error
RevertToLastGood re-applies the last successfully-loaded config (SBD-8 recovery path; the typed base render is the floor when there is no last-good yet).
func (*Reconciler) SetCertHosts ¶ added in v0.2.0
func (r *Reconciler) SetCertHosts(fn func() []string)
SetCertHosts registers a provider for cert-only ACME subjects (hostnames Helmsman must obtain a cert for without a proxy route — spec.cert_bindings).
type Route ¶
type Route struct {
AppID string
Hostname string
Upstream string // host:port of the app endpoint (single-replica)
Pool []string // host:port of each live replica (M14 auto-scaling); overrides Upstream when set
UpstreamScheme string // http | https
PathPrefix string
RedirectHTTP bool
HSTS bool
SecurityHeaders bool
Enabled bool
// contains filtered or unexported fields
}
Route is one operator-desired edge vhost (Layer 1, from app_routes).
type RouteStore ¶
type RouteStore struct {
// contains filtered or unexported fields
}
RouteStore persists the declarative app_routes set (Layer 1). The edge config is re-rendered as a WHOLE document from this set on every apply (never stored as text — SBD-7).
func NewRouteStore ¶
func NewRouteStore(db *store.DB) *RouteStore
NewRouteStore builds a RouteStore.
func (*RouteStore) Delete ¶
func (s *RouteStore) Delete(ctx context.Context, id int64) error
Delete removes a route by id.
func (*RouteStore) List ¶
func (s *RouteStore) List() ([]Route, error)
List returns all routes (for rendering + the UI).
func (*RouteStore) ReplaceProject ¶ added in v0.2.2
ReplaceProject atomically replaces all of one project's routes with the given set — the deploy-time op so a repo's helmsman.yaml is the source of truth for its edge routes. Each route is validated first; a cross-app hostname collision trips the UNIQUE(hostname, path_prefix) constraint and fails the whole transaction (nothing changes), so a deploy can't hijack another app's hostname. Callers should only invoke this when the definition DECLARES routes, so an app whose routes are managed in the dashboard (none in helmsman.yaml) is never silently wiped.
type Supervisor ¶
type Supervisor struct {
CaddyBin string
AdminListen string
InitialCfg []byte // the typed base render (Layer 0) — the recovery floor
Log *slog.Logger
}
Supervisor launches + supervises the child Caddy. It is fail-closed: if the host can't own the edge, Run logs and returns without starting anything.
func (*Supervisor) Run ¶
func (s *Supervisor) Run(ctx context.Context)
Run supervises the child with capped backoff until ctx is cancelled. NOTE: the actual process launch + its systemd slice/user/caps/egress-firewall are the OS deployment layer (plan §6); this owns the lifecycle. Not exercised off-Linux.