117 lines
4.6 KiB
Go
117 lines
4.6 KiB
Go
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 ""
|
|
}
|