diff --git a/internal/core/config.go b/internal/core/config.go index 8a59fa6..eb126de 100644 --- a/internal/core/config.go +++ b/internal/core/config.go @@ -1,6 +1,7 @@ package core import ( + "errors" "fmt" "github.com/caarlos0/env/v10" @@ -137,11 +138,23 @@ type Config struct { var config *Config +// startupErrors collects configuration failures raised while packages +// initialise. They cannot be logged where they happen, because logging is not +// configured until the command runs, and printing them bypasses the +// configured log format. Validate reports them instead. +var startupErrors []error + +// RecordStartupError stores a configuration failure for Validate to report. +// Only safe to call during package initialisation, which is single threaded. +func RecordStartupError(err error) { + if err != nil { + startupErrors = append(startupErrors, err) + } +} + func init() { config = &Config{} - if err := env.Parse(config); err != nil { - fmt.Printf("%+v\n", err) - } + RecordStartupError(env.Parse(config)) } func ReadConfig() *Config { @@ -150,6 +163,10 @@ func ReadConfig() *Config { // Validate reports settings that would otherwise fail quietly at request time. func (c *Config) Validate() error { + if len(startupErrors) > 0 { + return fmt.Errorf("invalid configuration: %w", errors.Join(startupErrors...)) + } + if c.OutputFormat.Default == "" { return nil } diff --git a/internal/core/config_test.go b/internal/core/config_test.go new file mode 100644 index 0000000..2c768de --- /dev/null +++ b/internal/core/config_test.go @@ -0,0 +1,56 @@ +package core + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +// A malformed setting used to be printed and ignored, so the service started +// with silent defaults. Validate reports it instead. +func TestValidateReportsStartupErrors(t *testing.T) { + saved := startupErrors + t.Cleanup(func() { startupErrors = saved }) + + startupErrors = []error{errors.New(`strconv.ParseInt: parsing "abc"`)} + + config := *ReadConfig() + err := config.Validate() + + require.Error(t, err) + require.Contains(t, err.Error(), "invalid configuration") + require.Contains(t, err.Error(), "abc") +} + +func TestValidateAcceptsAWorkingConfiguration(t *testing.T) { + saved := startupErrors + t.Cleanup(func() { startupErrors = saved }) + startupErrors = nil + + config := *ReadConfig() + config.OutputFormat.Default = "" + + require.NoError(t, config.Validate()) +} + +func TestValidateRejectsAnUnknownDefaultFormat(t *testing.T) { + saved := startupErrors + t.Cleanup(func() { startupErrors = saved }) + startupErrors = nil + + config := *ReadConfig() + config.OutputFormat.Default = "WEBP" + + require.Error(t, config.Validate(), "the map is keyed in lower case") +} + +func TestRecordStartupErrorIgnoresNil(t *testing.T) { + saved := startupErrors + t.Cleanup(func() { startupErrors = saved }) + startupErrors = nil + + RecordStartupError(nil) + + require.Empty(t, startupErrors) +} diff --git a/internal/core/encryption.go b/internal/core/encryption.go index bcda20d..6b4cc76 100644 --- a/internal/core/encryption.go +++ b/internal/core/encryption.go @@ -13,7 +13,6 @@ import ( "fmt" "github.com/caarlos0/env/v10" "io" - "log/slog" "strings" ) @@ -22,13 +21,11 @@ var salt = []byte("go-dims") func init() { envConfig := Signing{} - if err := env.Parse(&envConfig); err != nil { - fmt.Printf("%+v\n", err) - } + RecordStartupError(env.Parse(&envConfig)) var err error if encryptionKey, err = deriveKey(envConfig.SigningKey); err != nil { - slog.Error("failed to derive encryption key", "error", err) + RecordStartupError(fmt.Errorf("failed to derive the encryption key: %w", err)) return } } diff --git a/internal/core/image.go b/internal/core/image.go index efff7ab..a3dd2b3 100644 --- a/internal/core/image.go +++ b/internal/core/image.go @@ -1,7 +1,6 @@ package core import ( - "fmt" "github.com/beetlebugorg/go-dims/internal/gox/imagex/colorx" "github.com/caarlos0/env/v10" "golang.org/x/exp/slices" @@ -46,9 +45,7 @@ var defaultSourceBackend SourceBackend func RegisterImageBackend(sourceBackend SourceBackend) { config := Source{} - if err := env.Parse(&config); err != nil { - fmt.Printf("%+v\n", err) - } + RecordStartupError(env.Parse(&config)) allSourceBackends = append(allSourceBackends, sourceBackend) diff --git a/internal/source/file.go b/internal/source/file.go index 25edcbd..a52dcef 100644 --- a/internal/source/file.go +++ b/internal/source/file.go @@ -2,7 +2,6 @@ package source import ( "context" - "fmt" "github.com/beetlebugorg/go-dims/internal/core" "github.com/caarlos0/env/v10" "github.com/davidbyttow/govips/v2/vips" @@ -19,9 +18,7 @@ type fileSourceBackend struct { func init() { envConfig := core.FileSource{} - if err := env.Parse(&envConfig); err != nil { - fmt.Printf("%+v\n", err) - } + core.RecordStartupError(env.Parse(&envConfig)) core.RegisterImageBackend(NewFileSourceBackend(envConfig.BaseDir)) } diff --git a/internal/source/s3.go b/internal/source/s3.go index 8fb30aa..d3ff7a3 100644 --- a/internal/source/s3.go +++ b/internal/source/s3.go @@ -2,7 +2,6 @@ package source import ( "context" - "fmt" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" @@ -37,9 +36,7 @@ var s3Client = sync.OnceValues(func() (*s3.Client, error) { func init() { envConfig := core.S3{} - if err := env.Parse(&envConfig); err != nil { - fmt.Printf("%+v\n", err) - } + core.RecordStartupError(env.Parse(&envConfig)) core.RegisterImageBackend(NewS3SourceBackend(envConfig)) }