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---
127 lines
4.6 KiB
Go
127 lines
4.6 KiB
Go
package schema
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/jobspec"
|
|
)
|
|
|
|
// UpdateValidator validates the rolling/canary/blue-green update stanza
|
|
// (P03). The schema validator (ServiceValidator) already enforces that
|
|
// the strategy is one of rolling/canary/blue-green and that the update
|
|
// block is present for a Service. UpdateValidator adds the
|
|
// field-level validation:
|
|
//
|
|
// - max_parallel: integer in [1, count] (defaults to 1 when unset)
|
|
// - min_healthy_time: a valid time.Duration when set (time.ParseDuration)
|
|
// - healthy_deadline: a valid time.Duration when set (time.ParseDuration)
|
|
// - canary: an integer count in [0, count] OR a percentage string of
|
|
// the form "<n>%" where n is in [0, 100] (the parser already accepts
|
|
// both shapes; the validator accepts them too). Only meaningful
|
|
// for the canary strategy; ignored (but still validated for shape)
|
|
// for rolling/blue-green.
|
|
// - auto_promote: boolean (no validation beyond the parser's
|
|
// true/false parse; the field is always populated)
|
|
//
|
|
// The validator is pure (no I/O). Violations return a clear error
|
|
// listing every problem found, mirroring the per-field style of
|
|
// ServiceValidator.
|
|
type UpdateValidator struct{}
|
|
|
|
// Validate validates the UpdateBlock on the given spec. The spec must
|
|
// be non-nil and carry a Count (services have count ≥ 1 per
|
|
// ServiceValidator). When spec.Update is nil the validator returns an
|
|
// error (the update block is required for Service; this validator
|
|
// assumes the caller has already established the spec is a Service).
|
|
func (UpdateValidator) Validate(spec *jobspec.WorkloadSpec) error {
|
|
if spec == nil {
|
|
return fmt.Errorf("schema/Update: spec is nil")
|
|
}
|
|
if spec.Update == nil {
|
|
return fmt.Errorf("schema/Update: update block is nil")
|
|
}
|
|
var errs []string
|
|
u := spec.Update
|
|
|
|
switch u.Strategy {
|
|
case "rolling", "canary", "blue-green":
|
|
case "":
|
|
errs = append(errs, "update strategy required (one of rolling, canary, blue-green)")
|
|
default:
|
|
errs = append(errs, fmt.Sprintf("update strategy %q invalid (want one of rolling, canary, blue-green)", u.Strategy))
|
|
}
|
|
|
|
// max_parallel defaults to 1 when unset (0); validate the range
|
|
// only when the user has set it explicitly.
|
|
if u.MaxParallel != 0 {
|
|
if u.MaxParallel < 1 {
|
|
errs = append(errs, fmt.Sprintf("update.max_parallel must be ≥ 1, got %d", u.MaxParallel))
|
|
}
|
|
if spec.Count > 0 && u.MaxParallel > spec.Count {
|
|
errs = append(errs, fmt.Sprintf("update.max_parallel %d exceeds count %d (must be 1..count)", u.MaxParallel, spec.Count))
|
|
}
|
|
}
|
|
|
|
if u.MinHealthyTime != "" {
|
|
if _, err := time.ParseDuration(u.MinHealthyTime); err != nil {
|
|
errs = append(errs, fmt.Sprintf("update.min_healthy_time %q is not a valid duration: %v", u.MinHealthyTime, err))
|
|
}
|
|
}
|
|
if u.HealthyDeadline != "" {
|
|
if _, err := time.ParseDuration(u.HealthyDeadline); err != nil {
|
|
errs = append(errs, fmt.Sprintf("update.healthy_deadline %q is not a valid duration: %v", u.HealthyDeadline, err))
|
|
}
|
|
}
|
|
|
|
// canary accepts an integer count (0..count) or a percentage
|
|
// ("<n>%" with n in 0..100). The field is only meaningful for the
|
|
// canary strategy but we validate the shape regardless so a typo
|
|
// in a rolling/blue-green stanza still surfaces.
|
|
if u.Canary != "" {
|
|
if err := validateCanary(u.Canary, spec.Count); err != nil {
|
|
errs = append(errs, err.Error())
|
|
}
|
|
}
|
|
|
|
// auto_promote is a bool; no extra validation beyond the parser.
|
|
|
|
return composeErrors("schema/Update", errs)
|
|
}
|
|
|
|
// validateCanary validates the canary field shape: either an integer
|
|
// count (0..count) or a percentage string "<n>%" (n in 0..100). count
|
|
// is the spec.Count; when count is 0 (e.g. a DaemonSet or unset), the
|
|
// integer-count upper bound is not enforced (only the percentage
|
|
// bound is enforced, since percentage does not depend on count).
|
|
func validateCanary(canary string, count int) error {
|
|
c := strings.TrimSpace(canary)
|
|
if c == "" {
|
|
return nil
|
|
}
|
|
if strings.HasSuffix(c, "%") {
|
|
nStr := strings.TrimSuffix(c, "%")
|
|
n, err := strconv.Atoi(strings.TrimSpace(nStr))
|
|
if err != nil {
|
|
return fmt.Errorf("update.canary %q is not a valid percentage (want \"<n>%%\")", canary)
|
|
}
|
|
if n < 0 || n > 100 {
|
|
return fmt.Errorf("update.canary percentage %d out of range (want 0..100)", n)
|
|
}
|
|
return nil
|
|
}
|
|
n, err := strconv.Atoi(c)
|
|
if err != nil {
|
|
return fmt.Errorf("update.canary %q is not a valid count or percentage (want integer or \"<n>%%\")", canary)
|
|
}
|
|
if n < 0 {
|
|
return fmt.Errorf("update.canary count %d must be ≥ 0", n)
|
|
}
|
|
if count > 0 && n > count {
|
|
return fmt.Errorf("update.canary count %d exceeds count %d (must be 0..count)", n, count)
|
|
}
|
|
return nil
|
|
}
|