132 lines
3.4 KiB
Go
132 lines
3.4 KiB
Go
// Package config loads and validates statuspanel YAML configuration.
|
|
package config
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
// DefaultPath is the default configuration path used by statuspanel.
|
|
DefaultPath = "/etc/statuspanel.yaml"
|
|
)
|
|
|
|
// Duration wraps time.Duration with YAML string unmarshalling.
|
|
type Duration struct {
|
|
time.Duration
|
|
}
|
|
|
|
// UnmarshalYAML decodes duration strings such as "2s" or "500ms".
|
|
func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
|
|
if value.Kind != yaml.ScalarNode {
|
|
return fmt.Errorf("duration must be a scalar")
|
|
}
|
|
parsed, err := time.ParseDuration(value.Value)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing duration %q: %w", value.Value, err)
|
|
}
|
|
d.Duration = parsed
|
|
return nil
|
|
}
|
|
|
|
// Config describes statuspanel runtime configuration.
|
|
type Config struct {
|
|
Interface string `yaml:"interface"`
|
|
Gateway string `yaml:"gateway"`
|
|
Refresh Duration `yaml:"refresh"`
|
|
DNS DNSConfig `yaml:"dns"`
|
|
Ping PingConfig `yaml:"ping"`
|
|
Targets []PingTarget `yaml:"targets"`
|
|
}
|
|
|
|
// DNSConfig describes the DNS probe.
|
|
type DNSConfig struct {
|
|
Server string `yaml:"server"`
|
|
Domain string `yaml:"domain"`
|
|
Timeout Duration `yaml:"timeout"`
|
|
}
|
|
|
|
// PingConfig describes ICMP ping behavior.
|
|
type PingConfig struct {
|
|
Timeout Duration `yaml:"timeout"`
|
|
}
|
|
|
|
// PingTarget describes a host displayed in the reachability table.
|
|
type PingTarget struct {
|
|
Name string `yaml:"name"`
|
|
Address string `yaml:"address"`
|
|
}
|
|
|
|
// Default returns a configuration with safe runtime defaults.
|
|
func Default() Config {
|
|
return Config{
|
|
Refresh: Duration{Duration: 2 * time.Second},
|
|
DNS: DNSConfig{
|
|
Timeout: Duration{Duration: time.Second},
|
|
},
|
|
Ping: PingConfig{
|
|
Timeout: Duration{Duration: time.Second},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Load reads and validates a YAML configuration file.
|
|
func Load(ctx context.Context, path string) (Config, error) {
|
|
if path == "" {
|
|
path = DefaultPath
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return Config{}, fmt.Errorf("checking context before loading config: %w", err)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("reading config %s: %w", path, err)
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return Config{}, fmt.Errorf("checking context after loading config: %w", err)
|
|
}
|
|
|
|
cfg := Default()
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return Config{}, fmt.Errorf("parsing config %s: %w", path, err)
|
|
}
|
|
if err := cfg.Validate(); err != nil {
|
|
return Config{}, fmt.Errorf("validating config %s: %w", path, err)
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
// Validate checks whether the configuration can drive all probes.
|
|
func (c Config) Validate() error {
|
|
var errs []error
|
|
if c.Interface == "" {
|
|
errs = append(errs, errors.New("interface is required"))
|
|
}
|
|
if c.Refresh.Duration <= 0 {
|
|
errs = append(errs, errors.New("refresh must be greater than zero"))
|
|
}
|
|
if c.DNS.Server == "" {
|
|
errs = append(errs, errors.New("dns.server is required"))
|
|
}
|
|
if c.DNS.Domain == "" {
|
|
errs = append(errs, errors.New("dns.domain is required"))
|
|
}
|
|
if c.DNS.Timeout.Duration <= 0 {
|
|
errs = append(errs, errors.New("dns.timeout must be greater than zero"))
|
|
}
|
|
if c.Ping.Timeout.Duration <= 0 {
|
|
errs = append(errs, errors.New("ping.timeout must be greater than zero"))
|
|
}
|
|
for i, target := range c.Targets {
|
|
if target.Address == "" {
|
|
errs = append(errs, fmt.Errorf("targets[%d].address is required", i))
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|