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---
95 lines
3.5 KiB
Go
95 lines
3.5 KiB
Go
// Package emitter defines the Layer-4 emitter interface (REQ-074,
|
|
// I-B-002): the bridge between the declarative *jobspec.WorkloadSpec
|
|
// and the server-side files. An Emitter renders a *WorkloadSpec into a
|
|
// slice of File artifacts that the SSH-push transport SCPs to peers.
|
|
//
|
|
// Emitters are registered per workload kind + runtime (e.g.
|
|
// "service:wasm", "job:process", "daemonset:wasm"). The Registry looks
|
|
// up the right emitter by "<kind>:<runtime>" and delegates. Unknown
|
|
// combinations return an error so the caller can fail fast before any
|
|
// file is written.
|
|
//
|
|
// P0c only ships the interface, the Registry, a stub SystemdEmitter
|
|
// (process runtime), and the File/Node value types. The full emitter
|
|
// implementations (systemd lifecycle hooks, Traefik, Syncthing,
|
|
// sockets) land in later phases (P02 Traefik, P04 lifecycle, P08
|
|
// sockets, P09 Syncthing, v0.10-P03 secrets).
|
|
package emitter
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/jobspec"
|
|
)
|
|
|
|
// File is a single rendered artifact destined for a peer. The SSH-push
|
|
// transport writes Content to Path atomically (write-to-tmp + rename)
|
|
// with the given Mode (an octal string like "0644").
|
|
type File struct {
|
|
Path string
|
|
Content string
|
|
Mode string
|
|
}
|
|
|
|
// Node is the minimal peer description an emitter needs to render
|
|
// node-specific paths. It carries the hostname, the runtimes available
|
|
// on the node (so emitters can branch), and the node tags (used by
|
|
// DaemonSet matching and affinity in P05).
|
|
type Node struct {
|
|
Hostname string
|
|
Runtime []string
|
|
Tags []string
|
|
}
|
|
|
|
// Emitter renders a *jobspec.WorkloadSpec for a given Node into a slice
|
|
// of File artifacts. Implementations are registered with a Registry
|
|
// keyed by "<kind>:<runtime>".
|
|
type Emitter interface {
|
|
Render(spec *jobspec.WorkloadSpec, node *Node) ([]File, error)
|
|
}
|
|
|
|
// Registry holds emitters keyed by "<kind>:<runtime>" (e.g.
|
|
// "service:process", "job:wasm"). The zero-value Registry is not
|
|
// usable; construct one with NewRegistry.
|
|
type Registry struct {
|
|
emitters map[string]Emitter
|
|
}
|
|
|
|
// NewRegistry returns an empty Registry ready for Register calls.
|
|
func NewRegistry() *Registry {
|
|
return &Registry{emitters: make(map[string]Emitter)}
|
|
}
|
|
|
|
// Register registers an Emitter under the given key. The key is
|
|
// "<kind>:<runtime>" (e.g. "job:process"). Registering twice under the
|
|
// same key overwrites the prior registration (last-wins) to keep the
|
|
// surface simple; callers are responsible for not double-registering.
|
|
func (r *Registry) Register(key string, e Emitter) {
|
|
r.emitters[key] = e
|
|
}
|
|
|
|
// Render looks up the emitter for "<kind>:<runtime>" in the registry and
|
|
// delegates to it. The kind is lowercased so the canonical spec kinds
|
|
// (Job, Service, DaemonSet) map to the lowercase registry keys
|
|
// ("job:process", "service:wasm", "daemonset:process"). Returns an error
|
|
// if the spec is nil, the spec is missing its Kind, the runtime is
|
|
// missing, or no emitter is registered for the combination.
|
|
func (r *Registry) Render(spec *jobspec.WorkloadSpec, node *Node) ([]File, error) {
|
|
if spec == nil {
|
|
return nil, fmt.Errorf("emitter: spec is nil")
|
|
}
|
|
if strings.TrimSpace(spec.Kind) == "" {
|
|
return nil, fmt.Errorf("emitter: spec kind is empty")
|
|
}
|
|
if spec.Runtime == nil {
|
|
return nil, fmt.Errorf("emitter: spec runtime is nil")
|
|
}
|
|
key := strings.ToLower(spec.Kind) + ":" + spec.Runtime.OneOf
|
|
e, ok := r.emitters[key]
|
|
if !ok {
|
|
return nil, fmt.Errorf("emitter: no emitter registered for %q (kind:runtime)", key)
|
|
}
|
|
return e.Render(spec, node)
|
|
}
|