Initial implementation: package-updater v1 #1
@@ -1,3 +1,10 @@
|
|||||||
module github.com/shcizo/package-updater
|
module github.com/shcizo/package-updater
|
||||||
|
|
||||||
go 1.26.3
|
go 1.26.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||||
|
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Package config loads service configuration from environment variables.
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds all runtime configuration for the service.
|
||||||
|
type Config struct {
|
||||||
|
APIKey string
|
||||||
|
StacksRoot string
|
||||||
|
Port string
|
||||||
|
LogLevel string
|
||||||
|
UpdateTimeout time.Duration
|
||||||
|
OptInLabel string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads configuration from environment variables, applies defaults,
|
||||||
|
// and validates required fields. Returns an error if validation fails so
|
||||||
|
// the service can fail-fast at startup.
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
cfg := &Config{
|
||||||
|
APIKey: os.Getenv("UPDATER_API_KEY"),
|
||||||
|
StacksRoot: getenvDefault("STACKS_ROOT", "/home/shcizo/self-hosted"),
|
||||||
|
Port: getenvDefault("PORT", "8080"),
|
||||||
|
LogLevel: getenvDefault("LOG_LEVEL", "info"),
|
||||||
|
OptInLabel: getenvDefault("OPT_IN_LABEL", "se.shcizo.auto-update"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.APIKey == "" {
|
||||||
|
return nil, errors.New("UPDATER_API_KEY is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutStr := getenvDefault("UPDATE_TIMEOUT", "5m")
|
||||||
|
d, err := time.ParseDuration(timeoutStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("UPDATE_TIMEOUT %q is not a valid duration: %w", timeoutStr, err)
|
||||||
|
}
|
||||||
|
cfg.UpdateTimeout = d
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getenvDefault(key, fallback string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shcizo/package-updater/internal/config"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoad_RequiresAPIKey(t *testing.T) {
|
||||||
|
t.Setenv("UPDATER_API_KEY", "")
|
||||||
|
_, err := config.Load()
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "UPDATER_API_KEY")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad_AppliesDefaults(t *testing.T) {
|
||||||
|
t.Setenv("UPDATER_API_KEY", "secret")
|
||||||
|
t.Setenv("STACKS_ROOT", "")
|
||||||
|
t.Setenv("PORT", "")
|
||||||
|
t.Setenv("LOG_LEVEL", "")
|
||||||
|
t.Setenv("UPDATE_TIMEOUT", "")
|
||||||
|
t.Setenv("OPT_IN_LABEL", "")
|
||||||
|
|
||||||
|
cfg, err := config.Load()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "secret", cfg.APIKey)
|
||||||
|
require.Equal(t, "/home/shcizo/self-hosted", cfg.StacksRoot)
|
||||||
|
require.Equal(t, "8080", cfg.Port)
|
||||||
|
require.Equal(t, "info", cfg.LogLevel)
|
||||||
|
require.Equal(t, 5*time.Minute, cfg.UpdateTimeout)
|
||||||
|
require.Equal(t, "se.shcizo.auto-update", cfg.OptInLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad_OverridesViaEnv(t *testing.T) {
|
||||||
|
t.Setenv("UPDATER_API_KEY", "secret")
|
||||||
|
t.Setenv("STACKS_ROOT", "/srv/stacks")
|
||||||
|
t.Setenv("PORT", "9090")
|
||||||
|
t.Setenv("LOG_LEVEL", "debug")
|
||||||
|
t.Setenv("UPDATE_TIMEOUT", "30s")
|
||||||
|
t.Setenv("OPT_IN_LABEL", "io.example.update")
|
||||||
|
|
||||||
|
cfg, err := config.Load()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "/srv/stacks", cfg.StacksRoot)
|
||||||
|
require.Equal(t, "9090", cfg.Port)
|
||||||
|
require.Equal(t, "debug", cfg.LogLevel)
|
||||||
|
require.Equal(t, 30*time.Second, cfg.UpdateTimeout)
|
||||||
|
require.Equal(t, "io.example.update", cfg.OptInLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad_InvalidTimeoutErrors(t *testing.T) {
|
||||||
|
t.Setenv("UPDATER_API_KEY", "secret")
|
||||||
|
t.Setenv("UPDATE_TIMEOUT", "not-a-duration")
|
||||||
|
_, err := config.Load()
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "UPDATE_TIMEOUT")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user