60b0357eb6
P0c — Kind-specific schema validators + Layer 4 emitter interface.
Schemas (internal/spec/schema/schema.go, REQ-074):
- Validator interface with JobValidator, ServiceValidator, DaemonSetValidator.
JobValidator: count=1, no service block, optional schedule/timeout.
ServiceValidator: ports required, count>=1, restart+update+runtime required.
DaemonSetValidator: schedule mode required, no ports (D-175), no count.
ValidatorFor(kind) dispatcher. 96.2% coverage.
Emitter interface (internal/emitter/emitter.go, REQ-074, I-B-002):
- File{Path,Content,Mode}, Emitter interface { Render(spec,node) []File },
Registry keyed by kind:runtime, Register + Render lookup. 100% coverage.
Systemd stub (internal/emitter/systemd.go):
- SystemdEmitter for process runtime. Renders minimal [Service] unit at
/etc/systemd/system/orca-v1-alloc-<name>.service (orca-v1- prefix per
dual-write window REQ-090 — no overlap with v0.8 daemon's orca-<job>).
Flock test fix: TestFlock_concurrentBlocks rewritten to use non-blocking
tryFlockEx (LOCK_NB) instead of a leaked blocking goroutine. Eliminates
the temp-dir cleanup race.
20 packages pass, 20 bats pass, gofmt clean, verify-reqs 90 consistent.
---ci---
project: orca
phase: P0c
milestone: v0.9
status: execute
---/ci---
83 lines
1.8 KiB
Go
83 lines
1.8 KiB
Go
package security
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestFlock_acquireAndRelease(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.lock")
|
|
|
|
release, err := Flock(path)
|
|
if err != nil {
|
|
t.Fatalf("Flock: %v", err)
|
|
}
|
|
if _, statErr := os.Stat(path); statErr != nil {
|
|
t.Fatalf("lock file not created: %v", statErr)
|
|
}
|
|
release()
|
|
}
|
|
|
|
func TestFlock_reentrantAfterRelease(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.lock")
|
|
|
|
r1, err := Flock(path)
|
|
if err != nil {
|
|
t.Fatalf("first Flock: %v", err)
|
|
}
|
|
r1()
|
|
|
|
r2, err := Flock(path)
|
|
if err != nil {
|
|
t.Fatalf("second Flock after release: %v", err)
|
|
}
|
|
r2()
|
|
}
|
|
|
|
func TestFlock_concurrentBlocks(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.lock")
|
|
|
|
r1, err := Flock(path)
|
|
if err != nil {
|
|
t.Fatalf("first Flock: %v", err)
|
|
}
|
|
|
|
// Give the blocking goroutine a chance to start and block.
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Verify the second lock is blocked by checking it hasn't acquired after a short window.
|
|
// Use a non-blocking attempt: open the file and try LOCK_EX|LOCK_NB.
|
|
blocked := make(chan bool, 1)
|
|
go func() {
|
|
f, err := os.OpenFile(path, os.O_RDWR, 0o600)
|
|
if err != nil {
|
|
blocked <- false
|
|
return
|
|
}
|
|
defer f.Close()
|
|
// LOCK_NB = non-blocking; returns EWOULDBLOCK if locked.
|
|
if err := tryFlockEx(int(f.Fd())); err != nil {
|
|
blocked <- true // got EWOULDBLOCK = the lock is held by r1
|
|
return
|
|
}
|
|
releaseFlock(int(f.Fd()))
|
|
blocked <- false // acquired = r1 didn't hold the lock (bug)
|
|
}()
|
|
|
|
select {
|
|
case b := <-blocked:
|
|
if !b {
|
|
t.Fatal("second lock acquired while first holds it — lock not working")
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("non-blocking try-lock timed out")
|
|
}
|
|
|
|
r1()
|
|
}
|