feat(modules): config loader refactor — YAML → DB → env pipeline
Add ModulesConfig and ModuleToggle types to config.go for explicit module enable/disable in YAML. Add LoadState() that merges state from three sources: 1. Backward-compat YAML fields (auth.enabled, odoo.enabled) 2. Explicit modules.* YAML toggles (override compat) 3. Database module_state table (highest precedence) Validates dependency chain after loading. 5 loader tests. Ref #95
This commit is contained in:
@@ -18,6 +18,25 @@ type Config struct {
|
||||
Odoo OdooConfig `yaml:"odoo"`
|
||||
Auth AuthConfig `yaml:"auth"`
|
||||
Jobs JobsConfig `yaml:"jobs"`
|
||||
Modules ModulesConfig `yaml:"modules"`
|
||||
}
|
||||
|
||||
// ModulesConfig holds explicit enable/disable toggles for optional modules.
|
||||
// A nil pointer means "use the module's default state".
|
||||
type ModulesConfig struct {
|
||||
Auth *ModuleToggle `yaml:"auth"`
|
||||
Projects *ModuleToggle `yaml:"projects"`
|
||||
Audit *ModuleToggle `yaml:"audit"`
|
||||
Odoo *ModuleToggle `yaml:"odoo"`
|
||||
FreeCAD *ModuleToggle `yaml:"freecad"`
|
||||
Jobs *ModuleToggle `yaml:"jobs"`
|
||||
DAG *ModuleToggle `yaml:"dag"`
|
||||
}
|
||||
|
||||
// ModuleToggle holds an optional enabled flag. The pointer allows
|
||||
// distinguishing "not set" (nil) from "explicitly false".
|
||||
type ModuleToggle struct {
|
||||
Enabled *bool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
// AuthConfig holds authentication and authorization settings.
|
||||
|
||||
84
internal/modules/loader.go
Normal file
84
internal/modules/loader.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/kindredsystems/silo/internal/config"
|
||||
)
|
||||
|
||||
// LoadState applies module state from config YAML and database overrides.
|
||||
//
|
||||
// Precedence (highest wins):
|
||||
// 1. Database module_state table
|
||||
// 2. YAML modules.* toggles
|
||||
// 3. Backward-compat YAML fields (auth.enabled, odoo.enabled)
|
||||
// 4. Module defaults (set by NewRegistry)
|
||||
func LoadState(r *Registry, cfg *config.Config, pool *pgxpool.Pool) error {
|
||||
// Step 1: Apply backward-compat top-level YAML fields.
|
||||
// auth.enabled and odoo.enabled existed before the modules section.
|
||||
// Only apply if the new modules.* section doesn't override them.
|
||||
if cfg.Modules.Auth == nil {
|
||||
r.setEnabledUnchecked(Auth, cfg.Auth.Enabled)
|
||||
}
|
||||
if cfg.Modules.Odoo == nil {
|
||||
r.setEnabledUnchecked(Odoo, cfg.Odoo.Enabled)
|
||||
}
|
||||
|
||||
// Step 2: Apply explicit modules.* YAML toggles (override defaults + compat).
|
||||
applyToggle(r, Auth, cfg.Modules.Auth)
|
||||
applyToggle(r, Projects, cfg.Modules.Projects)
|
||||
applyToggle(r, Audit, cfg.Modules.Audit)
|
||||
applyToggle(r, Odoo, cfg.Modules.Odoo)
|
||||
applyToggle(r, FreeCAD, cfg.Modules.FreeCAD)
|
||||
applyToggle(r, Jobs, cfg.Modules.Jobs)
|
||||
applyToggle(r, DAG, cfg.Modules.DAG)
|
||||
|
||||
// Step 3: Apply database overrides (highest precedence).
|
||||
if pool != nil {
|
||||
if err := loadFromDB(r, pool); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Validate the final state.
|
||||
return r.ValidateDependencies()
|
||||
}
|
||||
|
||||
// applyToggle sets a module's state from a YAML ModuleToggle if present.
|
||||
func applyToggle(r *Registry, id string, toggle *config.ModuleToggle) {
|
||||
if toggle == nil || toggle.Enabled == nil {
|
||||
return
|
||||
}
|
||||
r.setEnabledUnchecked(id, *toggle.Enabled)
|
||||
}
|
||||
|
||||
// setEnabledUnchecked sets module state without dependency validation.
|
||||
// Used during loading when the full state is being assembled incrementally.
|
||||
func (r *Registry) setEnabledUnchecked(id string, enabled bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if m, ok := r.modules[id]; ok && !m.Required {
|
||||
m.enabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
// loadFromDB reads module_state rows and applies them to the registry.
|
||||
func loadFromDB(r *Registry, pool *pgxpool.Pool) error {
|
||||
rows, err := pool.Query(context.Background(),
|
||||
`SELECT module_id, enabled FROM module_state`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var enabled bool
|
||||
if err := rows.Scan(&id, &enabled); err != nil {
|
||||
return err
|
||||
}
|
||||
r.setEnabledUnchecked(id, enabled)
|
||||
}
|
||||
return rows.Err()
|
||||
}
|
||||
88
internal/modules/loader_test.go
Normal file
88
internal/modules/loader_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kindredsystems/silo/internal/config"
|
||||
)
|
||||
|
||||
func boolPtr(v bool) *bool { return &v }
|
||||
|
||||
func TestLoadState_DefaultsOnly(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
cfg := &config.Config{}
|
||||
|
||||
if err := LoadState(r, cfg, nil); err != nil {
|
||||
t.Fatalf("LoadState: %v", err)
|
||||
}
|
||||
|
||||
// Auth defaults to true from registry, but cfg.Auth.Enabled is false
|
||||
// (zero value) and backward-compat applies, so auth ends up disabled.
|
||||
if r.IsEnabled(Auth) {
|
||||
t.Error("auth should be disabled (cfg.Auth.Enabled is false by default)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadState_BackwardCompat(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
cfg := &config.Config{}
|
||||
cfg.Auth.Enabled = true
|
||||
cfg.Odoo.Enabled = true
|
||||
|
||||
if err := LoadState(r, cfg, nil); err != nil {
|
||||
t.Fatalf("LoadState: %v", err)
|
||||
}
|
||||
|
||||
if !r.IsEnabled(Auth) {
|
||||
t.Error("auth should be enabled via cfg.Auth.Enabled")
|
||||
}
|
||||
if !r.IsEnabled(Odoo) {
|
||||
t.Error("odoo should be enabled via cfg.Odoo.Enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadState_YAMLModulesOverrideCompat(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
cfg := &config.Config{}
|
||||
cfg.Auth.Enabled = true // compat says enabled
|
||||
cfg.Modules.Auth = &config.ModuleToggle{Enabled: boolPtr(false)} // explicit says disabled
|
||||
|
||||
if err := LoadState(r, cfg, nil); err != nil {
|
||||
t.Fatalf("LoadState: %v", err)
|
||||
}
|
||||
|
||||
if r.IsEnabled(Auth) {
|
||||
t.Error("modules.auth.enabled=false should override auth.enabled=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadState_EnableJobsAndDAG(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
cfg := &config.Config{}
|
||||
cfg.Auth.Enabled = true
|
||||
cfg.Modules.Jobs = &config.ModuleToggle{Enabled: boolPtr(true)}
|
||||
cfg.Modules.DAG = &config.ModuleToggle{Enabled: boolPtr(true)}
|
||||
|
||||
if err := LoadState(r, cfg, nil); err != nil {
|
||||
t.Fatalf("LoadState: %v", err)
|
||||
}
|
||||
|
||||
if !r.IsEnabled(Jobs) {
|
||||
t.Error("jobs should be enabled")
|
||||
}
|
||||
if !r.IsEnabled(DAG) {
|
||||
t.Error("dag should be enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadState_InvalidDependency(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
cfg := &config.Config{}
|
||||
// Auth disabled (default), but enable jobs which depends on auth.
|
||||
cfg.Modules.Jobs = &config.ModuleToggle{Enabled: boolPtr(true)}
|
||||
|
||||
err := LoadState(r, cfg, nil)
|
||||
if err == nil {
|
||||
t.Error("should fail: jobs enabled but auth disabled")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user