All checks were successful
Check / check (push) Successful in 4s
Bumps golangci-lint from v2.10.1 to v2.12.2 everywhere it is pinned and installs the canonical `.golangci.yml`, then fixes every finding the new linter surfaces so `make check` is green. ## Version pins - `Dockerfile` lint stage: `golangci/golangci-lint:v2.12.2` (Debian-based), tag plus digest pin - `script/bootstrap`: `GOLANGCI_LINT_VERSION=2.12.2` with updated `linux-amd64`/`linux-arm64` release-archive sha256 pins ## Config `.golangci.yml` replaced with the canonical config. Material change: the old file declared `version: "2"` but kept settings under the legacy top-level `linters-settings` key, which golangci-lint v2 ignores — so the intended thresholds (`lll` 88, `funlen` 80/50, `cyclop` 15, `dupl` 100) were not being applied. The canonical file moves them under `linters.settings` and drops `issues.exclude-use-default`. ## Lint fixes (216 findings) - `lll` (96): wrapped lines to the 88-column limit - `noctx` (46): `httptest.NewRequestWithContext` with `t.Context()` throughout the tests - `goconst` (24): shared constants for template/JSON keys in `internal/handlers` and repeated test literals - `gosec` (23): app-page redirects now go through a `redirectToApp` helper that path-escapes the app ID (G710 open redirect); `http.ServeFile` of the internally derived deployment log path annotated like the adjacent `os.Stat` (G703) - `dupl` (22): extracted a generic `findAllByAppID` in `internal/models`, a `deleteAppResource` helper in `internal/handlers`, a shared `parsePush` in `internal/service/webhook`, and table-driven/helper-based dedup in tests - `nolintlint` (5): removed `//nolint:funlen` directives made obsolete by the new limits (plus one more that became obsolete after refactoring) - `nilerr` (3, surfaced during fixing): resource-delete lookups now propagate the find error to the caller No behavior changes intended; all tests pass and `make check` is green. Note: golangci-lint v2.12 warns that `gomodguard` is deprecated in favor of `gomodguard_v2` — a future canonical-config update should address this centrally. Co-authored-by: sneak <sneak@sneak.berlin> Reviewed-on: #187 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
214 lines
5.3 KiB
Go
214 lines
5.3 KiB
Go
// Package config provides application configuration via Viper.
|
|
package config
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/spf13/viper"
|
|
"go.uber.org/fx"
|
|
|
|
"sneak.berlin/go/upaas/internal/globals"
|
|
"sneak.berlin/go/upaas/internal/logger"
|
|
)
|
|
|
|
// defaultPort is the default HTTP server port.
|
|
const defaultPort = 8080
|
|
|
|
// sessionSecretFile is the filename for the persisted session secret.
|
|
const sessionSecretFile = "session.key"
|
|
|
|
// sessionSecretBytes is the number of random bytes for session secret.
|
|
const sessionSecretBytes = 32
|
|
|
|
// File permission constants.
|
|
const (
|
|
dirPermissions = 0o700
|
|
filePermissions = 0o600
|
|
)
|
|
|
|
// Params contains dependencies for Config.
|
|
type Params struct {
|
|
fx.In
|
|
|
|
Globals *globals.Globals
|
|
Logger *logger.Logger
|
|
}
|
|
|
|
// Config holds application configuration.
|
|
type Config struct {
|
|
Port int
|
|
Debug bool
|
|
DataDir string
|
|
HostDataDir string // Host path for DataDir (Docker bind mounts in container)
|
|
DockerHost string
|
|
SentryDSN string
|
|
MaintenanceMode bool
|
|
MetricsUsername string
|
|
MetricsPassword string
|
|
SessionSecret string `json:"-"`
|
|
CORSOrigins string
|
|
params *Params
|
|
log *slog.Logger
|
|
}
|
|
|
|
// New creates a new Config instance from environment and config files.
|
|
func New(_ fx.Lifecycle, params Params) (*Config, error) {
|
|
log := params.Logger.Get()
|
|
|
|
name := params.Globals.Appname
|
|
if name == "" {
|
|
name = "upaas"
|
|
}
|
|
|
|
setupViper(name)
|
|
|
|
cfg, err := buildConfig(log, ¶ms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
configureDebugLogging(cfg, params)
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func setupViper(name string) {
|
|
// Config file settings
|
|
viper.SetConfigName(name)
|
|
viper.SetConfigType("yaml")
|
|
viper.AddConfigPath("/etc/" + name)
|
|
viper.AddConfigPath("$HOME/.config/" + name)
|
|
viper.AddConfigPath(".")
|
|
|
|
// Environment variables override everything
|
|
viper.SetEnvPrefix("UPAAS")
|
|
viper.AutomaticEnv()
|
|
|
|
// Defaults
|
|
// PORT is not prefixed with UPAAS_ for compatibility
|
|
_ = viper.BindEnv("PORT", "PORT")
|
|
|
|
viper.SetDefault("PORT", defaultPort)
|
|
viper.SetDefault("DEBUG", false)
|
|
viper.SetDefault("DATA_DIR", "./data")
|
|
viper.SetDefault("DOCKER_HOST", "unix:///var/run/docker.sock")
|
|
viper.SetDefault("SENTRY_DSN", "")
|
|
viper.SetDefault("MAINTENANCE_MODE", false)
|
|
viper.SetDefault("METRICS_USERNAME", "")
|
|
viper.SetDefault("METRICS_PASSWORD", "")
|
|
viper.SetDefault("SESSION_SECRET", "")
|
|
viper.SetDefault("CORS_ORIGINS", "")
|
|
}
|
|
|
|
func buildConfig(log *slog.Logger, params *Params) (*Config, error) {
|
|
// Read config file (optional)
|
|
err := viper.ReadInConfig()
|
|
if err != nil {
|
|
var configFileNotFoundError viper.ConfigFileNotFoundError
|
|
if !errors.As(err, &configFileNotFoundError) {
|
|
log.Error("config file malformed", "error", err)
|
|
|
|
return nil, fmt.Errorf("config file malformed: %w", err)
|
|
}
|
|
// Config file not found is OK
|
|
}
|
|
|
|
dataDir := viper.GetString("DATA_DIR")
|
|
hostDataDir := viper.GetString("HOST_DATA_DIR")
|
|
|
|
if hostDataDir == "" {
|
|
hostDataDir = dataDir
|
|
}
|
|
|
|
// Build config struct
|
|
cfg := &Config{
|
|
Port: viper.GetInt("PORT"),
|
|
Debug: viper.GetBool("DEBUG"),
|
|
DataDir: dataDir,
|
|
HostDataDir: hostDataDir,
|
|
DockerHost: viper.GetString("DOCKER_HOST"),
|
|
SentryDSN: viper.GetString("SENTRY_DSN"),
|
|
MaintenanceMode: viper.GetBool("MAINTENANCE_MODE"),
|
|
MetricsUsername: viper.GetString("METRICS_USERNAME"),
|
|
MetricsPassword: viper.GetString("METRICS_PASSWORD"),
|
|
SessionSecret: viper.GetString("SESSION_SECRET"),
|
|
CORSOrigins: viper.GetString("CORS_ORIGINS"),
|
|
params: params,
|
|
log: log,
|
|
}
|
|
|
|
// Load or generate session secret
|
|
if cfg.SessionSecret == "" {
|
|
secret, err := loadOrCreateSessionSecret(log, cfg.DataDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to initialize session secret: %w", err)
|
|
}
|
|
|
|
cfg.SessionSecret = secret
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func loadOrCreateSessionSecret(log *slog.Logger, dataDir string) (string, error) {
|
|
secretPath := filepath.Join(dataDir, sessionSecretFile)
|
|
|
|
// Try to read existing secret
|
|
//nolint:gosec // secretPath is constructed from trusted config, not user input
|
|
data, err := os.ReadFile(secretPath)
|
|
if err == nil {
|
|
log.Info("loaded session secret from file", "path", secretPath)
|
|
|
|
return string(data), nil
|
|
}
|
|
|
|
if !os.IsNotExist(err) {
|
|
return "", fmt.Errorf("failed to read session secret file: %w", err)
|
|
}
|
|
|
|
// Generate new secret
|
|
secretBytes := make([]byte, sessionSecretBytes)
|
|
|
|
_, err = rand.Read(secretBytes)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate random secret: %w", err)
|
|
}
|
|
|
|
secret := hex.EncodeToString(secretBytes)
|
|
|
|
// Ensure data directory exists
|
|
err = os.MkdirAll(dataDir, dirPermissions)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create data directory: %w", err)
|
|
}
|
|
|
|
// Write secret to file
|
|
err = os.WriteFile(secretPath, []byte(secret), filePermissions)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to write session secret file: %w", err)
|
|
}
|
|
|
|
log.Info("generated new session secret", "path", secretPath)
|
|
|
|
return secret, nil
|
|
}
|
|
|
|
func configureDebugLogging(cfg *Config, params Params) {
|
|
// Enable debug logging if configured
|
|
if cfg.Debug {
|
|
params.Logger.EnableDebugLogging()
|
|
cfg.log = params.Logger.Get()
|
|
}
|
|
}
|
|
|
|
// DatabasePath returns the full path to the SQLite database file.
|
|
func (c *Config) DatabasePath() string {
|
|
return c.DataDir + "/upaas.db"
|
|
}
|