services -> domains

This commit is contained in:
2026-07-10 20:46:22 +00:00
parent 3c99d26830
commit 79c0c32b98
45 changed files with 1447 additions and 1270 deletions

View File

@@ -79,8 +79,10 @@ type DHCPStaticLease struct {
Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`
}
// GlobalConfig represents the main configuration structure
type GlobalConfig struct {
// State represents Wild Central's runtime state — operator settings, networking
// configuration, DDNS, DHCP, etc. Persisted in {dataDir}/state.yaml and mutated
// via the API. This is NOT boot-time config (which comes from env vars).
type State struct {
Operator struct {
Email string `yaml:"email,omitempty" json:"email,omitempty"`
} `yaml:"operator,omitempty" json:"operator,omitempty"`
@@ -109,22 +111,21 @@ type GlobalConfig struct {
ExtraPorts []ExtraPort `yaml:"extraPorts,omitempty" json:"extraPorts,omitempty"`
} `yaml:"nftables,omitempty" json:"nftables,omitempty"`
DDNS struct {
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
Records []string `yaml:"records,omitempty" json:"records,omitempty"`
IntervalMinutes int `yaml:"intervalMinutes,omitempty" json:"intervalMinutes,omitempty"`
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
IntervalMinutes int `yaml:"intervalMinutes,omitempty" json:"intervalMinutes,omitempty"`
} `yaml:"ddns,omitempty" json:"ddns,omitempty"`
} `yaml:"cloud,omitempty" json:"cloud,omitempty"`
}
// LoadState loads state from the specified path
func LoadState(configPath string) (*GlobalConfig, error) {
func LoadState(configPath string) (*State, error) {
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("reading config file %s: %w", configPath, err)
}
config := &GlobalConfig{}
config := &State{}
if err := yaml.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("parsing config file: %w", err)
}
@@ -133,7 +134,7 @@ func LoadState(configPath string) (*GlobalConfig, error) {
}
// SaveState saves the state to the specified path
func SaveState(config *GlobalConfig, configPath string) error {
func SaveState(config *State, configPath string) error {
// Ensure the directory exists
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
return fmt.Errorf("creating config directory: %w", err)
@@ -148,7 +149,7 @@ func SaveState(config *GlobalConfig, configPath string) error {
}
// IsEmpty checks if the configuration is empty or uninitialized
func (c *GlobalConfig) IsEmpty() bool {
func (c *State) IsEmpty() bool {
if c == nil {
return true
}

View File

@@ -14,7 +14,7 @@ func TestLoadState(t *testing.T) {
tests := []struct {
name string
stateYAML string
verify func(t *testing.T, config *GlobalConfig)
verify func(t *testing.T, config *State)
wantErr bool
}{
{
@@ -25,7 +25,7 @@ cloud:
central:
domain: "central.example.com"
`,
verify: func(t *testing.T, config *GlobalConfig) {
verify: func(t *testing.T, config *State) {
if config.Operator.Email != "admin@example.com" {
t.Error("operator email not loaded correctly")
}
@@ -40,7 +40,7 @@ cloud:
stateYAML: `operator:
email: "admin@example.com"
`,
verify: func(t *testing.T, config *GlobalConfig) {
verify: func(t *testing.T, config *State) {
if config.Operator.Email != "admin@example.com" {
t.Error("operator email not loaded correctly")
}
@@ -50,7 +50,7 @@ cloud:
{
name: "loads empty state",
stateYAML: "{}\n",
verify: func(t *testing.T, config *GlobalConfig) {
verify: func(t *testing.T, config *State) {
if config.Operator.Email != "" {
t.Error("expected empty operator email")
}
@@ -139,13 +139,13 @@ func TestLoadState_Errors(t *testing.T) {
func TestSaveState(t *testing.T) {
tests := []struct {
name string
config *GlobalConfig
config *State
verify func(t *testing.T, statePath string)
}{
{
name: "saves complete state",
config: func() *GlobalConfig {
cfg := &GlobalConfig{}
config: func() *State {
cfg := &State{}
cfg.Operator.Email = "admin@example.com"
cfg.Cloud.Central.Domain = "central.example.com"
return cfg
@@ -166,7 +166,7 @@ func TestSaveState(t *testing.T) {
},
{
name: "saves empty state",
config: &GlobalConfig{},
config: &State{},
verify: func(t *testing.T, statePath string) {
if _, err := os.Stat(statePath); os.IsNotExist(err) {
t.Error("state file not created")
@@ -221,7 +221,7 @@ func TestSaveState_CreatesDirectory(t *testing.T) {
tempDir := t.TempDir()
statePath := filepath.Join(tempDir, "nested", "dirs", "state.yaml")
config := &GlobalConfig{}
config := &State{}
err := SaveState(config, statePath)
if err != nil {
t.Fatalf("SaveState failed: %v", err)
@@ -238,11 +238,11 @@ func TestSaveState_CreatesDirectory(t *testing.T) {
}
}
// Test: GlobalConfig.IsEmpty checks if config is empty
func TestGlobalConfig_IsEmpty(t *testing.T) {
// Test: State.IsEmpty checks if config is empty
func TestState_IsEmpty(t *testing.T) {
tests := []struct {
name string
config *GlobalConfig
config *State
want bool
}{
{
@@ -252,13 +252,13 @@ func TestGlobalConfig_IsEmpty(t *testing.T) {
},
{
name: "default config is empty",
config: &GlobalConfig{},
config: &State{},
want: true,
},
{
name: "config with only central domain is not empty",
config: func() *GlobalConfig {
cfg := &GlobalConfig{}
config: func() *State {
cfg := &State{}
cfg.Cloud.Central.Domain = "central.example.com"
return cfg
}(),
@@ -266,8 +266,8 @@ func TestGlobalConfig_IsEmpty(t *testing.T) {
},
{
name: "config with only operator email is not empty",
config: func() *GlobalConfig {
cfg := &GlobalConfig{}
config: func() *State {
cfg := &State{}
cfg.Operator.Email = "admin@example.com"
return cfg
}(),
@@ -275,8 +275,8 @@ func TestGlobalConfig_IsEmpty(t *testing.T) {
},
{
name: "config with all fields is not empty",
config: func() *GlobalConfig {
cfg := &GlobalConfig{}
config: func() *State {
cfg := &State{}
cfg.Cloud.Central.Domain = "central.example.com"
cfg.Operator.Email = "admin@example.com"
return cfg
@@ -477,12 +477,12 @@ func TestSaveCloudConfig(t *testing.T) {
}
// Test: Round-trip save and load preserves data
func TestGlobalConfig_RoundTrip(t *testing.T) {
func TestState_RoundTrip(t *testing.T) {
tempDir := t.TempDir()
statePath := filepath.Join(tempDir, "state.yaml")
// Create config with all fields
original := &GlobalConfig{}
original := &State{}
original.Operator.Email = "admin@example.com"
original.Cloud.Central.Domain = "central.example.com"

View File

@@ -10,7 +10,7 @@ import (
func TestExtraPortsJSONDecode_Legacy(t *testing.T) {
// Legacy format: plain integer array
input := `{"cloud":{"nftables":{"extraPorts":[22]}}}`
var cfg GlobalConfig
var cfg State
if err := json.Unmarshal([]byte(input), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
@@ -22,7 +22,7 @@ func TestExtraPortsJSONDecode_Legacy(t *testing.T) {
func TestExtraPortsJSONDecode_Structured(t *testing.T) {
// New structured format
input := `{"cloud":{"nftables":{"extraPorts":[{"port":22,"protocol":"tcp","label":"SSH"},{"port":5353,"protocol":"udp"}]}}}`
var cfg GlobalConfig
var cfg State
if err := json.Unmarshal([]byte(input), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
@@ -38,7 +38,7 @@ func TestExtraPortsJSONDecode_Structured(t *testing.T) {
}
func TestExtraPortsJSONEncode(t *testing.T) {
var cfg GlobalConfig
var cfg State
cfg.Cloud.Nftables.ExtraPorts = []ExtraPort{{Port: 22, Protocol: "tcp", Label: "SSH"}}
out, err := json.Marshal(cfg)
if err != nil {
@@ -68,7 +68,7 @@ func TestExtraPortsJSONEncode(t *testing.T) {
func TestExtraPortsYAML_LegacyRoundTrip(t *testing.T) {
// Legacy YAML format with plain integers
input := "cloud:\n nftables:\n extraPorts:\n - 22\n - 8080\n"
var cfg GlobalConfig
var cfg State
if err := yaml.Unmarshal([]byte(input), &cfg); err != nil {
t.Fatalf("yaml unmarshal: %v", err)
}
@@ -82,7 +82,7 @@ func TestExtraPortsYAML_LegacyRoundTrip(t *testing.T) {
func TestExtraPortsYAML_StructuredRoundTrip(t *testing.T) {
input := "cloud:\n nftables:\n extraPorts:\n - port: 22\n protocol: tcp\n label: SSH\n - port: 5353\n protocol: udp\n"
var cfg GlobalConfig
var cfg State
if err := yaml.Unmarshal([]byte(input), &cfg); err != nil {
t.Fatalf("yaml unmarshal: %v", err)
}