mamori

package module
v1.2.4 Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2026 License: MIT Imports: 30 Imported by: 0

README

守り  mamori

Typed, watchable config & secrets for Go

Load configuration and secrets from anywhere into validated Go structs - and keep them reconciled at runtime, without a restart.

Go Reference CI Go Report Card


mamori (守り - Japanese for protection / safeguard) is an embedded Go library that loads application configuration and secrets from heterogeneous sources - environment, files, AWS Secrets Manager, Vault, GCP, Azure, Kubernetes, Consul, and more - into typed, validated 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 hands your application a diff-aware callback so it can react (rotate a DB pool, rebuild a client) without restarting.

Think: External Secrets Operator's provider model, one layer down - as a library inside your process instead of an operator inside your cluster.

Why

The primitives exist, but nobody composed them. runtimevar watches one variable but has no struct composition or validation. Viper/koanf do multi-source config but treat secrets and rotation as afterthoughts. The AWS caching client and Vault's LifetimeWatcher refresh one provider each, in silos. So every production Go service hand-rolls a ConfigManager with a ticker, a mutex, and a prayer. mamori is that glue, done once, with a provider ecosystem and a conformance kit.

Install

go get github.com/xavidop/mamori

env: and file:// work out of the box. Cloud providers are separate modules so the core has zero cloud-SDK dependencies:

go get github.com/xavidop/mamori/providers/aws     # aws-sm://  aws-ps://
go get github.com/xavidop/mamori/providers/vault   # vault://
go get github.com/xavidop/mamori/providers/k8s     # k8s-secret://  k8s-cm://
# ... gcp, azure, consul, doppler, onepassword, sops

Quick start

type Config struct {
    // A secret string from AWS Secrets Manager (redacted in logs by default)
    DBPassword secret.String `source:"aws-sm://prod/db#password"`

    // Plain config from the environment, with a default and validation
    LogLevel   string        `source:"env:LOG_LEVEL" default:"info" validate:"oneof=debug info warn error"`
    Workers    int           `source:"env:WORKERS"   default:"4"    validate:"gte=1,lte=256"`

    // A precedence chain: an environment override wins if set, otherwise a
    // centrally managed Parameter Store value, otherwise the default.
    Port       string        `source:"env:PORT,aws-ps://svc/port" default:"8080"`

    // A file-backed value, hot-reloaded via fsnotify
    TLSCert    []byte        `source:"file:///etc/tls/tls.crt"`

    // A nested struct decoded from one JSON secret
    Redis      RedisConfig   `source:"aws-sm://prod/redis" flatten:"json"`
}

// One-shot load
cfg, err := mamori.Load[Config](ctx)

// Or: watch and reconcile at runtime
w, err := mamori.Watch[Config](ctx,
    mamori.OnChange(func(ev mamori.Change[Config]) {
        if ev.Changed("DBPassword") {
            pool.Rotate(ev.New.DBPassword.Reveal())
        }
    }),
    mamori.OnError(func(err error) { metrics.Inc("config_error") }),
)
defer w.Close()

cfg := w.Get() // lock-free snapshot; always the last *valid* config

What makes it different

  • Typed & tag-driven - one struct, multiple sources, generics API (Load[T] / Watch[T]).
  • Precedence chains - source:"env:PORT,aws-ps://svc/port" tries sources in priority order: the first to yield a value wins, not-found falls through to the next, and a real error stops the walk and applies the field's onfail policy instead of silently sliding to a lower-priority source. Every position is watched, so precedence is live.
  • Reconciled at runtime - native watch where the backend supports it (Kubernetes informers, Consul blocking queries, fsnotify), polling with jitter everywhere else, and lease-aware pre-expiry refresh for Vault.
  • Atomic & validated - an update that fails validation is rejected; Get() keeps returning the last good config. Config never enters a broken state mid-flight.
  • Coalesced - bursts of field changes within a debounce window produce a single Change event.
  • Pinnable - WithHistory(n) retains recent snapshots (w.History()); w.PinCurrent() / w.Pin(version) freeze Get() at one of them while you debug production, then w.Unpin() resumes and fires one coalesced Change for everything that changed in the meantime.
  • Secret hygiene by default - secret.String / secret.Bytes redact themselves in String(), fmt, MarshalJSON, and slog. Only the explicit, greppable Reveal() exposes the value. A shipped analyzer (run it as mamori vet ./..., or as a go vet tool with go vet -vettool=$(which mamori)) flags sensitive refs assigned to plain string fields.
  • Pluggable - providers register with the database/sql pattern; a providertest conformance kit guarantees they all behave identically.
  • Observable - w.Status() reports live per-field health, w.Health() backs a Kubernetes readiness probe, and mamori.Doctor[T] checks every ref is reachable before you ever deploy.
  • Testable - the mamoritest package gives application code a scriptable in-memory provider (Set/Del/Fail) plus deterministic wait helpers (WaitForSnapshot, WaitForError), so an OnChange handler or error path can be tested without a real backend.

Providers

Module Schemes Watch Errors classified beyond not-found
core (built-in) env: · dotenv:// · file:// · exec: (opt-in) fsnotify (file/dotenv) · poll (env/exec)
providers/aws aws-sm:// · aws-ps:// poll
providers/gcp gcp-sm:// poll
providers/azure azure-kv:// poll
providers/vault vault:// lease-aware poll (NotAfter)
providers/k8s k8s-secret:// · k8s-cm:// native (watch API)
providers/consul consul:// native (blocking queries)
providers/doppler doppler:// poll
providers/onepassword op:// poll
providers/sops sops:// fsnotify
providers/postgres postgres:// native (LISTEN/NOTIFY)
providers/mysql mysql:// poll
providers/sqlite sqlite:// fsnotify
providers/mongodb mongodb:// native (change streams)
providers/dynamodb dynamodb:// poll
providers/redis redis:// native (keyspace notifications)
providers/etcd etcd:// native (watch API)
providers/firestore firestore:// native (snapshot listeners)
providers/firebase-rc firebase-rc:// poll
providers/firebase-rtdb firebase-rtdb:// native (streaming) no (chain preserved)
providers/s3 s3:// poll (ETag)
providers/gcs gcs:// poll (generation)
providers/azblob azblob:// poll (ETag)
providers/cosmos cosmos:// poll (ETag)
providers/launchdarkly launchdarkly:// native (streaming)
providers/unleash unleash:// poll n/a (no error surface)
providers/flagsmith flagsmith:// poll no (chain preserved)
providers/configcat configcat:// poll n/a (no error surface)
providers/split split:// poll n/a (no error surface)
providers/growthbook growthbook:// poll no (chain preserved)
providers/flipt flipt:// poll
providers/goff goff:// (GO Feature Flag) poll

Every provider that passes the providertest conformance kit earns a badge. See each module's README for auth and ref grammar.

The error-classification sweep is complete: every one of the 35 providers now falls into exactly one of three honest states, and not_found itself is detected by every provider regardless of which one.

  • ✅ classifies - the provider maps real backend errors onto mamori.ErrorKind values beyond not_found (permission_denied, unauthenticated, unavailable, rate_limited, invalid, as the backend's own vocabulary supports): twenty-nine providers across twenty-six module rows, since core's single row covers four built-in providers (env:, dotenv://, file://, exec:).
  • no (chain preserved) - providers/firebase-rtdb, providers/growthbook, and providers/flagsmith have no backend-specific error vocabulary to map, so a non-not-found failure still reports unknown. Their Resolve wraps the underlying error with %w rather than flattening it, so errors.Is/errors.As and any mamori sentinel injected by a caller's own middleware still reach it - the chain is preserved even though nothing here narrows it to a more specific kind. Do not read this as classifying permission or availability errors these providers cannot see.
  • n/a (no error surface) - providers/unleash, providers/configcat, and providers/split wrap SDK client surfaces that return only bool/string values, with no per-key error at all; their Resolve can only ever produce mamori.ErrNotFound or a client-construction error, so there is nothing to classify or preserve a chain for. Each is explicitly exempted from the providertest conformance kit's ErrorClassification case via providertest.Config.NoResolveErrors, a deliberate, greppable declaration rather than a silent gap.

Middleware

Providers compose because they share one interface:

mamori.WithProvider(
    middleware.Cache(5*time.Minute,
        middleware.Audit(logger,
            middleware.Failover(primary, replica))))

Cache, Audit, Failover, RateLimit, and Prefix (multi-tenant namespace rewriting) ship in middleware/.

Observability

w.Status() returns a lock-free, point-in-time Report of every field's health (ref, staleness, last error kind), safe to log or serialize since values never appear and refs have sensitive query options redacted. w.Health() reduces that to a single readiness check: nil when every field is fresh and none carries a terminal error kind (not_found, permission_denied, unauthenticated, invalid), a *HealthError otherwise - a transient kind like unavailable or rate_limited only fails health once the field is also stale.

For a pre-deploy check, mamori.Doctor[Config](ctx, opts...) resolves every field once without starting a watcher and reports every failure at once, not just the first - run it as a build-tagged CI test to catch a rotated-away secret or a typo'd ref before it ships. An optional HTTP endpoint - mamori.Handler on your own mux, or a self-hosted server via mamori.WithAdminHTTP - serves that same Report as JSON, metadata only and never a configuration value, with a pluggable Authenticator (WithAuth; basic auth, bearer token, API key, mTLS, or your own) gating access and support for live credential rotation. See Observability and Auth for the full picture, including the readiness-probe pattern, the Doctor CI test, and credential rotation.

Config server

server/ is a separate module: a standalone process that fronts a fixed, operator-declared table of name-to-ref bindings (server.Bind/server.BindFile - never a client-supplied ref) and serves resolved values to authenticated, authorized callers over Unix sockets and TLS TCP, under a mandatory Policy and Authenticator. It reuses the same Authenticator/Identity as the admin endpoint above, plus a Unix-socket-only PeerCred scheme authenticated by kernel-verified uid/gid. It is deliberately the highest-blast-radius component in this project - it concentrates every backend credential its bindings touch into one process - so read the docs before deploying one, not just the quick start.

CLI

cmd/mamori is a standalone CLI with two halves that never mix: explain/schema/policy statically read your Go source and never resolve anything (struct field tables, JSON Schema, least-privilege IAM/GCP/ExternalSecret artifacts), while doctor/status are thin clients of a running process's admin endpoint (WithAdminHTTP above), exiting 0-4 so a script can tell a broken config apart from one it merely couldn't reach.

brew install xavidop/tap/mamori
# or
go install github.com/xavidop/mamori/cmd/mamori@latest

See mamorigo.dev/docs/cli for the full command reference.

Agent skill

Teach your AI coding agent (Claude Code, Cursor, Copilot, and others) how to use mamori with the shipped Agent Skill:

npx skills add xavidop/mamori

See mamorigo.dev/docs/skill for what it covers and manual install.

Documentation

Project layout

This is a multi-module monorepo. The core (github.com/xavidop/mamori) depends only on go-playground/validator, go-viper/mapstructure, and fsnotify. Each provider is its own module with its own release cadence, so a cloud SDK never leaks into your build unless you use that provider.

Contributing

Contributions welcome - new providers especially. See CONTRIBUTING.md and the Write a provider guide. A provider that passes providertest and the conformance kit gets listed here.

License

MIT © Xavier Portilla Edo

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

Constants

This section is empty.

Variables

View Source
var (
	ErrPermissionDenied = errors.New("mamori: permission denied")
	ErrUnauthenticated  = errors.New("mamori: unauthenticated")
	ErrUnavailable      = errors.New("mamori: unavailable")
	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.

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

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

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

func ContextWithPeerCred(ctx context.Context, cred Ucred) context.Context

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

func Load[T any](ctx context.Context, opts ...Option) (T, error)

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

func SelectKey(data []byte, key string) ([]byte, error)

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

func SentinelFor(k Kind) error

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

func VersionHash(b []byte) string

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

func WatchRef(ctx context.Context, p Provider, ref Ref, opts ...Option) <-chan Update

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 AuthFunc added in v1.2.0

type AuthFunc func(r *http.Request) (Identity, error)

AuthFunc adapts a plain function to Authenticator.

func (AuthFunc) Authenticate added in v1.2.0

func (f AuthFunc) Authenticate(r *http.Request) (Identity, error)

Authenticate calls f.

type Authenticator added in v1.2.0

type Authenticator interface {
	Authenticate(r *http.Request) (Identity, error)
}

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.

func (Change[T]) Changed

func (c Change[T]) Changed(path string) bool

Changed reports whether the field at the given dotted path is among the changed fields in this event.

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.

func SystemClock

func SystemClock() Clock

SystemClock returns the default, real-time Clock.

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

func NewFakeClock(start time.Time) *FakeClock

NewFakeClock returns a FakeClock started at the given time (or a fixed epoch if start is the zero value).

func (*FakeClock) Advance

func (c *FakeClock) Advance(d time.Duration)

Advance moves the fake clock forward by d, firing any timers and tickers whose deadlines fall within the interval, in chronological order.

func (*FakeClock) After

func (c *FakeClock) After(d time.Duration) <-chan time.Time

After registers a timer and returns its channel.

func (*FakeClock) NewTicker

func (c *FakeClock) NewTicker(d time.Duration) *Ticker

NewTicker registers a periodic ticker.

func (*FakeClock) NewTimer

func (c *FakeClock) NewTimer(d time.Duration) *Timer

NewTimer registers a one-shot timer.

func (*FakeClock) Now

func (c *FakeClock) Now() time.Time

Now returns the fake current time.

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

type Identity struct {
	Subject string
	Attrs   map[string][]string
}

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"
	// KindUnavailable means the backend could not be reached or did not respond,
	// 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

func ErrorKind(err error) Kind

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

func OnChange[T any](fn func(Change[T])) Option

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 OnError

func OnError(fn func(error)) Option

OnError installs a callback for runtime resolve/validation/stale errors.

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

func WithAdminTLS(cfg *tls.Config) Option

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

func WithBackoff(base, max time.Duration) Option

WithBackoff configures per-ref exponential backoff on resolve failure.

func WithClock

func WithClock(c Clock) Option

WithClock overrides the clock, primarily for deterministic tests.

func WithDebounce

func WithDebounce(d time.Duration) Option

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

func WithHistory(n int) Option

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

func WithJitter(f float64) Option

WithJitter sets the poll jitter fraction (0..1); a value of 0.2 randomizes each interval by ±20% to avoid thundering herds.

func WithMeter

func WithMeter(m Meter) Option

WithMeter installs a metrics sink (see the x/otel module for an OTel adapter).

func WithPollInterval

func WithPollInterval(d time.Duration) Option

WithPollInterval sets the fallback poll interval for non-watchable providers.

func WithProvider

func WithProvider(p Provider) Option

WithProvider registers a provider for this call only, taking precedence over the global registry for its scheme.

func WithQueueDepth

func WithQueueDepth(n int) Option

WithQueueDepth bounds the OnChange dispatch queue; when full, the oldest event is dropped (default 16).

func WithStale

func WithStale(maxAge time.Duration) Option

WithStale escalates staleness to a hard error: if a ref cannot be refreshed for longer than maxAge, OnError receives a *StaleError.

func WithTracer

func WithTracer(t Tracer) Option

WithTracer installs a tracing sink (see the x/otel module for an OTel adapter).

func WithValidator

func WithValidator(v Validator) Option

WithValidator overrides the default (go-playground/validator) validator.

type PeerCredOptions added in v1.2.0

type PeerCredOptions struct {
	UIDs []int
	GIDs []int
}

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

type ProviderError struct {
	Scheme string
	Ref    string
	Err    error
}

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

func ParseRef(tag string) (Ref, error)

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

func ParseRefs(tag string) ([]Ref, error)

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.

func (Ref) Opt

func (r Ref) Opt(name string) string

Opt returns the first value for the named option, or "" if unset.

func (Ref) String

func (r Ref) String() string

String renders the Ref back into its canonical tag form. Query options are omitted if empty. It is primarily useful for diagnostics.

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

func Doctor[T any](ctx context.Context, opts ...Option) (Report, error)

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

type StaleError struct {
	Ref string
	Err error
}

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 Ticker

type Ticker struct {
	C <-chan time.Time
	// contains filtered or unexported fields
}

Ticker is a Clock-provided periodic ticker. Its channel C receives ticks.

func (*Ticker) Stop

func (t *Ticker) Stop()

Stop halts the ticker. It does not close C.

type Timer

type Timer struct {
	C <-chan time.Time
	// contains filtered or unexported fields
}

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.

func (*Timer) Stop

func (t *Timer) Stop() bool

Stop halts the timer, returning true if it had not yet fired.

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

type Ucred struct {
	UID int
	GID int
	PID int
}

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

func PeerCredFromConn(conn net.Conn) (Ucred, error)

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

func Watch[T any](ctx context.Context, opts ...Option) (*Watcher[T], error)

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

func (w *Watcher[T]) AdminAddr() net.Addr

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

func (w *Watcher[T]) Close() error

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

func (w *Watcher[T]) Health() error

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

func (w *Watcher[T]) History() []Snapshot[T]

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

func (w *Watcher[T]) Pin(version uint64) error

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

func (w *Watcher[T]) PinCurrent() uint64

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

func (w *Watcher[T]) Pinned() (uint64, bool)

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

func (w *Watcher[T]) Status() Report

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.

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

Jump to

Keyboard shortcuts

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