// 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 ":" 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 ":". type Emitter interface { Render(spec *jobspec.WorkloadSpec, node *Node) ([]File, error) } // Registry holds emitters keyed by ":" (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 // ":" (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 ":" 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) }