feat: add harness runner (scenario executor, alert receiver, action executor, judge, revert)

This commit is contained in:
seon committed 2026-09-24 02:06:08 +09:00
1 parent 8f5378b6ea
commit 5daf13b669
16 files changed
+2550

No files matched your search

+116
View File
@@ -0,0 +1,116 @@
package main
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
// Scenario is one row of experiment design 7 (12 files under vm/harness/scenarios in the gitops repo).
type Scenario struct {
ID string `yaml:"id" json:"scenario_id"`
Path string `yaml:"path" json:"path"` // known | gray | irreversible | misjudge
Asset string `yaml:"asset" json:"asset"` // container | vm
ExpectedLayer int `yaml:"expected_layer" json:"expected_layer"`
GroundTruthLabel string `yaml:"ground_truth_label" json:"ground_truth_label"`
GroundTruthAction string `yaml:"ground_truth_action" json:"ground_truth_action"` // action_id | escalate | none
Description string `yaml:"description" json:"description"`
Inject Step `yaml:"inject" json:"inject"`
Revert Step `yaml:"revert" json:"revert"`
EscalateAfterS int `yaml:"escalate_after_s" json:"escalate_after_s"` // signature alerts: hand to layer 2 after this (design 4.3: 5min / 10min)
Notes map[string]any `yaml:"notes,omitempty" json:"notes,omitempty"`
}
// Step is an injection or revert instruction. Exactly one method applies; the others stay empty.
type Step struct {
Method string `yaml:"method" json:"method"` // none | chaos | chaos_delete | kubectl_scale | ssh | gcloud | psql
// chaos: manifest of a Chaos Mesh CR (namespace/name are set by the runner)
Manifest string `yaml:"manifest,omitempty" json:"manifest,omitempty"`
// kubectl_scale
Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"`
Deployment string `yaml:"deployment,omitempty" json:"deployment,omitempty"`
Replicas int32 `yaml:"replicas,omitempty" json:"replicas,omitempty"`
// ssh: host is an IP, a pet VM name (db-primary, db-replica) or legacy:N (N-th MIG instance, name order)
Host string `yaml:"host,omitempty" json:"host,omitempty"`
Command string `yaml:"command,omitempty" json:"command,omitempty"`
// gcloud: op = instance_delete | instance_stop | instance_start | instance_reset | mig_recreate | mig_resize
Op string `yaml:"op,omitempty" json:"op,omitempty"`
Instance string `yaml:"instance,omitempty" json:"instance,omitempty"` // pet VM name or legacy:N
MIG string `yaml:"mig,omitempty" json:"mig,omitempty"`
Size int64 `yaml:"size,omitempty" json:"size,omitempty"`
// psql: harness role over the network (hostssl, scram)
Database string `yaml:"database,omitempty" json:"database,omitempty"`
SQL string `yaml:"sql,omitempty" json:"sql,omitempty"`
// wait after the step (seconds) before continuing, e.g. tc rules that need time to bite
WaitS int `yaml:"wait_s,omitempty" json:"wait_s,omitempty"`
}
func LoadScenarios(dir string) (map[string]*Scenario, error) {
files, err := filepath.Glob(filepath.Join(dir, "*.yaml"))
if err != nil {
return nil, err
}
if len(files) == 0 {
return nil, fmt.Errorf("no scenario files in %s", dir)
}
out := map[string]*Scenario{}
for _, f := range files {
b, err := os.ReadFile(f)
if err != nil {
return nil, err
}
var s Scenario
if err := yaml.Unmarshal(b, &s); err != nil {
return nil, fmt.Errorf("%s: %w", f, err)
}
if s.ID == "" {
return nil, fmt.Errorf("%s: missing id", f)
}
if _, dup := out[s.ID]; dup {
return nil, fmt.Errorf("%s: duplicate id %s", f, s.ID)
}
if s.Inject.Method == "" {
s.Inject.Method = "none"
}
if s.Revert.Method == "" {
s.Revert.Method = "none"
}
out[s.ID] = &s
}
return out, nil
}
func sortScenarios(list []*Scenario) {
sort.Slice(list, func(i, j int) bool { return list[i].ID < list[j].ID })
}
// InjectionSpec is what goes into scenarios.injection_spec (jsonb): the step without its manifest body.
func (s *Scenario) InjectionSpec() map[string]any {
m := map[string]any{"method": s.Inject.Method}
switch s.Inject.Method {
case "chaos":
m["kind"] = manifestKind(s.Inject.Manifest)
case "kubectl_scale":
m["namespace"], m["deployment"], m["replicas"] = s.Inject.Namespace, s.Inject.Deployment, s.Inject.Replicas
case "ssh":
m["host"], m["command"] = s.Inject.Host, s.Inject.Command
case "gcloud":
m["op"], m["instance"], m["mig"], m["size"] = s.Inject.Op, s.Inject.Instance, s.Inject.MIG, s.Inject.Size
case "psql":
m["host"], m["database"], m["sql"] = s.Inject.Host, s.Inject.Database, s.Inject.SQL
}
return m
}
func manifestKind(manifest string) string {
for _, l := range strings.Split(manifest, "\n") {
if strings.HasPrefix(strings.TrimSpace(l), "kind:") {
return strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(l), "kind:"))
}
}
return ""
}