Documentation
¶
Overview ¶
Package ingress is your front door: automatic TLS certificates and hostname routing to any backend, changed live.
It is cloud's embedded, runtime-configurable edge, controlled at /v1/ingress. It makes the ONE hanzoai/cloud binary able to BE the fleet edge: terminate TLS, run ACME (Let's Encrypt), and reverse-proxy by Host to upstreams — all configured LIVE over an API, with NO static config file (routes.yaml) and NO restart to change a route.
Role model — one binary, role = runtime config ¶
The same artifact runs in either role; the emphasis is a single env flag:
app role (default, CLOUD_INGRESS_EDGE_ENABLED unset): the /v1/ingress CONTROL plane is mounted (so config can be authored/inspected) but the edge DATA plane never binds a listener. cloud is a pure application.
edge role (CLOUD_INGRESS_EDGE_ENABLED=true): additionally the edge data plane binds :80 (ACME HTTP-01 + HTTP router) and :443 (SNI TLS termination
router). This instance is now a fleet edge — it routes to other cloud instances / services / itself.
The two planes are orthogonal:
- CONTROL plane = this file's /v1/ingress/* zip handlers (routes, services, middlewares, tls, status). SuperAdmin-gated; per-tenant persistence in SQLite; every mutation hot-reloads the engine.
- DATA plane = edge.go's net/http listeners + engine.go's atomic host table. Built on github.com/vulcand/oxy/v2 (Traefik's proxy lineage) and golang.org/x/crypto/acme/autocert.
This is NOT "TLS in every app binary": TLS/ACME live behind the edge role, which a deployment selects for the ONE instance that is the edge. Every other instance runs app role with the listeners off.
Replacing standalone hanzoai/ingress ¶
A single-tenant / serve deploy that today fronts cloud with a standalone hanzoai/ingress can instead run its cloud in edge role: point DNS at that instance, POST its own host→backend routes and a TLS route, and the standalone ingress pod is gone — one binary is app + edge. The shared multi-tenant fleet edge can migrate the same way (a cloud-in-edge-role instance whose route table is the union of every tenant's routes, host-unique) — that migration is a PLAN, delivered but NOT applied here; the live routes.yaml is untouched.
Composition with /v1/gateway ¶
Ingress and gateway are orthogonal edge subsystems: ingress owns routing + TLS; gateway owns auth + rate-limit. They compose (ingress in front, gateway as a backend/middleware layer) without colliding — different concerns, different surfaces.
Index ¶
- Constants
- Variables
- func Mount(app cloud.Router, deps cloud.Deps) error
- func Shutdown(ctx context.Context) error
- type Backend
- type Edge
- type Engine
- type Middleware
- type Object
- type Route
- type Service
- type Store
- func (s *Store) All(ctx context.Context, kind string) ([]Object, error)
- func (s *Store) AllTLS(ctx context.Context) ([]string, error)
- func (s *Store) Close() error
- func (s *Store) Delete(ctx context.Context, org, kind, id string) (bool, error)
- func (s *Store) Get(ctx context.Context, org, kind, id string) (string, bool, error)
- func (s *Store) GetTLS(ctx context.Context, org string) (string, bool, error)
- func (s *Store) List(ctx context.Context, org, kind string) ([]Object, error)
- func (s *Store) Put(ctx context.Context, org, kind, id, doc, host string, now int64) error
- func (s *Store) PutTLS(ctx context.Context, org, doc string, now int64) error
- type TLSConfig
Constants ¶
const ( MaxHostLen = 253 // RFC 1035 max FQDN length MaxIDLen = 128 MaxBackends = 32 MaxMiddlewares = 16 MaxExtraHosts = 256 )
const ( KindRoute = "route" KindService = "service" KindMiddleware = "middleware" )
Kinds are the object namespaces in the store and the /v1/ingress path segments.
const ( MWRedirectScheme = "redirectScheme" // config: scheme (default https), permanent (=true → 301) MWStripPrefix = "stripPrefix" // config: prefixes (comma-separated) MWAddPrefix = "addPrefix" // config: prefix MWHeaders = "headers" // config: header→value (set on the response) )
Middleware types — the common edge middleware, each a small http.Handler wrap.
Variables ¶
var ErrHostTaken = errors.New("host already claimed by another route")
ErrHostTaken is returned when a route Put would collide on the globally-unique host — another route (in any org) already claims it.
Functions ¶
Types ¶
type Backend ¶
type Backend struct {
// URL is the upstream server, http(s)://host[:port].
URL string `json:"url"`
// Weight is this member's share of the round-robin; must be >= 0.
Weight int `json:"weight,omitempty"`
}
Backend is one upstream server URL with a round-robin weight (default 1). The URL can point at another cloud instance, an in-cluster service, or the local app's own listen address (loopback) — the edge just proxies to a URL.
type Edge ¶
type Edge struct {
// contains filtered or unexported fields
}
Edge is the cloud-in-ingress-role DATA plane: the net/http listeners that terminate TLS (ACME certs via autocert) and reverse-proxy by Host through the Engine. It is orthogonal to the zip control plane — /v1/ingress CONFIGURES the edge; the Edge SERVES it. It is started only when the ingress role is enabled; in app role the listeners never bind and cloud stays a pure application.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the runtime edge router: a lock-free atomic snapshot of the compiled host table, hot-swapped on every config change. apply recompiles the whole table and atomically replaces the pointer, so a config change takes effect with no restart and no per-request lock. ServeHTTP is the edge data plane; TLSHost feeds the ACME HostPolicy. The Engine is transport-agnostic — the same handler serves the :443 TLS listener and the :80 HTTP listener.
type Middleware ¶
type Middleware struct {
// ID identifies the transform within the org: [A-Za-z0-9-_.], at most 128
// chars. A create that omits it gets a generated one. Routes reference it by
// this id.
ID string `json:"id"`
// Type is the transform: redirectScheme, stripPrefix, addPrefix or headers.
Type string `json:"type"`
// Config is the transform's parameters: redirectScheme takes scheme (default
// https) and permanent ("true" ⇒ 301, else 302); stripPrefix REQUIRES
// prefixes (comma-separated, first match wins); addPrefix REQUIRES prefix;
// headers is a header→value map set on the response.
Config map[string]string `json:"config,omitempty"`
}
Middleware is one edge transform applied before a route reaches its service.
type Route ¶
type Route struct {
// ID identifies the route within the org: [A-Za-z0-9-_.], at most 128 chars.
// A create that omits it gets a generated one.
ID string `json:"id"`
// Host is the exact hostname this route matches, lowercased with any trailing
// dot stripped. It is a GLOBALLY unique claim — one route across the whole
// edge may hold a host, so no tenant can hijack another's.
Host string `json:"host"`
// PathPrefix narrows the match to requests under this path; it must start
// with "/". Empty matches every path on the host.
PathPrefix string `json:"pathPrefix,omitempty"`
// Service is the id of the backend pool this route dispatches to. A route
// naming a service that does not exist is skipped at compile, not served.
Service string `json:"service"`
// Middlewares are the ids of the edge transforms to apply, in this order,
// before the request reaches the service. At most 16.
Middlewares []string `json:"middlewares,omitempty"`
// TLS asks the edge to terminate TLS for Host with an ACME-managed certificate.
TLS bool `json:"tls,omitempty"`
// Priority orders routes that share a host: higher wins, and equal priorities
// fall back to the longer PathPrefix.
Priority int `json:"priority,omitempty"`
}
Route is a Traefik-style router: an exact Host (and optional PathPrefix) rule that dispatches to Service through the ordered Middlewares chain, optionally terminating TLS (an ACME-managed certificate) for Host.
type Service ¶
type Service struct {
// ID identifies the pool within the org: [A-Za-z0-9-_.], at most 128 chars.
// A create that omits it gets a generated one. Routes reference it by this id.
ID string `json:"id"`
// Backends are the upstream servers to balance across: 1..32 of them.
Backends []Backend `json:"backends"`
// PassHostHeader forwards the client's original Host header upstream instead
// of rewriting it to the backend's.
PassHostHeader bool `json:"passHostHeader,omitempty"`
}
Service is a Traefik-style load-balanced backend pool. Backends are weighted round-robin members (github.com/vulcand/oxy/v2 roundrobin).
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the ingress metastore over one SQLite file. MaxOpenConns(1) serializes writes against the file lock (same discipline as settings/eval).
func (*Store) All ¶
All returns every object of kind across ALL orgs — the input to the edge compile. It is a server-internal full-table read (NOT a tenant-scoped API call); the API handlers use List, which binds org.
func (*Store) AllTLS ¶
AllTLS returns every org's ACME config document — the union feeding the ACME HostPolicy's ExtraHosts set.
func (*Store) Get ¶
Get returns the object doc for (org, kind, id). found=false (nil error) when there is no row. Tenant isolation: the WHERE always binds org.
type TLSConfig ¶
type TLSConfig struct {
// ACMEEmail is the ACME account email. It binds an account for the lifetime
// of an edge process, so it applies only when the edge (re)starts.
ACMEEmail string `json:"acmeEmail,omitempty"`
// Staging issues from Let's Encrypt's staging directory (untrusted certs, high
// rate limits). Like ACMEEmail it applies only when the edge (re)starts.
Staging bool `json:"staging,omitempty"`
// ExtraHosts get certificates without owning a route — at most 256. They feed
// the ACME HostPolicy and hot-apply on the next reload.
ExtraHosts []string `json:"extraHosts,omitempty"`
}
TLSConfig is a deployment's ACME intent, persisted per-org. ACMEEmail + Staging bind an ACME account for the lifetime of an edge process and are applied when the edge (re)starts; ExtraHosts feed the ACME HostPolicy and hot-apply on reload alongside the per-route TLS flags. This split is honest: an ACME account is not a per-request knob, but WHICH hosts get certs is.