YAML only parsing, set defaults and minimums, included worker pool count
This commit is contained in:
parent
eac910b8e3
commit
62b05bc920
1 changed files with 105 additions and 31 deletions
|
|
@ -1,64 +1,99 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DEFAULT_FREQUENCY = time.Hour * 1
|
const DEFAULT_FREQUENCY = time.Hour * 1 // default to updating every hour
|
||||||
|
const MINIMUM_FREQUENCY = time.Minute * 1 // minimum update frequency is 1 minute
|
||||||
|
const DEFAULT_TIMEOUT = time.Second * 10 // default timeout for HTTP requests
|
||||||
|
const MINIMUM_TIMEOUT = time.Second * 1 // minimum timeout for HTTP requests
|
||||||
|
const DEFAULT_WORKER_COUNT = 10 // default number of concurrent workers
|
||||||
|
const MINIMUM_WORKER_COUNT = 1 // minimum number of concurrent workers
|
||||||
|
const MAXIMUM_WORKER_COUNT = 100 // maximum number of concurrent workers
|
||||||
|
|
||||||
type Domain struct {
|
type Domain struct {
|
||||||
Hostname string `json:"hostname" yaml:"hostname"`
|
Hostname string `yaml:"hostname"` // FQDN of the domain to update
|
||||||
Proxied *bool `json:"proxied" yaml:"proxied"`
|
Proxied *bool `yaml:"proxied"` // Whether the record is proxied through CloudFlare, nil = leave unchanged
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ZoneID string `json:"zone_id" yaml:"zone_id"` // CloudFlare Zone ID
|
ZoneID string `yaml:"zone_id"` // CloudFlare Zone ID
|
||||||
Token string `json:"token" yaml:"token"` // CloudFlare zone-scoped token (read/write)
|
Token string `yaml:"token"` // CloudFlare zone-scoped token (read/write)
|
||||||
Frequency time.Duration `json:"frequency" yaml:"frequency"` // Frequency at which to update the domains
|
Frequency time.Duration `yaml:"frequency"` // Frequency at which to update the domains
|
||||||
Verbose bool `json:"verbose" yaml:"verbose"` // Verbose logging output
|
Verbose bool `yaml:"verbose"` // Verbose logging output
|
||||||
IPv4 bool `json:"ipv4" yaml:"ipv4"` // use IPv4 A records
|
IPv4 *bool `yaml:"ipv4"` // use IPv4 A records
|
||||||
IPv6 bool `json:"ipv6" yaml:"ipv6"` // use IPv6 AAAA records
|
IPv6 *bool `yaml:"ipv6"` // use IPv6 AAAA records
|
||||||
Domains []Domain `json:"domains" yaml:"domains"` // List of domains to update
|
Domains []Domain `yaml:"domains"` // List of domain names to update
|
||||||
|
WorkerCount int `yaml:"worker_count"` // Number of concurrent workers
|
||||||
|
Timeout time.Duration `yaml:"timeout"` // HTTP timeout duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variable names for sensitive config values
|
||||||
|
var reEnv = regexp.MustCompile(`\$\{([A-Za-z_][A-Za-z0-9_]*)\}`)
|
||||||
|
|
||||||
|
// Replace ${VAR} with environment variable $VAR. If any referenced VAR is unset, return an error.
|
||||||
|
func expandEnv(data string) (string, error) {
|
||||||
|
missing := map[string]struct{}{}
|
||||||
|
data = reEnv.ReplaceAllStringFunc(data, func(m string) string {
|
||||||
|
name := reEnv.FindStringSubmatch(m)[1]
|
||||||
|
val, ok := os.LookupEnv(name)
|
||||||
|
if !ok {
|
||||||
|
missing[name] = struct{}{}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
})
|
||||||
|
if len(missing) > 0 {
|
||||||
|
names := make([]string, 0, len(missing))
|
||||||
|
for name := range missing {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("missing environment variables from configuration: %s", strings.Join(names, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(filename string) (*Config, error) {
|
func LoadConfig(filename string) (*Config, error) {
|
||||||
// Read the file content
|
// Read the file content
|
||||||
data, err := os.ReadFile(filename)
|
raw, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not read file: %w", err)
|
return nil, fmt.Errorf("could not read file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data, err := expandEnv(string(raw))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the Config struct
|
// Initialize the Config struct
|
||||||
var config Config
|
var config Config
|
||||||
|
|
||||||
// Check the file extension
|
decoder := yaml.NewDecoder(strings.NewReader(data))
|
||||||
ext := strings.ToLower(filepath.Ext(filename))
|
decoder.SetStrict(true)
|
||||||
switch ext {
|
|
||||||
case ".json":
|
if err := decoder.Decode(&config); err != nil {
|
||||||
// Parse JSON
|
return nil, fmt.Errorf("could not parse config file: %w", err)
|
||||||
if err := json.Unmarshal(data, &config); err != nil {
|
|
||||||
return nil, fmt.Errorf("could not parse JSON config file: %w", err)
|
|
||||||
}
|
|
||||||
case ".yaml", ".yml":
|
|
||||||
// Parse YAML
|
|
||||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
|
||||||
return nil, fmt.Errorf("could not parse YAML config file: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported file extension: %s", ext)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.ZoneID = strings.TrimSpace(config.ZoneID)
|
||||||
if config.ZoneID == "" {
|
if config.ZoneID == "" {
|
||||||
return nil, fmt.Errorf("zone id cannot be empty")
|
return nil, fmt.Errorf("zone id cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.Token = strings.TrimSpace(config.Token)
|
||||||
|
if config.Token == "" {
|
||||||
|
return nil, fmt.Errorf("API token cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
if len(config.Domains) == 0 {
|
if len(config.Domains) == 0 {
|
||||||
return nil, fmt.Errorf("domains list cannot be empty")
|
return nil, fmt.Errorf("domains list cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
@ -66,11 +101,50 @@ func LoadConfig(filename string) (*Config, error) {
|
||||||
if config.Frequency == 0 {
|
if config.Frequency == 0 {
|
||||||
config.Frequency = DEFAULT_FREQUENCY
|
config.Frequency = DEFAULT_FREQUENCY
|
||||||
}
|
}
|
||||||
|
if config.Frequency < MINIMUM_FREQUENCY {
|
||||||
|
log.Warn().Msgf("frequency %s is too low, setting to minimum of %s", config.Frequency.String(), MINIMUM_FREQUENCY.String())
|
||||||
|
config.Frequency = MINIMUM_FREQUENCY
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Timeout == 0 {
|
||||||
|
config.Timeout = DEFAULT_TIMEOUT
|
||||||
|
}
|
||||||
|
if config.Timeout < MINIMUM_TIMEOUT {
|
||||||
|
log.Warn().Msgf("timeout %s is too low, setting to minimum of %s", config.Timeout.String(), MINIMUM_TIMEOUT.String())
|
||||||
|
config.Timeout = MINIMUM_TIMEOUT
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.WorkerCount <= 0 {
|
||||||
|
config.WorkerCount = DEFAULT_WORKER_COUNT
|
||||||
|
}
|
||||||
|
if config.WorkerCount < MINIMUM_WORKER_COUNT {
|
||||||
|
log.Warn().Msgf("worker_count %d is too low, setting to minimum of %d", config.WorkerCount, MINIMUM_WORKER_COUNT)
|
||||||
|
config.WorkerCount = MINIMUM_WORKER_COUNT
|
||||||
|
}
|
||||||
|
if config.WorkerCount > MAXIMUM_WORKER_COUNT {
|
||||||
|
log.Warn().Msgf("worker_count %d is too high, setting to maximum of %d", config.WorkerCount, MAXIMUM_WORKER_COUNT)
|
||||||
|
config.WorkerCount = MAXIMUM_WORKER_COUNT
|
||||||
|
}
|
||||||
|
|
||||||
|
t := true
|
||||||
|
f := false
|
||||||
|
|
||||||
// if neither IPv6 nor IPv6 are explicitly specified, use both
|
// if neither IPv6 nor IPv6 are explicitly specified, use both
|
||||||
if !config.IPv4 && !config.IPv6 {
|
if config.IPv4 == nil && config.IPv6 == nil {
|
||||||
config.IPv4 = true
|
config.IPv4 = &t
|
||||||
config.IPv6 = true
|
config.IPv6 = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.IPv4 == nil {
|
||||||
|
config.IPv4 = &f
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.IPv6 == nil {
|
||||||
|
config.IPv6 = &f
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*config.IPv4 && !*config.IPv6 {
|
||||||
|
return nil, fmt.Errorf("at least one of ipv4 or ipv6 must be enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &config, nil
|
return &config, nil
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue