settings

package
v0.2.0 Latest Latest
Warning

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

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

Documentation

Overview

Package settings defines the on-disk settings directory — the JSON documents that hold WaveHouse's hot-reloadable configuration — and Validate, the single gate every consumer of that directory runs before adopting its contents.

The directory holds exactly four files: roles.json (the role registry), policies.json (the access-control policy), pipes.json (named queries), and config.json (behavioral tunables migrated out of boot config) — or, nested, one folder per tenant, each holding those four (see Tree). Validate is deliberately pure — no network, no ClickHouse, no side effects — so the same function can gate the `wavehouse validate` CLI, boot, and a live reload. Table/column existence is out of scope by design: WaveHouse is Bring-Your-Own-Schema, and schema discovery owns what exists in ClickHouse.

Index

Constants

View Source
const (
	FileRoles    = "roles.json"
	FilePolicies = "policies.json"
	FilePipes    = "pipes.json"
	FileConfig   = "config.json"
)

The four settings files. Any other entry in the directory is rejected — a typoed filename must fail loudly, not be silently ignored. The one exception is dot-prefixed entries (vim swap files, the `..data` machinery Kubernetes ConfigMap mounts publish through), which are skipped.

View Source
const MaxKeepaliveBuckets = 100

MaxKeepaliveBuckets caps stream.keepalive_buckets. The wheel ticks once per bucket per keepalive_interval, and holds a subscriber set per bucket, so the count is both a tenant's tick rate and the size of its ring: at the cap, a tenant at the 1-second minimum interval ticks a hundred times a second, and its ring is a hundred empty sets. A hundred buckets already spreads the writes to 1% of the tenant's streams per tick, past which more buckets buy nothing a proxy or client can tell apart.

View Source
const MinDedupeRetention = 2 * time.Minute

MinDedupeRetention is the shortest finite dedupe retention: the embedded queue's duplicate window (mq.EmbeddedDuplicateWindow). A record is published under an idempotency key derived from its id, so an id re-sent after a shorter retention but inside the window is claimed again and then dropped by the queue as a copy, while the client is told it was accepted.

Variables

This section is empty.

Functions

func Files

func Files() []string

Files returns every expected file. All four must exist (an empty document is written as {}), so an accidental deletion reads as an error, never as "defaults". A function rather than a package variable because Go has no const slices — this keeps the set genuinely immutable.

func HasErrors

func HasErrors(findings []Finding) bool

HasErrors reports whether any finding is an error (warnings alone leave the directory valid).

func Open

func Open(dir string) (*Registry, []Finding)

Open validates dir and returns a Registry serving it. A rejected directory — flat and invalid, of either shape with a finding about the directory itself, or nested with no tenant to serve — returns a nil Registry with the findings: the caller (boot) refuses to start. A nested directory with a rejected tenant folder beside a valid one still opens; the rejected tenant alone is not served.

func Seed

func Seed() (map[string][]byte, error)

Seed returns the starter settings directory's files by name.

func Validate

func Validate(root string) (*Tree, []Finding)

Validate checks a settings root of either shape in one pass and returns every finding, the way ValidateDir does for one directory. The shape is read off the root: holding any of the four settings file names makes it flat, whatever else is there; otherwise holding a folder makes it nested; otherwise — empty, missing, unreadable — it is flat again, so ValidateDir names the problem exactly as it always has. Mixing the shapes needs no rule of its own: a flat root rejects a folder and a nested root rejects a loose file.

A flat root's findings are ValidateDir's, untouched. A nested root's carry the tenant folder in File ("acme/policies.json"), and a folder whose name is not a tenant id is a finding against that folder alone. The Tree is nil when the finding is about the root itself — it cannot be listed, or a nested root holds a loose file or an entry that cannot be stat'ed — since no tenant can then be trusted to be what the root's author meant.

func ValidateDir

func ValidateDir(dir string) (*Document, []Finding)

ValidateDir reads, decodes, and checks one directory of the four settings files — a flat root, or one tenant's folder of a nested one — in one pass and returns every finding it can discover — an operator fixing a hand-edited directory wants the whole list, not a fix-rerun-fix loop. No side effects: no network, no ClickHouse, nothing written. The Document is returned only when no finding is an error (warnings alone leave it usable), so what a node adopts is byte-for-byte what was validated — this is the single checking path, and every consumer (the `wavehouse validate` CLI, boot, a live reload) goes through it, by way of Validate. Callers translate the result per their own contract: the CLI exits non-zero, boot refuses to start, and a live reload keeps serving the previous good document.

func WriteSeed

func WriteSeed(dir string) error

WriteSeed materializes the starter directory at dir, creating it if needed. It refuses a non-empty directory (the `initdb` contract): an existing settings directory is someone's config, never something to overwrite.

Types

type Auth

type Auth struct {
	JWKSURL   string
	JWTSecret string
	RoleClaim string
}

Auth is the adopted verifier wiring, HMAC secret included (the operator key is boot config).

type AuthConfig

type AuthConfig struct {
	// JWKSURL, when non-empty, makes JWKS the sole verifier (the HMAC
	// secret is then ignored). Must be an absolute http(s) URL.
	JWKSURL *string `json:"jwks_url"`
	// JWTSecret is the HMAC secret tokens are verified with when JWKSURL is
	// empty; with both empty no token validates, and every request is the
	// policy default_role.
	JWTSecret *string `json:"jwt_secret"`
	// RoleClaim is the dot-separated claim path the role is read from.
	RoleClaim *string `json:"role_claim"`
}

AuthConfig is the JWT verifier wiring, the HMAC secret included; the operator key is boot config (`auth.operator_key`), a credential of the platform, not of a tenant. A reload that changes it rebuilds the tenant's verifier; until a JWKS endpoint's key set has been fetched, a token-bearing request is refused (503) rather than verified.

type CORSConfig

type CORSConfig struct {
	AllowedOrigins []string `json:"allowed_origins"`
}

CORSConfig carries the per-request CORS allowlist. ["*"] allows any browser origin; see corsMiddleware in internal/api for why that is safe for a Bearer-token API.

type ClickHouse

type ClickHouse struct {
	Addr         string
	HTTPPort     int
	HTTPScheme   string
	Database     string
	Username     string
	Password     string
	QueryTimeout time.Duration
	TLS          TLS
	// Headers is this reader's own copy of the HTTP-interface headers.
	Headers      map[string]string
	MaxOpenConns int
	MaxIdleConns int
}

ClickHouse is the adopted connection wiring, resolved as one value from one snapshot so a reconnect never mixes the address of one document with the database of another.

type ClickHouseConfig

type ClickHouseConfig struct {
	// Addr is the native-protocol host:port (schema discovery, structured
	// queries, pipes, /readyz).
	Addr *string `json:"addr"`
	// HTTPPort and HTTPScheme address the HTTP interface (ingest INSERTs and
	// the raw-SQL proxy) on the same host as Addr.
	HTTPPort   *int    `json:"http_port"`
	HTTPScheme *string `json:"http_scheme"`
	Database   *string `json:"database"`
	Username   *string `json:"username"`
	// Password pairs with Username; may be empty (a passwordless user).
	Password *string `json:"password"`
	// QueryTimeout is the deadline in seconds (>= 1) of a call on the query
	// paths: structured queries, pipes (writes included) and the raw-SQL proxy.
	QueryTimeout *int `json:"query_timeout"`
	// TLS is the TLS wiring of both hops: `enabled` switches the native
	// protocol to TLS, `http_scheme` stays the HTTP hop's switch, and the
	// material applies to whichever hop uses TLS. Paths, checked for shape
	// only — the files are read when the connection is (re)built.
	TLS *ClickHouseTLS `json:"tls"`
	// Headers are set on every HTTP-interface request (ingest INSERTs, the
	// raw-SQL proxy) ahead of WaveHouse's own credential and content-type
	// headers, which therefore win. The native protocol carries none.
	Headers map[string]string `json:"headers"`
	// MaxOpenConns and MaxIdleConns size the native driver's pool: each
	// >= 1, open >= idle.
	MaxOpenConns *int `json:"max_open_conns"`
	MaxIdleConns *int `json:"max_idle_conns"`
}

ClickHouseConfig is the ClickHouse wiring, password included. A reload that changes any of it swaps the connection unconditionally; reachability is a runtime concern (schema discovery, /readyz), never a reload one. A certificate file that cannot be read or parsed is the one exception: boot refuses, and a reload keeps the previous connection.

type ClickHouseTLS

type ClickHouseTLS struct {
	Enabled            *bool   `json:"enabled"`
	CAFile             *string `json:"ca_file"`
	CertFile           *string `json:"cert_file"`
	KeyFile            *string `json:"key_file"`
	InsecureSkipVerify *bool   `json:"insecure_skip_verify"`
	ServerName         *string `json:"server_name"`
}

ClickHouseTLS is the `clickhouse.tls` block. Every key is required, like the rest of config.json; cert_file and key_file go together.

type DLQConfig

type DLQConfig struct {
	Enabled *bool `json:"enabled"`
	// Tables holds per-table overrides keyed by ClickHouse table name.
	Tables map[string]TableDLQ `json:"tables,omitempty"`
}

DLQConfig gates the Dead Letter Queue: whether a row ClickHouse still rejects after the row-by-row isolation retry is parked on the tenant's dead-letter queue (and its original acked) or left unacked to be redelivered indefinitely. The queue is opened when the tenant is first served — empty until something lands on it — so the switch is purely behavioral and resolves per table through the same override cascade as dedupe.

type Dedupe

type Dedupe struct {
	Enabled   bool
	IDField   string
	RequireID bool
	// Retention is how long a committed id stays a duplicate; 0 is forever.
	Retention time.Duration
}

Dedupe is a table's effective dedupe settings.

type DedupeConfig

type DedupeConfig struct {
	Enabled   *bool   `json:"enabled"`
	IDField   *string `json:"id_field"`
	RequireID *bool   `json:"require_id"`
	// Retention is how long a committed id stays a duplicate, as a Go
	// duration ("720h"); "0", or leaving it out, keeps it forever. A change
	// applies to ids committed after it.
	Retention *string `json:"retention,omitempty"`
	// Tables holds per-table overrides keyed by ClickHouse table name (#222).
	// Names are format-checked only — existence is schema discovery's runtime
	// concern, same as policies.json table keys.
	Tables map[string]TableDedupe `json:"tables,omitempty"`
}

DedupeConfig tunes dedupe behavior, including the switch itself: a reload that flips enabled opens or closes the tenant's store on the fly (dedupe.Managed, one per tenant, each a share of the one embedded Pebble instance), so the whole block is tenant-owned.

id_field and require_id are required here; retention is optional, and missing means "0" (forever). Every field is optional per table: a table override inherits whichever field it doesn't name. An empty, whitespace-only, or whitespace-padded id_field is rejected at every level, so the effective id_field can never be empty or silently unmatchable.

type Document

type Document struct {
	// Roles is the role registry every role reference is checked against.
	Roles []string
	// Policy is nil when policies.json is an empty document — no policy,
	// fail closed (matching the deleted-policy semantics of the store).
	Policy *policy.Policy
	Pipes  []pipes.NamedQuery
	Config TenantConfig
}

Document is a fully parsed and validated settings directory.

type Finding

type Finding struct {
	Severity Severity `json:"severity"`
	File     string   `json:"file,omitempty"`
	Path     string   `json:"path,omitempty"`
	Message  string   `json:"message"`
}

Finding is one validation result, located as precisely as the failure allows: File is empty for directory-level findings and, in a nested root, leads with the tenant folder ("acme/policies.json", or "acme" alone for a finding about the folder); Path is a dotted JSON path ("tables.clicks.analyst") and empty for whole-file findings. The JSON shape is part of the ops API: POST /v1/ops/settings/reload returns findings verbatim.

func (Finding) String

func (f Finding) String() string

String renders "error: policies.json: tables.clicks.admin: <message>", dropping the locators a finding doesn't have.

type MQConfig

type MQConfig struct {
	// MaxBytesGB caps the tenant's ingest queue (its dead-letter queue gets a
	// tenth of it). Must be >= 1. A reload updates the live queues in place:
	// growing takes effect immediately; shrinking below what is currently
	// buffered makes the ingest queue refuse new publishes (DiscardNew → 503
	// backpressure) until the sweeper purges it back under the limit —
	// nothing already buffered is dropped — and a dead-letter queue holding
	// more than a tenth of the new budget keeps what it holds rather than
	// dropping its oldest rows.
	MaxBytesGB *int `json:"max_bytes_gb"`
}

MQConfig sizes the tenant's message queue on disk.

type PipesFile

type PipesFile struct {
	Pipes []pipes.NamedQuery `json:"pipes"`
}

PipesFile is the shape of pipes.json.

type QueryConfig

type QueryConfig struct {
	// DefaultMaxRows is the result LIMIT applied to a structured query when
	// the caller and policy specify none. Must be >= 1.
	DefaultMaxRows *int `json:"default_max_rows"`
	// TimestampBucketSeconds truncates a structured query's relative time
	// range to this bucket so near-identical queries share a cache entry.
	// Must be >= 0; 0 disables bucketing.
	TimestampBucketSeconds *int `json:"timestamp_bucket_seconds"`
}

QueryConfig holds query-shaping defaults. Server-wide *resource* limits (memory, rows scanned, execution time) deliberately live in ClickHouse itself — its settings profiles and quotas — so they apply uniformly to every query; this block holds the result-LIMIT default and the time-range normalization that drives cache hits.

type Registry

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

Registry maps a tenant id to the Store holding that tenant's adopted settings, and owns everything that changes one. Open validates the directory and adopts it up front. Reload is the single code path every later trigger — SIGHUP, the directory watcher, and POST /v1/ops/settings/reload — funnels through. The stores are passive: a document pointer their getters read.

What a rejected directory costs depends on its shape (#583), which is fixed at Open — switching shapes is stop, restructure, start:

  • Flat, the four files in the directory itself: tenant.Default alone. Open refuses an invalid directory, so a flat *Registry never exists without a good document behind it, and a reload swaps the snapshot only when no finding is an error, so a bad edit (or a deleted file, or a vanished directory) can never evict the last good document.
  • Nested, one folder per tenant: fail closed per tenant, at Open and on reload alike. A folder with an error finding stops being served — the registry still knows the tenant, so its requests are refused rather than unknown — and every other tenant carries on; there is no previous-snapshot fallback. A reload mirrors the folders: a new one is served and a removed one is forgotten, the last one too. A finding about the directory itself (a loose file, an unreadable directory, a changed shape) rejects the reload whole and leaves every tenant as it was; at Open it refuses boot. So does a nested directory that would serve no tenant at Open — every folder rejected, or none named by a tenant id, as a fresh volume holding only lost+found is — since a server serving no tenant is what a wrong mount looks like, not what anyone meant; on reload the same tree drops every tenant and keeps running (#599).

func NewRegistry

func NewRegistry(store *Store) *Registry

NewRegistry returns a flat Registry serving store as tenant.Default, with no directory behind it: what a test that fixes its settings holds. The store is stamped with that id, as one a Registry creates is.

func (*Registry) AfterAdopt

func (r *Registry) AfterAdopt(fn func(adopted []tenant.ID))

AfterAdopt registers fn to run after each subsequent reload the registry applied, serialized with the reload itself, with the tenants it adopted: none for a reload that only rejected or removed a tenant, which a consumer holding a resource per tenant needs to hear of as much as an adoption. A reload rejected whole runs nothing, since nothing changed. Open's boot adoption has already happened by the time a caller can register, so the caller applies the boot state itself.

func (*Registry) All

func (r *Registry) All() iter.Seq2[tenant.ID, *Store]

All iterates over the tenants being served, in id order — for a consumer that owns one resource every tenant shares and has to weigh their settings against each other.

func (*Registry) Dir

func (r *Registry) Dir() string

Dir returns the directory this registry reads.

func (*Registry) For

func (r *Registry) For(id tenant.ID) (*Store, bool)

For returns the store of tenant id, or false when the registry is not serving that tenant — it has no such tenant, or the tenant's folder was rejected.

func (*Registry) Known

func (r *Registry) Known() iter.Seq2[tenant.ID, *Store]

Known iterates over every tenant the registry holds, served or rejected, in id order, each with the store holding its last adopted settings — nil for a tenant whose folder has not validated since boot — for a consumer that must keep a rejected tenant's resources current too: the tenant comes back into service with them, and a rejection is the common reload failure (a typo, fixed and reloaded minutes later). A rejected tenant's store serves no request; it is handed out for what those resources read of it.

func (*Registry) Nested

func (r *Registry) Nested() bool

Nested reports the directory's shape: one folder per tenant (true) or the four files of tenant.Default (false).

func (*Registry) Reload

func (r *Registry) Reload(trigger string) ([]Finding, bool)

Reload re-validates the directory and adopts what it finds (see Registry for what a rejection costs in each shape). Warnings don't block adoption, matching `wavehouse validate`. The returned bool reports whether everything was adopted: no finding is an error. In a nested directory false can therefore mean adopted in part — the tenants whose folders validated were adopted, warnings and all, and the ones with an error finding were not. trigger names the path that fired ("boot", "sighup", "watch", "api") and tags every log line so operators can tell them apart.

func (*Registry) ReloadTenant

func (r *Registry) ReloadTenant(id tenant.ID, trigger string) (findings []Finding, adopted, known bool)

ReloadTenant is Reload for one tenant's folder of a nested directory: the rest of the directory is not read, so it can neither adopt nor drop another tenant — what the writer of one folder calls when that folder is complete. The folder is adopted, or the tenant stops being served. known is false for a tenant the registry does not hold, and nothing is read: a whole-tree Reload is what picks up a new folder. A flat directory is its default tenant's folder, so there this is Reload.

func (*Registry) Resolve

func (r *Registry) Resolve(id tenant.ID) (store *Store, known bool)

Resolve is For for a caller that answers the two misses differently: a nil store with known=true is a tenant whose folder was rejected, and with known=false a tenant the registry has never heard of.

func (*Registry) Watch

func (r *Registry) Watch(ctx context.Context) error

Watch blocks until ctx is done, watching the settings directory and running the shared reload path when its contents change. A change that fails validation is logged and skipped — the previous good snapshot stays — so an operator mid-edit degrades to a log line, never to a broken server.

The directory itself is watched (not the four files): atomic-writer flows replace files wholesale, and Kubernetes ConfigMap mounts swap a symlink, both of which per-file watches lose track of. The parent directory is watched too, because fsnotify silently drops a watch whose directory is removed or renamed: with only the directory watch, a delete-and-recreate (or a rename-over, the atomic way to replace a whole directory) would leave every later edit unwatched. Parent events are filtered to the settings directory's own name, and the directory watch is re-added on every reload so it is restored once the directory exists again.

Once the watch is registered the directory is reloaded immediately, so an edit that landed between the boot read and the watch is not lost.

The setup error is returned (directory missing, fd limits); runtime watcher errors are logged and the loop continues — SIGHUP and the ops reload endpoint remain as triggers even if the watcher degrades.

Flat directories only, and a nested registry is refused rather than trusted to its caller: "the previous good snapshot stays" does not hold there. A watcher would validate a tenant's folder halfway through being written, and with no previous-snapshot fallback that drops the tenant.

type RolesFile

type RolesFile struct {
	Roles []string `json:"roles"`
}

RolesFile is the shape of roles.json.

type SchemaConfig

type SchemaConfig struct {
	// RefreshInterval is the auto-refresh period in seconds. Must be >= 1.
	RefreshInterval *int `json:"refresh_interval"`
}

SchemaConfig tunes ClickHouse schema discovery.

type Severity

type Severity string

Severity classifies a finding. Errors make the directory invalid; warnings flag config that is legal but almost certainly not what the author meant (e.g. a grant scoping the admin role, which is an unconditional bypass).

const (
	SeverityError   Severity = "error"
	SeverityWarning Severity = "warning"
)

type Store

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

Store holds the settings snapshot one tenant has adopted, and nothing else: the Registry validates, swaps the document in, and owns every reload trigger. Readers go through one lock-free atomic load per lookup; the typed accessors below each resolve from a single snapshot load, so a reload lands between lookups, never inside one.

There are no compiled defaults here on purpose, but one: every key but dedupe.retention (missing means "0", forever) is required by Validate, so the snapshot is exactly what the files said when they were adopted. Defaults live in the seed directory (Seed / WriteSeed).

func (*Store) Auth

func (s *Store) Auth() Auth

Auth returns the adopted verifier wiring.

func (*Store) CORSOrigins

func (s *Store) CORSOrigins() []string

CORSOrigins returns the allowed CORS origins; an empty list denies every browser origin and ["*"] is the only allow-all spelling.

func (*Store) ClickHouse

func (s *Store) ClickHouse() ClickHouse

ClickHouse returns the adopted ClickHouse wiring.

func (*Store) DLQFor

func (s *Store) DLQFor(table string) bool

DLQFor reports whether a poison row for table is parked on the DLQ (true) or left unacked for redelivery (false): the table override when present, the global switch otherwise.

func (*Store) DedupeEnabled

func (s *Store) DedupeEnabled() bool

DedupeEnabled reports the adopted dedupe.enabled switch.

func (*Store) DedupeFor

func (s *Store) DedupeFor(table string) Dedupe

DedupeFor resolves the effective dedupe settings for a table: the switch, then the table override for each field it names, the global value otherwise. Every field resolves from one snapshot load, so a reload can never hand a record the id_field of one document and the require_id, retention or switch of another.

func (*Store) DedupeRetentions

func (s *Store) DedupeRetentions() map[string]time.Duration

DedupeRetentions is the retention of the default (key "") and of every table override that names one, from one snapshot; 0 is forever.

func (*Store) DefaultMaxRows

func (s *Store) DefaultMaxRows() int

DefaultMaxRows returns the fallback result LIMIT for structured queries.

func (*Store) GapWindow

func (s *Store) GapWindow() time.Duration

GapWindow returns how much ACKed history the sweeper keeps for SSE gap-fill.

func (*Store) Keepalive

func (s *Store) Keepalive() (period time.Duration, buckets int)

Keepalive returns the SSE keepalive period and bucket count together, from one snapshot, so the wheel is never rebuilt from a mixed pair.

func (*Store) MQMaxBytes

func (s *Store) MQMaxBytes() int64

MQMaxBytes returns the disk budget of the tenant's ingest queue in bytes.

func (*Store) Pipe

func (s *Store) Pipe(name string) *pipes.NamedQuery

Pipe returns the adopted named query, or nil when pipes.json defines none by that name. Satisfies pipes.Source with Pipes.

func (*Store) Pipes

func (s *Store) Pipes() []*pipes.NamedQuery

Pipes returns every adopted named query.

func (*Store) Policy

func (s *Store) Policy() *policy.Policy

Policy returns the adopted access-control policy (policies.json). nil is the deliberate lockout an empty document spells: every token-based request is denied until a policy is adopted. Satisfies policy.Source.

func (*Store) SchemaRefreshInterval

func (s *Store) SchemaRefreshInterval() time.Duration

SchemaRefreshInterval returns the schema-discovery auto-refresh period.

func (*Store) Tenant

func (s *Store) Tenant() tenant.ID

Tenant returns the id of the tenant this store holds the settings of: how a handler holding the request's store names its tenant to a per-tenant resource (#583) without a second read of the context.

func (*Store) TimestampBucketSeconds

func (s *Store) TimestampBucketSeconds() int

TimestampBucketSeconds returns the time-range bucket structured queries truncate to (0 = no bucketing).

type StreamConfig

type StreamConfig struct {
	// KeepaliveInterval is the effective per-connection keepalive period in
	// seconds — the longest a quiet stream goes unwritten before a ":"
	// comment. Must be >= 1. A reload rebuilds the wheel in place; live
	// connections keep streaming.
	KeepaliveInterval *int `json:"keepalive_interval"`
	// KeepaliveBuckets spreads the keepalive writes across the interval so
	// each tick nudges ~1/N of live streams. Must be in 1-MaxKeepaliveBuckets.
	KeepaliveBuckets *int `json:"keepalive_buckets"`
	// GapWindowMinutes is how many minutes of ACKed messages the sweeper
	// keeps in NATS for SSE gap-fill (Last-Event-ID replay). Must be >= 0;
	// applies from the next sweep.
	GapWindowMinutes *int `json:"gap_window_minutes"`
}

StreamConfig tunes GET /v1/stream: the tenant's SSE keepalive wheel and how much NATS history the Active Sweeper keeps for its gap-fill. Keepalives are set to the proxy in front of the tenant's streams and the gap window is its replay budget, both of which change while the server runs.

type TLS

type TLS struct {
	Enabled            bool
	CAFile             string
	CertFile           string
	KeyFile            string
	InsecureSkipVerify bool
	ServerName         string
}

TLS is the adopted `clickhouse.tls` block (see ClickHouseTLS).

type TableDLQ

type TableDLQ struct {
	Enabled *bool `json:"enabled,omitempty"`
}

TableDLQ is one table's DLQ override.

type TableDedupe

type TableDedupe struct {
	IDField   *string `json:"id_field,omitempty"`
	RequireID *bool   `json:"require_id,omitempty"`
	Retention *string `json:"retention,omitempty"`
}

TableDedupe is one table's dedupe override; it overrides only the fields it names.

type TenantConfig

type TenantConfig struct {
	ClickHouse *ClickHouseConfig `json:"clickhouse"`
	Auth       *AuthConfig       `json:"auth"`
	Dedupe     *DedupeConfig     `json:"dedupe"`
	DLQ        *DLQConfig        `json:"dlq"`
	Query      *QueryConfig      `json:"query"`
	Schema     *SchemaConfig     `json:"schema"`
	Stream     *StreamConfig     `json:"stream"`
	MQ         *MQConfig         `json:"mq"`
	CORS       *CORSConfig       `json:"cors"`
}

TenantConfig is the shape of config.json: the behavioral tunables that migrate out of boot config. Boot config (config.yaml/env) keeps only what cannot change under a running process — the implementation each layer runs on (`mq.backend`, `cache.backend`, `dedupe.backend`, `coord.backend`), resource sizing (`data_dir`, `cache.l1_max_cost`, `clickhouse.max_total_conns`), listeners, the observability exporters — and the process-level secrets (`auth.operator_key`, `cache.redis.password`). A tenant's own secrets — `clickhouse.password`, `auth.jwt_secret` — live here with the wiring they belong to, so they reload with it (#529). Every block and every top-level key inside it is REQUIRED, but dedupe.retention (missing means "0", forever): the binary carries no other compiled default, so the adopted snapshot is exactly what the files say. Defaults live in the seed directory (see Seed) that `wavehouse bootstrap` writes. The fields are pointers only so Validate can tell "absent" from the zero value and report it by path.

type TenantResult

type TenantResult struct {
	Doc      *Document
	Findings []Finding
}

TenantResult is one tenant's share of a validation pass.

type Tree

type Tree struct {
	Nested bool
	// Tenants holds every tenant the root defines. Doc is nil for a tenant
	// whose folder has an error finding.
	Tenants map[tenant.ID]TenantResult
}

Tree is a validated settings root (#583). A flat root — the four files in the root itself — is tenant.Default alone. A nested root holds one folder per tenant and nothing else, the folder name being the tenant id.

Jump to

Keyboard shortcuts

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