Adding a new module
Checklist for implementing a new chaos kind. The existing
pkg/killing package is the best reference to copy from.
1. Create the package
pkg/<kind>/
├── config.go # Config, Scenario, ParseConfig([]byte)
├── module.go # Module struct, New(client, cfg), Name/Schedule/Run
├── register.go # Build function matching module.Builder
└── module_test.go # ParseConfig tests + Run tests with fake client
Stick to these four files. Anything else will be hard to review because it will diverge from the shape readers already expect.
2. Implement ParseConfig
type Config struct {
Kind string `yaml:"kind"`
Name string `yaml:"name"`
Metadata module.Metadata `yaml:"metadata"`
Scenario Scenario `yaml:"scenario"`
interval time.Duration // unexported, parsed from Scenario.RawInterval
wait time.Duration // unexported, parsed from Scenario.RawWait
}
type Scenario struct {
RawInterval string `yaml:"interval"`
RawWait string `yaml:"wait"`
DryRun bool `yaml:"dryRun"`
Matchers matchers.Matchers `yaml:"matchers"`
// ... your module-specific fields
}
func ParseConfig(data []byte) (Config, error) { /* ... */ }
Rules:
- All validation happens here, not in
New. - Return concrete error messages:
"<kind> config requires a name","invalid scenario.interval %q: %w". - Never touch
testing:/load:— those are centrally handled.
3. Implement Module
var _ module.ChaosModule = (*Module)(nil)
type Module struct { /* fields */ }
func New(client kubernetes.Interface, cfg Config) *Module { /* ... */ }
func (m *Module) Name() string { return m.name }
func (m *Module) Schedule() module.Schedule { /* ... */ }
func (m *Module) Run(ctx context.Context) error { /* ... */ }
Rules:
Name()returns the user-providedname:— it's used as the Prometheus label value and in every log line.Runmust honorctxon every API call.Runmust honordryRun: log the intended action, skip the mutation.- Always include
kind,name,namespacein log fields. - Target selection goes through
matchers.CollectPods— unless your module acts on workloads directly, likeRollout.
4. Expose Build
// register.go
func Build(client kubernetes.Interface, data []byte) (module.ChaosModule, error) {
cfg, err := ParseConfig(data)
if err != nil {
return nil, fmt.Errorf("invalid <kind> config: %w", err)
}
return New(client, cfg), nil
}
5. Register in main
Open cmd/chaos_zookoo/main.go and extend:
var builders = map[string]module.Builder{
"Killing": killing.Build,
"Rollout": rollout.Build,
"GorillaKill": gorillakill.Build,
"<Kind>": <pkg>.Build, // ← add this
}
The kind: string in YAML is matched verbatim against this map.
6. Add an example
Drop an annotated examples/<kind>.yaml. The existing samples are the
documentation surface — match their level of inline commentary. Every
field should have a comment explaining what it does, its default, and
its constraints.
7. Write tests
ParseConfig— table-driven over valid and invalid inputs. Every validation branch gets a case.Run— withfake.NewSimpleClientset, assert on the API calls made (or not made, indryRun).
8. Add user-facing documentation
Create documentation/docs/modules/<kind>.md following the shape of the
existing module pages: When to use it, Minimal config, Full
reference, Behavior, RBAC.
Update documentation/sidebars.ts so the new page appears in the
Modules category.
Adding a middleware
The middleware path is separate — it does not touch existing modules.
- Create
pkg/<middleware>/with:config.goexposing aSpecwithApplyDefaultsAndValidate(scenarioInterval time.Duration) error,middleware.goexposingNewMiddleware(sup *Supervisor, spec *Spec) module.Middleware,- a process-wide supervisor tracking in-flight work and a
Stop()that drains it.
- Extend
internal/config/crosscutting.gowith a new top-level YAML key and callspec.ApplyDefaultsAndValidate(interval)fromParseCrossCutting. - Instantiate the supervisor in
cmd/chaos_zookoo/main.goand wrapmwith the new middleware inregisterModules— pay attention to the wrap order (see Middlewares overview). - Add tests and a doc page.
The test that you got it right: you can disable the new middleware for all existing scenarios by removing the YAML block and nothing about the modules changes. That's the invariant.