edge

package
v0.3.17 Latest Latest
Warning

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

Go to latest
Published: Jun 19, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

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

func Available(caddyBin string) (bool, string)

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

func ValidateRoute(r Route) error

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

func VerifyDigest(caddyPath, want string) error

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).

func NewAdmin

func NewAdmin(listen string) *Admin

NewAdmin builds an admin client for a Caddy admin listen address: "unix//run/helmsman/caddy-admin.sock" (dialed over the socket) or "127.0.0.1:2019".

func (*Admin) Load

func (a *Admin) Load(ctx context.Context, configJSON []byte) error

Load POSTs the WHOLE config document to /load (declarative, never incremental). A non-2xx response means Caddy rejected it (the previous config keeps running).

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).

func (Route) ID

func (r Route) ID() int64

ID returns a route's row id (for the UI).

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

func (s *RouteStore) ReplaceProject(ctx context.Context, project string, routes []Route) error

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.

func (*RouteStore) Save

func (s *RouteStore) Save(ctx context.Context, r Route) error

Save validates + upserts a route by id (0 = insert). ValidateRoute rejects wildcards, control-plane upstreams, and loopback targets before it can persist.

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.

Jump to

Keyboard shortcuts

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