services -> domains
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user