Documentation
¶
Overview ¶
Package ingress is the Phase 0 spike for embedding Caddy as a Go library, driven entirely through its in-process admin API. There is no `caddy` binary anywhere in this package and no Caddyfile on disk. Config goes in as a JSON document built from typed Go structs (never a hand-written Caddyfile string), because Phase 1's real requirement is building that document programmatically from app.yaml (the declarative app spec's contract), not editing config by hand.
This package intentionally defines its own minimal structs mirroring the slice of Caddy's JSON config schema this spike needs (admin, apps.http, apps.tls), rather than importing caddy's own config types directly. Caddy's own types lean heavily on json.RawMessage for every extension point (module namespaces), which makes them awkward to construct directly from Go; a small hand-rolled struct set that marshals to the same wire shape is both easier to build from app.yaml data later and easier to table-test in isolation, without starting Caddy at all. See docs-local/research/caddy-spike.md for what this traded off.
Index ¶
- func IsWildcardDomain(domain string) bool
- func SetActiveCertStorage(storage *SQLiteStorage)
- func ValidateWildcardDomain(domain string) error
- type ACMEIssuer
- type AdminConfig
- type AdminConfigSettings
- type Apps
- type AutoHTTPSConfig
- type Automation
- type AutomationPolicy
- type BasicAuthAccount
- type BasicAuthHandler
- type BasicAuthHash
- type BasicAuthProviders
- type CA
- type CertKeyPEMPair
- type CertStore
- type CertificatesConfig
- type ChallengesConfig
- type CloudflareDNSProvider
- type Config
- type DNSChallengeConfig
- type Driver
- type FileServerHandler
- type FileStorage
- type HTTPApp
- type HTTPBasicAuthProvider
- type InternalIssuer
- type MaintenanceRoute
- type Matcher
- type PKIApp
- type ProxyOptions
- type ProxyRoute
- type ReverseProxyHandler
- type Route
- type RoutesOptions
- type SQLiteStorage
- func (s *SQLiteStorage) Delete(ctx context.Context, key string) error
- func (s *SQLiteStorage) Exists(ctx context.Context, key string) bool
- func (s *SQLiteStorage) List(ctx context.Context, prefix string, recursive bool) ([]string, error)
- func (s *SQLiteStorage) Load(ctx context.Context, key string) ([]byte, error)
- func (s *SQLiteStorage) Lock(ctx context.Context, name string) error
- func (s *SQLiteStorage) Stat(ctx context.Context, key string) (certmagic.KeyInfo, error)
- func (s *SQLiteStorage) Store(ctx context.Context, key string, value []byte) error
- func (s *SQLiteStorage) Unlock(ctx context.Context, name string) error
- type SQLiteStorageRef
- type Server
- type StaticResponseHandler
- type StaticRoute
- type TLSApp
- type TLSCertificateOverride
- type Upstream
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsWildcardDomain ¶
IsWildcardDomain reports whether domain is a wildcard hostname (e.g. "*.example.com"). A leading "*." is the whole convention: no separate schema field marks a domain wildcard-eligible.
func SetActiveCertStorage ¶
func SetActiveCertStorage(storage *SQLiteStorage)
SetActiveCertStorage registers storage as the backend the "sqlite" Caddy storage module resolves against (see NewSQLiteStorageRef). internal/reconcile/ingress.Controller's WithCertStore option calls this once per Controller, not once per reconcile: the whole point is that Caddy's repeated config reloads share the one already-open connection rather than each provisioning their own.
func ValidateWildcardDomain ¶
ValidateWildcardDomain rejects a malformed wildcard: a "*" anywhere but the single leading "*." label, or a base domain with no further label for a DNS-01 TXT record to attach under. Non-wildcard domains and syntactically valid wildcards both return nil.
Types ¶
type ACMEIssuer ¶
type ACMEIssuer struct {
Module string `json:"module"`
// Email is the ACME account contact address, Caddy's own "email"
// field verbatim. Not technically required by the ACME protocol
// itself, but this codebase's own settings validation
// (internal/api) requires a non-empty, syntactically valid address
// whenever ACME is enabled: an unreachable ACME account is exactly
// the kind of "renewal fails silently" risk this project treats as
// its central one to catch before it bites a real user.
Email string `json:"email,omitempty"`
// CA is Caddy's own "ca" field: the ACME directory endpoint URL.
// Empty leaves Caddy's compiled-in default in place, Let's Encrypt's
// real production directory
// (https://acme-v02.api.letsencrypt.org/directory, per
// caddytls.ACMEIssuer's own doc comment). An operator who wants to
// avoid Let's Encrypt's production rate limits while testing should
// set this explicitly to Let's Encrypt's staging directory
// (https://acme-staging-v02.api.letsencrypt.org/directory); this
// package does not default to staging on its own, since silently
// issuing a staging (browser-untrusted) certificate for what an
// operator believes is a production toggle would be a worse
// surprise than requiring them to opt in explicitly.
CA string `json:"ca,omitempty"`
// Challenges is non-nil only for wildcard subjects: RFC 8555 7.1.1
// restricts HTTP-01 to non-wildcard identifiers, so
// NewCloudflareDNSACMEIssuer sets DNS-01 here instead.
Challenges *ChallengesConfig `json:"challenges,omitempty"`
}
ACMEIssuer is Caddy's "acme" TLS issuer (tls.issuance.acme): real ACME issuance (Let's Encrypt or any RFC 8555-compatible CA), as opposed to InternalIssuer's fully offline, self-signed certificates. Field names and the "module" value ("acme", resolved within the tls.issuance namespace the same way InternalIssuer's "internal" is, per AutomationPolicy.Issuers' inline_key=module tag) are verified against the vendored source for the pinned caddyserver/caddy/v2 version this module uses (modules/caddytls/acmeissuer.go's ACMEIssuer struct and its CaddyModule ID "tls.issuance.acme"), not guessed: this package intentionally hand-rolls the wire-compatible subset of Caddy's own config types rather than importing them directly (see this file's package doc comment), and ACMEIssuer only needs the three fields an operator-facing settings form actually collects (email, an optional directory URL override, and the module discriminator), not the full surface (challenges config, account key, EAB, and so on) Caddy's own struct exposes for cases this codebase doesn't build a UI for yet.
ADR 005's Verified section is explicit that only the internal issuer path was proven end to end by that spike; this type exists to close that named, expected gap (see internal/api/certificates.go's own doc comment referencing the same open item), not to introduce a new architecture decision.
func NewACMEIssuer ¶
func NewACMEIssuer(email, directoryURL string) ACMEIssuer
NewACMEIssuer returns the JSON shape for Caddy's real ACME issuer module, used as an AutomationPolicy's Issuers entry in place of NewInternalIssuer when an operator has opted into real ACME (internal/store.IngressSettings.ACMEEnabled). directoryURL empty means "use Caddy's own default" (see ACMEIssuer.CA's doc comment); email empty is accepted by this constructor (it does not itself enforce the non-empty rule), since that validation belongs to the caller that owns the operator-facing form (internal/api), not to this low-level config builder.
func NewCloudflareDNSACMEIssuer ¶
func NewCloudflareDNSACMEIssuer(email, directoryURL, apiToken string) ACMEIssuer
NewCloudflareDNSACMEIssuer returns the JSON shape for Caddy's real ACME issuer configured to solve the DNS-01 challenge via Cloudflare instead of Caddy's default HTTP-01, for wildcard subjects. email and directoryURL pass straight through to the same fields NewACMEIssuer sets; apiToken populates the Cloudflare provider. This package performs no validation on apiToken; internal/api owns that.
type AdminConfig ¶
type AdminConfig struct {
// Listen is the admin API bind address. Empty means Caddy's default,
// localhost:2019.
Listen string `json:"listen,omitempty"`
// Disabled, if true, turns the admin listener off entirely. This
// spike leaves it false (the real default) so the admin endpoint
// actually comes up, since a production driver would want it
// reachable for introspection even though this package itself talks
// to Caddy via Go function calls (caddy.Load), not HTTP requests to
// that listener. See docs-local/research/caddy-spike.md for what
// happened when this collided with an already-bound port.
Disabled bool `json:"disabled,omitempty"`
// Config holds admin-level config-management settings, notably
// Persist. See AdminConfigSettings.
Config *AdminConfigSettings `json:"config,omitempty"`
}
AdminConfig configures Caddy's admin API endpoint. Leaving Listen empty (the zero value) means "don't set this field" (omitempty), which lets Caddy apply its own default of localhost:2019 rather than us duplicating that default here.
type AdminConfigSettings ¶
type AdminConfigSettings struct {
// Persist controls whether Caddy keeps its own on-disk copy of
// whatever config was last pushed to it (Caddy's default is true,
// written to a fixed OS-level app-data path independent of the
// storage module configured for certificates, see FileStorage's doc
// comment). This spike found that surprising: it means an embedded
// Caddy instance maintains a second, out-of-band record of its own
// config on disk even when the storage module is pointed elsewhere,
// which is exactly the "second config surface" ADR 005 argues
// against embedding Caddy specifically to avoid. This spike's
// builder pins it to false: the platform's own reconcile loop and
// database are the single source of truth for desired ingress
// state, not a file Caddy manages on its own. See
// docs-local/research/caddy-spike.md.
Persist *bool `json:"persist,omitempty"`
}
AdminConfigSettings mirrors Caddy's admin.config JSON field.
type Apps ¶
type Apps struct {
HTTP *HTTPApp `json:"http,omitempty"`
TLS *TLSApp `json:"tls,omitempty"`
PKI *PKIApp `json:"pki,omitempty"`
}
Apps holds the top-level Caddy apps this package configures.
type AutoHTTPSConfig ¶
type AutoHTTPSConfig struct {
Disabled bool `json:"disable,omitempty"`
DisableRedir bool `json:"disable_redirects,omitempty"`
}
AutoHTTPSConfig mirrors Caddy's automatic_https server field. See the Server.AutomaticHTTPS doc comment for why this spike sets DisableRedir.
type Automation ¶
type Automation struct {
Policies []AutomationPolicy `json:"policies,omitempty"`
}
Automation holds the ordered list of automation policies. The first policy whose Subjects match a given hostname wins.
type AutomationPolicy ¶
type AutomationPolicy struct {
Subjects []string `json:"subjects,omitempty"`
Issuers []any `json:"issuers,omitempty"`
}
AutomationPolicy binds a set of subjects (hostnames) to the issuer(s) allowed to obtain certificates for them.
type BasicAuthAccount ¶
type BasicAuthAccount struct {
Username string `json:"username"`
Password string `json:"password"`
}
BasicAuthAccount is one entry in HTTPBasicAuthProvider.Accounts. Password is always a bcrypt hash, never the plaintext credential: see NewBasicAuthHandler.
type BasicAuthHandler ¶
type BasicAuthHandler struct {
Handler string `json:"handler"`
Providers BasicAuthProviders `json:"providers"`
}
BasicAuthHandler is Caddy's "authentication" handler (http.handlers.authentication) configured with the http_basic provider (http.authentication.providers.http_basic): per-route HTTP Basic Auth checked against a bcrypt hash. A Route places this ahead of a ReverseProxyHandler in Handle so Caddy's own handler chain short-circuits with 401 before the request ever reaches the backend.
func NewBasicAuthHandler ¶
func NewBasicAuthHandler(username, bcryptHash string) BasicAuthHandler
NewBasicAuthHandler builds Caddy's http_basic authentication handler for a single account. bcryptHash must already be a bcrypt hash (bcrypt.GenerateFromPassword); this function never sees or hashes a plaintext password itself.
type BasicAuthHash ¶
type BasicAuthHash struct {
Algorithm string `json:"algorithm"`
}
BasicAuthHash selects the password hashing algorithm HTTPBasicAuthProvider checks Accounts against. This package only ever sets "bcrypt", the same algorithm internal/api already uses for user login passwords (golang.org/x/crypto/bcrypt).
type BasicAuthProviders ¶
type BasicAuthProviders struct {
HTTPBasic *HTTPBasicAuthProvider `json:"http_basic"`
}
BasicAuthProviders holds the one provider this package configures.
type CA ¶
type CA struct {
// InstallTrust controls whether Caddy attempts to install this CA's
// root certificate into the local OS/browser trust store on first
// use. Caddy's own default is true, which on this spike's dev
// machine meant an unprompted `sudo` invocation that failed
// noisily (no interactive terminal, no `certutil`) but did not
// block certificate issuance. That's tolerable on a developer
// laptop; it is the wrong default for a headless server process,
// which has no desktop trust store to install into and should
// never attempt an interactive sudo prompt at startup. This spike's
// TLS builder pins it to false explicitly rather than relying on
// Caddy's default. See docs-local/research/caddy-spike.md.
InstallTrust *bool `json:"install_trust,omitempty"`
}
CA configures a single certificate authority managed by the pki app.
type CertKeyPEMPair ¶
type CertKeyPEMPair struct {
CertificatePEM string `json:"certificate"`
KeyPEM string `json:"key"`
Tags []string `json:"tags,omitempty"`
}
CertKeyPEMPair mirrors Caddy's caddytls.CertKeyPEMPair (tls.certificates.load_pem's wire shape, verified against modules/caddytls/pemloader.go in the vendored caddyserver/caddy/v2 source): a certificate and its private key as inline PEM text, no file on disk for either. Loading a certificate this way is also what makes Caddy skip automatic ACME/internal issuance for a matching hostname on its own (caddyhttp.AutoHTTPSConfig.IgnoreLoadedCerts defaults to false): no separate "skip" list is needed in Server.AutomaticHTTPS.
type CertStore ¶
type CertStore interface {
SaveCertStorageValue(ctx context.Context, key string, value []byte) error
GetCertStorageValue(ctx context.Context, key string) (*store.CertStorageValue, error)
DeleteCertStorageValue(ctx context.Context, key string) error
ExistsCertStorageValue(ctx context.Context, key string) (bool, error)
ListCertStorageKeys(ctx context.Context, prefix string, recursive bool) ([]string, error)
StatCertStorageValue(ctx context.Context, key string) (*store.CertStorageKeyInfo, error)
AcquireCertStorageLock(ctx context.Context, name string, staleAfter time.Duration) (bool, error)
TouchCertStorageLock(ctx context.Context, name string) error
ReleaseCertStorageLock(ctx context.Context, name string) error
}
CertStore is the narrow surface SQLiteStorage needs from internal/store's SQLite-backed *store.DB, so tests can fake it without a real database. *store.DB satisfies this.
type CertificatesConfig ¶
CertificatesConfig is the wire shape for TLSApp.Certificates: each key names a tls.certificates.* loader module, value is that module's own JSON shape (typed any like Route.Handle, for the same reason: Caddy's module system is inherently polymorphic).
type ChallengesConfig ¶
type ChallengesConfig struct {
DNS *DNSChallengeConfig `json:"dns,omitempty"`
}
ChallengesConfig mirrors Caddy's caddytls.ChallengesConfig, scoped to the one field this package sets: DNS.
type CloudflareDNSProvider ¶
CloudflareDNSProvider is the wire shape for Caddy's Cloudflare DNS provider module (dns.providers.cloudflare, github.com/caddy-dns/cloudflare wrapping github.com/libdns/cloudflare), used as a DNSChallengeConfig's Provider. APIToken must be a scoped Cloudflare API token (Zone:DNS:Edit), never the global API key.
type Config ¶
type Config struct {
Admin *AdminConfig `json:"admin,omitempty"`
// Storage selects the Caddy storage module certificates, ACME
// account state, and the internal issuer's local CA are persisted
// through. Typed any rather than a single concrete struct for the
// same reason Route.Handle and AutomationPolicy.Issuers already are
// in this file: it is a Caddy module reference, and Caddy's module
// system is inherently polymorphic (any value marshaling to
// {"module": "<id>", ...} is valid here). *FileStorage (the local
// filesystem, via NewFileStorage) and SQLiteStorageRef
// (internal/store's SQLite, via NewSQLiteStorageRef, TASKS.md 3.6)
// are this package's two concrete options as of this writing; a nil
// Storage leaves Caddy's own OS-specific default in place.
Storage any `json:"storage,omitempty"`
Apps Apps `json:"apps"`
}
Config is the root of a Caddy JSON config document (https://caddyserver.com/docs/json/), scoped to the subset this spike exercises: the admin endpoint, the http app, and the tls app.
func BuildProxyConfig ¶
func BuildProxyConfig(opts ProxyOptions) (*Config, error)
BuildProxyConfig builds a Config with exactly one server and one route: reverse-proxy every matching request to opts.BackendDial. This is the pure, no-Caddy-required half of the spike, table-tested in config_test.go; driver_test.go covers the half that actually starts Caddy and proxies a real request through it.
func BuildRoutesConfig ¶
func BuildRoutesConfig(opts RoutesOptions) (*Config, error)
BuildRoutesConfig builds a Config with one server carrying one route per entry in opts.Routes (reverse_proxy), opts.StaticRoutes (file_server), and opts.MaintenanceRoutes (static_response), each matched by its own Hosts. All three kinds share the same listener and the same TLS automation policy; nothing about a static or maintenance route requires a different Server or a second Caddy config document. All three being empty is valid and produces a Config with a listener but no routes and no TLS app: this is the normal shape for a reconcile pass over zero currently-routable resources (every known service or static site either declares no domains or, for a container service, has no running container yet, and no domain is in maintenance mode), not an error.
type DNSChallengeConfig ¶
type DNSChallengeConfig struct {
// Provider is a dns.providers.* module reference, typed any like
// Route.Handle. Its inline_key is "name", not "module" (per
// caddytls.DNSChallengeConfig.ProviderRaw's struct tag).
Provider any `json:"provider"`
}
DNSChallengeConfig mirrors Caddy's caddytls.DNSChallengeConfig, scoped to the one field this package sets: Provider.
type Driver ¶
type Driver struct {
// contains filtered or unexported fields
}
Driver drives an in-process Caddy instance through the same code path as its HTTP admin API (caddy.Load calls the identical config-apply logic the admin API's POST /load handler calls, see docs-local/research/caddy-spike.md), without this package ever shelling out to a `caddy` binary or running one as a subprocess or sibling container. The embedded-ingress design requires this shape specifically: ingress state lives in the control plane's own process, not in something else's.
func New ¶
New builds a Driver. A nil logger falls back to slog.Default(), matching the convention in internal/reconcile.Engine.
func (*Driver) Apply ¶
Apply marshals cfg to Caddy's JSON config format and loads it into the running (package-global) Caddy instance, starting it on first call. Caddy's config model has no notion of "the config for this Driver" versus "the config for some other caller in the process": caddy.Load replaces the entire process-wide config, which is a real constraint Phase 1 needs to design around once ingress config is built incrementally from many apps rather than handed over as one document per call (see docs-local/research/caddy-spike.md).
type FileServerHandler ¶
type FileServerHandler struct {
Handler string `json:"handler"`
// Root is the local filesystem directory to serve, e.g.
// "/var/lib/levelrail-data/static/docs/abc1234". Caddy resolves
// requests underneath it directly; there is no upstream to dial and
// nothing here to route through Docker.
Root string `json:"root,omitempty"`
}
FileServerHandler is Caddy's "file_server" handler (http.handlers.file_server): serves files directly from Root, no backend, no container. This is the whole of what this design means by "static sites get served by the embedded Caddy directly with no container": a route whose Handle is this instead of a ReverseProxyHandler. Registered via the same modules/standard blank import driver.go already pulls in for reverse_proxy and the TLS issuers, no separate module wiring needed.
func NewFileServerHandler ¶
func NewFileServerHandler(root string) FileServerHandler
NewFileServerHandler builds the one handler shape a static route needs: serve files from root for everything matched by the route.
type FileStorage ¶
FileStorage is Caddy's default storage module (caddy.storage.file_system): certificates, ACME account state, and the internal issuer's local CA all live under Root. Caddy's own default, when Storage is left nil, is an OS-specific application-data directory outside the project entirely (e.g. ~/Library/Application Support/Caddy on macOS) which is the right thing for the `caddy` CLI on a developer's own machine, and the wrong thing for an embedded control plane: it scatters state outside the platform's own data directory and, worse, is shared and reused across every unrelated process on the machine that also happens to embed Caddy with default settings. See docs-local/research/caddy-spike.md.
func NewFileStorage ¶
func NewFileStorage(dir string) *FileStorage
NewFileStorage returns the JSON shape for Caddy's file-system storage module rooted at dir. Module is "file_system", not the fully-qualified "caddy.storage.file_system": the "storage" field resolves modules within the caddy.storage namespace already (see StorageRaw's caddy struct tag in caddy's own Config type), so the namespace prefix here would be doubled up, a real surprise this spike hit and is recording, see docs-local/research/caddy-spike.md.
type HTTPApp ¶
HTTPApp is Caddy's "http" app: one or more named servers, each with its own listeners and routes.
type HTTPBasicAuthProvider ¶
type HTTPBasicAuthProvider struct {
Accounts []BasicAuthAccount `json:"accounts"`
Hash BasicAuthHash `json:"hash"`
}
HTTPBasicAuthProvider mirrors Caddy's caddyauth.HTTPBasicAuth: a fixed account list checked against Hash.
type InternalIssuer ¶
type InternalIssuer struct {
Module string `json:"module"`
}
InternalIssuer is Caddy's "internal" TLS issuer (tls.issuance.internal): a locally-generated, self-signed CA, meant for exactly the case this spike is in, no public domain and no inbound port 80/443 reachable from the internet for real ACME. It works fully offline. See docs-local/research/caddy-spike.md for why this stands in for real ACME in this spike and what still needs verifying against a real domain.
func NewInternalIssuer ¶
func NewInternalIssuer() InternalIssuer
NewInternalIssuer returns the JSON shape for Caddy's internal issuer module, used as an AutomationPolicy's Issuers entry.
type MaintenanceRoute ¶
type MaintenanceRoute struct {
// Hosts are the Host header values that get the fixed maintenance
// response. Also contributes to the Subjects list used for TLS
// automation when RoutesOptions.TLS is true, the same as Hosts on
// ProxyRoute/StaticRoute: a domain in maintenance mode still needs
// a valid certificate, an operator flips this on and off far more
// often than they'd want to also manage TLS for.
Hosts []string
}
MaintenanceRoute is one domain currently in maintenance mode: every request gets NewMaintenanceResponseHandler's fixed response instead of ever reaching a backend, container or otherwise. Unlike ProxyRoute and StaticRoute, a MaintenanceRoute needs no backend address or root directory to have ever existed: "this domain is intentionally unavailable right now" is true independent of whether the service behind it has a running container this pass.
type Matcher ¶
type Matcher struct {
Host []string `json:"host,omitempty"`
}
Matcher is a single Caddy request matcher set. Only the host matcher is needed for this spike; more (path, header, etc.) would be added the same way once Phase 1 needs them.
type PKIApp ¶
type PKIApp struct {
CertificateAuthorities map[string]*CA `json:"certificate_authorities,omitempty"`
}
PKIApp is Caddy's "pki" app: the certificate authorities available to issuers like InternalIssuer.
type ProxyOptions ¶
type ProxyOptions struct {
// ServerName keys the server within apps.http.servers. Arbitrary,
// used only for logging/introspection.
ServerName string
// ListenAddr is a Caddy network address, e.g. ":8080" or
// "127.0.0.1:8443".
ListenAddr string
// BackendDial is the reverse-proxy target, e.g. "127.0.0.1:9090".
BackendDial string
// Hosts, if non-empty, restricts the route to these Host header
// values and is also the Subjects list used for TLS automation when
// TLS is enabled. Empty means "match every request on this
// listener", which is only valid for the plain-HTTP case: Caddy's
// automatic HTTPS needs at least one qualifying hostname to know
// what to issue a certificate for.
Hosts []string
// TLS, if true, adds a tls app automation policy scoping the
// internal issuer to Hosts, and disables the HTTP->HTTPS redirect
// (see Server.AutomaticHTTPS).
TLS bool
// AdminListen overrides Caddy's admin API bind address. Empty keeps
// Caddy's own default (localhost:2019).
AdminListen string
// StorageDir overrides Caddy's storage root (certificates, ACME
// account state, the internal issuer's local CA). Empty keeps
// Caddy's own OS-specific default. See FileStorage's doc comment for
// why Phase 1 should always set this rather than rely on the
// default.
StorageDir string
}
ProxyOptions is the input to BuildProxyConfig: everything needed to stand up one reverse-proxy route, with or without TLS.
type ProxyRoute ¶
type ProxyRoute struct {
// Hosts are the Host header values routed to BackendDial. Also
// contributes to the Subjects list used for TLS automation when
// RoutesOptions.TLS is true.
Hosts []string
// BackendDial is the reverse-proxy target, e.g. "127.0.0.1:9090".
BackendDial string
// BasicAuth, if non-nil, adds a BasicAuthHandler ahead of the
// reverse-proxy handler so every host in this route requires HTTP
// Basic Auth. Nil (the default) reproduces this package's prior
// behavior exactly: a plain reverse-proxy route with no
// authentication handler.
BasicAuth *BasicAuthAccount
}
ProxyRoute is one reverse-proxy backend routed by hostname within a Server potentially shared with other routes. Unlike ProxyOptions (BuildProxyConfig), where Hosts is optional (an empty Hosts list means "match everything on this listener," valid for a single-backend listener), a Route here always requires host matching: hostname is the only thing that disambiguates two different backends sharing one listener.
type ReverseProxyHandler ¶
type ReverseProxyHandler struct {
Handler string `json:"handler"`
Upstreams []Upstream `json:"upstreams"`
}
ReverseProxyHandler is Caddy's "reverse_proxy" handler (http.handlers.reverse_proxy). Handler is always the literal string "reverse_proxy"; it is a field rather than a constant embedded via MarshalJSON so the struct stays a plain, table-testable value.
func NewReverseProxyHandler ¶
func NewReverseProxyHandler(backendDial string) ReverseProxyHandler
NewReverseProxyHandler builds the one handler shape this spike needs: proxy everything matched by the route to a single backend.
type RoutesOptions ¶
type RoutesOptions struct {
// ServerName keys the server within apps.http.servers. Arbitrary,
// used only for logging/introspection.
ServerName string
// ListenAddr is a Caddy network address shared by every route, e.g.
// ":443".
ListenAddr string
// Routes is every reverse-proxy backend to route on this listener.
// Empty is valid: it produces a listener with no routes and no TLS
// automation policy, the normal shape for a reconcile pass over zero
// currently-routable services, not an error condition.
Routes []ProxyRoute
// StaticRoutes is every static site to serve directly on this same
// listener, alongside Routes: one shared Caddy server can carry both
// reverse-proxy and file_server routes, disambiguated by Host the
// same way two ProxyRoutes already are. Empty (the default) is valid
// for exactly the same reason an empty Routes is.
StaticRoutes []StaticRoute
// MaintenanceRoutes is every domain currently in maintenance mode,
// sharing this same listener the same way StaticRoutes does. A
// caller must never put the same host in both Routes/StaticRoutes
// and MaintenanceRoutes in one call: BuildRoutesConfig builds
// whatever it's given, it does not itself resolve that conflict
// (the ingress reconciler's job, splitting a service's hosts before
// ever calling this function).
MaintenanceRoutes []MaintenanceRoute
// TLS, if true, adds a tls app automation policy scoped to every
// route's Hosts, both Routes and StaticRoutes (skipped if both are
// empty, since automatic HTTPS needs at least one subject to issue a
// certificate for), and disables the HTTP->HTTPS redirect (see
// Server.AutomaticHTTPS).
TLS bool
// ACMEEnabled, if true (and TLS is true and there's at least one
// host), scopes every routed host's single automation policy to a
// real ACME issuer (NewACMEIssuer) instead of Caddy's offline
// internal issuer. This is the operator-facing toggle
// internal/store.IngressSettings.ACMEEnabled drives
// (internal/reconcile/ingress reads that row fresh every reconcile
// pass and threads it straight through). False, the default,
// reproduces this package's original, ADR-005-verified behavior
// exactly: every route gets the internal issuer, byte-identical to
// before this field existed. There is deliberately no per-host mix
// of ACME and internal issuers in this pass: every currently-routed
// host shares one automation policy, either all-internal or
// all-ACME. Real per-domain granularity is genuine, existing Caddy
// capability (AutomationPolicy already supports an ordered list of
// policies, "the first policy whose Subjects match a given hostname
// wins," see that type's own doc comment) but is an explicit,
// deliberately out-of-scope v1 gap, not something this field's
// absence of a per-route override quietly forecloses.
ACMEEnabled bool
// ACMEEmail is the ACME account contact address, passed straight
// through to NewACMEIssuer when ACMEEnabled is true. This package
// performs no validation on it (empty is accepted structurally);
// internal/api's PUT /api/v1/settings/ingress is where "required and
// syntactically valid whenever ACME is enabled" is actually
// enforced, before a value ever reaches this builder.
ACMEEmail string
// ACMEDirectoryURL overrides the ACME CA's directory endpoint,
// passed straight through to NewACMEIssuer when ACMEEnabled is true.
// Empty keeps Caddy's own compiled-in default (Let's Encrypt
// production). See ACMEIssuer.CA's own doc comment for why this
// package never silently substitutes Let's Encrypt's staging
// directory on an operator's behalf.
ACMEDirectoryURL string
// CloudflareDNSAPIToken, if non-empty (and ACMEEnabled is true and
// at least one route's host is a wildcard, see IsWildcardDomain),
// scopes every wildcard subject to a separate automation policy
// using DNS-01 via Cloudflare (NewCloudflareDNSACMEIssuer) instead
// of the plain ACME policy every non-wildcard host still gets:
// HTTP-01 cannot solve a wildcard identifier. Empty reproduces this
// package's prior behavior exactly, wildcard hosts included, byte-
// identical to before this field existed; internal/store.
// CloudflareDNSSettings.Enabled plus internal/secrets' stored token
// is what an operator-facing settings flow resolves this from.
CloudflareDNSAPIToken string
// AdminListen overrides Caddy's admin API bind address. Empty keeps
// Caddy's own default (localhost:2019).
AdminListen string
// StorageDir overrides Caddy's storage root. Empty keeps Caddy's own
// OS-specific default. See FileStorage's doc comment. Ignored when
// CertStorage is set.
StorageDir string
// TLSCertificates is every domain currently configured with a BYO TLS
// certificate (internal/store.DomainTLSCert), sourced fresh from the
// store and internal/secrets every reconcile pass by the caller (no
// caching here, matching every other RoutesOptions field). Each
// entry's Host must already be present in Routes/StaticRoutes/
// MaintenanceRoutes' combined host list for Caddy to ever select the
// loaded certificate for a real connection; a host with no route at
// all still loads harmlessly, it's just never selected. Empty (the
// default) reproduces this package's prior behavior exactly: every
// host gets Caddy's automatic ACME/internal issuance as before this
// field existed.
TLSCertificates []TLSCertificateOverride
// CertStorage, if non-nil, overrides StorageDir with an arbitrary
// Caddy storage module reference (e.g. NewSQLiteStorageRef()),
// letting the caller point Caddy's certificate/ACME-account storage
// at internal/store's SQLite (TASKS.md 3.6) instead of the local
// filesystem, so certificate storage lives in the database and
// multi-node deployments can share cert state. Takes precedence over
// StorageDir when both are set, rather than being an error, so a
// caller migrating from one to the other doesn't need a separate
// code path just to clear the old field.
CertStorage any
}
RoutesOptions is the input to BuildRoutesConfig: everything needed to stand up one Caddy server carrying many independently host-routed backends on a single shared listener. This is the shape a real ingress controller needs (TASKS.md 1.6): ADR 005's Verified section found that caddy.Load replaces Caddy's entire process-wide config on every call, so a controller tracking many services builds one complete Config from every currently routable service and applies it whole on every reconcile, rather than incrementally updating a single service's route. BuildProxyConfig, by contrast, only ever builds a single route to a single backend and stays as-is for that narrower spike use case.
type SQLiteStorage ¶
type SQLiteStorage struct {
// contains filtered or unexported fields
}
SQLiteStorage implements certmagic.Storage over internal/store's embedded SQLite, replacing Caddy's default FileStorage module for certificates and ACME account state. This is the concrete shape internal/reconcile/ingress/controller.go's own package doc comment already names as the real requirement (TASKS.md 3.6): certificate storage lives in the database so multi-node deployments share cert state, and every ingress-driving process pointed at the same database file sees the same certificates and issuance locks through this type, so two of them never independently re-obtain a certificate for a domain the other already has one for.
func NewSQLiteStorage ¶
func NewSQLiteStorage(certStore CertStore, logger *slog.Logger) *SQLiteStorage
NewSQLiteStorage builds a SQLiteStorage backed by certStore. A nil logger falls back to slog.Default(), matching this package's Driver convention (see driver.go's New).
func (*SQLiteStorage) Delete ¶
func (s *SQLiteStorage) Delete(ctx context.Context, key string) error
Delete implements certmagic.Storage.
func (*SQLiteStorage) Exists ¶
func (s *SQLiteStorage) Exists(ctx context.Context, key string) bool
Exists implements certmagic.Storage. The interface has no error return, so a lookup failure is treated as "doesn't exist" rather than panicking, logged so it's never silently swallowed: a false negative here just makes certmagic re-store/re-issue, safe, if wasteful, unlike a false positive, which could skip issuing a certificate that's actually missing.
func (*SQLiteStorage) List ¶
List implements certmagic.Storage. Mirrors certmagic.FileStorage.List: listing a prefix that matches nothing returns fs.ErrNotExist, the same error a filepath.Walk over a missing directory would surface.
func (*SQLiteStorage) Lock ¶
func (s *SQLiteStorage) Lock(ctx context.Context, name string) error
Lock implements certmagic.Locker (embedded in certmagic.Storage). It blocks, polling every lockPollInterval, until it claims name's lock or ctx is cancelled. While held, a background goroutine refreshes the lock row every lockRefreshEvery so a live holder's lock never looks stale to a competitor; a holder that crashes without calling Unlock stops refreshing, and the row is treated as abandoned (and may be taken over) once it is older than lockStaleAfter, mirroring certmagic's own FileStorage doc comment on stale-lock handling.
type SQLiteStorageRef ¶
type SQLiteStorageRef struct {
Module string `json:"module"`
}
SQLiteStorageRef is the JSON shape assigned to Config.Storage (or RoutesOptions.CertStorage) to select the "sqlite" storage module. See NewSQLiteStorageRef.
func NewSQLiteStorageRef ¶
func NewSQLiteStorageRef() SQLiteStorageRef
NewSQLiteStorageRef returns the JSON shape for the sqlite storage module. Pair with SetActiveCertStorage (directly, or via internal/reconcile/ingress.WithCertStore) so CertMagicStorage has a real backend to resolve to; a Config referencing this without an active backend registered fails to apply with a clear error rather than silently falling back to Caddy's file storage default.
type Server ¶
type Server struct {
Listen []string `json:"listen,omitempty"`
Routes []Route `json:"routes,omitempty"`
// AutomaticHTTPS controls Caddy's automatic-HTTPS behavior for this
// server specifically. This spike disables the HTTP->HTTPS redirect
// (DisableRedir) for the TLS server: automatic redirects bind Caddy's
// default HTTP port (80), which needs root and would surprise a local
// spike run under a normal user, so the standalone TLS demo makes
// that trade-off explicit instead of silently trying to grab port 80.
AutomaticHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"`
}
Server is a single HTTP(S) listener plus the routes it serves.
type StaticResponseHandler ¶
type StaticResponseHandler struct {
Handler string `json:"handler"`
// StatusCode is an int, not caddyhttp.WeakString's own
// string-or-number wire shape: Caddy accepts a plain JSON number
// here for a fixed, non-placeholder status code, which is all this
// package ever needs.
StatusCode int `json:"status_code,omitempty"`
Body string `json:"body,omitempty"`
Headers map[string][]string `json:"headers,omitempty"`
}
StaticResponseHandler is Caddy's "static_response" handler (http.handlers.static_response): a fixed response with no upstream, no container dial, and no filesystem read. This is the handler maintenance-mode routes use (routes.go's MaintenanceRoute): every request to a domain in maintenance mode gets this same fixed response instead of ever reaching a ReverseProxyHandler, so it works identically whether or not the service behind it currently has any running container at all.
func NewMaintenanceResponseHandler ¶
func NewMaintenanceResponseHandler() StaticResponseHandler
NewMaintenanceResponseHandler builds the fixed maintenance-mode response: 503 Service Unavailable, the correct status for "this exists but is deliberately not accepting requests right now, try later" (as opposed to 404, which would say the route doesn't exist at all).
type StaticRoute ¶
type StaticRoute struct {
// Hosts are the Host header values served from RootDir. Also
// contributes to the Subjects list used for TLS automation when
// RoutesOptions.TLS is true.
Hosts []string
// RootDir is the local filesystem directory to serve.
RootDir string
}
StaticRoute is one static site routed by hostname within a Server potentially shared with other routes and with ProxyRoutes: a static site and a container service can be routed by the very same Caddy server, on different hostnames, because both ultimately produce a Route in the same Routes slice, just with a different Handle (FileServerHandler instead of ReverseProxyHandler). This is the shape that lets a static site bypass the container reconciler entirely, not a parallel ingress mechanism.
type TLSApp ¶
type TLSApp struct {
Automation *Automation `json:"automation,omitempty"`
// Certificates mirrors Caddy's tls.certificates field (a
// caddy.ModuleMap keyed by loader module name within the
// tls.certificates namespace). This package only ever sets the
// "load_pem" key (see CertKeyPEMPair), for BYO TLS certificates.
Certificates CertificatesConfig `json:"certificates,omitempty"`
}
TLSApp is Caddy's "tls" app: certificate automation policy plus any manually loaded certificates.
type TLSCertificateOverride ¶
TLSCertificateOverride is one domain with an operator-supplied certificate and key (internal/store.DomainTLSCert, BYO TLS), used in place of Caddy's automatic ACME/internal issuance for that host.