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---
129 lines
5.7 KiB
Go
129 lines
5.7 KiB
Go
package emitter
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/jobspec"
|
|
)
|
|
|
|
// SocketEmitter renders the systemd directives that implement the
|
|
// R-007 socket-plumbing contract: workloads bind to
|
|
// /run/orca/alloc-<id>/port-<name>.sock unless overridden via
|
|
// service.bind = "127.0.0.1" (the only documented opt-in).
|
|
//
|
|
// The systemd side of the contract uses two directives:
|
|
//
|
|
// - RuntimeDirectory=orca/alloc-<alloc-id> — systemd creates
|
|
// /run/orca/alloc-<alloc-id>/ owned by the service user (orca:orca)
|
|
// with mode 0750. The directory is removed when the unit stops
|
|
// (RuntimeDirectory= semantics). P08 emits one RuntimeDirectory=
|
|
// line per port so each port's socket directory is created; the
|
|
// alloc-id placeholder is spec.Name (the real alloc-id is assigned
|
|
// by the scheduler at submit time — see allocIDFor).
|
|
//
|
|
// - ExecStartPre= — only when service.bind is "127.0.0.1" (the TCP
|
|
// opt-in). In that case the workload binds a TCP port directly
|
|
// (no socket), and the ExecStartPre is a placeholder that records
|
|
// the bind (the actual bind happens in the process; the directive
|
|
// is a no-op marker so operators can see the bind mode in the unit
|
|
// file). When service.bind is empty (the default), the workload
|
|
// binds the socket and no ExecStartPre is emitted for sockets.
|
|
//
|
|
// The socket path format is /run/orca/alloc-<alloc-id>/port-<port-name>.sock
|
|
// where alloc-id is a PLACEHOLDER (spec.Name) — the real alloc-id is
|
|
// assigned at submit time by the scheduler. The placeholder is
|
|
// documented in the rendered unit via a comment so operators reading
|
|
// the unit file understand the substitution.
|
|
//
|
|
// P08 is a PLAN/plumbing layer — the actual socket activation (socket
|
|
// unit files, systemd socket-activation passing the pre-bound socket
|
|
// fd to the process) lands in v0.10. P08 just renders the
|
|
// RuntimeDirectory= lines and the optional TCP-bind ExecStartPre so
|
|
// the directory exists at runtime.
|
|
type SocketEmitter struct{}
|
|
|
|
// runtimeDirectoryRoot is the systemd RuntimeDirectory path root.
|
|
// systemd joins this with the RuntimeDirectory= value to create
|
|
// /run/orca/alloc-<id>. The leading slash is implicit in systemd
|
|
// (RuntimeDirectory= is relative to /run).
|
|
const runtimeDirectoryRoot = "orca"
|
|
|
|
// SocketPath returns the R-007 socket path for a port on the given
|
|
// alloc-id. The alloc-id is the placeholder spec.Name when the real
|
|
// alloc-id is not yet known (the scheduler assigns the real alloc-id
|
|
// at submit time).
|
|
func SocketPath(allocID, portName string) string {
|
|
return fmt.Sprintf("/run/orca/alloc-%s/port-%s.sock", allocID, portName)
|
|
}
|
|
|
|
// RenderSocketLines renders the systemd directives that implement
|
|
// the R-007 socket plumbing for the given spec. The lines are returned
|
|
// WITHOUT a trailing newline so the caller (the systemd emitter) can
|
|
// append them to the [Service] block with consistent formatting.
|
|
//
|
|
// The returned lines are:
|
|
//
|
|
// - one RuntimeDirectory= line per port (so each port's socket
|
|
// directory is created by systemd at unit start).
|
|
// - a comment documenting the alloc-id placeholder.
|
|
// - when service.bind is "127.0.0.1", an ExecStartPre= marker that
|
|
// records the TCP opt-in (the actual bind is in the process).
|
|
//
|
|
// Returns an empty slice when the spec has no ports (no socket
|
|
// plumbing needed — e.g. a Job or a port-less DaemonSet).
|
|
func (SocketEmitter) RenderSocketLines(spec *jobspec.WorkloadSpec) []string {
|
|
if spec == nil || len(spec.Ports) == 0 {
|
|
return nil
|
|
}
|
|
allocID := allocIDForSocket(spec)
|
|
var lines []string
|
|
// One RuntimeDirectory= per port. systemd dedupes identical
|
|
// values, but we emit one per port so the unit file is
|
|
// self-documenting (each port maps to a directory entry).
|
|
for _, p := range spec.Ports {
|
|
lines = append(lines, fmt.Sprintf("RuntimeDirectory=%s/alloc-%s", runtimeDirectoryRoot, allocID))
|
|
// Document the socket path this directory serves. systemd
|
|
// ignores comment lines (lines starting with '#').
|
|
lines = append(lines, fmt.Sprintf("# socket: %s", SocketPath(allocID, p.Name)))
|
|
}
|
|
// TCP opt-in: when service.bind is 127.0.0.1, the workload binds
|
|
// a TCP port directly instead of the socket. We emit an
|
|
// ExecStartPre marker so the bind mode is visible in the unit
|
|
// file. The actual bind is in the process; the marker is a
|
|
// no-op (echo to journald).
|
|
if spec.Service != nil && strings.TrimSpace(spec.Service.Bind) != "" {
|
|
if isTCPOptIn(spec.Service.Bind) {
|
|
for _, p := range spec.Ports {
|
|
lines = append(lines, fmt.Sprintf("ExecStartPre=/bin/echo orca: bind %s port %s (tcp, R-007 opt-in)", spec.Service.Bind, p.Name))
|
|
}
|
|
}
|
|
}
|
|
return lines
|
|
}
|
|
|
|
// allocIDForSocket returns the alloc-id placeholder for the spec. The
|
|
// real alloc-id is assigned by the scheduler at submit time; P08 uses
|
|
// spec.Name as a deterministic placeholder so the rendered unit is
|
|
// stable across re-renders. This mirrors the Traefik emitter's
|
|
// allocIDFor (which uses the node hostname for the Traefik
|
|
// dynamic-config server URL); the systemd unit is per-alloc, so
|
|
// spec.Name is the right placeholder here.
|
|
func allocIDForSocket(spec *jobspec.WorkloadSpec) string {
|
|
if spec == nil || strings.TrimSpace(spec.Name) == "" {
|
|
return "<allocID>"
|
|
}
|
|
return spec.Name
|
|
}
|
|
|
|
// isTCPOptIn returns true when the bind value is the documented
|
|
// 127.0.0.1 TCP opt-in (R-007). Other valid IPs (::1, etc.) are also
|
|
// TCP opt-ins (any non-empty bind opts out of the socket default); we
|
|
// only emit the marker for 127.0.0.1 because that is the only
|
|
// documented opt-in per the PRD — other IPs are accepted by the
|
|
// schema validator but are operator-specific and we do not
|
|
// second-guess them.
|
|
func isTCPOptIn(bind string) bool {
|
|
return strings.TrimSpace(bind) == "127.0.0.1"
|
|
}
|