Documentation
¶
Overview ¶
Package configawss3 adapts an AWS S3 bucket to the config.FS interface, so a tool whose configuration lives as an object in S3 can hand that bucket to a config Store and read, watch and write it exactly as it would a local file.
client := s3.NewFromConfig(awsCfg) store, err := config.NewStore(ctx, config.WithFiles(configawss3.Wrap(client, "my-bucket"), "app/config.yaml"))
This is a filesystem adapter, not a backend: it reads and writes a config *file* that happens to live in a bucket, through the same codec and stage-and-rename write machinery a local file uses. It does not model object keys as config keys — that is the parameter-store shape config-aws-ssm implements, and a different thing.
What an object store makes different ¶
S3 is not a POSIX filesystem, and the divergences are the substance:
Rename is not atomic. S3 has no move, so Wrap's Rename is copy-then-delete (CopyObject then DeleteObject). The target is always replaced whole — a reader never sees a half-written object — but a crash between the copy and the delete leaves the staged object behind. To keep that orphan findable, the staged object uses a recognisable, target-derived key: the resolved target key with a ".config-stage" suffix, which a bucket lifecycle rule can expire. The leak is never corruption and never a lost write.
There are no directories, so MkdirAll is a no-op.
There is no POSIX mode, so WriteFile ignores its perm argument and Stat synthesises a fixed 0o644 regular-file mode with a nil Sys.
There is no native change feed, so the Store polls. Because every poll is a billed GetObject, the adapter implements config.PollIntervalHinter and hints a 60-second cadence rather than the core's 2-second default; config.WithPollInterval still overrides it.
Because the consumer supplies the *s3.Client, they own its region, credentials, endpoint and retry policy — which is what lets the very same adapter drive an S3-compatible store (MinIO, Ceph, LocalStack) by pointing the client's BaseEndpoint at it.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNoBucket = errors.NewSentinel("configawss3.no_bucket",
"no bucket supplied; pass the bucket the configuration object lives in")
ErrNoBucket reports a rung called with no bucket name.
var ErrNoRegion = errors.NewSentinel("configawss3.no_region",
"no AWS region in the supplied config; set one, or set AWS_REGION for the ambient rung")
ErrNoRegion reports a configuration that names no AWS region.
A config with no region names a bucket in an account nobody chose, so it is refused here rather than failing at the first request. There is no default: AWS documents none, and inventing one is a different act from adopting a provider's own — which is why go/awsclient refuses too.
Functions ¶
func FSFromConfig ¶ added in v0.3.0
FSFromConfig builds the S3 client from an aws.Config the caller resolved — a specific profile, an assumed role, a client pointed at MinIO or LocalStack — and returns a config.FS over the bucket.
s3.NewFromConfig does no I/O, returns no error and needs no close, so this rung constructs eagerly and reaching the bucket stays deferred to the first read.
Where the ambient rung is ¶
Deliberately NOT here. Resolving the ambient AWS chain costs this module seven further dependencies — fewer than its Parameter Store and Secrets Manager siblings pay, because S3's larger service graph already carries internal/v4a, accept-encoding and presigned-url — but seven a consumer who builds their own client should not inherit.
It lives in the ambient subpackage instead, over go/awsclient:
import s3ambient "gitlab.com/phpboyscout/go/config-aws-s3/ambient" fsys, err := s3ambient.Default(ctx, "my-bucket")
To share ONE resolved chain across several adapters — and across go/signing and go/encryption — resolve it with go/awsclient and pass the config here (spec 0012 L-4, L-5).
func Wrap ¶
Wrap adapts a configured *s3.Client and bucket to a read+write config.FS.
The client carries every credential, region, endpoint and retry decision; Wrap adds none — it only translates the six config.FS calls onto object operations. The returned value is neither a config.RealPather (an object key is not an operating-system path) nor a config.LinkReader (S3 has no symlinks), so the Store polls it — on the adapter's 60-second config.PollIntervalHinter hint.
Example ¶
ExampleWrap reads a config object from an S3 bucket through a config Store. The consumer builds the *s3.Client — owning region, credentials and endpoint — and hands it to Wrap; the adapter only translates the filesystem calls onto object operations.
package main
import (
"context"
"log"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
cfg "gitlab.com/phpboyscout/go/config"
configawss3 "gitlab.com/phpboyscout/go/config-aws-s3"
)
func main() {
ctx := context.Background()
awsCfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
log.Fatal(err)
}
client := s3.NewFromConfig(awsCfg)
store, err := cfg.NewStore(ctx,
cfg.WithFiles(configawss3.Wrap(client, "my-bucket"), "app/config.yaml"))
if err != nil {
log.Fatal(err)
}
_ = store.View().GetString("server.host")
}
Output:
Types ¶
type Option ¶
type Option func(*adapter)
Option configures the adapter. See WithKeyPrefix.
func WithKeyPrefix ¶
WithKeyPrefix scopes the bucket by joining prefix onto each config.FS name to form the object key, so a bucket shared with unrelated objects can be carved out for configuration. The staging key a write commits through derives from the resolved (prefixed) target key, so prefix and staging compose cleanly. With no prefix, a name is used as the object key verbatim (spec D3).
Example ¶
ExampleWithKeyPrefix scopes a bucket shared with unrelated objects: the config.FS name "config.yaml" resolves to the object key "team/app/config.yaml".
package main
import (
"context"
"log"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
cfg "gitlab.com/phpboyscout/go/config"
configawss3 "gitlab.com/phpboyscout/go/config-aws-s3"
)
func main() {
ctx := context.Background()
awsCfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
log.Fatal(err)
}
client := s3.NewFromConfig(awsCfg)
store, err := cfg.NewStore(ctx,
cfg.WithFiles(
configawss3.Wrap(client, "my-bucket", configawss3.WithKeyPrefix("team/app")),
"config.yaml"))
if err != nil {
log.Fatal(err)
}
_ = store.View().GetString("server.host")
}
Output: