03f3585f16
internal/drift/drift.go: Detector (Watch via iter.Seq2, Aggregate,
Remediate with cooldown-on-success, Acknowledge), Config with tiered
cadence (critical 5s + Path units, standard 30s, default 60s).
internal/cli/drift.go: orca drift {show,watch,acknowledge,remediate,
config}. internal/emitter/drift_path.go: systemd Path+service unit
emitter (User=orca, ProtectSystem=strict). scripts/orca-drift-notify.sh
(sha256 event JSON), orca-remediate.sh (cooldown-on-success, transient
retry). Pre-flight gate (R-020, --force + per-ns scoping). orca
system user (REQ-111), NFS detection (D-233), orca job restart for
EnvironmentFile drift (D-235).
---ci---
project: orca
phase: 10b
milestone: v0.11
status: execute
---/ci---
159 lines
4.2 KiB
Go
159 lines
4.2 KiB
Go
package emitter
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestDriftPathUnit_RenderUnits(t *testing.T) {
|
|
specs := []DriftPathSpec{
|
|
{Pattern: "/etc/traefik/dynamic/orca.yml", SystemdPathUnit: true},
|
|
{Pattern: "/etc/nftables.d/orca.nft", SystemdPathUnit: true},
|
|
}
|
|
files, err := (DriftPathUnit{}).RenderDriftUnits(specs)
|
|
if err != nil {
|
|
t.Fatalf("RenderDriftUnits: %v", err)
|
|
}
|
|
if len(files) != 4 {
|
|
t.Fatalf("got %d files, want 4 (2 .path + 2 .service)", len(files))
|
|
}
|
|
var pathUnits, svcUnits int
|
|
for _, f := range files {
|
|
if strings.HasSuffix(f.Path, ".path") {
|
|
pathUnits++
|
|
}
|
|
if strings.HasSuffix(f.Path, ".service") {
|
|
svcUnits++
|
|
}
|
|
}
|
|
if pathUnits != 2 || svcUnits != 2 {
|
|
t.Errorf("path=%d service=%d, want 2 and 2", pathUnits, svcUnits)
|
|
}
|
|
}
|
|
|
|
func TestDriftPathUnit_PathUnitContent(t *testing.T) {
|
|
files, err := (DriftPathUnit{}).RenderDriftUnits([]DriftPathSpec{
|
|
{Pattern: "/etc/traefik/dynamic/orca.yml", SystemdPathUnit: true},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
var pathFile *File
|
|
for i := range files {
|
|
if strings.HasSuffix(files[i].Path, ".path") {
|
|
pathFile = &files[i]
|
|
}
|
|
}
|
|
if pathFile == nil {
|
|
t.Fatal("no .path unit emitted")
|
|
}
|
|
c := pathFile.Content
|
|
for _, want := range []string{
|
|
"PathChanged=/etc/traefik/dynamic/orca.yml",
|
|
"RateLimitIntervalSec=1s",
|
|
"RateLimitBurst=5",
|
|
"WantedBy=multi-user.target",
|
|
} {
|
|
if !strings.Contains(c, want) {
|
|
t.Errorf("path unit missing %q:\n%s", want, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDriftPathUnit_ServiceUnitSecurity(t *testing.T) {
|
|
files, err := (DriftPathUnit{}).RenderDriftUnits([]DriftPathSpec{
|
|
{Pattern: "/etc/nftables.d/orca.nft", SystemdPathUnit: true},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
var svcFile *File
|
|
for i := range files {
|
|
if strings.HasSuffix(files[i].Path, ".service") {
|
|
svcFile = &files[i]
|
|
}
|
|
}
|
|
if svcFile == nil {
|
|
t.Fatal("no .service unit emitted")
|
|
}
|
|
c := svcFile.Content
|
|
for _, want := range []string{
|
|
"Type=oneshot",
|
|
"ExecStart=/usr/local/bin/orca-drift-notify.sh %f",
|
|
"User=orca",
|
|
"Group=orca",
|
|
"NoNewPrivileges=yes",
|
|
"ProtectSystem=strict",
|
|
"ReadWritePaths=/etc/orca/state/drift-events",
|
|
"ProtectHome=yes",
|
|
} {
|
|
if !strings.Contains(c, want) {
|
|
t.Errorf("service unit missing %q:\n%s", want, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDriftPathUnit_UnitNameSlug(t *testing.T) {
|
|
cases := []struct {
|
|
pattern string
|
|
want string
|
|
}{
|
|
{"/etc/traefik/dynamic/orca.yml", "orca-drift-etc-traefik-dynamic-orca-yml"},
|
|
{"/etc/systemd/system/orca-alloc-*.service", "orca-drift-etc-systemd-system-orca-alloc-service"},
|
|
{"/etc/orca/actual/*/etc/sudoers.d/orca-*", "orca-drift-etc-orca-actual-etc-sudoers-d-orca"},
|
|
}
|
|
for _, c := range cases {
|
|
got := driftUnitName(c.pattern)
|
|
if got != c.want {
|
|
t.Errorf("driftUnitName(%q) = %q, want %q", c.pattern, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDriftPathUnit_EmptyPatternErrors(t *testing.T) {
|
|
_, err := (DriftPathUnit{}).RenderDriftUnits([]DriftPathSpec{{Pattern: ""}})
|
|
if err == nil {
|
|
t.Fatal("expected error for empty pattern")
|
|
}
|
|
}
|
|
|
|
func TestDriftPathUnit_NoSpecsNoFiles(t *testing.T) {
|
|
files, err := (DriftPathUnit{}).RenderDriftUnits(nil)
|
|
if err != nil {
|
|
t.Fatalf("RenderDriftUnits nil: %v", err)
|
|
}
|
|
if files != nil {
|
|
t.Errorf("got %d files, want nil", len(files))
|
|
}
|
|
}
|
|
|
|
func TestDriftPathUnit_DedupesSameSlug(t *testing.T) {
|
|
// Two patterns that produce the same slug (after non-alnum collapse)
|
|
// should dedupe to a single .path + .service pair.
|
|
files, err := (DriftPathUnit{}).RenderDriftUnits([]DriftPathSpec{
|
|
{Pattern: "/etc/x/a.yml", SystemdPathUnit: true},
|
|
{Pattern: "/etc/x/a.yml", SystemdPathUnit: true},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
if len(files) != 2 {
|
|
t.Errorf("got %d files, want 2 (deduped identical patterns)", len(files))
|
|
}
|
|
}
|
|
|
|
func TestDriftPathUnit_ServiceAfterPath(t *testing.T) {
|
|
files, _ := (DriftPathUnit{}).RenderDriftUnits([]DriftPathSpec{
|
|
{Pattern: "/etc/traefik/dynamic/orca.yml", SystemdPathUnit: true},
|
|
})
|
|
for _, f := range files {
|
|
if strings.HasSuffix(f.Path, ".service") {
|
|
if !strings.Contains(f.Content, "After=orca-drift-etc-traefik-dynamic-orca-yml.path") {
|
|
t.Errorf("service missing After=<name>.path:\n%s", f.Content)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("no service file found")
|
|
}
|