ae6eb5a27b
P03 — Update stanza (rolling/canary/blue-green): - internal/spec/schema/update.go: UpdateValidator (strategy enum, max_parallel 1..count, duration parsing, canary int/% forms, auto_promote). 98.2% cov. - internal/emitter/update.go: RenderUpdatePlan computes the step sequence (rolling batches, canary 1+promote+rest, blue-green all+cutover). Pure plan, no execution (v0.10-P10 is transactional). 73.7-100% cov. P04 — Lifecycle hooks (systemd ExecStop semantics): - Extended internal/emitter/systemd.go: post_start -> ExecStartPost=, pre_stop -> ExecStop=. Order: ExecStart -> ExecStartPost -> ExecStop -> socket lines. 8 lifecycle tests. 100% cov on systemd.go. P08 — Socket plumbing (R-007): - internal/emitter/socket.go: SocketEmitter renders RuntimeDirectory=orca/ alloc-<id> per port (mode 0750, orca:orca). ExecStartPre TCP-bind marker when service.bind=127.0.0.1. SocketPath(allocID,portName) helper. 100% cov. - Alloc-id is spec.Name placeholder; real id assigned by scheduler at submit. 22 packages pass, 20 bats pass, gofmt clean, verify-reqs 90 consistent. ---ci--- project: orca phase: P03/P04/P08 milestone: v0.9 status: execute ---/ci---
173 lines
5.3 KiB
Go
173 lines
5.3 KiB
Go
package emitter
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/jobspec"
|
|
)
|
|
|
|
func TestSystemdEmitter_LifecyclePostStart(t *testing.T) {
|
|
// lifecycle.post_start → ExecStartPost (one line per command).
|
|
spec := &jobspec.WorkloadSpec{
|
|
Kind: "Service",
|
|
Name: "web",
|
|
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/usr/local/bin/httpd -f"},
|
|
Lifecycle: &jobspec.LifecycleBlock{
|
|
PostStart: []string{"/usr/bin/sleep 1", "/usr/bin/curl localhost/healthz"},
|
|
},
|
|
}
|
|
files, err := SystemdEmitter{}.Render(spec, &Node{Hostname: "n1"})
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
c := files[0].Content
|
|
if !strings.Contains(c, "ExecStartPost=/usr/bin/sleep 1\n") {
|
|
t.Errorf("missing ExecStartPost for sleep 1\n%s", c)
|
|
}
|
|
if !strings.Contains(c, "ExecStartPost=/usr/bin/curl localhost/healthz\n") {
|
|
t.Errorf("missing ExecStartPost for curl\n%s", c)
|
|
}
|
|
// ExecStart must still be present.
|
|
if !strings.Contains(c, "ExecStart=/usr/local/bin/httpd -f\n") {
|
|
t.Errorf("missing ExecStart\n%s", c)
|
|
}
|
|
}
|
|
|
|
func TestSystemdEmitter_LifecyclePreStop(t *testing.T) {
|
|
// lifecycle.pre_stop → ExecStop (one line per command).
|
|
spec := &jobspec.WorkloadSpec{
|
|
Kind: "Service",
|
|
Name: "web",
|
|
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/usr/local/bin/httpd -f"},
|
|
Lifecycle: &jobspec.LifecycleBlock{
|
|
PreStop: []string{"/usr/local/bin/httpd -graceful", "/usr/bin/sleep 5"},
|
|
},
|
|
}
|
|
files, err := SystemdEmitter{}.Render(spec, &Node{Hostname: "n1"})
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
c := files[0].Content
|
|
if !strings.Contains(c, "ExecStop=/usr/local/bin/httpd -graceful\n") {
|
|
t.Errorf("missing ExecStop for graceful\n%s", c)
|
|
}
|
|
if !strings.Contains(c, "ExecStop=/usr/bin/sleep 5\n") {
|
|
t.Errorf("missing ExecStop for sleep 5\n%s", c)
|
|
}
|
|
}
|
|
|
|
func TestSystemdEmitter_LifecycleBoth(t *testing.T) {
|
|
spec := &jobspec.WorkloadSpec{
|
|
Kind: "Service",
|
|
Name: "web",
|
|
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/httpd"},
|
|
Lifecycle: &jobspec.LifecycleBlock{
|
|
PostStart: []string{"/bin/after-start"},
|
|
PreStop: []string{"/bin/before-stop"},
|
|
},
|
|
}
|
|
files, err := SystemdEmitter{}.Render(spec, &Node{})
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
c := files[0].Content
|
|
// ExecStartPost must appear before ExecStop (post_start runs after
|
|
// start; pre_stop runs before stop — the order in the unit file
|
|
// reflects the lifecycle order).
|
|
startIdx := strings.Index(c, "ExecStart=")
|
|
postIdx := strings.Index(c, "ExecStartPost=")
|
|
stopIdx := strings.Index(c, "ExecStop=")
|
|
if startIdx < 0 || postIdx < 0 || stopIdx < 0 {
|
|
t.Fatalf("missing one of ExecStart/ExecStartPost/ExecStop\n%s", c)
|
|
}
|
|
if !(startIdx < postIdx && postIdx < stopIdx) {
|
|
t.Errorf("expected order ExecStart < ExecStartPost < ExecStop\n%s", c)
|
|
}
|
|
}
|
|
|
|
func TestSystemdEmitter_LifecycleNilOmitsDirectives(t *testing.T) {
|
|
// No lifecycle block → no ExecStartPost / ExecStop lines.
|
|
spec := &jobspec.WorkloadSpec{
|
|
Kind: "Job",
|
|
Name: "backup",
|
|
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/rsync"},
|
|
}
|
|
files, err := SystemdEmitter{}.Render(spec, &Node{})
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
c := files[0].Content
|
|
if strings.Contains(c, "ExecStartPost=") {
|
|
t.Errorf("ExecStartPost should be omitted when no lifecycle\n%s", c)
|
|
}
|
|
if strings.Contains(c, "ExecStop=") {
|
|
t.Errorf("ExecStop should be omitted when no lifecycle\n%s", c)
|
|
}
|
|
}
|
|
|
|
func TestSystemdEmitter_LifecycleEmptyListsOmitted(t *testing.T) {
|
|
// Lifecycle block present but empty lists → no ExecStartPost / ExecStop.
|
|
spec := &jobspec.WorkloadSpec{
|
|
Kind: "Job",
|
|
Name: "x",
|
|
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/x"},
|
|
Lifecycle: &jobspec.LifecycleBlock{},
|
|
}
|
|
files, err := SystemdEmitter{}.Render(spec, &Node{})
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
c := files[0].Content
|
|
if strings.Contains(c, "ExecStartPost=") {
|
|
t.Errorf("ExecStartPost should be omitted for empty PostStart\n%s", c)
|
|
}
|
|
if strings.Contains(c, "ExecStop=") {
|
|
t.Errorf("ExecStop should be omitted for empty PreStop\n%s", c)
|
|
}
|
|
}
|
|
|
|
func TestSystemdEmitter_LifecycleOnlyPostStart(t *testing.T) {
|
|
spec := &jobspec.WorkloadSpec{
|
|
Kind: "Job",
|
|
Name: "x",
|
|
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/x"},
|
|
Lifecycle: &jobspec.LifecycleBlock{
|
|
PostStart: []string{"/bin/notify-up"},
|
|
},
|
|
}
|
|
files, err := SystemdEmitter{}.Render(spec, &Node{})
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
c := files[0].Content
|
|
if !strings.Contains(c, "ExecStartPost=/bin/notify-up\n") {
|
|
t.Errorf("missing ExecStartPost\n%s", c)
|
|
}
|
|
if strings.Contains(c, "ExecStop=") {
|
|
t.Errorf("ExecStop should be omitted when only PostStart set\n%s", c)
|
|
}
|
|
}
|
|
|
|
func TestSystemdEmitter_LifecycleOnlyPreStop(t *testing.T) {
|
|
spec := &jobspec.WorkloadSpec{
|
|
Kind: "Job",
|
|
Name: "x",
|
|
Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/x"},
|
|
Lifecycle: &jobspec.LifecycleBlock{
|
|
PreStop: []string{"/bin/notify-down"},
|
|
},
|
|
}
|
|
files, err := SystemdEmitter{}.Render(spec, &Node{})
|
|
if err != nil {
|
|
t.Fatalf("Render: %v", err)
|
|
}
|
|
c := files[0].Content
|
|
if !strings.Contains(c, "ExecStop=/bin/notify-down\n") {
|
|
t.Errorf("missing ExecStop\n%s", c)
|
|
}
|
|
if strings.Contains(c, "ExecStartPost=") {
|
|
t.Errorf("ExecStartPost should be omitted when only PreStop set\n%s", c)
|
|
}
|
|
}
|