vaultclient

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 16, 2026 License: MIT Imports: 6 Imported by: 0

README

vaultclient

Resolve the Vault client an adapter talks to — once and shared, or afresh per operation, as the caller chooses.

import "gitlab.com/phpboyscout/go/vaultclient"

src := vaultclient.Ambient()          // VAULT_ADDR and friends, resolved lazily

client, err := src.VaultClient(ctx)

Why it exists

Vault is the provider where the client is the connection prerequisite. There is no separate configuration object to hand a service constructor: a *vaultapi.Client carries the address, the namespace, the token and the retry policy, and every adapter takes that one type.

The rungs

Constructor Builds Use when
FromClient(client) nothing you already built one
FromConfig(cfg) now — NewClient does no I/O you assembled a config
Ambient() once, lazily, shared anything long-lived
PerCall() every call, retaining nothing holding a client between operations would be wrong

Four rungs rather than three, because for Vault a client and a config are both things a caller plausibly already holds, and the two are type-distinct so neither can be confused for the other. All return the same Source.

This module does not guess, but Vault does

Its AWS counterpart refuses to default a region, because guessing one names a key in an account nobody chose. Vault is different: vaultapi.DefaultConfig documents its own default of https://127.0.0.1:8200, overridden by VAULT_ADDR. Adopting a provider's documented default is not guessing, so Ambient takes it as it stands.

What this module adds is a check the SDK makes easy to miss. vaultapi.DefaultConfig reports failure by populating the Error field on the config it returns, not by returning an error — so a caller who does not look gets a config that appears fine and fails at the first request. Every rung here inspects it.

An empty address is refused too, with ErrNoAddress. It cannot arise from Ambient, but a caller assembling a *vaultapi.Config by hand can leave it blank.

Options

Option Effect
WithAddress(a) Overrides the resolved address. Empty strings ignored.
WithNamespace(n) Sets the Enterprise namespace — on an injected client too.
WithBuildTimeout(d) Bounds one attempt (default 30s), per attempt.
WithLifetimeContext(ctx) Scopes an Ambient source's attempts. Ignored by PerCall.
WithLogger(l) DEBUG diagnostics. Off by default.

Logging

Off unless you pass a logger. Records carry the address and how long the build took. A Vault token is never logged, at any level, and nothing is logged above DEBUG — both have tests, including one that sets a token and fails if it appears.

What it costs

Seventeen Vault-graph modules, plus clientlifecycle and errors — both dependency-free. depfootprint_test.go asserts the exact set and fails if a Vault auth-method package appears: pulling in approle would privilege one way of authenticating over the others and drag its dependencies into every consumer.

Specification

org 0003 (P-2, P-3, P-12, P-13, P-14), over org 0002. The state machine beneath Ambient is clientlifecycle.

Licence

MIT. See LICENSE.

Documentation

Overview

Package vaultclient resolves the Vault client an adapter talks to — once and shared, or afresh per operation, as the caller chooses.

Vault is the provider where the client *is* the connection prerequisite. There is no separate configuration object to hand a service constructor: a *vaultapi.Client carries the address, the namespace, the token and the retry policy, and every adapter takes that one type.

Choosing a rung

vaultclient.FromClient(client)  // you built it; used as-is
vaultclient.FromConfig(cfg)     // you configured it; the client is built here
vaultclient.Ambient()           // VAULT_ADDR and friends, resolved once and shared
vaultclient.PerCall()           // the same, resolved afresh every time

Four rungs rather than three, because for Vault a client and a config are both things a caller plausibly already holds, and the two are type-distinct so neither can be confused for the other.

This module does not guess, but Vault does

Its AWS counterpart refuses to default a region, because guessing one names a key in an account nobody chose. Vault is different: vaultapi.DefaultConfig documents its own default of https://127.0.0.1:8200, overridden by VAULT_ADDR. Adopting a provider's documented default is not guessing, so Ambient takes it as it stands.

What this module does add is a check the SDK makes easy to miss. vaultapi.DefaultConfig reports failure by populating the Error field on the config it returns rather than by returning an error, so a caller who does not look gets a config that appears fine and fails later. Every rung here inspects it.

Sharing is explicit

Two adapters that each call Ambient get two independent sources. A caller who wants one client builds one Source and hands it to each adapter.

Specified by org 0003 (P-2, P-3, P-12, P-13, P-14), which applies org 0002's connection lifecycle across the estate.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoClient reports a nil client or config supplied to an injecting rung.
	ErrNoClient = errors.NewSentinel("vaultclient.no_client",
		"no Vault client or config supplied; pass one, or use Ambient to read VAULT_ADDR")

	// ErrNoAddress reports a config that names no Vault address.
	//
	// It cannot arise from [Ambient], which inherits Vault's own documented
	// default, but a caller assembling a *vaultapi.Config by hand can leave it
	// empty — and an empty address fails at the first request rather than here.
	ErrNoAddress = errors.NewSentinel("vaultclient.no_address",
		"no Vault address configured; set Address on the config, pass WithAddress, or set VAULT_ADDR")
)

Functions

This section is empty.

Types

type Option

type Option func(*options)

Option configures a source.

func WithAddress

func WithAddress(address string) Option

WithAddress overrides the Vault address the ambient config resolved. An empty string is ignored, so an unset flag cannot clear an address another option set.

func WithBuildTimeout

func WithBuildTimeout(d time.Duration) Option

WithBuildTimeout bounds a single build attempt. Per attempt, not a total budget: a failure caches nothing, so a later call gets a fresh allowance.

func WithLifetimeContext

func WithLifetimeContext(ctx context.Context) Option

WithLifetimeContext scopes an Ambient source's build attempts to the life of whatever owns it. PerCall ignores it, being already scoped to its caller.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger enables DEBUG diagnostics. nil disables them, which is the default.

Records carry only non-secret identifiers — the address and how long the build took. A Vault token is never logged, at any level, and nothing is logged above DEBUG.

func WithNamespace

func WithNamespace(namespace string) Option

WithNamespace sets the Vault Enterprise namespace on the built client. An empty string is ignored.

type Source

type Source interface {
	// VaultClient returns the client, building it if the source's strategy says
	// to. It is safe for concurrent use.
	VaultClient(ctx context.Context) (*vaultapi.Client, error)
}

Source yields the Vault client an adapter talks to.

The method is named for what it returns rather than a generic Get, so a call site reads as what it is and two providers' sources are not accidentally interchangeable.

func Ambient

func Ambient(opts ...Option) Source

Ambient returns a source that builds its client from the ambient environment — VAULT_ADDR, VAULT_TOKEN, VAULT_NAMESPACE and the rest — the first time it is asked, and shares the result.

It returns immediately and performs no I/O.

func FromClient

func FromClient(client *vaultapi.Client, opts ...Option) (Source, error)

FromClient returns a source over a client the caller already built.

The client is used as-is and nothing is resolved, so this rung has no build policy to apply.

func FromConfig

func FromConfig(cfg *vaultapi.Config, opts ...Option) (Source, error)

FromConfig returns a source over a config the caller assembled, building the client here.

vaultapi.NewClient does no I/O, so this rung never runs the lazy build either — the client is constructed now and any error returned now.

Example

ExampleFromConfig shows the config rung. NewClient does no I/O, so the client is built now and any error reported now.

package main

import (
	"context"
	"fmt"

	vaultapi "github.com/hashicorp/vault/api"
	"gitlab.com/phpboyscout/go/vaultclient"
)

func main() {
	src, err := vaultclient.FromConfig(&vaultapi.Config{Address: "https://vault.example:8200"})
	if err != nil {
		fmt.Println("construct:", err)

		return
	}

	client, err := src.VaultClient(context.Background())
	if err != nil {
		fmt.Println("resolve:", err)

		return
	}

	fmt.Println(client.Address())
}
Output:
https://vault.example:8200
Example (ConfigError)

ExampleFromConfig_configError shows the check the SDK makes easy to miss: DefaultConfig reports failure by populating Error rather than returning one, so a config that looks fine can already be broken.

package main

import (
	"errors"
	"fmt"

	vaultapi "github.com/hashicorp/vault/api"
	"gitlab.com/phpboyscout/go/vaultclient"
)

func main() {
	cfg := &vaultapi.Config{Address: "https://vault.example:8200"}
	cfg.Error = errors.New("VAULT_MAX_RETRIES is not a number")

	_, err := vaultclient.FromConfig(cfg)
	fmt.Println(err)
}
Output:
reading the Vault configuration: VAULT_MAX_RETRIES is not a number
Example (NoAddress)

ExampleFromConfig_noAddress shows the other refusal.

package main

import (
	"errors"
	"fmt"

	vaultapi "github.com/hashicorp/vault/api"
	"gitlab.com/phpboyscout/go/vaultclient"
)

func main() {
	_, err := vaultclient.FromConfig(&vaultapi.Config{})
	fmt.Println(errors.Is(err, vaultclient.ErrNoAddress))
}
Output:
true

func PerCall

func PerCall(opts ...Option) Source

PerCall returns a source that builds afresh on every call and retains nothing.

Concurrent calls are deliberately not collapsed: sharing one client between callers who asked not to share one is the thing this rung exists to avoid.

Jump to

Keyboard shortcuts

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