Documentation
¶
Overview ¶
Package mamori loads application configuration and secrets from heterogeneous sources (environment, files, cloud secret managers, Vault, Kubernetes, ...) into typed, validated Go structs, and keeps them reconciled at runtime.
When a source value changes, mamori detects it, re-validates the whole configuration, and - only if the new snapshot is valid - atomically swaps it in and notifies the application with a diff-aware callback so it can react (rotate a database pool, rebuild a client, ...) without restarting.
Loading ¶
Define a struct whose fields carry a `source` tag describing where each value comes from, then call Load:
type Config struct {
DBPassword secret.String `source:"aws-sm://prod/db#password"`
LogLevel string `source:"env:LOG_LEVEL" default:"info"`
Workers int `source:"env:WORKERS" default:"4" validate:"gte=1,lte=256"`
}
cfg, err := mamori.Load[Config](ctx)
Watching ¶
Watch performs an initial fail-fast load and then keeps the configuration reconciled, delivering validated, diff-aware updates:
w, err := mamori.Watch[Config](ctx,
mamori.OnChange(func(ev mamori.Change[Config]) {
if ev.Changed("DBPassword") {
pool.Rotate(ev.New.DBPassword.Reveal())
}
}),
)
defer w.Close()
cfg := w.Get() // lock-free snapshot; always the last valid config
Providers ¶
Sources are pluggable via the Provider SPI and registered with Register using the database/sql pattern. The core module has zero cloud-SDK dependencies; each cloud provider ships as its own module under providers/.
Index ¶
- Variables
- func ContextWithPeerCred(ctx context.Context, cred Ucred) context.Context
- func Handler[T any](w *Watcher[T], opts ...HandlerOption) http.Handler
- func Load[T any](ctx context.Context, opts ...Option) (T, error)
- func Register(p Provider)
- func RegisteredSchemes() []string
- func SelectKey(data []byte, key string) ([]byte, error)
- func SentinelFor(k Kind) error
- func VersionHash(b []byte) string
- func WatchRef(ctx context.Context, p Provider, ref Ref, opts ...Option) <-chan Update
- type AuthFunc
- type Authenticator
- func APIKey(header string, key secret.String) Authenticator
- func APIKeyFunc(header string, fn func() secret.String) Authenticator
- func AllOf(as ...Authenticator) Authenticator
- func AnyOf(as ...Authenticator) Authenticator
- func BasicAuth(user string, pass secret.String) Authenticator
- func BasicAuthFunc(fn func() (string, secret.String)) Authenticator
- func BearerToken(token secret.String) Authenticator
- func BearerTokenFunc(fn func() secret.String) Authenticator
- func MTLS(opts MTLSOptions) Authenticator
- func PeerCred(opts PeerCredOptions) Authenticator
- type BatchProvider
- type Challenger
- type Change
- type Clock
- type FakeClock
- type FieldChange
- type FieldStatus
- type HandlerOption
- type HealthError
- type Identity
- type Kind
- type MTLSOptions
- type Meter
- type Option
- func OnChange[T any](fn func(Change[T])) Option
- func OnError(fn func(error)) Option
- func WithAdminHTTP(addr string, opts ...HandlerOption) Option
- func WithAdminTLS(cfg *tls.Config) Option
- func WithBackoff(base, max time.Duration) Option
- func WithClock(c Clock) Option
- func WithDebounce(d time.Duration) Option
- func WithDecodeHook(h mapstructure.DecodeHookFunc) Option
- func WithExecProvider() Option
- func WithHistory(n int) Option
- func WithJitter(f float64) Option
- func WithMeter(m Meter) Option
- func WithPollInterval(d time.Duration) Option
- func WithProvider(p Provider) Option
- func WithQueueDepth(n int) Option
- func WithStale(maxAge time.Duration) Option
- func WithTracer(t Tracer) Option
- func WithValidator(v Validator) Option
- type PeerCredOptions
- type Provider
- type ProviderError
- type Ref
- type Report
- type Snapshot
- type StaleError
- type Ticker
- type Timer
- type Tracer
- type Ucred
- type Update
- type ValidationError
- type Validator
- type Value
- type WatchableProvider
- type Watcher
- func (w *Watcher[T]) AdminAddr() net.Addr
- func (w *Watcher[T]) Close() error
- func (w *Watcher[T]) Get() T
- func (w *Watcher[T]) Health() error
- func (w *Watcher[T]) History() []Snapshot[T]
- func (w *Watcher[T]) Pin(version uint64) error
- func (w *Watcher[T]) PinCurrent() uint64
- func (w *Watcher[T]) Pinned() (uint64, bool)
- func (w *Watcher[T]) Status() Report
- func (w *Watcher[T]) Unpin()
Constants ¶
This section is empty.
Variables ¶
var ( ErrPermissionDenied = errors.New("mamori: permission denied") ErrUnauthenticated = errors.New("mamori: unauthenticated") ErrRateLimited = errors.New("mamori: rate limited") // ErrInvalid covers both a malformed ref and a payload that could not be // parsed. ErrInvalid = errors.New("mamori: invalid") )
Classification sentinels. Providers wrap the underlying SDK error with the matching sentinel using two %w verbs, which preserves both errors.Is for the sentinel and errors.As access to the original SDK error type:
return mamori.Value{}, fmt.Errorf("%w: %w", mamori.ErrPermissionDenied, err)
A single %w with the SDK error formatted as %v only wraps the sentinel; the SDK error becomes a flattened string and errors.As can no longer reach it.
Only ErrNotFound changes mamori's behavior (it is what triggers `default:` and `optional` handling). The rest are diagnostic.
var ErrForbidden = errors.New("mamori: forbidden")
ErrForbidden, returned from Authenticate, produces a 403 rather than a 401. Use it when the caller is authenticated but not permitted. Any other error produces a 401.
var ErrNoSuchSnapshot = errors.New("mamori: no such snapshot version")
ErrNoSuchSnapshot is returned by Watcher.Pin when the requested version is not retained. Increase WithHistory to pin older versions.
var ErrNotFound = errors.New("mamori: not found")
ErrNotFound is the sentinel error providers wrap (or return) when a referenced value does not exist. Consumers and mamori itself test for it with errors.Is(err, ErrNotFound); mamori applies defaults / optional handling only for not-found, never for other errors.
Functions ¶
func ContextWithPeerCred ¶ added in v1.2.0
ContextWithPeerCred returns a copy of ctx carrying cred as the peer credentials of the Unix-socket connection ctx (or a context derived from it) belongs to. This is the injection half of the ConnContext seam: an HTTP server whose Listener accepts Unix connections calls this from http.Server.ConnContext, once per accepted connection - see PeerCred's doc comment for the full plumbing this is designed to support. The listener wrapper that calls it lives in the server module (a later task); core provides only this injection point and the matching reader (peerCredFromContext) so both sides agree on the seam without core depending on the server's listener machinery.
func Handler ¶ added in v1.2.0
func Handler[T any](w *Watcher[T], opts ...HandlerOption) http.Handler
Handler returns an http.Handler exposing a Watcher's health over HTTP. It serves exactly two routes: GET / (the Report, as JSON) and GET /healthz (a bare liveness/readiness signal). Every other path, and every other method, is 404. There is no route that serves a configuration value, under any option: the Report returned by w.Status() already redacts refs and omits values, so exposing it as-is is safe by construction, and Handler has no way to add a field beyond what Report already carries.
Mount the result on your own mux, optionally under HandlerPrefix.
func Load ¶
Load resolves all refs of T once, applies defaults, validates, and returns the typed config. It fails fast: on any resolve or validation error it returns a non-nil error and the zero value of T; partial config is never returned.
func Register ¶
func Register(p Provider)
Register makes a provider available under its Scheme(), following the database/sql driver pattern. It is normally called from a provider package's init function. Register panics if a provider for the same scheme is already registered (fail fast at process init) or if p is nil.
func RegisteredSchemes ¶
func RegisteredSchemes() []string
RegisteredSchemes returns the sorted list of registered provider schemes. It is useful for diagnostics and for the docs/provider gallery.
func SelectKey ¶
SelectKey extracts a single field named key from a JSON object payload, for refs of the form scheme://path#key. If key is empty, data is returned unchanged. String values are returned unquoted; objects, arrays, numbers, and booleans are returned as their JSON encoding. If the key is absent, SelectKey returns an error wrapping ErrNotFound.
Provider authors should call SelectKey with ref.Key after fetching the raw payload, so that `#key` selection behaves identically across all providers.
func SentinelFor ¶ added in v1.2.0
SentinelFor returns the sentinel error corresponding to k, or nil for KindUnknown and the empty Kind, neither of which has one.
It is the inverse of ErrorKind, and exists so a classification that arrived as a value rather than an error (over the wire, from a config file) can be turned back into an error that errors.Is matches.
A nil return never means the operation succeeded. It means no sentinel corresponds to k: either k is KindUnknown (a real failure whose cause could not be classified) or k is some string that does not name a recognized Kind at all, such as one from an untrusted or forward-versioned wire value. Callers reconstructing an error from such a value must treat a nil return as an unclassified failure, never as absence of an error. Do not write `if err := SentinelFor(k); err != nil { ... }` as a substitute for checking whether the original operation failed.
func VersionHash ¶
VersionHash produces a stable version string from bytes. Provider authors can use it to synthesize a Value.Version when the backend has no native revision identifier, giving mamori cheap change detection without a byte comparison.
func WatchRef ¶ added in v1.2.0
WatchRef watches a single ref for changes, choosing a provider's native watch when it implements WatchableProvider and falling back to the shared polling adapter (pollWatch) otherwise. This is exactly the per-position source-selection engine.start performs for every ref in a Watch[T]'s field specs (see reconciler.go); it is exported here so a caller that only wants to watch one ref directly - the config server, most notably - gets the identical selection behavior and options handling as the reconciler, rather than a second, independently-maintained copy of the same decision.
opts is turned into an *options the same way Load and Watch build theirs: defaultOptions() overlaid with opts in the order given. Of everything an *options carries, only clock, pollInterval, and jitter matter here - the fields pollWatch itself reads - but the full Option surface (WithClock, WithPollInterval, WithJitter, ...) is accepted so a caller does not need a second, narrower configuration vocabulary just for this entry point.
The returned channel is closed when ctx is cancelled, matching both WatchableProvider's and pollWatch's own contract.
Types ¶
type Authenticator ¶ added in v1.2.0
Authenticator decides whether an HTTP request may proceed, and says who the caller is. A nil error allows the request; any error denies it.
The returned Identity is ignored by the admin endpoint (which only serves metadata) and consumed by the config server, whose authorization policy is expressed in terms of it. It is one interface rather than two so an Authenticator written for one surface works unchanged on the other.
func APIKey ¶ added in v1.2.0
func APIKey(header string, key secret.String) Authenticator
APIKey authenticates requests carrying the expected key in a named header (for example "X-API-Key") against a fixed key. The key is compared in constant time (see apiKey.Authenticate) so a failed request discloses nothing about the key through response timing.
func APIKeyFunc ¶ added in v1.2.0
func APIKeyFunc(header string, fn func() secret.String) Authenticator
APIKeyFunc reads the expected key on every request rather than freezing it at construction, so a mamori Watcher can rotate it live. A zero key (secret.String's IsZero) denies every request; see BasicAuthFunc for why unset must never be treated as unauthenticated-allowed.
func AllOf ¶ added in v1.2.0
func AllOf(as ...Authenticator) Authenticator
AllOf allows a request only if every member allows it, for composing independent checks that must both hold, such as a bearer token AND an mTLS-verified network identity. The first denial fails the whole evaluation and the rest are skipped: unlike AnyOf, there is no matching timing oracle to defend against here, since a partial failure is already a total denial regardless of what a later member would have decided.
The identity of the first member is returned: by convention the first member of an AllOf is the primary authenticator (the one that names a principal), and later members perform supplementary checks, such as a certificate or IP restriction, whose own Identity is typically empty.
func AnyOf ¶ added in v1.2.0
func AnyOf(as ...Authenticator) Authenticator
AnyOf allows a request if any member allows it, for composing schemes such as "a static admin token OR mTLS from the mesh sidecar". It evaluates every member on every request, even after one has already succeeded or failed, so the total work AnyOf performs never depends on which member matched (or on how early in the list a mismatch was found). Without this, an attacker could measure response timing to learn which scheme very nearly accepted their request, narrowing an attack from "guess a token" to "guess a token this scheme almost validated".
On total failure it returns an error naming no presented credential (see each member's own Authenticate). If any member implements Challenger, the first such member in argument order determines AnyOf's own Challenge; if none do, the returned Authenticator implements no Challenger either, and a failed request gets a bare 401, matching what a single member with the same property would produce.
func BasicAuth ¶ added in v1.2.0
func BasicAuth(user string, pass secret.String) Authenticator
BasicAuth authenticates HTTP Basic credentials against a fixed user and password. Both the username and the password are compared in constant time (see basicAuth.Authenticate), so a failed request discloses neither the password through response timing nor whether user is even the right username. pass is a secret.String so it redacts in logs and error values.
func BasicAuthFunc ¶ added in v1.2.0
func BasicAuthFunc(fn func() (string, secret.String)) Authenticator
BasicAuthFunc reads the expected username and password on every request rather than freezing them at construction, so a mamori Watcher can rotate the admin credential live. If fn returns a zero password (secret.String's IsZero), every request is denied: this is what makes rotation safe during the window before the credential has been populated for the first time, since the alternative, treating "unset" as "no password required", would silently open the endpoint.
func BearerToken ¶ added in v1.2.0
func BearerToken(token secret.String) Authenticator
BearerToken authenticates requests carrying "Authorization: Bearer <token>" against a fixed token. The token is compared in constant time (see bearerToken.Authenticate) so a failed request discloses nothing about the token through response timing.
func BearerTokenFunc ¶ added in v1.2.0
func BearerTokenFunc(fn func() secret.String) Authenticator
BearerTokenFunc reads the expected token on every request rather than freezing it at construction, so a mamori Watcher can rotate it live. A zero token (secret.String's IsZero) denies every request; see BasicAuthFunc for why unset must never be treated as unauthenticated-allowed.
func MTLS ¶ added in v1.2.0
func MTLS(opts MTLSOptions) Authenticator
MTLS authenticates a client by its verified TLS client certificate. It requires the server to be configured with tls.RequireAndVerifyClientCert (see WithAdminTLS): the Go TLS stack has already validated the certificate chain by the time Authenticate runs, so this scheme only needs to check which verified identity is allowed, not whether the certificate is trustworthy. On a non-TLS connection, or a TLS connection presenting no client certificate, MTLS denies every request; there is no fallback and no separate secret, so for an endpoint exposing operational detail about a cluster's secret material this is the strongest option that needs no external dependency.
func PeerCred ¶ added in v1.2.0
func PeerCred(opts PeerCredOptions) Authenticator
PeerCred authenticates a Unix-domain-socket client by its kernel-verified peer uid/gid. Because the identity comes from the kernel at accept time rather than anything the client presents in the request, it cannot be spoofed by a client that can merely connect to the socket - the strongest authenticator in this package for a sidecar deployment where the config server listens only on a Unix socket shared with trusted local processes.
Support is platform-specific: SO_PEERCRED on Linux (authpeercred_linux.go), LOCAL_PEERCRED via GetsockoptXucred on Darwin (authpeercred_darwin.go), and an unconditional deny with a clear error on every other platform (authpeercred_other.go) - never a silent allow just because credentials could not be checked there.
The ConnContext seam ¶
By the time Authenticate runs, only the *http.Request and its context are in scope; net/http gives no direct path from a request back to the net.Conn it arrived on. The seam that bridges the two is split across core and the server module so both sides agree on it without core depending on the server's listener machinery:
- Core (this package) exports Ucred, ContextWithPeerCred, and, per supported platform, PeerCredFromConn: a way to read a connection's peer credentials and a way to stash them in a context.
- The server module's Unix-socket listener wrapper (a later task) calls PeerCredFromConn once per accepted connection and passes the result to ContextWithPeerCred from http.Server.ConnContext, so every request arriving on that connection derives a context carrying the same peer identity the kernel reported at accept time.
- PeerCred.Authenticate reads it back with the unexported peerCredFromContext.
A request whose context carries no such value - a non-unix listener, TLS, or simply a server that never wired up ConnContext - is denied outright, never treated as "no restriction configured".
type BatchProvider ¶
type BatchProvider interface {
Provider
// ResolveBatch resolves all refs, returning a map keyed by each input Ref's
// Raw string (equivalently Ref.String()). A ref that is not found should be
// omitted from the map (mamori applies the default) rather than causing the
// whole batch to fail. Ref itself is not comparable (it holds url.Values), so
// the raw tag string is used as the key.
ResolveBatch(ctx context.Context, refs []Ref) (map[string]Value, error)
}
BatchProvider is an optional interface for providers that can resolve many refs in one backend call (SM BatchGetSecretValue, one file read for many keys). mamori uses it automatically when available.
type Challenger ¶ added in v1.2.0
type Challenger interface {
Challenge() string
}
Challenger is optionally implemented by an Authenticator to supply the value of the WWW-Authenticate header sent with a 401. A scheme that does not implement it produces a bare 401.
type Change ¶
type Change[T any] struct { Old T New T Fields []FieldChange }
Change is delivered to OnChange when a reconciled, re-validated update is applied. Old and New are immutable full snapshots; Fields lists what changed.
type Clock ¶
type Clock interface {
// Now returns the current time.
Now() time.Time
// NewTimer returns a timer that fires once after d.
NewTimer(d time.Duration) *Timer
// NewTicker returns a ticker that fires every d.
NewTicker(d time.Duration) *Ticker
// After is shorthand for NewTimer(d).C.
After(d time.Duration) <-chan time.Time
}
Clock abstracts time so the reconciler can be tested deterministically. All time-dependent engine code uses a Clock rather than the time package directly. The default is the system clock; pass a different one with WithClock.
type FakeClock ¶
type FakeClock struct {
// contains filtered or unexported fields
}
FakeClock is a manually-driven Clock for deterministic tests. Advance moves time forward, firing any timers/tickers whose deadline is reached.
func NewFakeClock ¶
NewFakeClock returns a FakeClock started at the given time (or a fixed epoch if start is the zero value).
func (*FakeClock) Advance ¶
Advance moves the fake clock forward by d, firing any timers and tickers whose deadlines fall within the interval, in chronological order.
type FieldChange ¶
type FieldChange struct {
Path string // dotted field path, e.g. "Redis.Password"
OldVersion string
NewVersion string
}
FieldChange records one field that changed between two snapshots.
type FieldStatus ¶ added in v1.2.0
type FieldStatus struct {
Path string // dotted field path, e.g. "Redis.Password"
Scheme string // provider scheme of the ref
Ref string // the ref, with sensitive query options redacted
Version string // provider version of the currently observed value
LastOK time.Time // last successful resolve, zero if never
Age time.Duration // GeneratedAt minus LastOK, recomputed at read time
Stale bool // Age exceeds the configured WithStale threshold
LastError string // text of the last resolve error, empty if none
LastKind Kind // classification of LastError, empty if none
Sensitive bool // field is a secret.String or secret.Bytes
}
FieldStatus is the live state of one configured field, as reported by Watcher.Status and Doctor. It is safe to serialize and safe to serve over HTTP: Ref has sensitive query options redacted, and no field value appears.
type HandlerOption ¶ added in v1.2.0
type HandlerOption func(*handlerOptions)
HandlerOption configures Handler.
func HandlerMiddleware ¶ added in v1.2.0
func HandlerMiddleware(mw func(http.Handler) http.Handler) HandlerOption
HandlerMiddleware wraps the handler with a non-authentication concern such as request logging or rate limiting. It runs outside the Authenticator (and outside HandlerPrefix's stripping), so it sees the request before either applies, in the order the options were given.
func HandlerPrefix ¶ added in v1.2.0
func HandlerPrefix(prefix string) HandlerOption
HandlerPrefix strips prefix from request paths before routing, so the handler can be mounted under a subpath of an existing mux (for example "/admin") instead of always owning the root of whatever mux it is attached to.
func WithAuth ¶ added in v1.2.0
func WithAuth(a Authenticator) HandlerOption
WithAuth requires every request to pass a before it is served, with one exemption: /healthz still answers without credentials, just without the failing-field detail (see serveHealthz). Applying WithAuth more than once is a construction error rather than a silent overwrite, since "the second call wins" and "the first call wins" are both plausible reads of the call site and neither is obviously right; compose multiple schemes explicitly with AnyOf or AllOf instead so the intended semantics are visible at the call site.
type HealthError ¶ added in v1.2.0
type HealthError struct {
Fields []FieldStatus
}
HealthError is returned by Watcher.Health when one or more fields are unhealthy. It names the offending fields so a readiness probe can log which config is broken rather than a bare "unhealthy".
func (*HealthError) Error ¶ added in v1.2.0
func (e *HealthError) Error() string
type Identity ¶ added in v1.2.0
Identity is the authenticated caller. Subject is a stable principal name; Attrs carries scheme-specific detail (certificate SANs, token claims, a peer uid). Attrs is multi-valued because authorization commonly needs multi-valued claims: groups, scopes, token audiences, or multiple certificate SANs; a single string per key would force a scheme to join-encode them. Both may be empty for schemes that authenticate without naming a principal, such as a shared bearer token.
type Kind ¶ added in v1.2.0
type Kind string
Kind is a coarse, provider-independent classification of a resolve failure. It exists so diagnostics can distinguish conditions that need human action (a missing secret, a denied permission) from transient ones (an unreachable backend, a throttled request). Providers produce it by wrapping one of the sentinels below; consumers read it with ErrorKind.
const ( // KindNotFound represents a missing key, secret, path, or version. This is the // only kind that triggers a field's default: or optional: handling; the rest are // diagnostic only. KindNotFound Kind = "not_found" // KindPermissionDenied means the caller has authenticated successfully but is // not authorized to access the requested value, such as an IAM deny, Vault // policy, or Kubernetes RBAC denial. Distinguish this from KindUnauthenticated, // where the caller has not proven their identity. KindPermissionDenied Kind = "permission_denied" // KindUnauthenticated means credentials are missing, malformed, or expired, or a // token renewal failed. The caller has not proven who they are. Distinguish from // KindPermissionDenied, where the caller has authenticated but been refused. KindUnauthenticated Kind = "unauthenticated" // due to network failure, DNS issues, timeout, a 5xx response, or an open // circuit breaker. KindUnavailable Kind = "unavailable" // KindRateLimited means the backend deliberately rejected this request due to // throttling, quota exhaustion, or similar rate control. Distinguish from // KindUnavailable, where the backend is reachable and healthy. KindRateLimited Kind = "rate_limited" // KindInvalid means the ref is malformed for this provider, or the returned // payload could not be parsed. KindInvalid Kind = "invalid" // KindUnknown is the honest answer for an error a provider cannot map. It is // a legal outcome, not a failure: a provider that guesses is worse than one // that admits it does not know. KindUnknown Kind = "unknown" )
func ErrorKind ¶ added in v1.2.0
ErrorKind classifies err by walking its errors.Is chain. It returns the empty Kind for a nil error and KindUnknown for an error carrying no sentinel.
An error that lost its chain (a provider that formatted with %v rather than %w) reports KindUnknown. That is the failure the providertest conformance case exists to catch.
context.DeadlineExceeded and context.Canceled are handled explicitly, after the sentinel checks so an explicit mamori sentinel always wins, and are deliberately classified asymmetrically:
- context.DeadlineExceeded reports KindUnavailable: the backend genuinely did not respond in time, which is exactly what KindUnavailable denotes.
- context.Canceled still reports KindUnknown: the caller withdrew the request, which says nothing about the backend's health. This is a deliberate decision, not an oversight.
type MTLSOptions ¶ added in v1.2.0
type MTLSOptions struct {
// AllowedCNs, if non-empty, permits only certificates whose
// Subject.CommonName is in this list.
AllowedCNs []string
// AllowedDNSNames, if non-empty, permits only certificates with at least
// one DNS SAN in this list.
AllowedDNSNames []string
}
MTLSOptions configures certificate-based authentication. Both fields are optional allowlists: a name matching either is accepted. If both are empty, any client certificate that the TLS stack has already verified is accepted, on the theory that verification itself (see MTLS) is the security boundary and a further, unconfigured name check would only be security theater.
type Meter ¶
type Meter interface {
// RecordResolve reports a provider resolve: its scheme, duration, and error
// (nil on success).
RecordResolve(scheme string, dur time.Duration, err error)
// RecordRefresh reports that a watched value changed and was reconciled.
RecordRefresh(scheme string)
// RecordWatchError reports a provider watch-channel error.
RecordWatchError(scheme string)
}
Meter is the minimal metrics sink mamori emits to. It is deliberately tiny so the core module takes no OpenTelemetry dependency; the x/otel module provides an adapter that implements this interface on top of an OTel meter. Pass one with WithMeter. All methods must be safe for concurrent use.
type Option ¶
type Option func(*options)
Option configures Load and Watch.
func OnChange ¶
OnChange installs the callback invoked (on a single, serialized goroutine) for each applied update. It is typed to the same T passed to Watch.
func WithAdminHTTP ¶ added in v1.2.0
func WithAdminHTTP(addr string, opts ...HandlerOption) Option
WithAdminHTTP makes Watch run its own HTTP server on addr, serving the same routes Handler would (see handler.go): GET / and GET /healthz. It exists so a caller who does not already run a mux of their own does not have to wire one up just to expose health.
It is off by default: with no WithAdminHTTP option, no listener is constructed, no port is bound, and no goroutine is started. When set, Watch binds the listener before it returns, so a bind failure (port in use, permission denied) fails Watch with the bind error rather than logging it and leaving the caller believing the endpoint is up; this mirrors Watch's existing fail-fast behavior on the initial Load.
The server's lifetime is tied to the Watcher's: its goroutine is tracked by the same wait group as the reconciliation engine, and Watcher.Close shuts it down gracefully (bounded by a grace period) before returning, so Close returning means the port is free again.
Load accepts this option too, since Load and Watch share the same Option type, but Load has no long-lived Watcher to run a server against and so it silently ignores WithAdminHTTP.
func WithAdminTLS ¶ added in v1.2.0
WithAdminTLS serves the WithAdminHTTP endpoint over TLS instead of plaintext, using cfg's certificates and, if set, its ClientCAs/ClientAuth for mutual TLS. It has no effect without WithAdminHTTP.
The shipped Authenticator schemes (basic auth, bearer tokens) are only as safe as the transport they run over: a credential sent in the clear is not really a credential. WithAdminTLS exists so those schemes can be used safely on the self-hosted admin endpoint.
func WithBackoff ¶
WithBackoff configures per-ref exponential backoff on resolve failure.
func WithDebounce ¶
WithDebounce sets the coalescing window for change events (default 500ms). A per-field `?debounce=` ref option overrides this for that field.
func WithDecodeHook ¶ added in v1.1.0
func WithDecodeHook(h mapstructure.DecodeHookFunc) Option
WithDecodeHook adds a mapstructure decode hook applied when decoding a flatten:"json|yaml|env" payload into a nested struct. Hooks run after the built-in secret/duration hook, in the order registered, so you can convert custom field types (a time.Time layout, a net.IP, an enum, ...).
func WithExecProvider ¶
func WithExecProvider() Option
WithExecProvider enables the exec: provider for this Load or Watch call only. It is not registered globally; you must opt in explicitly.
func WithHistory ¶ added in v1.2.0
WithHistory retains the n most recent snapshots in addition to the current one, readable via Watcher.History and pinnable via Watcher.Pin. It defaults to 0. A negative n clamps to 0.
Retained snapshots hold full copies of T, including any secret material that has since been rotated. Enabling history extends the in-memory lifetime of old secrets; enable it deliberately.
func WithJitter ¶
WithJitter sets the poll jitter fraction (0..1); a value of 0.2 randomizes each interval by ±20% to avoid thundering herds.
func WithPollInterval ¶
WithPollInterval sets the fallback poll interval for non-watchable providers.
func WithProvider ¶
WithProvider registers a provider for this call only, taking precedence over the global registry for its scheme.
func WithQueueDepth ¶
WithQueueDepth bounds the OnChange dispatch queue; when full, the oldest event is dropped (default 16).
func WithStale ¶
WithStale escalates staleness to a hard error: if a ref cannot be refreshed for longer than maxAge, OnError receives a *StaleError.
func WithTracer ¶
WithTracer installs a tracing sink (see the x/otel module for an OTel adapter).
func WithValidator ¶
WithValidator overrides the default (go-playground/validator) validator.
type PeerCredOptions ¶ added in v1.2.0
PeerCredOptions configures PeerCred. UIDs and GIDs are both optional allowlists: a peer is permitted if its uid is in UIDs, OR its gid is in GIDs (either suffices; they are not ANDed). If both are empty, any peer whose credentials were successfully read is permitted - the same "verification itself is the security boundary" default MTLSOptions documents for an empty certificate allowlist: needing no further name check is not the same as needing no verification at all, and Authenticate still denies outright whenever there are no kernel-verified credentials to check in the first place (a non-unix connection, or a unix one whose creds were never plumbed into the request context - see PeerCred's doc comment).
type Provider ¶
type Provider interface {
// Scheme returns the URL scheme this provider handles, e.g. "aws-sm".
Scheme() string
// Resolve fetches the current Value for ref. It must return an error that
// satisfies errors.Is(err, ErrNotFound) when the referenced value does not
// exist, so defaults and optional fields can be applied.
Resolve(ctx context.Context, ref Ref) (Value, error)
}
Provider resolves refs of a single scheme into Values. It is the minimum a source must implement. Providers should be safe for concurrent use.
type ProviderError ¶
ProviderError wraps an error from a specific provider resolve, tagging it with the scheme and ref for diagnostics and metrics. It is delivered to OnError for runtime resolve failures.
func (*ProviderError) Error ¶
func (e *ProviderError) Error() string
func (*ProviderError) Unwrap ¶
func (e *ProviderError) Unwrap() error
Unwrap allows errors.Is/As to reach the underlying error (e.g. ErrNotFound).
type Ref ¶
type Ref struct {
// Scheme selects the provider, e.g. "aws-sm", "vault", "env", "file".
Scheme string
// Path is the provider-specific location of the value, e.g. "prod/db",
// "kv/data/api", "/etc/tls/tls.crt", or "LOG_LEVEL".
Path string
// Key selects a single field from a structured payload (the URL fragment,
// i.e. the part after '#'). It is empty when no key is requested.
Key string
// Opts holds provider-specific options parsed from the query string, plus a
// small set of core-recognized options (debounce, optional, version).
Opts url.Values
// Raw is the original, unparsed tag value, retained for error messages.
Raw string
}
Ref is a parsed reference to a value in a provider. It is produced from the `source` struct tag by ParseRef. The general grammar is:
<scheme>://<path>[#<key>][?<opt>=<v>&...]
Opaque schemes such as env: and exec: take everything after the colon as the Path (no "//" authority section):
env:LOG_LEVEL exec:echo hello
func ParseRef ¶
ParseRef parses a `source` tag value into a Ref. It returns an error for an empty tag or a tag without a scheme.
The grammar is scheme-agnostic and, per the mamori spec, places the optional #key fragment BEFORE the optional ?opts query (the reverse of a standard URL). Parsing is therefore done by hand rather than via net/url:
scheme://path[#key][?opts] (hierarchical: aws-sm, vault, file, ...) scheme:path[#key][?opts] (opaque: env, exec)
func ParseRefs ¶ added in v1.2.0
ParseRefs parses a `source` tag that may hold a comma-separated precedence chain of refs, e.g. "env:PORT,aws-ps://svc/port". This is the entry point for chained sources (spec 10.2): the first ref to yield a value wins, so the chain lets a field prefer a fast/local source and fall back to a slower or more authoritative one without the caller writing that fallback logic by hand.
A comma is treated as a separator only when the text after it begins with a scheme-like token (see schemeStart); a comma inside a query option (?tags=a,b) or an opaque exec: path (exec:echo a,b) is preserved as part of that ref. To force a literal comma that would otherwise be read as a separator, percent-encode it as %2C.
Because of that rule, a doubled or trailing comma is not a split point (no scheme token follows it) and is instead kept as part of the adjacent ref's value, e.g. "env:A,,env:B" yields a first ref with path "A," rather than an empty chain entry. Such a malformed ref simply resolves not-found at lookup time and the chain falls through to the next entry, so this is treated as a caller error rather than something ParseRefs rejects outright.
A single-ref tag (the common case) yields a one-element slice, so callers that do not use chains see no behavior change from switching to ParseRefs.
type Report ¶ added in v1.2.0
type Report struct {
Fields []FieldStatus
Snapshot uint64 // version of the snapshot Get currently returns (the pinned version, while Pinned)
Live uint64 // newest validated snapshot; diverges from Snapshot while Pinned
Pinned bool // true when Get is frozen at Snapshot while Live keeps advancing; see Watcher.Pin
Healthy bool // no field is stale or carries a terminal error kind
GeneratedAt time.Time // when this report was built
}
Report is a point-in-time snapshot of a Watcher's health, or the result of a one-shot Doctor probe. Fields are in struct declaration order.
func Doctor ¶ added in v1.2.0
Doctor resolves every field of T exactly once and returns a Report describing what succeeded and what failed, without starting a watcher. It accepts the same Options as Load and Watch, so it exercises the caller's real provider wiring, middleware, and Prefix rewriting: run it in a build-tagged CI test to catch a misconfigured ref before it ships.
The returned error is non-nil only when T itself cannot be walked as a config struct. Individual field failures are recorded in the Report, not returned, so a caller sees every problem at once rather than only the first. Doctor does not decode or validate; a field that resolves but fails validation is Load's concern, not a reachability check's.
Report.Snapshot and Report.Live are always 0, signaling a one-shot probe rather than a running watcher's snapshot (whose version starts at 1).
type Snapshot ¶ added in v1.2.0
type Snapshot[T any] struct { Version uint64 At time.Time Config T Fields []FieldChange // what changed relative to the previous snapshot // contains filtered or unexported fields }
Snapshot is one validated configuration the Watcher applied, retained when WithHistory is enabled. Config is a full copy of T at that version, so enabling history extends the in-memory lifetime of any secret material it holds; that is why history defaults to off.
type StaleError ¶
StaleError is returned/delivered when a value has exceeded the configured WithStale max age without a successful refresh.
func (*StaleError) Error ¶
func (e *StaleError) Error() string
func (*StaleError) Unwrap ¶
func (e *StaleError) Unwrap() error
type Timer ¶
Timer is a Clock-provided one-shot timer. Its channel C receives the time it fires. Stop prevents a not-yet-fired timer from firing.
type Tracer ¶
type Tracer interface {
// StartResolve begins a span for a resolve and returns a derived context plus
// a finish function to be called with the resolve error (nil on success).
StartResolve(ctx context.Context, scheme, ref string) (context.Context, func(err error))
}
Tracer is the minimal tracing sink mamori emits to (see Meter for the no-dep rationale). Pass one with WithTracer.
type Ucred ¶ added in v1.2.0
Ucred is a Unix-socket peer's kernel-verified credentials, as read once by a listener at accept time (SO_PEERCRED on Linux, LOCAL_PEERCRED via GetsockoptXucred on Darwin - see the platform-specific PeerCredFromConn in authpeercred_linux.go / authpeercred_darwin.go). A connection's peer identity cannot change over its lifetime, so reading it once per accepted connection, rather than re-deriving it per request, loses nothing.
func PeerCredFromConn ¶ added in v1.2.0
PeerCredFromConn reads conn's peer credentials via SO_PEERCRED, the Linux kernel's record of the uid/gid/pid of the process on the other end of a Unix-domain-socket connection at the time it was accepted. It is exported for the server module's Unix-socket listener wrapper (a later task; see PeerCred's doc comment in authpeercred.go for the full ConnContext plumbing this feeds): the listener calls this once per accepted connection and passes the result to ContextWithPeerCred.
type Update ¶
type Update struct {
// Value is the new value. Valid only when Err is nil.
Value Value
// Err carries a transient watch error. The channel remains open; mamori
// surfaces the error and keeps the last-good value.
Err error
}
Update is a single event delivered on a WatchableProvider's channel.
type ValidationError ¶
type ValidationError struct {
Err error
}
ValidationError wraps a validation failure. When an updated snapshot fails validation the update is rejected atomically and this error is delivered to OnError; Get continues to return the last valid config.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
func (*ValidationError) Unwrap ¶
func (e *ValidationError) Unwrap() error
type Validator ¶
type Validator interface {
// Validate returns nil if v is valid, or a non-nil error describing the
// failure (which mamori wraps in *ValidationError).
Validate(v any) error
}
Validator validates a fully-decoded config value. It is invoked on the initial load and on every reconciled update. The default implementation wraps go-playground/validator, reading `validate` struct tags. Supply an alternative with WithValidator.
type Value ¶
type Value struct {
// Bytes is the raw resolved payload.
Bytes []byte
// Version is a provider-supplied revision identifier (Secrets Manager
// VersionId, Vault version, a file mtime+size hash, ...). It enables cheap
// change detection: mamori treats a changed Version as a changed value
// without comparing bytes. If empty, mamori falls back to byte comparison.
Version string
// Sensitive marks the value as secret, driving redaction downstream. It is
// set by secret-bearing providers and by the decode layer for
// secret.String / secret.Bytes fields.
Sensitive bool
// NotAfter, when non-zero, is the time at which this value is known to expire
// (e.g. a Vault lease). mamori schedules a refresh before this instant rather
// than waiting for the next poll tick.
NotAfter time.Time
// Metadata carries optional provider-specific annotations. It must never
// contain the secret payload.
Metadata map[string]string
}
Value is what a Provider returns for a resolved Ref. It carries the raw bytes plus the metadata mamori needs for change detection, redaction, and lease-aware refresh.
type WatchableProvider ¶
type WatchableProvider interface {
Provider
// Watch returns a channel of Updates for ref. The channel is closed when the
// watch ends (including on ctx cancellation). Transient errors are delivered
// as Updates with a non-nil Err; channel closure signals termination.
Watch(ctx context.Context, ref Ref) (<-chan Update, error)
}
WatchableProvider is an optional interface for providers with native change notification (Vault leases, Kubernetes informers, Consul blocking queries). Providers without native watch support are wrapped by mamori in a polling adapter - provider authors must never fake a Watch with an internal ticker.
type Watcher ¶
type Watcher[T any] struct { // contains filtered or unexported fields }
Watcher holds the reconciled configuration of type T and manages the background watch goroutines. Obtain one from Watch and always Close it.
func Watch ¶
Watch performs an initial, fail-fast Load of T and then keeps it reconciled at runtime, delivering validated, diff-aware updates to OnChange. It returns after the initial configuration is resolved (OnChange fires only on subsequent changes).
func (*Watcher[T]) AdminAddr ¶ added in v1.2.0
AdminAddr returns the address the admin HTTP server is bound to, or nil if WithAdminHTTP was not passed to Watch. This is the way to discover which port was actually chosen when binding to port 0 (letting the OS pick a free port), for example to register it with service discovery or to point a test client at it.
func (*Watcher[T]) Close ¶
Close cancels all provider watches, drains the callback queue, shuts down the admin HTTP server if one was started, and returns. By the time Close returns, the admin port (if any) is free: Close waits for the server's graceful Shutdown, bounded by adminShutdownGrace, before waiting on wg.
func (*Watcher[T]) Get ¶
func (w *Watcher[T]) Get() T
Get returns the current configuration snapshot. It is lock-free and always returns the last valid configuration (never a partially-updated or validation-failing one).
func (*Watcher[T]) Health ¶ added in v1.2.0
Health returns nil when every field is fresh and no field carries a terminal error kind. It wraps the offending fields in a HealthError otherwise, so a caller can log which fields are broken instead of a bare "unhealthy". Intended for use as a readiness probe.
func (*Watcher[T]) History ¶ added in v1.2.0
History returns the retained snapshots, newest first, always including the current one. With WithHistory(0) (the default) only the current snapshot is returned. It is lock-free: it only ever Load()s the pointer most recently published by the reconciler goroutine and copies out of it, so a concurrent publish from recordSnapshot can never be observed mid-mutation.
func (*Watcher[T]) Pin ¶ added in v1.2.0
Pin freezes Get at the snapshot with the given version and stops applying reconciled updates to Get, though sources keep being watched and Status keeps reporting the diverging Live version underneath. It returns ErrNoSuchSnapshot if that version is not retained; raise WithHistory to pin further back.
func (*Watcher[T]) PinCurrent ¶ added in v1.2.0
PinCurrent freezes Get at whatever snapshot it returns right now and returns that version. Unlike Pin, it always succeeds and needs no retained history, since it pins to the live snapshot rather than looking one up in it.
func (*Watcher[T]) Pinned ¶ added in v1.2.0
Pinned reports whether Get is currently frozen at a pinned snapshot and, if so, which version. It reads the report the reconciler goroutine most recently published, the same lock-free pointer Status reads: while pinned, Report.Snapshot IS the pinned version being served (see engine.buildReport in report.go), so Pinned needs no extra field or control-channel round trip to answer, only Report.Pinned to disambiguate "pinned at version 0" (impossible; versions start at 1) from "not pinned".
func (*Watcher[T]) Status ¶ added in v1.2.0
Status returns a point-in-time report of the watcher's per-field health. It is lock-free: it only ever Load()s the report pointer most recently published by the reconciler goroutine and works on a copy, never touching the engine's maps directly. That matters because the reconciler goroutine may be mid-mutation of those maps on its own goroutine at the exact moment Status is called from a caller's goroutine.
Age and Stale are recomputed against the watcher's clock (not the stored report's build time) so a watcher that has gone quiet does not keep reporting the age it had at the last reconcile.
func (*Watcher[T]) Unpin ¶ added in v1.2.0
func (w *Watcher[T]) Unpin()
Unpin resumes applying reconciled updates to Get: it applies the newest validated snapshot and delivers exactly one Change whose Fields is the accumulated diff of everything that changed while pinned, no matter how many updates were reconciled (and recorded to history) in the meantime. It is a no-op if the watcher is not currently pinned.
Source Files
¶
- adminhttp.go
- auth.go
- authpeercred.go
- authpeercred_linux.go
- authschemes.go
- builtin.go
- builtin_dotenv.go
- builtin_env.go
- builtin_exec.go
- builtin_file.go
- clock.go
- decode.go
- doc.go
- doctor.go
- errors.go
- handler.go
- helpers.go
- history.go
- observ.go
- pin.go
- poll.go
- provider.go
- reconcile.go
- reconciler.go
- ref.go
- registry.go
- report.go
- resolve.go
- status.go
- validator.go
- value.go
- watchref.go
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
mamori
module
|
|
|
examples
|
|
|
basic
command
Command basic demonstrates mamori: loading typed config from the environment and a file, then watching for changes and reacting without a restart.
|
Command basic demonstrates mamori: loading typed config from the environment and a file, then watching for changes and reacting without a restart. |
|
Package mamoritest provides an in-memory, scriptable mamori.Provider for testing application code that consumes mamori.
|
Package mamoritest provides an in-memory, scriptable mamori.Provider for testing application code that consumes mamori. |
|
Package middleware provides composable Provider decorators: Cache, Audit, Failover, RateLimit, and Prefix.
|
Package middleware provides composable Provider decorators: Cache, Audit, Failover, RateLimit, and Prefix. |
|
Package providertest is the mamori provider conformance kit.
|
Package providertest is the mamori provider conformance kit. |
|
Package secret provides wrapper types for sensitive configuration values that redact themselves everywhere a value is normally rendered - String, fmt, encoding/json, and log/slog - so a secret cannot leak through a stray log line or error message.
|
Package secret provides wrapper types for sensitive configuration values that redact themselves everywhere a value is normally rendered - String, fmt, encoding/json, and log/slog - so a secret cannot leak through a stray log line or error message. |
|
tools
|
|
|
reconcilevet
module
|