feat: add harness runner (scenario executor, alert receiver, action executor, judge, revert)
This commit is contained in:
1 parent
8f5378b6ea
commit
5daf13b669
16 files changed
+2550
No files matched your search
@@ -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 ""
|
||||
}
|
||||
Reference in new issue
Block a user