Initial SWOOSH executor and reducer implementation
This commit is contained in:
633
reducer.go
Normal file
633
reducer.go
Normal file
@@ -0,0 +1,633 @@
|
||||
package swoosh
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrGuardRejected is returned when guard outcomes prevent a transition from applying.
|
||||
ErrGuardRejected = errors.New("guard rejected transition")
|
||||
// ErrUnknownTransition is returned when Reduce encounters an unhandled transition name.
|
||||
ErrUnknownTransition = errors.New("unknown transition")
|
||||
// ErrQuarantined indicates transitions are blocked while policy quarantine is active.
|
||||
ErrQuarantined = errors.New("transition blocked by quarantine")
|
||||
// ErrInvalidPhase signals an unexpected phase for the requested transition.
|
||||
ErrInvalidPhase = errors.New("invalid phase for transition")
|
||||
// ErrPrecondition signals a precondition failure unrelated to phases or guards.
|
||||
ErrPrecondition = errors.New("precondition failed")
|
||||
)
|
||||
|
||||
// Reduce applies a validated transition to the given state and returns a new state.
|
||||
// Reducer must remain deterministic and side-effect free.
|
||||
func Reduce(oldState OrchestratorState, t TransitionProposal, g GuardOutcome) (OrchestratorState, error) {
|
||||
newState := oldState
|
||||
changed, err := applyTransition(&newState, t, g)
|
||||
if err != nil {
|
||||
return OrchestratorState{}, err
|
||||
}
|
||||
if !changed {
|
||||
return oldState, nil
|
||||
}
|
||||
|
||||
newState.HLCLast = t.HLC
|
||||
hash, err := computeStateHash(newState)
|
||||
if err != nil {
|
||||
return OrchestratorState{}, fmt.Errorf("compute state hash: %w", err)
|
||||
}
|
||||
newState.StateHash = hash
|
||||
|
||||
return newState, nil
|
||||
}
|
||||
|
||||
func applyTransition(state *OrchestratorState, t TransitionProposal, g GuardOutcome) (bool, error) {
|
||||
name := t.TransitionName
|
||||
switch name {
|
||||
case "":
|
||||
return false, fmt.Errorf("%w: empty transition name", ErrUnknownTransition)
|
||||
}
|
||||
|
||||
if state.Policy.Quarantined && name != "TRANSITION_QUARANTINE_RELEASE" && name != "TRANSITION_QUARANTINE_CONFIRM_BLOCK" {
|
||||
return false, fmt.Errorf("%w: %s", ErrQuarantined, name)
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "BOOT_CONFIG_OK":
|
||||
if state.Boot.Licensed {
|
||||
return false, fmt.Errorf("%w: boot already licensed", ErrPrecondition)
|
||||
}
|
||||
return false, nil
|
||||
|
||||
case "LICENSE_GRANTED":
|
||||
if state.Boot.Licensed {
|
||||
return false, nil
|
||||
}
|
||||
if !g.LicenseOK {
|
||||
return false, fmt.Errorf("%w: license check failed", ErrGuardRejected)
|
||||
}
|
||||
state.Boot.Licensed = true
|
||||
return true, nil
|
||||
|
||||
case "INGESTION_SOURCES_RESOLVED":
|
||||
if state.Ingestion.Phase == "FETCH" && state.Ingestion.LastError == "" {
|
||||
return false, nil
|
||||
}
|
||||
if state.Ingestion.Phase != "" && state.Ingestion.Phase != "DISCOVER" {
|
||||
return false, fmt.Errorf("%w: ingestion phase %q", ErrInvalidPhase, state.Ingestion.Phase)
|
||||
}
|
||||
changed := false
|
||||
if state.Ingestion.Phase != "FETCH" {
|
||||
state.Ingestion.Phase = "FETCH"
|
||||
changed = true
|
||||
}
|
||||
if state.Ingestion.LastError != "" {
|
||||
state.Ingestion.LastError = ""
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
state.Ingestion.Epoch++
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "INGESTION_BYTES_OK":
|
||||
if state.Ingestion.Phase == "VALIDATE" && state.Ingestion.LastError == "" {
|
||||
return false, nil
|
||||
}
|
||||
if state.Ingestion.Phase != "FETCH" {
|
||||
return false, fmt.Errorf("%w: ingestion phase %q", ErrInvalidPhase, state.Ingestion.Phase)
|
||||
}
|
||||
changed := false
|
||||
if state.Ingestion.Phase != "VALIDATE" {
|
||||
state.Ingestion.Phase = "VALIDATE"
|
||||
changed = true
|
||||
}
|
||||
if state.Ingestion.LastError != "" {
|
||||
state.Ingestion.LastError = ""
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
state.Ingestion.Epoch++
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "INGESTION_SCHEMA_OK":
|
||||
if state.Ingestion.Phase == "INDEX" && state.Ingestion.LastError == "" {
|
||||
return false, nil
|
||||
}
|
||||
if state.Ingestion.Phase != "VALIDATE" {
|
||||
return false, fmt.Errorf("%w: ingestion phase %q", ErrInvalidPhase, state.Ingestion.Phase)
|
||||
}
|
||||
if !g.PolicyOK || !g.BackbeatOK {
|
||||
return false, fmt.Errorf("%w: policy=%t backbeat=%t", ErrGuardRejected, g.PolicyOK, g.BackbeatOK)
|
||||
}
|
||||
changed := false
|
||||
if state.Ingestion.Phase != "INDEX" {
|
||||
state.Ingestion.Phase = "INDEX"
|
||||
changed = true
|
||||
}
|
||||
if state.Ingestion.LastError != "" {
|
||||
state.Ingestion.LastError = ""
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
state.Ingestion.Epoch++
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "INGESTION_CORPUS_BUILT":
|
||||
if state.Ingestion.Phase == "READY" && state.Ingestion.LastError == "" {
|
||||
return false, nil
|
||||
}
|
||||
if state.Ingestion.Phase != "INDEX" {
|
||||
return false, fmt.Errorf("%w: ingestion phase %q", ErrInvalidPhase, state.Ingestion.Phase)
|
||||
}
|
||||
changed := false
|
||||
if state.Ingestion.Phase != "READY" {
|
||||
state.Ingestion.Phase = "READY"
|
||||
changed = true
|
||||
}
|
||||
if state.Ingestion.LastError != "" {
|
||||
state.Ingestion.LastError = ""
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
state.Ingestion.Epoch++
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "INGESTION_POLICY_VIOLATION":
|
||||
if g.PolicyOK {
|
||||
return false, fmt.Errorf("%w: policy guard true", ErrGuardRejected)
|
||||
}
|
||||
if state.Ingestion.Phase != "VALIDATE" && state.Ingestion.Phase != "INDEX" {
|
||||
return false, fmt.Errorf("%w: ingestion phase %q", ErrInvalidPhase, state.Ingestion.Phase)
|
||||
}
|
||||
rationale := joinRationale(g.Rationale)
|
||||
changed := false
|
||||
if !state.Policy.Quarantined {
|
||||
state.Policy.Quarantined = true
|
||||
changed = true
|
||||
}
|
||||
if state.Policy.Rationale != rationale {
|
||||
state.Policy.Rationale = rationale
|
||||
changed = true
|
||||
}
|
||||
if !state.Control.Degraded {
|
||||
state.Control.Degraded = true
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
state.Ingestion.Epoch++
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "COUNCIL_PROFILES_LOADED":
|
||||
if state.Council.Phase == "ELECT" {
|
||||
return false, nil
|
||||
}
|
||||
if state.Council.Phase != "" && state.Council.Phase != "PLAN_ROLES" {
|
||||
return false, fmt.Errorf("%w: council phase %q", ErrInvalidPhase, state.Council.Phase)
|
||||
}
|
||||
state.Council.Phase = "ELECT"
|
||||
state.Council.Epoch++
|
||||
return true, nil
|
||||
|
||||
case "COUNCIL_QUORUM_CERT":
|
||||
if state.Council.Phase == "TOOLING_SYNC" {
|
||||
return false, nil
|
||||
}
|
||||
if state.Council.Phase != "ELECT" {
|
||||
return false, fmt.Errorf("%w: council phase %q", ErrInvalidPhase, state.Council.Phase)
|
||||
}
|
||||
if !g.QuorumOK || !g.PolicyOK {
|
||||
return false, fmt.Errorf("%w: quorum=%t policy=%t", ErrGuardRejected, g.QuorumOK, g.PolicyOK)
|
||||
}
|
||||
state.Council.Phase = "TOOLING_SYNC"
|
||||
state.Council.Epoch++
|
||||
return true, nil
|
||||
|
||||
case "COUNCIL_MCP_GREEN":
|
||||
if state.Council.Phase == "READY" && state.Council.MCPHealthGreen {
|
||||
return false, nil
|
||||
}
|
||||
if state.Council.Phase != "TOOLING_SYNC" {
|
||||
return false, fmt.Errorf("%w: council phase %q", ErrInvalidPhase, state.Council.Phase)
|
||||
}
|
||||
if !g.MCPHealthy {
|
||||
return false, fmt.Errorf("%w: mcp unhealthy", ErrGuardRejected)
|
||||
}
|
||||
changed := false
|
||||
if state.Council.Phase != "READY" {
|
||||
state.Council.Phase = "READY"
|
||||
changed = true
|
||||
}
|
||||
if !state.Council.MCPHealthGreen {
|
||||
state.Council.MCPHealthGreen = true
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
state.Council.Epoch++
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "ENV_CAPACITY_OK":
|
||||
if state.Environment.Phase == "PROVISION" && state.Environment.CapacityOK && state.Environment.Health == "" {
|
||||
return false, nil
|
||||
}
|
||||
if state.Environment.Phase != "" && state.Environment.Phase != "ALLOCATE" {
|
||||
return false, fmt.Errorf("%w: environment phase %q", ErrInvalidPhase, state.Environment.Phase)
|
||||
}
|
||||
if !g.BackbeatOK {
|
||||
return false, fmt.Errorf("%w: backbeat guard false", ErrGuardRejected)
|
||||
}
|
||||
changed := false
|
||||
if state.Environment.Phase != "PROVISION" {
|
||||
state.Environment.Phase = "PROVISION"
|
||||
changed = true
|
||||
}
|
||||
if !state.Environment.CapacityOK {
|
||||
state.Environment.CapacityOK = true
|
||||
changed = true
|
||||
}
|
||||
if state.Environment.Health != "" {
|
||||
state.Environment.Health = ""
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
state.Environment.Epoch++
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "ENV_INSTALLED":
|
||||
if state.Environment.Phase == "HEALTHCHECK" {
|
||||
return false, nil
|
||||
}
|
||||
if state.Environment.Phase != "PROVISION" {
|
||||
return false, fmt.Errorf("%w: environment phase %q", ErrInvalidPhase, state.Environment.Phase)
|
||||
}
|
||||
state.Environment.Phase = "HEALTHCHECK"
|
||||
state.Environment.Epoch++
|
||||
return true, nil
|
||||
|
||||
case "ENV_HEALTH_GREEN":
|
||||
if state.Environment.Phase == "READY" && state.Environment.Health == "green" && !state.Control.Degraded {
|
||||
return false, nil
|
||||
}
|
||||
if state.Environment.Phase != "HEALTHCHECK" {
|
||||
return false, fmt.Errorf("%w: environment phase %q", ErrInvalidPhase, state.Environment.Phase)
|
||||
}
|
||||
changed := false
|
||||
if state.Environment.Phase != "READY" {
|
||||
state.Environment.Phase = "READY"
|
||||
changed = true
|
||||
}
|
||||
if state.Environment.Health != "green" {
|
||||
state.Environment.Health = "green"
|
||||
changed = true
|
||||
}
|
||||
if state.Control.Degraded {
|
||||
state.Control.Degraded = false
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
state.Environment.Epoch++
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "ENV_HEALTH_AMBER":
|
||||
if state.Environment.Phase == "DEGRADED" && state.Environment.Health == "amber" && state.Control.Degraded {
|
||||
return false, nil
|
||||
}
|
||||
if state.Environment.Phase != "HEALTHCHECK" {
|
||||
return false, fmt.Errorf("%w: environment phase %q", ErrInvalidPhase, state.Environment.Phase)
|
||||
}
|
||||
changed := false
|
||||
if state.Environment.Phase != "DEGRADED" {
|
||||
state.Environment.Phase = "DEGRADED"
|
||||
changed = true
|
||||
}
|
||||
if state.Environment.Health != "amber" {
|
||||
state.Environment.Health = "amber"
|
||||
changed = true
|
||||
}
|
||||
if !state.Control.Degraded {
|
||||
state.Control.Degraded = true
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
state.Environment.Epoch++
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "ENV_RECOVERED_GREEN":
|
||||
if state.Environment.Phase == "READY" && state.Environment.Health == "green" && !state.Control.Degraded {
|
||||
return false, nil
|
||||
}
|
||||
if state.Environment.Phase != "DEGRADED" {
|
||||
return false, fmt.Errorf("%w: environment phase %q", ErrInvalidPhase, state.Environment.Phase)
|
||||
}
|
||||
if !g.MCPHealthy {
|
||||
return false, fmt.Errorf("%w: mcp unhealthy", ErrGuardRejected)
|
||||
}
|
||||
changed := false
|
||||
if state.Environment.Phase != "READY" {
|
||||
state.Environment.Phase = "READY"
|
||||
changed = true
|
||||
}
|
||||
if state.Environment.Health != "green" {
|
||||
state.Environment.Health = "green"
|
||||
changed = true
|
||||
}
|
||||
if state.Control.Degraded {
|
||||
state.Control.Degraded = false
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
state.Environment.Epoch++
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "EXEC_PLAN_LOCKED":
|
||||
if state.Execution.Phase == "WORK" && state.Execution.PlanLocked && state.Execution.ActiveWindowID == t.WindowID {
|
||||
return false, nil
|
||||
}
|
||||
if state.Execution.Phase != "" && state.Execution.Phase != "PLAN" {
|
||||
return false, fmt.Errorf("%w: execution phase %q", ErrInvalidPhase, state.Execution.Phase)
|
||||
}
|
||||
if !g.BackbeatOK {
|
||||
return false, fmt.Errorf("%w: backbeat guard false", ErrGuardRejected)
|
||||
}
|
||||
if state.Ingestion.Phase != "READY" || state.Council.Phase != "READY" || state.Environment.Phase != "READY" {
|
||||
return false, fmt.Errorf("%w: prerequisites not ready", ErrPrecondition)
|
||||
}
|
||||
changed := false
|
||||
if state.Execution.Phase != "WORK" {
|
||||
state.Execution.Phase = "WORK"
|
||||
changed = true
|
||||
}
|
||||
if !state.Execution.PlanLocked {
|
||||
state.Execution.PlanLocked = true
|
||||
changed = true
|
||||
}
|
||||
if state.Execution.ActiveWindowID != t.WindowID {
|
||||
state.Execution.ActiveWindowID = t.WindowID
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
state.Execution.Epoch++
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "EXEC_BEAT_REVIEW_GATE":
|
||||
if state.Execution.Phase == "REVIEW" {
|
||||
return false, nil
|
||||
}
|
||||
if state.Execution.Phase != "WORK" {
|
||||
return false, fmt.Errorf("%w: execution phase %q", ErrInvalidPhase, state.Execution.Phase)
|
||||
}
|
||||
if !g.BackbeatOK {
|
||||
return false, fmt.Errorf("%w: backbeat guard false", ErrGuardRejected)
|
||||
}
|
||||
state.Execution.Phase = "REVIEW"
|
||||
state.Execution.PlanLocked = true
|
||||
state.Execution.Epoch++
|
||||
return true, nil
|
||||
|
||||
case "EXEC_APPROVALS_THRESHOLD":
|
||||
if state.Execution.Phase == "REVERB" {
|
||||
return false, nil
|
||||
}
|
||||
if state.Execution.Phase != "REVIEW" {
|
||||
return false, fmt.Errorf("%w: execution phase %q", ErrInvalidPhase, state.Execution.Phase)
|
||||
}
|
||||
if !g.QuorumOK || !g.PolicyOK {
|
||||
return false, fmt.Errorf("%w: quorum=%t policy=%t", ErrGuardRejected, g.QuorumOK, g.PolicyOK)
|
||||
}
|
||||
state.Execution.Phase = "REVERB"
|
||||
state.Execution.Epoch++
|
||||
return true, nil
|
||||
|
||||
case "EXEC_CHANGES_REQUESTED":
|
||||
if state.Execution.Phase == "WORK" {
|
||||
return false, nil
|
||||
}
|
||||
if state.Execution.Phase != "REVIEW" {
|
||||
return false, fmt.Errorf("%w: execution phase %q", ErrInvalidPhase, state.Execution.Phase)
|
||||
}
|
||||
if !g.PolicyOK {
|
||||
return false, fmt.Errorf("%w: policy guard false", ErrGuardRejected)
|
||||
}
|
||||
state.Execution.Phase = "WORK"
|
||||
state.Execution.PlanLocked = true
|
||||
state.Execution.Epoch++
|
||||
return true, nil
|
||||
|
||||
case "EXEC_NEXT_WINDOW":
|
||||
if state.Execution.Phase == "PLAN" && !state.Execution.PlanLocked && state.Execution.ActiveWindowID == t.WindowID && state.Execution.Approvals == 0 {
|
||||
return false, nil
|
||||
}
|
||||
if state.Execution.Phase != "REVERB" {
|
||||
return false, fmt.Errorf("%w: execution phase %q", ErrInvalidPhase, state.Execution.Phase)
|
||||
}
|
||||
if !g.BackbeatOK {
|
||||
return false, fmt.Errorf("%w: backbeat guard false", ErrGuardRejected)
|
||||
}
|
||||
changed := false
|
||||
if state.Execution.Phase != "PLAN" {
|
||||
state.Execution.Phase = "PLAN"
|
||||
changed = true
|
||||
}
|
||||
if state.Execution.PlanLocked {
|
||||
state.Execution.PlanLocked = false
|
||||
changed = true
|
||||
}
|
||||
if state.Execution.ActiveWindowID != t.WindowID {
|
||||
state.Execution.ActiveWindowID = t.WindowID
|
||||
changed = true
|
||||
}
|
||||
if state.Execution.Approvals != 0 {
|
||||
state.Execution.Approvals = 0
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
state.Execution.Epoch++
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "CONTROL_PAUSE":
|
||||
needPause := !state.Control.Paused
|
||||
needRecoverReset := state.Control.Recovering
|
||||
if !needPause && !needRecoverReset {
|
||||
return false, nil
|
||||
}
|
||||
if !g.PolicyOK {
|
||||
return false, fmt.Errorf("%w: policy guard false", ErrGuardRejected)
|
||||
}
|
||||
if needPause {
|
||||
state.Control.Paused = true
|
||||
}
|
||||
if needRecoverReset {
|
||||
state.Control.Recovering = false
|
||||
}
|
||||
return true, nil
|
||||
|
||||
case "CONTROL_RESUME":
|
||||
if !state.Control.Paused {
|
||||
return false, fmt.Errorf("%w: system not paused", ErrPrecondition)
|
||||
}
|
||||
if !g.PolicyOK {
|
||||
return false, fmt.Errorf("%w: policy guard false", ErrGuardRejected)
|
||||
}
|
||||
changed := false
|
||||
if state.Control.Paused {
|
||||
state.Control.Paused = false
|
||||
changed = true
|
||||
}
|
||||
if state.Control.Recovering {
|
||||
state.Control.Recovering = false
|
||||
changed = true
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "CONTROL_RECOVERY_ENTER":
|
||||
if state.Control.Recovering && state.Control.Degraded {
|
||||
return false, nil
|
||||
}
|
||||
if g.QuorumOK && !state.Control.Degraded {
|
||||
return false, fmt.Errorf("%w: no recovery trigger", ErrGuardRejected)
|
||||
}
|
||||
changed := false
|
||||
if !state.Control.Recovering {
|
||||
state.Control.Recovering = true
|
||||
changed = true
|
||||
}
|
||||
if !state.Control.Degraded {
|
||||
state.Control.Degraded = true
|
||||
changed = true
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "CONTROL_RECOVERY_RESOLVED":
|
||||
if !state.Control.Recovering {
|
||||
return false, fmt.Errorf("%w: not in recovery", ErrPrecondition)
|
||||
}
|
||||
if !g.QuorumOK && state.Environment.Health != "green" {
|
||||
return false, fmt.Errorf("%w: recovery not satisfied", ErrGuardRejected)
|
||||
}
|
||||
changed := false
|
||||
if state.Control.Recovering {
|
||||
state.Control.Recovering = false
|
||||
changed = true
|
||||
}
|
||||
if state.Control.Degraded {
|
||||
state.Control.Degraded = false
|
||||
changed = true
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "TRANSITION_QUARANTINE_ENTER":
|
||||
if g.PolicyOK {
|
||||
return false, fmt.Errorf("%w: policy guard true", ErrGuardRejected)
|
||||
}
|
||||
rationale := joinRationale(g.Rationale)
|
||||
changed := false
|
||||
if !state.Policy.Quarantined {
|
||||
state.Policy.Quarantined = true
|
||||
changed = true
|
||||
}
|
||||
if state.Policy.Rationale != rationale {
|
||||
state.Policy.Rationale = rationale
|
||||
changed = true
|
||||
}
|
||||
if !state.Control.Paused {
|
||||
state.Control.Paused = true
|
||||
changed = true
|
||||
}
|
||||
if state.Control.Recovering {
|
||||
state.Control.Recovering = false
|
||||
changed = true
|
||||
}
|
||||
if !state.Control.Degraded {
|
||||
state.Control.Degraded = true
|
||||
changed = true
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "TRANSITION_QUARANTINE_RELEASE":
|
||||
if !state.Policy.Quarantined {
|
||||
return false, fmt.Errorf("%w: not quarantined", ErrPrecondition)
|
||||
}
|
||||
if !g.PolicyOK {
|
||||
return false, fmt.Errorf("%w: policy guard false", ErrGuardRejected)
|
||||
}
|
||||
changed := false
|
||||
if state.Policy.Quarantined {
|
||||
state.Policy.Quarantined = false
|
||||
changed = true
|
||||
}
|
||||
if state.Control.Paused {
|
||||
state.Control.Paused = false
|
||||
changed = true
|
||||
}
|
||||
if state.Control.Degraded {
|
||||
state.Control.Degraded = false
|
||||
changed = true
|
||||
}
|
||||
if state.Control.Recovering {
|
||||
state.Control.Recovering = false
|
||||
changed = true
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
case "TRANSITION_QUARANTINE_CONFIRM_BLOCK":
|
||||
if !state.Policy.Quarantined {
|
||||
return false, fmt.Errorf("%w: not quarantined", ErrPrecondition)
|
||||
}
|
||||
changed := false
|
||||
if !state.Control.Paused {
|
||||
state.Control.Paused = true
|
||||
changed = true
|
||||
}
|
||||
if !state.Control.Degraded {
|
||||
state.Control.Degraded = true
|
||||
changed = true
|
||||
}
|
||||
if state.Control.Recovering {
|
||||
state.Control.Recovering = false
|
||||
changed = true
|
||||
}
|
||||
return changed, nil
|
||||
|
||||
default:
|
||||
return false, fmt.Errorf("%w: %s", ErrUnknownTransition, name)
|
||||
}
|
||||
}
|
||||
|
||||
func joinRationale(r []string) string {
|
||||
if len(r) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(r, "; ")
|
||||
}
|
||||
|
||||
func computeStateHash(state OrchestratorState) (string, error) {
|
||||
payload, err := canonicalJSON(state)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(payload)
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
func canonicalJSON(value any) ([]byte, error) {
|
||||
buf, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
Reference in New Issue
Block a user