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 "" }