Documentation
¶
Overview ¶
Package awssecretcache provides a local, thread-safe, fixed-size cache for AWS Secrets Manager lookups, with single-flight deduplication.
New wraps an aws-sdk-go-v2 SecretsManager client (https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/secretsmanager) in a github.com/tecnickcom/nurago/pkg/sfcache cache. Concurrent callers asking for the same secret share a single upstream GetSecretValue call; the result is cached for the TTL.
Usage ¶
cache, err := awssecretcache.New(
ctx,
128, // maximum number of cached secrets
5*time.Minute, // TTL per entry
awssecretcache.WithEndpointMutable("https://secretsmanager.us-east-1.amazonaws.com"),
)
if err != nil {
return err
}
value, err := cache.GetSecretString(ctx, "prod/myapp/db-password")
if err != nil {
return err
}
After a secret rotation, evict the entry immediately:
cache.Remove("prod/myapp/db-password")
Caching ¶
Only successful lookups are cached, for the TTL. Errors are shared with the callers coalesced onto the same lookup but never cached, so the next call retries.
Expired entries are not removed at the TTL: they are replaced by the next lookup or evicted under capacity pressure, so **expired secret material can remain in process memory until then**. Use Cache.Remove, Cache.Reset or Cache.PurgeExpired when prompt removal matters.
The size given to New bounds the number of SECRETS held, never the number of distinct secrets requested. Cache.Len can exceed it: it also counts the secrets being fetched and the residue of a failed fetch, and a stale serve that can evict nothing holds one secret more.
When the cache is full, storing a secret evicts an expired entry holding nothing worth keeping first, then a secret only being served stale, and only then the entry closest to expiration (expiry-ordered, not LRU: reads do not refresh recency). A fetch that FAILS evicts nothing of value.
Stale-if-error ¶
WithStaleOnFailure serves the last known good secret for a window measured from the first failed refresh, so it protects rarely read secrets too. WithStaleIfError is the RFC 5861 variant, whose window is anchored to the secret's original expiration and therefore only covers secrets read more often than ttl + maxStale.
With either enabled, a failed refresh returns the last known good secret with a NIL error: callers cannot tell a stale secret from a fresh one, and a secret rotated upstream during an outage keeps being served until the window closes.
Retrieval ¶
Cache.GetSecretData returns the raw SDK output, shared by reference: treat it as read-only. Cache.GetSecretString and Cache.GetSecretBinary return a copy and handle both storage formats (SecretString and SecretBinary).
AWS configuration ¶
WithAWSOptions, WithSrvOptionFuncs, WithEndpointMutable and WithEndpointImmutable customize the SDK client. WithSecretsManagerClient injects a client directly, in which case the AWS configuration is neither loaded nor used.
Example ¶
package main
import (
"context"
"fmt"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/tecnickcom/nurago/pkg/awssecretcache"
)
// exampleSMClient is a stand-in for the AWS Secrets Manager client,
// implementing only the awssecretcache.SecretsManagerClient interface. Real
// code would let New build the client from aws.Config instead.
type exampleSMClient struct{}
func (exampleSMClient) GetSecretValue(
_ context.Context,
params *awssm.GetSecretValueInput,
_ ...func(*awssm.Options),
) (*awssm.GetSecretValueOutput, error) {
return &awssm.GetSecretValueOutput{
SecretString: aws.String("s3cr3t-for-" + aws.ToString(params.SecretId)),
}, nil
}
func main() {
// A caller would normally configure a real client via options such as
// WithAWSOptions or WithEndpointMutable; here an injected client keeps the
// example self-contained.
cache, err := awssecretcache.New(
context.TODO(),
128, // maximum number of cached secrets
5*time.Minute, // TTL per entry
awssecretcache.WithSecretsManagerClient(exampleSMClient{}),
)
if err != nil {
fmt.Println("error:", err)
return
}
// The first call fetches upstream; subsequent calls within the TTL are
// served from memory, and concurrent callers for the same key share a
// single in-flight lookup.
value, err := cache.GetSecretString(context.TODO(), "prod/myapp/db-password")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(value)
}
Output: s3cr3t-for-prod/myapp/db-password
Index ¶
- Variables
- type Cache
- func (c *Cache) GetSecretBinary(ctx context.Context, key string) ([]byte, error)
- func (c *Cache) GetSecretData(ctx context.Context, key string) (*awssm.GetSecretValueOutput, error)
- func (c *Cache) GetSecretString(ctx context.Context, key string) (string, error)
- func (c *Cache) Len() int
- func (c *Cache) PurgeExpired() int
- func (c *Cache) Remove(key string)
- func (c *Cache) Reset()
- type Option
- func WithAWSOptions(opt awsopt.Options) Option
- func WithEndpointImmutable(url string) Option
- func WithEndpointMutable(url string) Option
- func WithSecretsManagerClient(smclient SecretsManagerClient) Option
- func WithSrvOptionFuncs(opt ...SrvOptionFunc) Option
- func WithStaleIfError(maxStale time.Duration) Option
- func WithStaleOnFailure(maxStaleOnFailure time.Duration) Option
- type SecretsManagerClient
- type SrvOptionFunc
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrEmptySecretID is returned by the Cache getters when the requested // secret id (key) is empty, before any upstream call is made. ErrEmptySecretID = errors.New("awssecretcache: secret id must not be empty") // ErrEmptySecret is returned by the Cache getters when the Secrets Manager // response carries no secret material: a nil response, or a response with // neither SecretString nor SecretBinary set. ErrEmptySecret = errors.New("awssecretcache: secret contains no value") )
Exported sentinel errors returned by this package. Match them with errors.Is.
Functions ¶
This section is empty.
Types ¶
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache provides TTL and single-flight caching for AWS Secrets Manager lookups.
func New ¶
New constructs a Secrets Manager cache with single-flight lookups and TTL-based storage. The returned Cache is safe for concurrent use.
A size <= 0 is clamped to a capacity of 1. A ttl <= 0 disables value caching (every call performs a fresh upstream lookup) while still coalescing concurrent lookups for the same key.
func (*Cache) GetSecretBinary ¶
GetSecretBinary returns key as bytes, regardless of how it is stored in AWS.
If the secret is stored as SecretString, the value is converted to []byte; otherwise a copy of SecretBinary is returned. The returned slice is never shared with the cache, so callers may safely zero it after use without corrupting the value served to other callers. When the response holds no value at all (neither SecretString nor SecretBinary), it returns ErrEmptySecret.
func (*Cache) GetSecretData ¶
GetSecretData returns the full Secrets Manager response for key.
On cache hit, it serves data from memory. On cache miss or expiry, one goroutine performs the upstream GetSecretValue call while concurrent callers for the same key wait and share that result (single-flight behavior).
Use it when callers need the metadata in GetSecretValueOutput in addition to the secret payload.
Error fan-out: when the upstream GetSecretValue call fails, that error is shared with the callers coalesced into the same in-flight lookup, with two exceptions: a failure caused by the initiating caller's own context makes one waiting caller retry the lookup with its own context instead, and a caller whose own context ends while waiting receives an error wrapping github.com/tecnickcom/nurago/pkg/sfcache.ErrLookupAborted. The first exception is matched on the error's identity, so an SDK timeout error, which wraps context.DeadlineExceeded, also makes one waiting caller retry when the initiating caller's context has ended too: that costs one extra upstream call against an already timing-out endpoint, never a wrong result. Failed lookups are not cached, so a subsequent call after the flight completes triggers a fresh upstream request. Enable WithStaleOnFailure or WithStaleIfError to serve the last known good secret during upstream outages.
With either stale option enabled, a failed refresh returns the last successfully fetched secret with a NIL error: callers cannot tell a stale secret from a freshly fetched one, and a secret rotated upstream during an outage keeps being served until the window closes.
The returned output is shared by reference with every other caller of the same key: treat it as read-only. Use Cache.GetSecretBinary or Cache.GetSecretString for values that are safe to modify.
An empty key yields ErrEmptySecretID before any upstream call is made.
Caching note: a value-less or nil response is cached like any successful value for the TTL, so ErrEmptySecret persists until expiry or an explicit Cache.Remove/Cache.Reset; genuine upstream errors are never cached and are retried on the next call.
Error matching: a nil upstream response yields ErrEmptySecret. Underlying errors propagate through the %w wrapping, so callers can still match AWS SDK typed errors with errors.As (e.g. new(*types.ResourceNotFoundException) for a missing secret) and context/abort errors with errors.Is (against github.com/tecnickcom/nurago/pkg/sfcache.ErrLookupAborted, context.Canceled, or context.DeadlineExceeded).
func (*Cache) GetSecretString ¶
GetSecretString returns key as text, regardless of how it is stored in AWS.
If the secret is stored as SecretBinary, the bytes are converted to string; otherwise SecretString is returned directly. When the response holds no value at all (neither SecretString nor SecretBinary), it returns ErrEmptySecret.
func (*Cache) Len ¶
Len reports the current number of cached entries.
It is not the cache's occupancy against its capacity: it also counts the secrets being fetched and the residue of a failed fetch, neither of which holds a value, so it can exceed the configured size. The overrun is bounded by the caller's own request concurrency plus two, never by the number of distinct secrets requested.
During an outage, a stale serve that could evict nothing holds one secret more (see WithStaleOnFailure). It is reclaimed by the next secret successfully fetched.
func (*Cache) PurgeExpired ¶
PurgeExpired removes all expired entries from the cache and returns the number of entries removed. It bounds how long expired secret material stays in process memory: expired entries are otherwise removed only when capacity pressure or a new lookup replaces them.
NOTE: it also removes the values retained by WithStaleIfError and WithStaleOnFailure, forfeiting stale protection for those keys until the next successful lookup.
func (*Cache) Remove ¶
Remove evicts key from the cache.
A fetch in flight for key is invalidated: its result is returned to the caller that started it but not cached, and callers waiting on it are released to fetch again. Removing a secret whose fetch is still in flight therefore allows a second concurrent upstream call for it — the price of not serving the pre-rotation value.
type Option ¶
type Option func(*cfg)
Option is a type to allow setting custom client options.
func WithAWSOptions ¶
WithAWSOptions appends raw AWS SDK load options used to build aws.Config: region, credentials, retryers, and similar settings.
func WithEndpointImmutable ¶
WithEndpointImmutable installs a custom EndpointResolverV2 with a fixed URL, so endpoint selection is deterministic and the default resolver logic is bypassed.
func WithEndpointMutable ¶
WithEndpointMutable sets BaseEndpoint on the SDK client. BaseEndpoint remains mutable, so the SDK can still adjust request routing details.
func WithSecretsManagerClient ¶
func WithSecretsManagerClient(smclient SecretsManagerClient) Option
WithSecretsManagerClient injects a custom SecretsManagerClient implementation, so no real secretsmanager.Client is built from aws.Config.
When set, the injected client is used as-is: the AWS configuration is not loaded and the AWS/service options (WithAWSOptions, WithSrvOptionFuncs, WithEndpointMutable, WithEndpointImmutable) are ignored.
func WithSrvOptionFuncs ¶
func WithSrvOptionFuncs(opt ...SrvOptionFunc) Option
WithSrvOptionFuncs appends Secrets Manager service-level option functions, applied when constructing the underlying secretsmanager.Client.
func WithStaleIfError ¶
WithStaleIfError serves the last known good secret when a refresh fails, with the stale window anchored to the secret's original expiration (RFC 5861 stale-if-error, see github.com/tecnickcom/nurago/pkg/sfcache.Config.MaxStale).
If fetching an expired secret returns an error (throttling, outage, timeout), the previously cached value is returned with a nil error instead, but only until its original expiration plus maxStale. Every call still attempts a fresh upstream lookup, so recovery is automatic on the first success. Callers cannot distinguish a stale secret from a fresh one, and stale protection is best-effort: the retained value is lost to cache eviction under capacity pressure, Cache.PurgeExpired, Cache.Remove, and Cache.Reset. A maxStale <= 0 disables the behavior (default).
PRECONDITION: because the window is anchored to the expiration and not to the failure, only a secret read more often than ttl + maxStale is protected. One idle for longer than that has no stale protection at all: the outage error is returned even though the last known good value is still cached. Use WithStaleOnFailure to protect rarely read secrets.
Size maxStale against the rotation policy, so a rotated-out secret cannot be served long after the rotation event.
func WithStaleOnFailure ¶ added in v1.152.0
WithStaleOnFailure serves the last known good secret for up to maxStaleOnFailure after a refresh first fails, however long the secret had been idle before the failure (see github.com/tecnickcom/nurago/pkg/sfcache.Config.MaxStaleOnFailure).
Unlike WithStaleIfError it holds for rarely read secrets. The window is anchored once, by the first failed refresh: further failures keep serving the same secret until that deadline but never push it back, so an outage cannot make a secret immortal. Every call still attempts a fresh upstream lookup and recovery is automatic on the first success. The caveats of WithStaleIfError otherwise apply unchanged: callers cannot distinguish a stale secret from a fresh one, and retention is best-effort.
A maxStaleOnFailure <= 0 disables the behavior (default). Size it against the rotation policy, so a rotated-out secret cannot be served long after the rotation event. When both options are set, the secret is served stale until the later of the two deadlines.
type SecretsManagerClient ¶
type SecretsManagerClient interface {
GetSecretValue(ctx context.Context, params *awssm.GetSecretValueInput, optFns ...func(*awssm.Options)) (*awssm.GetSecretValueOutput, error)
}
SecretsManagerClient defines the AWS Secrets Manager calls used by this package.
type SrvOptionFunc ¶
type SrvOptionFunc = func(*secretsmanager.Options)
SrvOptionFunc is an alias for this service option function.