Documentation
¶
Overview ¶
Package configazureappconfig contributes configuration from an Azure App Configuration 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 key-filter prefix scopes the backend to one namespace of the store; the keys beneath it, split on "/", become the layer's nested tree. App Configuration stores a string value per setting, 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-azure-appconfig spec, D3.
A setting's identity is (key, label), not key alone. WithLabel scopes a backend to one label, which both bounds the read and pins the write; omitted, the backend uses the store's no-label default. Composing several labels is a consumer concern: stack two backends as two layers and let the Store's precedence and per-key merge compose them (spec D2).
The service is reached through the narrow Store interface, which Wrap adapts from a configured *azappconfig.Client. Injecting the client keeps every credential, endpoint and connection-string decision with the consumer and lets the whole unit suite run against a fake, needing no Azure account.
Index ¶
- Variables
- func FromClient(client *azappconfig.Client, prefix string, opts ...Option) config.Backend
- func FromConnectionString(conn, prefix string, opts ...Option) (config.Backend, error)
- func FromCredential(cred azcore.TokenCredential, endpoint, prefix string, opts ...Option) (config.Backend, error)
- func New(store Store, prefix string, opts ...Option) config.Backend
- type Option
- type Setting
- type Store
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoCredential reports a nil credential supplied to [FromCredential]. // // It covers the TYPED nil too: a nil *azidentity.DefaultAzureCredential // carried in an [azcore.TokenCredential] interface compares unequal to nil, // which is what a dropped constructor error leaves you holding, and without // this guard it would panic at the first request. ErrNoCredential = errors.NewSentinel("configazureappconfig.no_credential", "no Azure credential supplied; build one with azidentity, or use the ambient rung") // ErrNoEndpoint reports a rung called with no store endpoint. // // Unlike an AWS region there is nothing ambient to fall back on: an Azure // credential names a principal and carries no endpoint, so the store must be // named explicitly however the credential was obtained. ErrNoEndpoint = errors.NewSentinel("configazureappconfig.no_endpoint", "no App Configuration endpoint supplied; pass https://<name>.azconfig.io") // ErrNoConnectionString reports an empty connection string supplied to // [FromConnectionString]. ErrNoConnectionString = errors.NewSentinel("configazureappconfig.no_connection_string", "no connection string supplied; read it from the store's access keys") )
Functions ¶
func FromClient ¶
FromClient is the common path: New over a Wrap-ped App Configuration client, so a consumer writes configazureappconfig.FromClient(client, "app/") without touching the narrow interface.
Example ¶
ExampleFromClient reads configuration from an Azure App Configuration store. You build and configure the client — here from a connection string, though a managed identity or service principal via azidentity is the common path — and hand it in with a prefix that scopes and is stripped from the keys, so app/server/port in the store reads back as server.port. The credential is yours: azidentity is your dependency, not this module's.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2"
"gitlab.com/phpboyscout/go/config"
configazureappconfig "gitlab.com/phpboyscout/go/config-azure-appconfig"
)
func main() {
client, err := azappconfig.NewClientFromConnectionString(os.Getenv("APPCONFIG_CONNECTION_STRING"), nil)
if err != nil {
log.Fatal(err)
}
store, err := config.NewStore(context.Background(),
config.WithBackend(configazureappconfig.FromClient(client, "app/")),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(store.View().GetInt("server.port"))
}
Output:
func FromConnectionString ¶ added in v0.3.0
FromConnectionString builds the client from one of the store's access keys.
It sits alongside FromCredential rather than beneath it because App Configuration genuinely has two injection shapes: a connection string carries the endpoint AND the secret together, where a credential names a principal and needs the endpoint supplied separately. Neither is a special case of the other.
The connection string is a SECRET — it embeds the access key. It is never logged here, and a caller should treat it as they would a password: out of source control, out of process listings, and read from wherever their other secrets live.
func FromCredential ¶ added in v0.3.0
func FromCredential( cred azcore.TokenCredential, endpoint, prefix string, opts ...Option, ) (config.Backend, error)
FromCredential builds the App Configuration client from a credential the caller holds and returns a backend over the prefix.
azappconfig.NewClient does no I/O — it errors only on a malformed endpoint — so this rung constructs eagerly and reaching the store stays deferred to Load, which has a context to bound it.
Where the ambient rung is ¶
Deliberately NOT here. Resolving the ambient Azure identity chain costs this module seven further dependencies, and charging every consumer for an identity graph they may never use would spend the dependency-footprint guarantee this adapter states in its own test. It lives in the ambient subpackage instead:
import acambient "gitlab.com/phpboyscout/go/config-azure-appconfig/ambient" b, err := acambient.Default(ctx, "https://my-store.azconfig.io", "app/")
To share ONE resolved credential across several adapters, resolve it with go/azureclient and pass it here (spec 0012 L-4, L-5).
Types ¶
type Option ¶
type Option func(*backend)
Option configures a backend.
func WithLabel ¶
WithLabel scopes the backend to label: the read lists only settings with that label, and a write pins new settings to it. Omitted, the backend uses the store's no-label default. One label per backend instance — composing several is a consumer concern, done by stacking backends as layers (spec D2 / Resolved §1).
func WithSentinelKey ¶
WithSentinelKey makes Watch poll a single sentinel setting by conditional GET on its ETag: App Configuration returns 304 while the sentinel is unchanged and the new setting when it moves, at which point onChange fires and the Store re-reads. This is Azure's own recommended refresh mechanism and keeps the steady-state cost to one conditional request per interval. Omitted, Watch falls back to re-listing the whole prefix each interval and firing when any setting's ETag changed — correct, but heavier (spec D7).
func WithValueCodec ¶
WithValueCodec decodes each setting's 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. A setting's declared content type is never used to pick a codec (spec D3 / Resolved §3); a Key Vault reference is always left as an opaque string, codec or no codec.
Writes always target flat settings; writing into a value that a codec decoded from a blob is not supported and lands as a sibling flat setting.
type Setting ¶
type Setting struct {
Key string
Label string
Value string
ContentType string
ETag string // opaque; azcore.ETag rendered to string
}
Setting is one App Configuration setting: its identity (key, label), its string value, its declared content type, and the ETag the write and watch paths compare against.
type Store ¶
type Store interface {
// List returns every setting whose key matches keyFilter and whose label is
// label, walking the SDK pager to completion, each with the ETag identifying
// its state at this read. keyFilter is App Configuration's server-side filter
// (e.g. "app/*"); label is the exact label ("" for the store's no-label
// default).
List(ctx context.Context, keyFilter, label string) ([]Setting, error)
// Get fetches one setting. With a non-empty sinceETag it reports
// changed=false when the setting is unchanged since sinceETag (the App
// Configuration conditional-GET / 304 path); with an empty sinceETag it is an
// unconditional read reporting changed=true. An absent setting reports
// changed=false with a zero Setting and a nil error. Used by [Verify]
// (unconditional) and the sentinel-key watch (conditional, spec D7).
Get(ctx context.Context, key, label, sinceETag string) (s Setting, changed bool, err error)
// Set writes value at (key, label). etag empty means create-if-absent; a
// non-empty etag means overwrite only if the store's ETag still matches
// (If-Match). ok is false, with a nil error, when the ETag no longer matches —
// the setting moved since Load.
Set(ctx context.Context, key, label, value, contentType, etag string) (ok bool, err error)
// Delete removes (key, label) only if the store's ETag still matches. ok is
// false, with a nil error, when it moved.
Delete(ctx context.Context, key, label, etag string) (ok bool, err error)
}
Store is the slice of App Configuration this adapter uses, behind an interface it owns so a fake drives the unit suite and the real client is adapted by Wrap.
func Wrap ¶
func Wrap(client *azappconfig.Client) Store
Wrap adapts a configured App Configuration SDK client to the narrow Store interface. The client carries every credential, endpoint and connection-string decision; Wrap adds none — it only translates calls. It never touches azidentity: building the credential is the consumer's job (spec D6).