- CLI: enroll, run, status, version, help (core.Main with a role factory); the agent binaries use a placeholder factory until the BIND and Kea roles are wired in. - Enrollment: local ECDSA P-256 key and CSR, TLS 1.3 to the agent API with the server chain pinned to the CA fingerprint. - ServerSource: mTLS gRPC with keepalive, Connect stream (Hello, Welcome, heartbeats, apply and renew commands), reconnect with exponential backoff and jitter, GetDesiredState polling as fallback, certificate renewal after half of the lifetime. - Apply pipeline: fetch, bundle verification with replay protection, store, Validate, Activate, Health with retries, rollback to the last good revision; outcomes succeeded/rejected/rolled_back/failed are queued in state.json and reported; failed revisions are retried only by a newer revision or an explicit push. - Lease reporting with change detection, persistent sequence and full snapshots at start, per connection and periodically. - Tests with fake roles, a fake source and an in-process fake AgentService over real mTLS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
3.9 KiB
Go
129 lines
3.9 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package core
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestParseConfigDefaults(t *testing.T) {
|
|
for _, data := range []string{"", " \n", "# only a comment\n"} {
|
|
cfg, err := ParseConfig([]byte(data), KindDNS)
|
|
if err != nil {
|
|
t.Fatalf("%q: %v", data, err)
|
|
}
|
|
if cfg.StateDir != DefaultStateDir(KindDNS) || cfg.ApplyTimeout != DefaultApplyTimeout ||
|
|
cfg.HealthTimeout != 30*time.Second || cfg.LeaseInterval != 30*time.Second || cfg.LeaseFullSync != time.Hour {
|
|
t.Errorf("defaults = %+v", cfg)
|
|
}
|
|
var role struct{ X int }
|
|
role.X = 7
|
|
if err := cfg.DecodeRole(&role); err != nil || role.X != 7 {
|
|
t.Errorf("absent role section: %v, %+v", err, role)
|
|
}
|
|
}
|
|
if !strings.HasSuffix(filepath.ToSlash(DefaultConfigPath(KindDHCP)), "/agent-dhcp.yaml") {
|
|
t.Errorf("DefaultConfigPath = %s", DefaultConfigPath(KindDHCP))
|
|
}
|
|
}
|
|
|
|
func TestParseConfigFull(t *testing.T) {
|
|
data := `
|
|
state_dir: /tmp/agent
|
|
log:
|
|
level: debug
|
|
format: text
|
|
apply_timeout: 2m
|
|
health_timeout: 45s
|
|
lease_interval: 10s
|
|
lease_full_sync: 30m
|
|
dhcp:
|
|
config_file: /etc/kea/kea-dhcp4.conf
|
|
control_socket: /run/kea/ctrl.sock
|
|
`
|
|
cfg, err := ParseConfig([]byte(data), KindDHCP)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.StateDir != "/tmp/agent" || cfg.Log.Level != "debug" || cfg.Log.Format != "text" ||
|
|
cfg.ApplyTimeout != 2*time.Minute || cfg.HealthTimeout != 45*time.Second ||
|
|
cfg.LeaseInterval != 10*time.Second || cfg.LeaseFullSync != 30*time.Minute {
|
|
t.Errorf("config = %+v", cfg)
|
|
}
|
|
var role struct {
|
|
ConfigFile string `yaml:"config_file"`
|
|
ControlSocket string `yaml:"control_socket"`
|
|
}
|
|
if err := cfg.DecodeRole(&role); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if role.ConfigFile != "/etc/kea/kea-dhcp4.conf" || role.ControlSocket != "/run/kea/ctrl.sock" {
|
|
t.Errorf("role = %+v", role)
|
|
}
|
|
// Strict decoding of the role section.
|
|
var narrow struct {
|
|
ConfigFile string `yaml:"config_file"`
|
|
}
|
|
if err := cfg.DecodeRole(&narrow); err == nil {
|
|
t.Error("unknown role key accepted")
|
|
}
|
|
}
|
|
|
|
func TestParseConfigErrors(t *testing.T) {
|
|
cases := map[string]string{
|
|
"unknown key": "flavor: blue\n",
|
|
"other role": "dhcp:\n config_file: x\n",
|
|
"short timeout": "health_timeout: 10ms\n",
|
|
"full sync": "lease_interval: 2m\nlease_full_sync: 1m\n",
|
|
"bad level": "log:\n level: loud\n",
|
|
"bad duration": "apply_timeout: soon\n",
|
|
"not a mapping": "- a\n- b\n",
|
|
"unknown log key": "log:\n flavor: x\n",
|
|
"negative timeout": "apply_timeout: -5s\n",
|
|
}
|
|
for name, data := range cases {
|
|
if _, err := ParseConfig([]byte(data), KindDNS); err == nil {
|
|
t.Errorf("%s: accepted", name)
|
|
}
|
|
}
|
|
if _, err := ParseConfig(nil, Kind("ntp")); err == nil {
|
|
t.Error("unknown role accepted")
|
|
}
|
|
if _, err := ParseConfig([]byte("dns:\n zone_dir: x\n"), KindDHCP); err == nil {
|
|
t.Error("dns section accepted by the DHCP agent")
|
|
}
|
|
// A null section is fine.
|
|
if _, err := ParseConfig([]byte("dhcp:\n"), KindDNS); err != nil {
|
|
t.Errorf("null section: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfig(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg, err := LoadConfig(filepath.Join(dir, "missing.yaml"), KindDNS)
|
|
if err != nil || cfg.StateDir != DefaultStateDir(KindDNS) {
|
|
t.Fatalf("missing file: %+v, %v", cfg, err)
|
|
}
|
|
path := filepath.Join(dir, "agent-dns.yaml")
|
|
if err := os.WriteFile(path, []byte("state_dir: "+filepath.ToSlash(dir)+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err = LoadConfig(path, KindDNS)
|
|
if err != nil || cfg.StateDir != filepath.ToSlash(dir) {
|
|
t.Fatalf("file: %+v, %v", cfg, err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("bogus: 1\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := LoadConfig(path, KindDNS); err == nil || !strings.Contains(err.Error(), path) {
|
|
t.Errorf("error does not name the file: %v", err)
|
|
}
|
|
if _, err := LoadConfig(dir, KindDNS); err == nil {
|
|
t.Error("directory accepted as configuration file")
|
|
}
|
|
}
|