The DNS role needs to feed nsupdate scripts on stdin and to log its dynamic updates. command.InputRunner adds RunInput (errors never quote the input); roles implementing core.LogSetter get the core's logger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
3.7 KiB
Go
128 lines
3.7 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package core
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/netip"
|
|
"time"
|
|
|
|
"git.beugsiud.de/beug.k/DDI/internal/bundle"
|
|
)
|
|
|
|
// Kind identifies an agent role.
|
|
type Kind string
|
|
|
|
// Supported roles. The values equal the role in bundle manifests.
|
|
const (
|
|
KindDNS Kind = "dns"
|
|
KindDHCP Kind = "dhcp"
|
|
)
|
|
|
|
// Revision is a verified configuration revision stored by the core under
|
|
// <state_dir>/revisions/<Number>/. Roles must treat it as read-only.
|
|
type Revision struct {
|
|
Number uint64
|
|
// Dir holds the artifacts at their manifest paths (bundle.CheckPath
|
|
// guarantees they stay inside Dir).
|
|
Dir string
|
|
Files []bundle.File
|
|
}
|
|
|
|
// Role applies revisions to one managed service (BIND or Kea). The core
|
|
// drives it as follows (docs/ARCHITECTURE.md, section 4.2):
|
|
//
|
|
// 1. Validate(rev). An error means the revision is rejected; nothing on the
|
|
// host may have changed.
|
|
// 2. Activate(rev, prev). Makes rev the live configuration and reloads the
|
|
// service. An error triggers a rollback.
|
|
// 3. Health(&rev), retried for a while. A non-OK state triggers a rollback.
|
|
// 4. Rollback: Activate(lastGood, &rev) followed by Health(&lastGood).
|
|
//
|
|
// Files marked CreateOnly are written only if they do not exist and are never
|
|
// overwritten or removed. Activate removes files that prev had and rev does
|
|
// not (except CreateOnly ones).
|
|
type Role interface {
|
|
// Kind returns the role.
|
|
Kind() Kind
|
|
// ServiceVersion returns the managed service's version, e.g.
|
|
// "BIND 9.20.3" or "Kea 3.2.1".
|
|
ServiceVersion(ctx context.Context) (string, error)
|
|
// Validate checks rev without changing the live configuration.
|
|
Validate(ctx context.Context, rev Revision) error
|
|
// Activate installs rev and reloads the service. prev is the revision
|
|
// that was active before (nil if none).
|
|
Activate(ctx context.Context, rev Revision, prev *Revision) error
|
|
// Health checks the running service. expected is the revision that
|
|
// should be active (nil if none); the DNS role compares SOA serials.
|
|
Health(ctx context.Context, expected *Revision) Health
|
|
}
|
|
|
|
// HealthState summarizes a health check.
|
|
type HealthState int
|
|
|
|
// Health states (mirroring the protocol's HealthState).
|
|
const (
|
|
HealthUnknown HealthState = iota
|
|
HealthOK
|
|
HealthDegraded
|
|
HealthFailed
|
|
)
|
|
|
|
// Health is the result of Role.Health.
|
|
type Health struct {
|
|
State HealthState
|
|
Message string
|
|
Checks []HealthCheck
|
|
}
|
|
|
|
// HealthCheck is one named check within a Health result.
|
|
type HealthCheck struct {
|
|
Name string
|
|
State HealthState
|
|
Message string
|
|
}
|
|
|
|
// LeaseState mirrors the protocol's LeaseState.
|
|
type LeaseState int
|
|
|
|
// Lease states.
|
|
const (
|
|
LeaseUnknown LeaseState = iota
|
|
LeaseActive
|
|
LeaseExpired
|
|
LeaseReleased
|
|
LeaseDeclined
|
|
)
|
|
|
|
// Lease is one DHCP lease as seen by the managed service.
|
|
type Lease struct {
|
|
IP netip.Addr
|
|
MAC string // lowercase, colon separated
|
|
Hostname string
|
|
State LeaseState
|
|
Expires time.Time
|
|
Updated time.Time // last client transaction
|
|
SubnetID uint32
|
|
ClientID string
|
|
}
|
|
|
|
// LeaseSource is implemented by roles that can list leases (DHCP). The core
|
|
// polls it, computes changes and reports them to the server.
|
|
type LeaseSource interface {
|
|
Leases(ctx context.Context) ([]Lease, error)
|
|
}
|
|
|
|
// LogSetter is implemented by roles that log on their own (e.g. the DNS
|
|
// role's dynamic updates). The core passes its logger, which carries the
|
|
// agent ID and role, before it uses the role.
|
|
type LogSetter interface {
|
|
SetLogger(log *slog.Logger)
|
|
}
|
|
|
|
// RoleFactory creates a role from its section of the agent configuration
|
|
// file. decode unmarshals that section (YAML) into the role's own struct; an
|
|
// absent section decodes nothing, so roles must start from defaults.
|
|
type RoleFactory func(decode func(v any) error) (Role, error)
|