check / check (pull_request) Successful in 2m35s
With the s3.* config form and an endpoint written without a scheme, use_ssl being omitted built an http:// endpoint, while config.example.yml documented use_ssl as defaulting to true. Over plain HTTP a network observer sees manifests, object names, sizes and the access key id, and can alter responses. use_ssl is now *bool: omitted (nil) means the default, TLS; only an explicit use_ssl: false forces plain HTTP. This matches the s3:// URL form, which already defaults to TLS. The config init template dropped its misleading use_ssl line from the s3:// block (that key is never read for URLs; ?ssl=false controls TLS there) and points at ?ssl=false instead. Model: opus-4-8
144 lines
3.8 KiB
Go
144 lines
3.8 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"go.uber.org/fx"
|
|
"sneak.berlin/go/vaultik/internal/config"
|
|
"sneak.berlin/go/vaultik/internal/s3"
|
|
)
|
|
|
|
// defaultS3Region is used when neither the URL nor the config specify one.
|
|
const defaultS3Region = "us-east-1"
|
|
|
|
// defaultS3Endpoint is the AWS endpoint used when none is configured.
|
|
const defaultS3Endpoint = "s3.amazonaws.com"
|
|
|
|
// Module exports storage functionality as an fx module.
|
|
// It provides a Storer implementation based on the configured storage URL
|
|
// or falls back to legacy S3 configuration.
|
|
//
|
|
//nolint:gochecknoglobals // fx module definitions are package globals
|
|
var Module = fx.Module("storage",
|
|
fx.Provide(NewStorer),
|
|
)
|
|
|
|
// NewStorer creates a Storer based on configuration.
|
|
// If StorageURL is set, it uses URL-based configuration.
|
|
// Otherwise, it falls back to legacy S3 configuration.
|
|
//
|
|
//nolint:ireturn // fx provider intentionally returns the Storer interface
|
|
func NewStorer(cfg *config.Config) (Storer, error) {
|
|
if cfg.StorageURL != "" {
|
|
return storerFromURL(cfg.StorageURL, cfg)
|
|
}
|
|
|
|
return storerFromLegacyS3Config(cfg)
|
|
}
|
|
|
|
//nolint:ireturn // factory intentionally returns the Storer interface
|
|
func storerFromURL(rawURL string, cfg *config.Config) (Storer, error) {
|
|
parsed, err := ParseStorageURL(rawURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing storage URL: %w", err)
|
|
}
|
|
|
|
switch parsed.Scheme {
|
|
case schemeFile:
|
|
return NewFileStorer(parsed.Prefix)
|
|
|
|
case schemeS3:
|
|
return storerFromParsedS3URL(parsed, cfg)
|
|
|
|
case schemeRclone:
|
|
return NewRcloneStorer(
|
|
context.Background(), parsed.RcloneRemote, parsed.Prefix)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("%w: %s", ErrUnsupportedStorage, parsed.Scheme)
|
|
}
|
|
}
|
|
|
|
// storerFromParsedS3URL builds an S3 storer from a parsed s3:// URL,
|
|
// filling endpoint protocol and region defaults from the config.
|
|
//
|
|
//nolint:ireturn // factory intentionally returns the Storer interface
|
|
func storerFromParsedS3URL(parsed *URL, cfg *config.Config) (Storer, error) {
|
|
// Build endpoint URL
|
|
endpoint := parsed.Endpoint
|
|
if endpoint == "" {
|
|
endpoint = defaultS3Endpoint
|
|
}
|
|
|
|
// Add protocol if not present
|
|
hasProtocol := strings.HasPrefix(endpoint, "https://") ||
|
|
strings.HasPrefix(endpoint, "http://")
|
|
if !hasProtocol {
|
|
if parsed.UseSSL {
|
|
endpoint = "https://" + endpoint
|
|
} else {
|
|
endpoint = "http://" + endpoint
|
|
}
|
|
}
|
|
|
|
region := parsed.Region
|
|
if region == "" {
|
|
region = cfg.S3.Region
|
|
if region == "" {
|
|
region = defaultS3Region
|
|
}
|
|
}
|
|
|
|
// Credentials come from config (not URL for security)
|
|
client, err := s3.NewClient(context.Background(), s3.Config{
|
|
Endpoint: endpoint,
|
|
Bucket: parsed.Bucket,
|
|
Prefix: parsed.Prefix,
|
|
AccessKeyID: cfg.S3.AccessKeyID,
|
|
SecretAccessKey: cfg.S3.SecretAccessKey,
|
|
Region: region,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating S3 client: %w", err)
|
|
}
|
|
|
|
return NewS3Storer(client), nil
|
|
}
|
|
|
|
//nolint:ireturn // factory intentionally returns the Storer interface
|
|
func storerFromLegacyS3Config(cfg *config.Config) (Storer, error) {
|
|
endpoint := cfg.S3.Endpoint
|
|
|
|
// Ensure protocol is present. Absent an explicit use_ssl, default to TLS;
|
|
// plain HTTP only when use_ssl is written as false.
|
|
if !strings.HasPrefix(endpoint, "http://") &&
|
|
!strings.HasPrefix(endpoint, "https://") {
|
|
if cfg.S3.UseSSL == nil || *cfg.S3.UseSSL {
|
|
endpoint = "https://" + endpoint
|
|
} else {
|
|
endpoint = "http://" + endpoint
|
|
}
|
|
}
|
|
|
|
region := cfg.S3.Region
|
|
if region == "" {
|
|
region = defaultS3Region
|
|
}
|
|
|
|
client, err := s3.NewClient(context.Background(), s3.Config{
|
|
Endpoint: endpoint,
|
|
Bucket: cfg.S3.Bucket,
|
|
Prefix: cfg.S3.Prefix,
|
|
AccessKeyID: cfg.S3.AccessKeyID,
|
|
SecretAccessKey: cfg.S3.SecretAccessKey,
|
|
Region: region,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating S3 client: %w", err)
|
|
}
|
|
|
|
return NewS3Storer(client), nil
|
|
}
|