Files
godoxy-yusing/internal/config
Yuzerion 7b00a60f77 fix(docker): merge YAML objects into nested proxy labels (#219)
Sort proxy.* keys by dot depth, then name, before building the tree so
broader paths apply before deeper ones. When a new value would sit on a
node that is already a map, parse it as a YAML object (tabs normalized to
two spaces), deep-merge, and treat an empty string as an empty object.
Return clear errors when a scalar and a nested map disagree.

Drop the preallocated refPrefixes table in favor of refPrefix(n). Add
internal tests for parseLabelObject, mergeLabelMaps, key order, and
flatten; extend export tests for mixed OIDC-style labels and conflicts.

* refactor(docker): extract label parse and flatten helpers

Refactor ParseLabels by moving proxy label application into applyLabel,
descendLabelMap, and setLabelValue so traversal and leaf merge share one path
without labelLoop continues.

Add splitAliasLabel for ExpandWildcard so proxy.* prefix handling stays in one
place and uses CutPrefix/Cut consistently.

Deduplicate flattenMap and flattenMapAny value handling with flattenValue plus
joinLabelKey and stringifyLabelKey for flattened key construction.

* refactor(docker): structured errors for label type clashes

Replace ad hoc fmt.Errorf messages in descendLabelMap, setLabelValue, and
mergeLabelMaps with UnexpectedTypeError so wording is consistent and mapping
vs scalar conflicts stay explicit.

Hoist requireMap in label tests to a shared helper.

Normalize tabs to two spaces in expandYamlWildcard so wildcard YAML matches
the indentation used in the object-merge path.

* refactor(docker): optional UnexpectedTypeError message for merge conflicts

Extend UnexpectedTypeError with an optional Message field; when set, Error()
returns it instead of the default expect-versus-actual formatting.

mergeLabelMaps sets that message when a mapping would merge into an existing
scalar, so the error states the situation instead of only "expect scalar".

Update TestMergeLabelMaps to assert the new wording.
2026-04-13 15:21:42 +08:00
..

internal/config

Centralized YAML configuration management with thread-safe state access and provider initialization.

Overview

The config package implements the core configuration management system for GoDoxy, handling YAML configuration loading, provider initialization, route loading, and state transitions. It uses atomic pointers for thread-safe state access and integrates all configuration components.

Primary consumers

  • cmd/main.go - Initializes configuration state on startup
  • internal/route/provider - Accesses configuration for route creation
  • internal/api/v1 - Exposes configuration via REST API
  • All packages that need to access active configuration

Non-goals

  • Dynamic provider registration after initialization (require config reload)

Stability

Stable internal package. Public API consists of State interface and state management functions.

Public API

Exported types

type Config struct {
    ACL             *acl.Config
    AutoCert        *autocert.Config
    Entrypoint      entrypoint.Config
    Providers       Providers
    MatchDomains    []string
    Homepage        homepage.Config
    Defaults        Defaults
    TimeoutShutdown int
}

type Providers struct {
    Files        []string
    Docker       map[string]types.DockerProviderConfig
    Agents       []*agent.AgentConfig
    Notification []*notif.NotificationConfig
    Proxmox      []proxmox.Config
    MaxMind      *maxmind.Config
}

State interface

type State interface {
    Task() *task.Task
    Context() context.Context
    Value() *Config
    Entrypoint() entrypoint.Entrypoint
    ShortLinkMatcher() config.ShortLinkMatcher
    AutoCertProvider() server.CertProvider
    LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool)
    DeleteProvider(key string)
    IterProviders() iter.Seq2[string, types.RouteProvider]
    StartProviders() error
    NumProviders() int

    // Lifecycle management
    StartAPIServers()
    StartMetrics()

    FlushTmpLog()
}

Exported functions

func NewState() config.State

Creates a new configuration state with empty providers map.

func GetState() config.State

Returns the active configuration state. Thread-safe via atomic load.

func SetState(state config.State)

Sets the active configuration state. Also updates active configs for ACL, entrypoint, homepage, and autocert.

func HasState() bool

Returns true if a state is currently active.

func Value() *config.Config

Returns the current configuration values.

func (state *state) InitFromFile(filename string) error

Initializes state from a YAML file. Uses default config if file doesn't exist.

func (state *state) Init(data []byte) error

Initializes state from raw YAML data. Validates, then initializes MaxMind, Proxmox, providers, AutoCert, notifications, access logger, and entrypoint.

func (state *state) StartProviders() error

Starts all route providers concurrently.

func (state *state) IterProviders() iter.Seq2[string, types.RouteProvider]

Returns an iterator over all providers.

Architecture

Core components

graph TD
    A[config.yml] --> B[State]
    B --> C{Initialize}
    C --> D[Validate YAML]
    C --> E[Init MaxMind]
    C --> F[Init Proxmox]
    C --> G[Load Route Providers]
    C --> H[Init AutoCert]
    C --> I[Init Notifications]
    C --> J[Init Entrypoint]

    K[ActiveConfig] -.-> B

    subgraph Providers
        G --> L[Docker Provider]
        G --> M[File Provider]
        G --> N[Agent Provider]
    end

    subgraph State Management
        B --> O[xsync.Map Providers]
        B --> P[Entrypoint]
        B --> Q[AutoCert Provider]
        B --> R[task.Task]
    end

Initialization pipeline

sequenceDiagram
    participant YAML
    participant State
    participant MaxMind
    participant Proxmox
    participant Providers
    participant AutoCert
    participant Notif
    participant Entrypoint

    YAML->>State: Parse & Validate
    par Initialize in parallel
        State->>MaxMind: Initialize
        State->>Proxmox: Initialize
    and
        State->>Providers: Load Route Providers
        Providers->>State: Store Providers
    end
    State->>AutoCert: Initialize
    State->>Notif: Initialize
    State->>Entrypoint: Configure
    State->>State: Start Providers

Thread safety model

var stateMu sync.RWMutex

func GetState() config.State {
    return config.ActiveState.Load()
}

func SetState(state config.State) {
    stateMu.Lock()
    defer stateMu.Unlock()
    config.ActiveState.Store(state)
}

Uses sync.RWMutex for write synchronization and sync/atomic for read operations.

Configuration Surface

Config sources

Configuration is loaded from config/config.yml.

Hot-reloading

Configuration supports hot-reloading via editing config/config.yml.

Dependency and Integration Map

Internal dependencies

  • internal/acl - Access control configuration
  • internal/autocert - SSL certificate management
  • internal/entrypoint - HTTP entrypoint setup (now via interface)
  • internal/route/provider - Route providers (Docker, file, agent)
  • internal/maxmind - GeoIP configuration
  • internal/notif - Notification providers
  • internal/proxmox - LXC container management
  • internal/homepage/types - Dashboard configuration
  • internal/api - REST API servers
  • internal/metrics/systeminfo - System metrics polling
  • internal/metrics/uptime - Uptime tracking
  • github.com/yusing/goutils/task - Object lifecycle management

External dependencies

  • github.com/goccy/go-yaml - YAML parsing
  • github.com/puzpuzpuz/xsync/v4 - Concurrent map

Integration points

// API uses config/query to access state
providers := statequery.RouteProviderList()

// Route providers access config state
for _, p := range config.GetState().IterProviders() {
    // Process provider
}

Observability

Logs

  • Configuration parsing and validation errors
  • Provider initialization results
  • Route loading summary
  • Full configuration dump (at debug level)

Metrics

No metrics are currently exposed.

Security Considerations

  • Configuration file permissions should be restricted (contains secrets)
  • TLS certificates are loaded from files specified in config
  • Agent credentials are passed via configuration
  • No secrets are logged (except in debug mode with full config dump)

Failure Modes and Recovery

Failure Behavior Recovery
Invalid YAML Init returns error Fix YAML syntax
Missing required fields Validation fails Add required fields
Provider initialization fails Error aggregated and returned Fix provider configuration
Duplicate provider key Error logged, first provider kept Rename provider
Route loading fails Error aggregated, other routes load Fix route configuration

Performance Characteristics

  • Providers are loaded concurrently
  • Routes are loaded concurrently per provider
  • State access is lock-free for reads
  • Atomic pointer for state swap

Usage Examples

Loading configuration

state := config.NewState()
err := state.InitFromFile("config.yml")
if err != nil {
    log.Fatal(err)
}

config.SetState(state)

Accessing configuration

if config.HasState() {
    cfg := config.Value()
    log.Printf("Entrypoint middleware count: %d", len(cfg.Entrypoint.Middlewares))
    log.Printf("Docker providers: %d", len(cfg.Providers.Docker))
}

Iterating providers

for name, provider := range config.GetState().IterProviders() {
    log.Printf("Provider: %s, Routes: %d", name, provider.NumRoutes())
}

Accessing entrypoint handler

state := config.GetState()
// Get entrypoint interface for route management
ep := state.Entrypoint()
// Add routes directly to entrypoint
ep.AddRoute(route)