ingress

package
v1.801.360 Latest Latest
Warning

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

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

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

View Source
const (
	MaxHostLen     = 253 // RFC 1035 max FQDN length
	MaxIDLen       = 128
	MaxBackends    = 32
	MaxMiddlewares = 16
	MaxExtraHosts  = 256
)
View Source
const (
	KindRoute      = "route"
	KindService    = "service"
	KindMiddleware = "middleware"
)

Kinds are the object namespaces in the store and the /v1/ingress path segments.

View Source
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

View Source
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

func Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount wires the /v1/ingress control plane onto app and, in edge role, starts the edge data plane.

func Shutdown

func Shutdown(ctx context.Context) error

Shutdown drains the edge listeners and closes the store. Idempotent.

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.

func (*Engine) ServeHTTP

func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP is the edge: it routes by Host to the first matching route's handler chain (which reverse-proxies to the service's backend pool). No route → 404.

func (*Engine) TLSHost

func (e *Engine) TLSHost(host string) bool

TLSHost reports whether the edge terminates TLS (issues an ACME cert) for host. It is the autocert HostPolicy predicate — certs are issued ONLY for configured hosts, never for arbitrary SNI.

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 Object

type Object struct {
	ID  string
	Doc string
}

Object is one persisted config object as an opaque JSON doc.

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

func (s *Store) All(ctx context.Context, kind string) ([]Object, error)

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

func (s *Store) AllTLS(ctx context.Context) ([]string, error)

AllTLS returns every org's ACME config document — the union feeding the ACME HostPolicy's ExtraHosts set.

func (*Store) Close

func (s *Store) Close() error

Close closes the underlying database.

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, org, kind, id string) (bool, error)

Delete removes (org, kind, id). Returns whether a row was deleted.

func (*Store) Get

func (s *Store) Get(ctx context.Context, org, kind, id string) (string, bool, error)

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.

func (*Store) GetTLS

func (s *Store) GetTLS(ctx context.Context, org string) (string, bool, error)

GetTLS returns the per-org ACME config document.

func (*Store) List

func (s *Store) List(ctx context.Context, org, kind string) ([]Object, error)

List returns every object of kind for org (tenant-scoped read).

func (*Store) Put

func (s *Store) Put(ctx context.Context, org, kind, id, doc, host string, now int64) error

Put upserts an object for (org, kind, id). host is the denormalized route host used for the global-uniqueness index ("" for non-route kinds). A collision on another route's host returns ErrHostTaken.

func (*Store) PutTLS

func (s *Store) PutTLS(ctx context.Context, org, doc string, now int64) error

PutTLS upserts the per-org ACME config document.

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.

Jump to

Keyboard shortcuts

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