refactor: remove file-based configuration, use env vars only
All checks were successful
check / check (push) Successful in 1m0s

Remove the entire pkg/config package (Viper-based YAML config file
loader) and simplify internal/config to read all settings directly from
environment variables via os.Getenv(). This eliminates the spurious
"Failed to load config" log messages that appeared when no config.yaml
file was present.

- Delete pkg/config/ (YAML loader, resolver, manager, tests)
- Delete configs/config.yaml.example
- Simplify internal/config helper functions to use os.Getenv() with
  defaults instead of falling back to pkgconfig
- Update tests to set env vars directly instead of creating in-memory
  YAML config files via afero
- Remove afero, cloud.google.com/*, aws-sdk-go dependencies from go.mod
- Update README: document env-var-only configuration, remove YAML/Viper
  references
- Keep godotenv/autoload for .env file convenience in local development

closes #27
This commit is contained in:
clawbot
2026-03-01 23:01:45 -08:00
parent 10db6c5b84
commit 49852e7506
20 changed files with 43 additions and 2312 deletions

View File

@@ -10,7 +10,6 @@ import (
"go.uber.org/fx"
"sneak.berlin/go/webhooker/internal/globals"
"sneak.berlin/go/webhooker/internal/logger"
pkgconfig "sneak.berlin/go/webhooker/pkg/config"
// Populates the environment from a ./.env file automatically for
// development configuration. Kept in one place only (here).
@@ -56,38 +55,30 @@ func (c *Config) IsProd() bool {
return c.Environment == EnvironmentProd
}
// envString returns the env var value if set, otherwise falls back to pkgconfig.
func envString(envKey, configKey string) string {
if v := os.Getenv(envKey); v != "" {
return v
}
return pkgconfig.GetString(configKey)
// envString returns the value of the named environment variable, or
// an empty string if not set.
func envString(key string) string {
return os.Getenv(key)
}
// envSecretString returns the env var value if set, otherwise falls back to pkgconfig secrets.
func envSecretString(envKey, configKey string) string {
if v := os.Getenv(envKey); v != "" {
return v
}
return pkgconfig.GetSecretString(configKey)
}
// envBool returns the env var value parsed as bool, otherwise falls back to pkgconfig.
func envBool(envKey, configKey string) bool {
if v := os.Getenv(envKey); v != "" {
// envBool returns the value of the named environment variable parsed as a
// boolean. Returns defaultValue if not set.
func envBool(key string, defaultValue bool) bool {
if v := os.Getenv(key); v != "" {
return strings.EqualFold(v, "true") || v == "1"
}
return pkgconfig.GetBool(configKey)
return defaultValue
}
// envInt returns the env var value parsed as int, otherwise falls back to pkgconfig.
func envInt(envKey, configKey string, defaultValue ...int) int {
if v := os.Getenv(envKey); v != "" {
// envInt returns the value of the named environment variable parsed as an
// integer. Returns defaultValue if not set or unparseable.
func envInt(key string, defaultValue int) int {
if v := os.Getenv(key); v != "" {
if i, err := strconv.Atoi(v); err == nil {
return i
}
}
return pkgconfig.GetInt(configKey, defaultValue...)
return defaultValue
}
// nolint:revive // lc parameter is required by fx even if unused
@@ -106,21 +97,18 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
EnvironmentDev, EnvironmentProd, environment)
}
// Set the environment in the config package (for fallback resolution)
pkgconfig.SetEnvironment(environment)
// Load configuration values — env vars take precedence over config.yaml
// Load configuration values from environment variables
s := &Config{
DBURL: envString("DBURL", "dburl"),
DataDir: envString("DATA_DIR", "dataDir"),
Debug: envBool("DEBUG", "debug"),
MaintenanceMode: envBool("MAINTENANCE_MODE", "maintenanceMode"),
DevelopmentMode: envBool("DEVELOPMENT_MODE", "developmentMode"),
DBURL: envString("DBURL"),
DataDir: envString("DATA_DIR"),
Debug: envBool("DEBUG", false),
MaintenanceMode: envBool("MAINTENANCE_MODE", false),
DevelopmentMode: envBool("DEVELOPMENT_MODE", false),
Environment: environment,
MetricsUsername: envString("METRICS_USERNAME", "metricsUsername"),
MetricsPassword: envString("METRICS_PASSWORD", "metricsPassword"),
Port: envInt("PORT", "port", 8080),
SentryDSN: envSecretString("SENTRY_DSN", "sentryDSN"),
MetricsUsername: envString("METRICS_USERNAME"),
MetricsPassword: envString("METRICS_PASSWORD"),
Port: envInt("PORT", 8080),
SentryDSN: envString("SENTRY_DSN"),
log: log,
params: &params,
}

View File

@@ -4,58 +4,14 @@ import (
"os"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/fx"
"go.uber.org/fx/fxtest"
"sneak.berlin/go/webhooker/internal/globals"
"sneak.berlin/go/webhooker/internal/logger"
pkgconfig "sneak.berlin/go/webhooker/pkg/config"
)
// createTestConfig creates a test configuration file in memory
func createTestConfig(fs afero.Fs) error {
configYAML := `
environments:
dev:
config:
port: 8080
debug: true
maintenanceMode: false
developmentMode: true
environment: dev
dburl: postgres://test:test@localhost:5432/test_dev?sslmode=disable
metricsUsername: testuser
metricsPassword: testpass
secrets:
sentryDSN: ""
prod:
config:
port: $ENV:PORT
debug: $ENV:DEBUG
maintenanceMode: $ENV:MAINTENANCE_MODE
developmentMode: false
environment: prod
dburl: $ENV:DBURL
metricsUsername: $ENV:METRICS_USERNAME
metricsPassword: $ENV:METRICS_PASSWORD
secrets:
sentryDSN: $ENV:SENTRY_DSN
configDefaults:
port: 8080
debug: false
maintenanceMode: false
developmentMode: false
environment: dev
metricsUsername: ""
metricsPassword: ""
`
return afero.WriteFile(fs, "config.yaml", []byte(configYAML), 0644)
}
func TestEnvironmentConfig(t *testing.T) {
tests := []struct {
name string
@@ -68,6 +24,7 @@ func TestEnvironmentConfig(t *testing.T) {
{
name: "default is dev",
envValue: "",
envVars: map[string]string{"DBURL": "file::memory:?cache=shared"},
expectError: false,
isDev: true,
isProd: false,
@@ -75,6 +32,7 @@ func TestEnvironmentConfig(t *testing.T) {
{
name: "explicit dev",
envValue: "dev",
envVars: map[string]string{"DBURL": "file::memory:?cache=shared"},
expectError: false,
isDev: true,
isProd: false,
@@ -92,21 +50,19 @@ func TestEnvironmentConfig(t *testing.T) {
{
name: "invalid environment",
envValue: "staging",
envVars: map[string]string{"DBURL": "file::memory:?cache=shared"},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create in-memory filesystem with test config
fs := afero.NewMemMapFs()
require.NoError(t, createTestConfig(fs))
pkgconfig.SetFs(fs)
// Set environment variable if specified
if tt.envValue != "" {
os.Setenv("WEBHOOKER_ENVIRONMENT", tt.envValue)
defer os.Unsetenv("WEBHOOKER_ENVIRONMENT")
} else {
os.Unsetenv("WEBHOOKER_ENVIRONMENT")
}
// Set additional environment variables

View File

@@ -2,38 +2,19 @@ package database
import (
"context"
"os"
"testing"
"github.com/spf13/afero"
"go.uber.org/fx/fxtest"
"sneak.berlin/go/webhooker/internal/config"
"sneak.berlin/go/webhooker/internal/globals"
"sneak.berlin/go/webhooker/internal/logger"
pkgconfig "sneak.berlin/go/webhooker/pkg/config"
)
func TestDatabaseConnection(t *testing.T) {
// Set up in-memory config so the test does not depend on config.yaml on disk
fs := afero.NewMemMapFs()
testConfigYAML := `
environments:
dev:
config:
port: 8080
debug: false
maintenanceMode: false
developmentMode: true
environment: dev
dburl: "file::memory:?cache=shared"
secrets:
sentryDSN: ""
configDefaults:
port: 8080
`
if err := afero.WriteFile(fs, "config.yaml", []byte(testConfigYAML), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
pkgconfig.SetFs(fs)
// Set DBURL env var for config loading
os.Setenv("DBURL", "file::memory:?cache=shared")
defer os.Unsetenv("DBURL")
// Set up test dependencies
lc := fxtest.NewLifecycle(t)

View File

@@ -7,32 +7,20 @@ import (
"testing"
"github.com/google/uuid"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/fx/fxtest"
"sneak.berlin/go/webhooker/internal/config"
"sneak.berlin/go/webhooker/internal/globals"
"sneak.berlin/go/webhooker/internal/logger"
pkgconfig "sneak.berlin/go/webhooker/pkg/config"
)
func setupTestWebhookDBManager(t *testing.T) (*WebhookDBManager, *fxtest.Lifecycle) {
t.Helper()
fs := afero.NewMemMapFs()
testConfigYAML := `
environments:
dev:
config:
port: 8080
debug: false
dburl: "file::memory:?cache=shared"
configDefaults:
port: 8080
`
require.NoError(t, afero.WriteFile(fs, "config.yaml", []byte(testConfigYAML), 0644))
pkgconfig.SetFs(fs)
// Set DBURL env var for config loading
os.Setenv("DBURL", "file::memory:?cache=shared")
t.Cleanup(func() { os.Unsetenv("DBURL") })
lc := fxtest.NewLifecycle(t)
@@ -52,7 +40,6 @@ configDefaults:
DBURL: "file::memory:?cache=shared",
DataDir: dataDir,
}
_ = cfg
mgr, err := NewWebhookDBManager(lc, WebhookDBManagerParams{
Config: cfg,