outlet

package
v0.2.33 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: MPL-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package outlet implements outlet://<name>/<op>: a stack-declared, named connection to an external service the tenant already runs. The chassis holds the credential, owns the connection pool, bounds every call and hands the stack JSON; the stack names an outlet and an operation and never sees a DSN, a password or a socket.

A stack declares an outlet as one small YAML file under the reserved OUTLETS/ subtree, beside FILES/ and DATASETS/:

OPS/<stack>/OUTLETS/<name>.yaml

The file deploys with `txco apply` as a stack_files row (inline on a single node, fingerprint + CAS on the fleet — the DATASETS/ manifest treatment) and is validated at apply; nothing connects until an op runs.

This file is the leaf layer: the reserved-path vocabulary, with no dependency on drivers or stores, so the CLI, the admin producer and the control-event applier can import it cheaply.

Index

Constants

View Source
const (
	AccessRead  = "read"
	AccessWrite = "write"
)

Access levels. `read` refuses outlet://<name>/exec before it reaches the driver; `write` allows it. The database role behind the DSN remains the real boundary for what a write may touch.

View Source
const (
	OpQuery = "query"
	OpExec  = "exec"
)

The two operations every driver answers. `query` runs read-only (a Postgres driver wraps it in BEGIN READ ONLY so the database itself refuses a mutation); `exec` may mutate and always runs as an explicit transaction so a ceiling crossed by RETURNING rows rolls it back.

View Source
const (
	// EgressDirect dials the destination from this node's own address.
	EgressDirect = "direct"
	// EgressRelay asks one of the node's relays (--outlet-egress-relays)
	// to make the connection, so it originates from the relay's address —
	// the one a customer allowlists. Only the dial changes: the pool, the
	// statement, TLS and every limit stay on the chassis.
	EgressRelay = "relay"
)

Egress modes. A declaration's `egress` picks one; an omitted field takes the node's default (--outlet-egress).

View Source
const (
	// CodeNotDeclared: the outlet isn't in the stack's declaration (also
	// refused at apply).
	CodeNotDeclared = "txco_outlet_not_declared"
	// CodeMissingSecret: the declared secret isn't set for this stack or
	// tenant, or no secret store is configured.
	CodeMissingSecret = "txco_outlet_missing_secret"
	// CodeInvalidRequest: wrong argument shape, an argument the driver can't
	// bind, a bad `into`, or exec on a read outlet.
	CodeInvalidRequest = "txco_outlet_invalid_request"
	// CodeResultTooLarge: the row or byte ceiling was crossed. No rows are
	// returned, and an exec rolled back.
	CodeResultTooLarge = "txco_outlet_result_too_large"
	// CodeOutcomeUnknown: a mutating transaction's fate couldn't be
	// determined (the connection died while COMMIT was in flight). Never
	// retried automatically; the op must look before acting again.
	CodeOutcomeUnknown = "txco_outlet_outcome_unknown"
	// CodeConnectFailed: DNS, TLS, refused, or blocked by the egress guard.
	// The statement was never sent.
	CodeConnectFailed = "txco_outlet_connect_failed"
	// CodeAuthFailed: the database rejected the credentials.
	CodeAuthFailed = "txco_outlet_auth_failed"
	// CodeTimeout: the deadline passed, pool wait included.
	CodeTimeout = "txco_outlet_timeout"
	// CodeConstraint: a constraint violation (SQLSTATE class 23).
	CodeConstraint = "txco_outlet_constraint"
	// CodeQueryFailed: any other database error; SQLState is set when known.
	CodeQueryFailed = "txco_outlet_query_failed"
	// CodeUnavailable: shutdown, failover, too many connections, or the
	// outlet runtime itself isn't configured.
	CodeUnavailable = "txco_outlet_unavailable"
)

Error codes a stack can dispatch on at `<into>.error.code`. Every failure of an outlet call is data at `into` with a nil Go error, so the op merges and the next scope can branch — the ai://decide shape.

View Source
const (
	Dir     = "OUTLETS"
	DeclExt = ".yaml"
)

Reserved top-level directory and the declaration extension.

View Source
const SchemePrefix = "outlet://"

SchemePrefix is the EXEC scheme every driver answers behind.

Variables

View Source
var ErrNoRelay = errors.New("outlet: relay egress is not configured on this node")

ErrNoRelay is the dial error for `egress: relay` on a node that has no relays configured. Run-time state, so it reaches the op as data (CodeConnectFailed), never a deploy error.

View Source
var ErrNotDeclared = errors.New("outlet: not declared")

ErrNotDeclared is returned by a DeclSource when the stack's active version has no declaration of that name.

Functions

func CheckOp

func CheckOp(exec string, with map[string]ast.Value, decls map[string]*Decl) error

CheckOp is the apply-time check of one EXEC "outlet://..." op against the stack's declarations. It runs client-side in `txco apply` and `txco lint` for fast feedback and server-side on validate/activate as the authority. Anything knowable from stack source fails here; anything that depends on the world at call time (a missing secret, a database that is down) comes back as failure data at run time instead.

The statement checks are lint, not a boundary: they catch an op pointed at the wrong verb while the author is writing it. What actually keeps the call safe is bound parameters, the READ ONLY transaction behind `query`, the extended protocol (one statement per call) and the database role behind the DSN.

func CheckSQL

func CheckSQL(sql, op string) error

CheckSQL lints one statement for an operation: non-empty, one statement, and a leading keyword that matches the verb. Comments and string literals are skipped, so a `;` inside a quoted value is not a second statement.

func DeclNames

func DeclNames(decls map[string]*Decl) []string

DeclNames returns the keys of a declaration map, sorted — for "unknown outlet X, have [a b c]" messages and deterministic validation order.

func DeclPath

func DeclPath(name string) string

DeclPath is the stack_files path of an outlet's declaration.

func DialFunc

func DialFunc(p EgressParams, guard egress.Guard, timeout time.Duration) func(ctx context.Context, network, address string) (net.Conn, error)

DialFunc returns the dial function a driver installs on its client.

Direct dials from this node, with the egress guard checking every resolved address before the socket connects. Relay dials ask a relay to connect instead: the destination is checked against the same guard first — drivers resolve the DSN host themselves and hand over ip:port, and a hostname is resolved here — and the relay is then asked for that exact address, so it can never arrive somewhere the chassis didn't check. The relay itself is reached with a plain dialer: it lives on the fleet's private network, which the guard would otherwise refuse. SOCKS is involved only while the connection is established; afterwards the socket is an ordinary TCP stream, and TLS runs end to end over it.

func Hash

func Hash(data []byte) string

Hash fingerprints a declaration body. It is part of the pool key, so a redeploy that changes the declaration resolves to a new pool.

func IsOutletExec

func IsOutletExec(exec string) bool

IsOutletExec reports whether an EXEC target names an outlet.

func IsOutletPath

func IsOutletPath(p string) bool

IsOutletPath reports whether a stack_files path lives under OUTLETS/.

func Known

func Known(name string) bool

Known reports whether a driver of that name is built into this binary.

func Name

func Name(p string) string

Name returns the outlet a declaration path belongs to: "OUTLETS/crm.yaml" → "crm". It returns "" when the path is not under OUTLETS/, is nested, has the wrong extension, or names an invalid outlet — the same shape validateStackFilePath enforces at upload.

func ParseRef

func ParseRef(exec string) (name, op string, err error)

ParseRef splits `outlet://<name>/<op>` into its outlet name and operation. The URI carries nothing else on purpose: host, port, database and TLS mode come only from the declared secret's DSN, so a string with userinfo, a port, a query or a fragment is refused — at apply, and again here.

func Register

func Register(d Driver)

Register adds a driver under its Name. Re-registering overwrites (test support).

func Registered

func Registered() []string

Registered returns the registered driver names, sorted.

func ValidEgress

func ValidEgress(s string) bool

ValidEgress reports whether s is an egress mode a declaration may name.

func ValidName

func ValidName(s string) bool

ValidName reports whether s is an acceptable outlet name.

Types

type Call

type Call struct {
	Tenant string
	Stack  string
	Outlet string
	Op     string
	SQL    string
	Args   []any
}

Call is one outlet operation as the processor hands it over: the trusted tenant and stack of the dispatching op (never envelope fields), the outlet and operation from the EXEC target, the literal statement and the converted arguments.

type Conn

type Conn interface {
	Query(ctx context.Context, req Request) (*Result, error)
	Exec(ctx context.Context, req Request) (*Result, error)
	Close() error
}

Conn is one open pool. It is safe for concurrent use; the runtime hands the same Conn to every call on the outlet until the pool is superseded or idle-closed.

Errors: a Conn returns *Error for every failure it can classify (the caller reports Code and Message to the stack verbatim); anything else is reported as CodeUnavailable with a fixed message.

type ContextDialer

type ContextDialer interface {
	DialContext(ctx context.Context, network, address string) (net.Conn, error)
}

ContextDialer reaches a relay. nil means the host network; a WireGuard tunnel living in this process (chassis/egress/wgtunnel) is the other one.

type Decl

type Decl struct {
	Driver  string `yaml:"driver"`
	Secret  string `yaml:"secret"`
	Access  string `yaml:"access"`
	MaxRows int    `yaml:"max_rows"`
	Timeout int    `yaml:"timeout"`
	Egress  string `yaml:"egress"`
}

Decl is one OUTLETS/<name>.yaml: where to connect and the ceilings. The SQL lives in the ops that call the outlet, not here.

driver: postgres
secret: CRM_DSN      # a secret NAME; its value is the DSN
access: write        # read (default) | write
max_rows: 500        # tightens the node ceiling, never raises it
timeout: 5000        # ms; caps every call on this outlet
egress: relay        # direct | relay; omitted = the node's --outlet-egress

Parsed strictly: an unknown key is a deploy error, not a silent ignore.

func ParseDecl

func ParseDecl(data []byte, knownDriver func(string) bool) (*Decl, error)

ParseDecl strictly decodes and validates a declaration body. knownDriver reports whether a driver name is built into this binary; nil skips that check (the runtime looks the driver up again when it opens the pool).

func (*Decl) Writable

func (d *Decl) Writable() bool

Writable reports whether outlet://<name>/exec is allowed on this outlet.

type DeclSource

type DeclSource interface {
	Lookup(ctx context.Context, tenant, stack, name string) (*Decl, string, error)
}

DeclSource resolves (tenant, stack, name) → the stack's ACTIVE declaration and a hash of its bytes. It returns ErrNotDeclared when the stack has no outlet of that name. Package server implements it over the dbcache snapshot and the content store, the way txco://dataset finds a manifest.

type Deps

type Deps struct {
	Lookup  func(driver string) (Driver, bool)
	Guard   egress.Guard
	Secrets SecretSource
	Decls   DeclSource
	Logger  *zap.Logger
	Limits  Limits
	Egress  EgressConfig
	Now     func() time.Time
}

Deps is everything a Runtime needs. Lookup defaults to the package registry; Now to time.Now.

type Driver

type Driver interface {
	Name() string
	Open(ctx context.Context, p OpenParams) (Conn, error)
}

Driver opens pools for one protocol. Open must not block on the network: the first statement surfaces connect and auth failures, so a database that is down never blocks anything but the op that needs it.

func Lookup

func Lookup(name string) (Driver, bool)

Lookup returns the registered driver, or (nil, false).

type EgressConfig

type EgressConfig struct {
	// Default is what a declaration without `egress` gets: EgressDirect
	// or EgressRelay. Empty means direct.
	Default string
	// Relays are SOCKS5 relays as ip:port on the fleet's private network
	// (or on the tunnel, when Forward is set), tried in order. Empty means
	// relay egress is unavailable on this node.
	Relays []string
	// Forward is how relays are reached; nil means the host network.
	Forward ContextDialer
}

EgressConfig is the node's egress default and its relays.

type EgressParams

type EgressParams struct {
	Mode    string
	Relays  []string
	Forward ContextDialer
}

EgressParams is the egress a driver is handed for one pool.

type Error

type Error struct {
	Code     string
	Message  string
	SQLState string
	Rows     int
	Bytes    int64
}

Error is the classified failure of an outlet call. Message is a fixed string chosen by the chassis or the driver — never a driver's own error text, which names hosts, users and databases. SQLState carries the database's code when there is one. Rows and Bytes report how far a read got before a ceiling fired (CodeResultTooLarge), for the trace only.

func AsError

func AsError(err error, fallback, message string) *Error

AsError extracts an *Error from err, or wraps an unclassified error under fallback with a fixed message so no driver text reaches stack state.

func ConvertArgs

func ConvertArgs(r gjson.Result) ([]any, *Error)

ConvertArgs turns the WITH `args` array into positional bind values.

Accepted: null, boolean, string, JSON number, and a homogeneous array of those — so `WHERE id = ANY($1)` works, which is the answer to the variable-length IN list a literal `sql` can't otherwise express. Homogeneous means every non-null element binds as the same type; nulls are allowed anywhere, since the database's arrays hold them naturally. Numbers are one JSON type: an array of numbers binds as integers when every element is integral, else as floats.

An integral number binds as an integer — unlike txco://dataset, which binds every number as float64 and so can't satisfy an integer parameter. Objects and nested arrays are refused: almost certainly a resolve mistake in the op, so fail loudly rather than bind their JSON text.

func NewError

func NewError(code, message string) *Error

NewError builds a classified failure with a fixed message.

func (*Error) Error

func (e *Error) Error() string

type Limits

type Limits struct {
	MaxRows      int
	MaxBytes     int64
	PoolMaxConns int
	PoolIdle     time.Duration
	// IdleClose is how long an unused pool stays open before the runtime
	// closes it. The next call reopens it.
	IdleClose time.Duration
}

Limits are the node's ceilings and pool defaults; a declaration can only tighten them.

type OpenParams

type OpenParams struct {
	// DSN is the materialized secret value. The runtime zeroes it the
	// moment Open returns; a driver that must keep connection parameters
	// (a pool config) copies what it needs and must never log or encode it.
	DSN []byte
	// Guard is the egress policy for every dial. When non-nil the driver
	// MUST route its connections through it (net.Dialer.Control =
	// egress.DialControl(guard)) so a DSN cannot point the chassis at
	// loopback, link-local or private address space. nil = no policy
	// (dev/test).
	Guard egress.Guard
	// Logger is scoped for this outlet. Drivers log codes and durations,
	// never SQL, values, rows or the DSN.
	Logger *zap.Logger
	Pool   PoolLimits
	// Egress says how to dial: install DialFunc(Egress, Guard, timeout)
	// on the client rather than a dialer of the driver's own, so every
	// driver shares one direct/relay path.
	Egress EgressParams
}

OpenParams carries everything a Driver needs to open one pool. The credential is live only for the duration of the Open call.

type Outcome

type Outcome struct {
	Result   *Result
	Err      *Error
	Driver   string
	Egress   string
	Duration time.Duration
}

Outcome is what the processor reports: the result or the classified error, plus the driver name and duration for the trace.

type PoolLimits

type PoolLimits struct {
	// MaxConns caps live connections; the minimum is always zero, so an
	// idle outlet costs nothing.
	MaxConns int
	// IdleTimeout closes a connection unused for this long.
	IdleTimeout time.Duration
}

PoolLimits bounds one outlet's pool on this node. Pools are per node, so a customer database sees roughly nodes × MaxConns connections.

type Request

type Request struct {
	SQL  string
	Args []any
	// MaxRows and MaxBytes are safety ceilings: crossing one aborts the read
	// with CodeResultTooLarge and no rows. A driver measures while rows
	// stream so a huge result is dropped early rather than buffered.
	MaxRows  int
	MaxBytes int64
}

Request is one statement with its bound arguments and ceilings.

type Result

type Result struct {
	Columns      []string
	RowsJSON     []byte
	Count        int
	RowsAffected int64
	// Bytes is the size of RowsJSON — what fuel is charged on.
	Bytes int64
}

Result is what a statement produced. RowsJSON is a JSON array of objects, one per row in column order, "[]" when there are none; Columns gives SELECT order, which object keys don't. RowsAffected is set by exec.

type Runtime

type Runtime struct {
	// contains filtered or unexported fields
}

Runtime owns every open outlet pool on this node.

func NewRuntime

func NewRuntime(d Deps) *Runtime

NewRuntime builds a runtime; call Start to run the idle sweeper.

func (*Runtime) Call

func (r *Runtime) Call(ctx context.Context, c Call) Outcome

Call runs one operation end to end: declaration, access, secret, pool, statement. Every failure is an *Error the processor writes to the envelope; nothing here returns a Go error.

func (*Runtime) Close

func (r *Runtime) Close()

Close stops accepting calls and closes every pool. The server calls it after in-flight runs have drained.

func (*Runtime) OpenPools

func (r *Runtime) OpenPools() int

OpenPools reports how many pools are open now (tests, later metrics).

func (*Runtime) Start

func (r *Runtime) Start(ctx context.Context)

Start runs the idle sweeper until ctx ends.

func (*Runtime) Sweep

func (r *Runtime) Sweep()

Sweep closes every pool that is idle past the limit or superseded and no longer in use. Exposed for tests; the sweeper calls it on a ticker.

type SecretSource

type SecretSource interface {
	MaterializeForOpSlug(ctx context.Context, tenantSlug, stack, name string) ([]byte, *secrets.SecretMetadata, error)
}

SecretSource materializes a declared secret by name for the calling tenant and stack, with the store's ordinary scoping: stack-scoped first, then tenant-wide. *secrets.Resolver satisfies it.

Directories

Path Synopsis
Package outlettest is an in-memory outlet driver for tests: canned rows, injectable delays and failures, counters.
Package outlettest is an in-memory outlet driver for tests: canned rows, injectable delays and failures, counters.
Package postgres is the PostgreSQL outlet driver: pgx pools opened from a DSN the chassis holds, every dial through the egress guard, `query` in a READ ONLY transaction and `exec` in an explicit one that rolls back when a ceiling is crossed.
Package postgres is the PostgreSQL outlet driver: pgx pools opened from a DSN the chassis holds, every dial through the egress guard, `query` in a READ ONLY transaction and `exec` in an explicit one that rolls back when a ceiling is crossed.

Jump to

Keyboard shortcuts

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