edge

package
v0.13.1 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Overview

Package edge owns the managed edge (plan §6): Mooring 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; Mooring's control plane still serves).

func DNSProviderModule added in v0.5.2

func DNSProviderModule(provider string) (string, bool)

DNSProviderModule returns the caddy module path for a provider name, and whether it's known.

func EnsureDNSProvider added in v0.5.2

func EnsureDNSProvider(ctx context.Context, caddyBin, provider string, log *slog.Logger) error

EnsureDNSProvider makes sure the caddy binary has the DNS provider module compiled in, installing it automatically with `caddy add-package <module>` when missing — so the operator never builds Caddy. add-package fetches a plugin-enabled caddy from Caddy's official build server and replaces the binary in place; it needs the caddy binary to be writable by this process and egress to caddyserver.com. Idempotent (a no-op when the module is already present). On any failure it returns an error and the caller proceeds with the current binary, so the DNS-01 config load then fails visibly (nothing is silently mis-issued).

func KnownDNSProviders added in v0.5.2

func KnownDNSProviders() []string

KnownDNSProviders returns the sorted list of supported provider names (for error messages).

func PoolKey

func PoolKey(rt Route) string

PoolKey identifies the upstream a route's replica pool is computed for — its owning app plus its service:port selector. Routes that share an upstream share a pool.

func Render

func Render(base BaseConfig, routes []Route, certOnly []CertHost) ([]byte, error)

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/mooring/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/mooring/caddy-admin.sock" or "127.0.0.1:2019"
	ACMEEmail      string   // pinned ACME contact
	ACMECA         string   // pinned default issuer directory URL
	CAs            []CA     // extra named issuers (private CAs) a subject can opt into
	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)
	// Wildcards enable OPTIONAL *.<Domain> wildcard certs via ACME DNS-01 — one entry per
	// operator-declared subdomain namespace (the default base_domain and each edge.base_domains)
	// that sets dns01. Any default-CA subject at/under a wildcard's Domain is served by that
	// wildcard (dropped from per-name HTTP-01 issuance). Empty slice = all per-name HTTP-01.
	Wildcards []WildcardCert
	// AccessLog, when true, makes the edge emit a per-request JSON access log to STDOUT (captured
	// in-process by the supervisor for the latency aggregator) while keeping Caddy's own logs +
	// errors on STDERR (→ journald). Enabled only when a scaled service opts into an edge metric,
	// so most edges pay nothing for it.
	AccessLog bool
}

BaseConfig is Layer 0 — the protected base, injected from typed config (never operator text).

type CA

type CA struct {
	Name         string
	DirectoryURL string
	Email        string   // "" → falls back to BaseConfig.ACMEEmail
	TrustedRoots []string // PEM file paths Caddy trusts for the CA's own https ("" → system roots)
}

CA is an additional ACME issuer (a private/internal CA) a subject can opt into by Name. Mapped from config.yaml edge.cas.

type CertHost

type CertHost struct {
	Hostname string
	CA       string
}

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 mooring.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. CertHost is a cert-only ACME subject (a cert binding's hostname) + the named CA it should be issued from ("" = the default issuer).

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

func (r *Reconciler) ReconcilePool(ctx context.Context, app, service string, replicas int) error

ReconcilePool satisfies scale.EdgeReconciler: after the auto-scaler changes a service's replica count, re-render the WHOLE edge config (which re-discovers every route's live pool) and apply it. The app/service/replicas args are advisory — the reconcile recomputes from live container discovery, so it always reflects truth.

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) SetAccessLog added in v0.11.0

func (r *Reconciler) SetAccessLog(fn func() bool)

SetAccessLog registers the predicate that decides, per reconcile, whether to render the edge's per-request access log (STDOUT JSON, consumed in-process for edge-measured autoscaling). It is consulted on every reconcile so enabling a source:edge metric takes effect on the next cycle (≤ the refresh cadence) with no restart. nil (the default) means never emit it.

func (*Reconciler) SetCertHosts

func (r *Reconciler) SetCertHosts(fn func() []CertHost)

SetCertHosts registers a provider for cert-only ACME subjects (hostnames Mooring must obtain a cert for without a proxy route — spec.cert_bindings).

func (*Reconciler) SetPoolDiscoverer

func (r *Reconciler) SetPoolDiscoverer(fn func(ctx context.Context, routes []Route) map[string][]string)

SetPoolDiscoverer registers the live-replica endpoint discoverer. When set, each reconcile asks fn for a route's current replica endpoints (ip:port) and, if it returns any, dials that pool (least-conn + passive health, via Render) instead of the single service-name upstream. fn must return only endpoints that are safe to dial; Render re-validates every member regardless (SBD-4 backstop).

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
	CA              string // "" = default issuer (BaseConfig.ACMECA); else a BaseConfig.CAs name
	// 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) HostnameOwner added in v0.5.0

func (s *RouteStore) HostnameOwner(ctx context.Context, hostname, pathPrefix, exceptProject string) (string, bool, error)

HostnameOwner reports which OTHER app already claims (hostname, pathPrefix), so a caller can reject a collision with a clear "already taken" message before the UNIQUE(hostname, path_prefix) constraint would trip with a cryptic DB error. It matches the constraint EXACTLY — same hostname AND same path prefix — so two apps legitimately sharing a hostname on different path prefixes are NOT falsely rejected. Matching is case-insensitive on the hostname; exceptProject (the app being deployed) is excluded so a redeploy never collides with itself. Returns ("", false, nil) when the pair is free.

func (*RouteStore) List

func (s *RouteStore) List() ([]Route, error)

List returns all routes (for rendering + the UI).

func (*RouteStore) ReplaceProject

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 mooring.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 mooring.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
	// AccessLine, when set, captures Caddy's STDOUT line-by-line and hands each raw line here (the
	// per-request JSON access log, when the render opted into it — see BaseConfig.AccessLog). It
	// feeds the in-process latency aggregator for edge-measured autoscaling. The callback MUST NOT
	// retain the slice and MUST NOT block (it runs on the single stdout-drain goroutine). nil keeps
	// stdout on os.Stdout as before. Caddy's own logs + errors always stay on STDERR (→ journald),
	// so nothing here affects error visibility.
	AccessLine func(line []byte)
}

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.

type WildcardCert added in v0.8.0

type WildcardCert struct {
	Domain        string
	DNS01Provider string // caddy DNS module name
	DNS01Token    string // provider credential (secret)
}

WildcardCert is one *.<Domain> DNS-01 wildcard: a namespace apex + the DNS provider credentials that answer its ACME DNS-01 challenge. The provider module must be compiled into the caddy binary (Mooring installs it via `caddy add-package`).

Jump to

Keyboard shortcuts

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