refactor: Improve configuration loading and blacklist management
- Enhance config loading in `init_config.go` with more robust handling of container environments - Add support for forced blacklist reloading and improved logging - Implement methods to dynamically add IPs and ranges to blacklist - Improve CIDR parsing and validation in blacklist configuration - Optimize config file creation and environment variable handling
- commit
- 411b75655753fcfc0c62512c9fae07340f814a5d
- author
- bamen · · 2025-03-09 08:21 UTC
- parent
- 922efce
- signature
- unsigned
2 files · +130 -24
M internal/common/init_config.go +19 -17
| @@ -268,8 +268,10 @@ func (config *Config) LoadConfig() (*Config, error) { | |||
| 268 | 268 | configFilePath = filepath.Join(configDir, "config.yml") | |
| 269 | 269 | } | |
| 270 | 270 | ||
| 271 | + | configExists := true | |
| 271 | 272 | _, err = os.Stat(configFilePath) | |
| 272 | 273 | if errors.Is(err, fs.ErrNotExist) { | |
| 274 | + | configExists = false | |
| 273 | 275 | fmt.Printf("Config file not found, creating it at %s\n", configFilePath) | |
| 274 | 276 | ||
| 275 | 277 | // Create config dir if it doesn't exist | |
| @@ -314,36 +316,28 @@ func (config *Config) LoadConfig() (*Config, error) { | |||
| 314 | 316 | }, | |
| 315 | 317 | } | |
| 316 | 318 | ||
| 317 | - | // If running in a container, skip the prompt and load from env vars | |
| 319 | + | // If running in a container, skip the prompt | |
| 318 | 320 | if docker.IsRunningInContainer() { | |
| 319 | 321 | config.ContainerEngine.Podman = false | |
| 320 | - | fmt.Println("Running in container, loading configuration from environment variables...") | |
| 321 | - | loadConfigFromEnv(config) | |
| 322 | - | ||
| 323 | 322 | // Handle the Podman case if the GORDON_CONTAINER_PODMAN env var is set to true | |
| 324 | 323 | if os.Getenv("GORDON_CONTAINER_PODMAN") == "true" { | |
| 325 | 324 | config.ContainerEngine.Podman = true | |
| 326 | 325 | fmt.Println("Podman container engine detected via environment variable") | |
| 327 | 326 | } | |
| 328 | 327 | } | |
| 329 | - | ||
| 330 | - | err = config.SaveConfig() | |
| 331 | - | if err != nil { | |
| 332 | - | return nil, fmt.Errorf("error saving new configuration: %w", err) | |
| 333 | - | } | |
| 334 | - | return config, nil | |
| 335 | - | } | |
| 336 | - | ||
| 337 | - | if err != nil { | |
| 328 | + | } else if err != nil { | |
| 338 | 329 | return nil, fmt.Errorf("error checking configuration file: %w", err) | |
| 339 | 330 | } | |
| 340 | 331 | ||
| 341 | - | err = readAndUnmarshalConfig(os.DirFS(configDir), "config.yml", config) | |
| 342 | - | if err != nil { | |
| 343 | - | return nil, err | |
| 332 | + | // If config file exists, read it | |
| 333 | + | if configExists { | |
| 334 | + | err = readAndUnmarshalConfig(os.DirFS(filepath.Dir(configFilePath)), filepath.Base(configFilePath), config) | |
| 335 | + | if err != nil { | |
| 336 | + | return nil, err | |
| 337 | + | } | |
| 344 | 338 | } | |
| 345 | 339 | ||
| 346 | - | // If running in a container, override with env vars | |
| 340 | + | // Only load environment variables once, after either creating a new config or reading an existing one | |
| 347 | 341 | if docker.IsRunningInContainer() { | |
| 348 | 342 | fmt.Println("Running in container, overriding configuration with environment variables...") | |
| 349 | 343 | loadConfigFromEnv(config) | |
| @@ -356,6 +350,14 @@ func (config *Config) LoadConfig() (*Config, error) { | |||
| 356 | 350 | config.Build.RunEnv = "prod" | |
| 357 | 351 | } | |
| 358 | 352 | ||
| 353 | + | // If we created a new config, save it | |
| 354 | + | if !configExists { | |
| 355 | + | err = config.SaveConfig() | |
| 356 | + | if err != nil { | |
| 357 | + | return nil, fmt.Errorf("error saving new configuration: %w", err) | |
| 358 | + | } | |
| 359 | + | } | |
| 360 | + | ||
| 359 | 361 | // Debug output to verify config was loaded correctly | |
| 360 | 362 | fmt.Printf("Loaded configuration from %s\n", configFilePath) | |
| 361 | 363 | fmt.Printf("Container engine config - Docker socket: %s, Podman socket: %s, Using Podman: %t, Network: %s\n", | |
| @@ -268,8 +268,10 @@ func (config *Config) LoadConfig() (*Config, error) { | |||||
| 268 | configFilePath = filepath.Join(configDir, "config.yml") | 268 | configFilePath = filepath.Join(configDir, "config.yml") | ||
| 269 | } | 269 | } | ||
| 270 | 270 | ||||
| 271 | + | configExists := true | |||
| 271 | _, err = os.Stat(configFilePath) | 272 | _, err = os.Stat(configFilePath) | ||
| 272 | if errors.Is(err, fs.ErrNotExist) { | 273 | if errors.Is(err, fs.ErrNotExist) { | ||
| 274 | + | configExists = false | |||
| 273 | fmt.Printf("Config file not found, creating it at %s\n", configFilePath) | 275 | fmt.Printf("Config file not found, creating it at %s\n", configFilePath) | ||
| 274 | 276 | ||||
| 275 | // Create config dir if it doesn't exist | 277 | // Create config dir if it doesn't exist | ||
| @@ -314,36 +316,28 @@ func (config *Config) LoadConfig() (*Config, error) { | |||||
| 314 | }, | 316 | }, | ||
| 315 | } | 317 | } | ||
| 316 | 318 | ||||
| 317 | - | // If running in a container, skip the prompt and load from env vars | 319 | + | // If running in a container, skip the prompt |
| 318 | if docker.IsRunningInContainer() { | 320 | if docker.IsRunningInContainer() { | ||
| 319 | config.ContainerEngine.Podman = false | 321 | config.ContainerEngine.Podman = false | ||
| 320 | - | fmt.Println("Running in container, loading configuration from environment variables...") | |||
| 321 | - | loadConfigFromEnv(config) | |||
| 322 | - | ||||
| 323 | // Handle the Podman case if the GORDON_CONTAINER_PODMAN env var is set to true | 322 | // Handle the Podman case if the GORDON_CONTAINER_PODMAN env var is set to true | ||
| 324 | if os.Getenv("GORDON_CONTAINER_PODMAN") == "true" { | 323 | if os.Getenv("GORDON_CONTAINER_PODMAN") == "true" { | ||
| 325 | config.ContainerEngine.Podman = true | 324 | config.ContainerEngine.Podman = true | ||
| 326 | fmt.Println("Podman container engine detected via environment variable") | 325 | fmt.Println("Podman container engine detected via environment variable") | ||
| 327 | } | 326 | } | ||
| 328 | } | 327 | } | ||
| 329 | - | 328 | + | } else if err != nil { | |
| 330 | - | err = config.SaveConfig() | |||
| 331 | - | if err != nil { | |||
| 332 | - | return nil, fmt.Errorf("error saving new configuration: %w", err) | |||
| 333 | - | } | |||
| 334 | - | return config, nil | |||
| 335 | - | } | |||
| 336 | - | ||||
| 337 | - | if err != nil { | |||
| 338 | return nil, fmt.Errorf("error checking configuration file: %w", err) | 329 | return nil, fmt.Errorf("error checking configuration file: %w", err) | ||
| 339 | } | 330 | } | ||
| 340 | 331 | ||||
| 341 | - | err = readAndUnmarshalConfig(os.DirFS(configDir), "config.yml", config) | 332 | + | // If config file exists, read it |
| 342 | - | if err != nil { | 333 | + | if configExists { |
| 343 | - | return nil, err | 334 | + | err = readAndUnmarshalConfig(os.DirFS(filepath.Dir(configFilePath)), filepath.Base(configFilePath), config) |
| 335 | + | if err != nil { | |||
| 336 | + | return nil, err | |||
| 337 | + | } | |||
| 344 | } | 338 | } | ||
| 345 | 339 | ||||
| 346 | - | // If running in a container, override with env vars | 340 | + | // Only load environment variables once, after either creating a new config or reading an existing one |
| 347 | if docker.IsRunningInContainer() { | 341 | if docker.IsRunningInContainer() { | ||
| 348 | fmt.Println("Running in container, overriding configuration with environment variables...") | 342 | fmt.Println("Running in container, overriding configuration with environment variables...") | ||
| 349 | loadConfigFromEnv(config) | 343 | loadConfigFromEnv(config) | ||
| @@ -356,6 +350,14 @@ func (config *Config) LoadConfig() (*Config, error) { | |||||
| 356 | config.Build.RunEnv = "prod" | 350 | config.Build.RunEnv = "prod" | ||
| 357 | } | 351 | } | ||
| 358 | 352 | ||||
| 353 | + | // If we created a new config, save it | |||
| 354 | + | if !configExists { | |||
| 355 | + | err = config.SaveConfig() | |||
| 356 | + | if err != nil { | |||
| 357 | + | return nil, fmt.Errorf("error saving new configuration: %w", err) | |||
| 358 | + | } | |||
| 359 | + | } | |||
| 360 | + | ||||
| 359 | // Debug output to verify config was loaded correctly | 361 | // Debug output to verify config was loaded correctly | ||
| 360 | fmt.Printf("Loaded configuration from %s\n", configFilePath) | 362 | fmt.Printf("Loaded configuration from %s\n", configFilePath) | ||
| 361 | fmt.Printf("Container engine config - Docker socket: %s, Podman socket: %s, Using Podman: %t, Network: %s\n", | 363 | fmt.Printf("Container engine config - Docker socket: %s, Podman socket: %s, Using Podman: %t, Network: %s\n", | ||
M internal/proxy/blacklist.go +111 -7
| @@ -1,9 +1,11 @@ | |||
| 1 | 1 | package proxy | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 4 | 5 | "net" | |
| 5 | 6 | "os" | |
| 6 | 7 | "path/filepath" | |
| 8 | + | "strings" | |
| 7 | 9 | "sync" | |
| 8 | 10 | "time" | |
| 9 | 11 | ||
| @@ -71,9 +73,11 @@ func (b *BlacklistConfig) Load() error { | |||
| 71 | 73 | return err | |
| 72 | 74 | } | |
| 73 | 75 | ||
| 74 | - | // Check if file hasn't been modified | |
| 75 | - | if fileInfo.ModTime() == b.LastMod { | |
| 76 | - | return nil // File hasn't changed | |
| 76 | + | // Check if file hasn't been modified in the last 10 seconds | |
| 77 | + | // This ensures we check more frequently, but not on every single request | |
| 78 | + | forceReload := time.Since(b.LastMod) > 10*time.Second | |
| 79 | + | if fileInfo.ModTime() == b.LastMod && !forceReload { | |
| 80 | + | return nil // File hasn't changed and we're within the refresh window | |
| 77 | 81 | } | |
| 78 | 82 | ||
| 79 | 83 | // Read the blacklist file | |
| @@ -91,6 +95,11 @@ func (b *BlacklistConfig) Load() error { | |||
| 91 | 95 | // Parse IP ranges into networks | |
| 92 | 96 | networks := make([]*net.IPNet, 0, len(tempConfig.Ranges)) | |
| 93 | 97 | for _, cidr := range tempConfig.Ranges { | |
| 98 | + | // Add /32 to plain IPs if not already a CIDR | |
| 99 | + | if !strings.Contains(cidr, "/") { | |
| 100 | + | cidr = cidr + "/32" | |
| 101 | + | } | |
| 102 | + | ||
| 94 | 103 | _, network, err := net.ParseCIDR(cidr) | |
| 95 | 104 | if err != nil { | |
| 96 | 105 | log.Warn("Invalid CIDR in blacklist", "cidr", cidr, "error", err) | |
| @@ -105,10 +114,17 @@ func (b *BlacklistConfig) Load() error { | |||
| 105 | 114 | b.Networks = networks | |
| 106 | 115 | b.LastMod = fileInfo.ModTime() | |
| 107 | 116 | ||
| 108 | - | log.Info("Loaded blacklist", | |
| 109 | - | "ips", len(b.IPs), | |
| 110 | - | "ranges", len(b.Ranges), | |
| 111 | - | "last_modified", b.LastMod.Format(time.RFC3339)) | |
| 117 | + | // If the file has actually changed, log about it | |
| 118 | + | if fileInfo.ModTime() != b.LastMod { | |
| 119 | + | log.Info("Loaded blacklist", | |
| 120 | + | "ips", len(b.IPs), | |
| 121 | + | "ranges", len(b.Ranges), | |
| 122 | + | "last_modified", b.LastMod.Format(time.RFC3339)) | |
| 123 | + | } else if forceReload { | |
| 124 | + | log.Debug("Reloaded blacklist (forced refresh)", | |
| 125 | + | "ips", len(b.IPs), | |
| 126 | + | "ranges", len(b.Ranges)) | |
| 127 | + | } | |
| 112 | 128 | ||
| 113 | 129 | return nil | |
| 114 | 130 | } | |
| @@ -146,3 +162,91 @@ func (b *BlacklistConfig) IsBlocked(ip string) bool { | |||
| 146 | 162 | ||
| 147 | 163 | return false | |
| 148 | 164 | } | |
| 165 | + | ||
| 166 | + | // AddIP adds an IP to the blacklist immediately | |
| 167 | + | func (b *BlacklistConfig) AddIP(ip string) error { | |
| 168 | + | b.mu.Lock() | |
| 169 | + | defer b.mu.Unlock() | |
| 170 | + | ||
| 171 | + | // Read the current file | |
| 172 | + | data, err := os.ReadFile(b.path) | |
| 173 | + | if err != nil { | |
| 174 | + | return err | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | // Parse the current data | |
| 178 | + | var config BlacklistConfig | |
| 179 | + | if err := yaml.Unmarshal(data, &config); err != nil { | |
| 180 | + | return err | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | // Check if the IP is already in the list | |
| 184 | + | for _, blockedIP := range config.IPs { | |
| 185 | + | if blockedIP == ip { | |
| 186 | + | return nil // Already in the list | |
| 187 | + | } | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // Add the IP to the list | |
| 191 | + | config.IPs = append(config.IPs, ip) | |
| 192 | + | ||
| 193 | + | // Write the updated config back to the file | |
| 194 | + | newData, err := yaml.Marshal(config) | |
| 195 | + | if err != nil { | |
| 196 | + | return err | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | if err := os.WriteFile(b.path, newData, 0644); err != nil { | |
| 200 | + | return err | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // Force a reload | |
| 204 | + | return b.Load() | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | // AddRange adds an IP range to the blacklist immediately | |
| 208 | + | func (b *BlacklistConfig) AddRange(cidr string) error { | |
| 209 | + | // Make sure the CIDR is valid | |
| 210 | + | _, _, err := net.ParseCIDR(cidr) | |
| 211 | + | if err != nil { | |
| 212 | + | return fmt.Errorf("invalid CIDR: %w", err) | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | b.mu.Lock() | |
| 216 | + | defer b.mu.Unlock() | |
| 217 | + | ||
| 218 | + | // Read the current file | |
| 219 | + | data, err := os.ReadFile(b.path) | |
| 220 | + | if err != nil { | |
| 221 | + | return err | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | // Parse the current data | |
| 225 | + | var config BlacklistConfig | |
| 226 | + | if err := yaml.Unmarshal(data, &config); err != nil { | |
| 227 | + | return err | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | // Check if the range is already in the list | |
| 231 | + | for _, blockedRange := range config.Ranges { | |
| 232 | + | if blockedRange == cidr { | |
| 233 | + | return nil // Already in the list | |
| 234 | + | } | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // Add the range to the list | |
| 238 | + | config.Ranges = append(config.Ranges, cidr) | |
| 239 | + | ||
| 240 | + | // Write the updated config back to the file | |
| 241 | + | newData, err := yaml.Marshal(config) | |
| 242 | + | if err != nil { | |
| 243 | + | return err | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | if err := os.WriteFile(b.path, newData, 0644); err != nil { | |
| 247 | + | return err | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | // Force a reload | |
| 251 | + | return b.Load() | |
| 252 | + | } | |
| @@ -1,9 +1,11 @@ | |||||
| 1 | package proxy | 1 | package proxy | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "fmt" | |||
| 4 | "net" | 5 | "net" | ||
| 5 | "os" | 6 | "os" | ||
| 6 | "path/filepath" | 7 | "path/filepath" | ||
| 8 | + | "strings" | |||
| 7 | "sync" | 9 | "sync" | ||
| 8 | "time" | 10 | "time" | ||
| 9 | 11 | ||||
| @@ -71,9 +73,11 @@ func (b *BlacklistConfig) Load() error { | |||||
| 71 | return err | 73 | return err | ||
| 72 | } | 74 | } | ||
| 73 | 75 | ||||
| 74 | - | // Check if file hasn't been modified | 76 | + | // Check if file hasn't been modified in the last 10 seconds |
| 75 | - | if fileInfo.ModTime() == b.LastMod { | 77 | + | // This ensures we check more frequently, but not on every single request |
| 76 | - | return nil // File hasn't changed | 78 | + | forceReload := time.Since(b.LastMod) > 10*time.Second |
| 79 | + | if fileInfo.ModTime() == b.LastMod && !forceReload { | |||
| 80 | + | return nil // File hasn't changed and we're within the refresh window | |||
| 77 | } | 81 | } | ||
| 78 | 82 | ||||
| 79 | // Read the blacklist file | 83 | // Read the blacklist file | ||
| @@ -91,6 +95,11 @@ func (b *BlacklistConfig) Load() error { | |||||
| 91 | // Parse IP ranges into networks | 95 | // Parse IP ranges into networks | ||
| 92 | networks := make([]*net.IPNet, 0, len(tempConfig.Ranges)) | 96 | networks := make([]*net.IPNet, 0, len(tempConfig.Ranges)) | ||
| 93 | for _, cidr := range tempConfig.Ranges { | 97 | for _, cidr := range tempConfig.Ranges { | ||
| 98 | + | // Add /32 to plain IPs if not already a CIDR | |||
| 99 | + | if !strings.Contains(cidr, "/") { | |||
| 100 | + | cidr = cidr + "/32" | |||
| 101 | + | } | |||
| 102 | + | ||||
| 94 | _, network, err := net.ParseCIDR(cidr) | 103 | _, network, err := net.ParseCIDR(cidr) | ||
| 95 | if err != nil { | 104 | if err != nil { | ||
| 96 | log.Warn("Invalid CIDR in blacklist", "cidr", cidr, "error", err) | 105 | log.Warn("Invalid CIDR in blacklist", "cidr", cidr, "error", err) | ||
| @@ -105,10 +114,17 @@ func (b *BlacklistConfig) Load() error { | |||||
| 105 | b.Networks = networks | 114 | b.Networks = networks | ||
| 106 | b.LastMod = fileInfo.ModTime() | 115 | b.LastMod = fileInfo.ModTime() | ||
| 107 | 116 | ||||
| 108 | - | log.Info("Loaded blacklist", | 117 | + | // If the file has actually changed, log about it |
| 109 | - | "ips", len(b.IPs), | 118 | + | if fileInfo.ModTime() != b.LastMod { |
| 110 | - | "ranges", len(b.Ranges), | 119 | + | log.Info("Loaded blacklist", |
| 111 | - | "last_modified", b.LastMod.Format(time.RFC3339)) | 120 | + | "ips", len(b.IPs), |
| 121 | + | "ranges", len(b.Ranges), | |||
| 122 | + | "last_modified", b.LastMod.Format(time.RFC3339)) | |||
| 123 | + | } else if forceReload { | |||
| 124 | + | log.Debug("Reloaded blacklist (forced refresh)", | |||
| 125 | + | "ips", len(b.IPs), | |||
| 126 | + | "ranges", len(b.Ranges)) | |||
| 127 | + | } | |||
| 112 | 128 | ||||
| 113 | return nil | 129 | return nil | ||
| 114 | } | 130 | } | ||
| @@ -146,3 +162,91 @@ func (b *BlacklistConfig) IsBlocked(ip string) bool { | |||||
| 146 | 162 | ||||
| 147 | return false | 163 | return false | ||
| 148 | } | 164 | } | ||
| 165 | + | ||||
| 166 | + | // AddIP adds an IP to the blacklist immediately | |||
| 167 | + | func (b *BlacklistConfig) AddIP(ip string) error { | |||
| 168 | + | b.mu.Lock() | |||
| 169 | + | defer b.mu.Unlock() | |||
| 170 | + | ||||
| 171 | + | // Read the current file | |||
| 172 | + | data, err := os.ReadFile(b.path) | |||
| 173 | + | if err != nil { | |||
| 174 | + | return err | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | // Parse the current data | |||
| 178 | + | var config BlacklistConfig | |||
| 179 | + | if err := yaml.Unmarshal(data, &config); err != nil { | |||
| 180 | + | return err | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | // Check if the IP is already in the list | |||
| 184 | + | for _, blockedIP := range config.IPs { | |||
| 185 | + | if blockedIP == ip { | |||
| 186 | + | return nil // Already in the list | |||
| 187 | + | } | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // Add the IP to the list | |||
| 191 | + | config.IPs = append(config.IPs, ip) | |||
| 192 | + | ||||
| 193 | + | // Write the updated config back to the file | |||
| 194 | + | newData, err := yaml.Marshal(config) | |||
| 195 | + | if err != nil { | |||
| 196 | + | return err | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | if err := os.WriteFile(b.path, newData, 0644); err != nil { | |||
| 200 | + | return err | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | // Force a reload | |||
| 204 | + | return b.Load() | |||
| 205 | + | } | |||
| 206 | + | ||||
| 207 | + | // AddRange adds an IP range to the blacklist immediately | |||
| 208 | + | func (b *BlacklistConfig) AddRange(cidr string) error { | |||
| 209 | + | // Make sure the CIDR is valid | |||
| 210 | + | _, _, err := net.ParseCIDR(cidr) | |||
| 211 | + | if err != nil { | |||
| 212 | + | return fmt.Errorf("invalid CIDR: %w", err) | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | b.mu.Lock() | |||
| 216 | + | defer b.mu.Unlock() | |||
| 217 | + | ||||
| 218 | + | // Read the current file | |||
| 219 | + | data, err := os.ReadFile(b.path) | |||
| 220 | + | if err != nil { | |||
| 221 | + | return err | |||
| 222 | + | } | |||
| 223 | + | ||||
| 224 | + | // Parse the current data | |||
| 225 | + | var config BlacklistConfig | |||
| 226 | + | if err := yaml.Unmarshal(data, &config); err != nil { | |||
| 227 | + | return err | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | // Check if the range is already in the list | |||
| 231 | + | for _, blockedRange := range config.Ranges { | |||
| 232 | + | if blockedRange == cidr { | |||
| 233 | + | return nil // Already in the list | |||
| 234 | + | } | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | // Add the range to the list | |||
| 238 | + | config.Ranges = append(config.Ranges, cidr) | |||
| 239 | + | ||||
| 240 | + | // Write the updated config back to the file | |||
| 241 | + | newData, err := yaml.Marshal(config) | |||
| 242 | + | if err != nil { | |||
| 243 | + | return err | |||
| 244 | + | } | |||
| 245 | + | ||||
| 246 | + | if err := os.WriteFile(b.path, newData, 0644); err != nil { | |||
| 247 | + | return err | |||
| 248 | + | } | |||
| 249 | + | ||||
| 250 | + | // Force a reload | |||
| 251 | + | return b.Load() | |||
| 252 | + | } | |||