primeiro commit
This commit is contained in:
131
internal/config/config.go
Normal file
131
internal/config/config.go
Normal file
@@ -0,0 +1,131 @@
|
||||
// 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...)
|
||||
}
|
||||
90
internal/config/config_test.go
Normal file
90
internal/config/config_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"statuspanel/internal/config"
|
||||
)
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
t.Run("loads valid yaml with defaults", func(t *testing.T) {
|
||||
path := writeConfig(t, `
|
||||
interface: eth0
|
||||
dns:
|
||||
server: 1.1.1.1
|
||||
domain: example.com
|
||||
targets:
|
||||
- name: router
|
||||
address: 192.0.2.1
|
||||
`)
|
||||
|
||||
cfg, err := config.Load(context.Background(), path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
if cfg.Interface != "eth0" {
|
||||
t.Fatalf("Interface = %q, want eth0", cfg.Interface)
|
||||
}
|
||||
if cfg.Refresh.Duration != 2*time.Second {
|
||||
t.Fatalf("Refresh = %v, want 2s", cfg.Refresh.Duration)
|
||||
}
|
||||
if cfg.Ping.Timeout.Duration != time.Second {
|
||||
t.Fatalf("Ping timeout = %v, want 1s", cfg.Ping.Timeout.Duration)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects missing required values", func(t *testing.T) {
|
||||
path := writeConfig(t, `refresh: 1s`)
|
||||
|
||||
_, err := config.Load(context.Background(), path)
|
||||
if err == nil {
|
||||
t.Fatal("Load() error = nil, want validation error")
|
||||
}
|
||||
for _, want := range []string{"interface is required", "dns.server is required", "dns.domain is required"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Fatalf("Load() error = %q, want containing %q", err, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDurationUnmarshalYAML(t *testing.T) {
|
||||
path := writeConfig(t, `
|
||||
interface: eth0
|
||||
refresh: 500ms
|
||||
dns:
|
||||
server: 1.1.1.1
|
||||
domain: example.com
|
||||
timeout: 250ms
|
||||
ping:
|
||||
timeout: 750ms
|
||||
`)
|
||||
|
||||
cfg, err := config.Load(context.Background(), path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
if cfg.Refresh.Duration != 500*time.Millisecond {
|
||||
t.Fatalf("Refresh = %v, want 500ms", cfg.Refresh.Duration)
|
||||
}
|
||||
if cfg.DNS.Timeout.Duration != 250*time.Millisecond {
|
||||
t.Fatalf("DNS timeout = %v, want 250ms", cfg.DNS.Timeout.Duration)
|
||||
}
|
||||
if cfg.Ping.Timeout.Duration != 750*time.Millisecond {
|
||||
t.Fatalf("Ping timeout = %v, want 750ms", cfg.Ping.Timeout.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
func writeConfig(t *testing.T, contents string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "statuspanel.yaml")
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatalf("writing config: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
Reference in New Issue
Block a user