Documentation
¶
Overview ¶
Package configconsul contributes configuration from a HashiCorp Consul key-value store as a first-class config layer: precedence, per-key merge, provenance, safe hot-reload and compare-and-swap writes, exactly like a file layer.
A prefix scopes the backend to one namespace of the KV store; the keys beneath it, split on "/", become the layer's nested tree. Consul stores bytes, so a value is a scalar string by default and the View's typed accessors coerce it (GetInt("server.port") parses "8080"). A value that is itself a JSON or YAML document is decoded into a subtree when a codec is supplied with WithValueCodec — see the config-consul spec, D3.
The service is reached through the narrow KV interface, which Wrap adapts from a configured *api.Client. Injecting the client keeps every credential, address and TLS decision with the consumer and lets the whole unit suite run against a fake, needing no Consul.
Index ¶
- Variables
- func Default(prefix string, opts ...Option) (config.Backend, error)
- func FromClient(client *capi.Client, prefix string, opts ...Option) config.Backend
- func FromConfig(cfg *capi.Config, prefix string, opts ...Option) (config.Backend, error)
- func New(kv KV, prefix string, opts ...Option) config.Backend
- type KV
- type Op
- type Option
- type Pair
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNoConfig = errors.NewSentinel("configconsul.no_config",
"no Consul config supplied; pass one, or use Default to read the ambient environment")
ErrNoConfig reports a nil config supplied to FromConfig.
Functions ¶
func Default ¶ added in v0.3.0
Default builds a backend from the ambient Consul environment — CONSUL_HTTP_ADDR, CONSUL_HTTP_TOKEN, CONSUL_HTTP_SSL and the rest — as capi.DefaultConfig reads them.
This is the zero-conf rung: the casual consumer who wants their tool to read configuration from the Consul the rest of their tooling already talks to, without wiring a client first.
The defaults are Consul's own, not this adapter's. With nothing set, DefaultConfig resolves 127.0.0.1:8500, which is what every other Consul client on the machine does — adopting a provider's documented default is not the same act as inventing one, which is why config-etcd has no rung here at all (spec 0012 R1).
Nothing is contacted at construction; the first Load reaches the agent, and an unreachable one is an ordinary error from there.
The token is read once, and this rung does not renew it ¶
capi.NewClient resolves the ACL token a single time and copies it onto the client, where every request reads it. Nothing re-reads the environment afterwards — and CONSUL_HTTP_TOKEN_FILE is read once too, at construction, not per request. The SDK's own comment on that path says derived clients are expected to re-read the file; a backend built here never becomes a derived client.
Consul ACL tokens may carry an expiry (capi.ACLToken has ExpirationTTL and ExpirationTime). Where one does, a long-lived process reloading configuration fails every Load once it lapses, with no path back — the backend cannot learn its credential expired, and re-reading a rotated token file is exactly what it does not do.
That is fine for a command. For a long-lived process with an expiring token, build the client yourself and rebuild it when the token rotates, then use FromClient. A token with no expiry set is unaffected.
func FromClient ¶
FromClient is the common path: New over a Wrap-ped Consul client, so a consumer writes configconsul.FromClient(client, "app/") without touching the narrow interface.
Example ¶
ExampleFromClient reads configuration from a Consul KV store. You build and configure the Consul client — address, token, TLS all stay yours — and hand it in with a prefix that scopes and is stripped from the keys, so app/server/port in Consul reads back as server.port.
package main
import (
"context"
"fmt"
"log"
capi "github.com/hashicorp/consul/api/v2"
"gitlab.com/phpboyscout/go/config"
configconsul "gitlab.com/phpboyscout/go/config-consul"
)
func main() {
client, err := capi.NewClient(capi.DefaultConfig())
if err != nil {
log.Fatal(err)
}
store, err := config.NewStore(context.Background(),
config.WithBackend(configconsul.FromClient(client, "app/")),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(store.View().GetInt("server.port"))
}
Output:
func FromConfig ¶ added in v0.3.0
FromConfig builds the Consul client from a config the caller assembled — a specific address, datacenter, token or TLS setup — and returns a backend over it.
capi.NewClient does no network I/O: it validates the config and constructs an HTTP client, so a failure here is a malformed configuration rather than an unreachable Consul. Reaching the agent is deferred to Load, which has a context to bound it.
Types ¶
type KV ¶
type KV interface {
// List returns every pair under prefix and the index identifying that read.
// The index is what the write and watch paths compare against, so it must be
// the one Consul reports for the read, not a fresh fetch.
List(ctx context.Context, prefix string) (pairs []Pair, index uint64, err error)
// Watch blocks until prefix changes past index or wait elapses, returning the
// new index. A timeout with no change returns the same index and a nil error.
Watch(ctx context.Context, prefix string, index uint64, wait time.Duration) (uint64, error)
// Commit applies ops as one atomic compare-and-swap transaction. ok is false,
// with a nil error, when the CAS failed because a key moved since Load.
Commit(ctx context.Context, ops []Op) (ok bool, err error)
}
KV is the slice of Consul this adapter uses, behind an interface it owns so a fake drives the unit suite and the real client is adapted by Wrap.
type Op ¶
Op is one write in a commit: a set or a delete of a full Consul key, guarded by the ModifyIndex the key had at Load. Index zero requires the key be absent — a create — so a stale write is rejected rather than overwriting a change.
type Option ¶
type Option func(*backend)
Option configures a backend.
func WithValueCodec ¶
WithValueCodec decodes each Consul value through codec: a value that decodes to a mapping becomes a subtree at its key's path, and a value the codec rejects — a bare scalar, or bytes that are not a document — stays a scalar string, so a prefix mixing flat keys and object blobs reads correctly. Omitted, every value is a scalar string.
codec is any config.Codec — the interface the sibling format adapters implement — so a JSON-blob store is read with configjson.Codec{} and a YAML-blob store with the core's, and this module takes no codec dependency of its own.
Writes always target flat Consul keys; writing into a value that a codec decoded from a blob is not supported and lands as a sibling flat key.
type Pair ¶
type Pair struct {
// Key is the full Consul key, prefix included.
Key string
// Value is the raw bytes Consul holds.
Value []byte
// ModifyIndex is the key's version, for compare-and-swap on write.
ModifyIndex uint64
}
Pair is one Consul key/value, with the modify index the write path uses for compare-and-swap.