How to Write Custom Executors for Adding New Experiment Target Types in ChaosBlade
To add a new experiment target type in ChaosBlade, implement the spec.Executor interface with Name(), Exec(), and SetChannel() methods, expose a NewExecutor() factory function, register it in cli/cmd/exp.go via specutil.ParseSpecsToModel(), and provide a YAML spec file describing the target's actions and flags.
ChaosBlade is an open-source chaos engineering platform that uses modular executors to run experiments against different target types such as OS processes, Kubernetes pods, and Docker containers. When you need to support a proprietary service, custom middleware, or a new infrastructure component, you must write a custom executor that satisfies the spec.Executor interface defined in the chaosblade-spec-go module. This guide walks you through implementing this interface and wiring your executor into the CLI using the actual source code structure from the chaosblade-io/chaosblade repository.
Understanding the Executor Architecture
ChaosBlade’s experiment execution is driven by executors – Go structs that implement the spec.Executor interface located in chaosblade-spec-go/spec/executor.go. Each target type (e.g., os, k8s, docker) maintains its own executor implementation under exec/<target>/executor.go.
The core contract requires three methods:
Name() string– Returns the target identifier (e.g.,"redis","os").SetChannel(ch spec.Channel)– Receives a communication channel for local, SSH, or CRI exec operations.Exec(uid string, ctx context.Context, model *spec.ExpModel) *spec.Response– Executes the chaos experiment and returns a standardized response.
The CLI stores executors in a map within cli/cmd/exp.go, keyed by target and action names. When users run blade create <target> <action>, the framework invokes specutil.ParseSpecsToModel() to parse the target's YAML specification and bind it to your executor implementation.
Implementing the spec.Executor Interface
Creating the Package Structure
Create a new directory under exec/ for your target type. For example, to add a Redis target:
exec/redis/
executor.go
executor_test.go
Writing the Executor Boilerplate
Implement the interface in exec/redis/executor.go. The Exec method must handle both create and destroy operations by checking the context:
package redis
import (
"context"
"fmt"
"os/exec"
"github.com/chaosblade-io/chaosblade-spec-go/spec"
"github.com/chaosblade-io/chaosblade-spec-go/log"
)
// Executor implements spec.Executor for the "redis" target.
type Executor struct{}
// NewExecutor returns a spec.Executor that the CLI can use.
func NewExecutor() spec.Executor {
return &Executor{}
}
// Name returns the target name – must match the target name used in the spec yaml.
func (e *Executor) Name() string { return "redis" }
// SetChannel allows the CLI to inject a communication channel (local, ssh, etc.).
func (e *Executor) SetChannel(ch spec.Channel) { /* no special handling needed */ }
// Exec is the heart of the executor. It receives the experiment model generated
// from the CLI flags and must return a *spec.Response.
func (e *Executor) Exec(uid string, ctx context.Context, model *spec.ExpModel) *spec.Response {
// Determine if we are creating or destroying the experiment.
mode, _ := spec.IsDestroy(ctx)
if mode == spec.Destroy {
return e.destroy(uid, ctx, model)
}
return e.create(uid, ctx, model)
}
// create builds and runs the redis‑specific chaos command.
func (e *Executor) create(uid string, ctx context.Context, model *spec.ExpModel) *spec.Response {
args := []string{"redis-cli"}
for k, v := range model.ActionFlags {
if v == "" {
continue
}
args = append(args, fmt.Sprintf("--%s=%s", k, v))
}
args = append(args, fmt.Sprintf("--uid=%s", uid))
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
log.Debugf(ctx, "redis executor run: %v", args)
output, err := cmd.CombinedOutput()
if err != nil {
return spec.ReturnFail(spec.OsCmdExecFailed, fmt.Sprintf("redis command error: %s", err))
}
return spec.Decode(string(output), nil)
}
// destroy reverses the chaos effect.
func (e *Executor) destroy(uid string, ctx context.Context, model *spec.ExpModel) *spec.Response {
args := []string{"redis-cli", "FLUSHALL", fmt.Sprintf("--uid=%s", uid)}
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
output, err := cmd.CombinedOutput()
if err != nil {
return spec.ReturnFail(spec.OsCmdExecFailed, fmt.Sprintf("redis destroy error: %s", err))
}
return spec.Decode(string(output), nil)
}
Key implementation details from the source code:
- Use
spec.IsDestroy(ctx)to determine operation mode, as seen inexec/os/executor.go. - Access CLI flags via
model.ActionFlags, which is populated by the YAML spec parser inbuild/spec/spec.go. - Return
spec.ReturnFail()for errors orspec.Decode()for successful output parsing.
Registering the Executor with the ChaosBlade CLI
The CLI automatically binds executors to commands when it parses a target’s spec file. You must add a registration function in cli/cmd/exp.go that calls specutil.ParseSpecsToModel():
func (ec *baseExpCommandService) registerRedisExpCommands() []*modelCommand {
file := path.Join(specutil.GetYamlHome(),
fmt.Sprintf("chaosblade-redis-spec-%s.yaml", version.Ver))
models, err := specutil.ParseSpecsToModel(file, redis.NewExecutor())
if err != nil {
return nil
}
cmds := make([]*modelCommand, 0)
for i := range models.Models {
model := &models.Models[i]
cmd := ec.registerExpCommand(model, "")
cmds = append(cmds, cmd)
}
return cmds
}
Then hook this function into registerSubCommands() around line 120 in cli/cmd/exp.go:
func (ec *baseExpCommandService) registerSubCommands() {
// … existing registrations …
ec.registerRedisExpCommands()
}
The ParseSpecsToModel function (located in build/spec/spec.go) reads your YAML file and binds the executor instance to the command tree. The CLI stores the executor in a map keyed by createExecutorKey(target, action), which is later retrieved when users invoke blade create redis network.
Defining the Target Specification YAML
Create a YAML file describing your target’s actions, flags, and matchers. Place it in the directory returned by specutil.GetYamlHome() (typically the spec/ directory or binary location):
# chaosblade-redis-spec-1.8.0.yaml
target: redis
scope: container
desc: "Chaos experiments for Redis"
actions:
- name: network
desc: "Inject network latency to Redis"
flags:
- name: latency
desc: "Latency in milliseconds"
required: true
flag_type: uint
matchers:
- name: address
desc: "Target IP address"
required: false
The CLI uses this file to generate cobra commands and validate user input. When a user runs blade create redis network --latency 1000, the framework populates model.ActionFlags["latency"] with "1000" and passes the model to your executor's Exec method.
Complete Usage Example
Once implemented and registered, your custom executor works like any built-in target:
# Create a network latency experiment
blade create redis network --latency 2000 --address 127.0.0.1 --uid $(uuidgen)
# Check experiment status
blade status redis network
# Destroy the experiment using the returned UID
blade destroy <uid> --target redis
The execution flow follows this path:
- CLI parses flags and builds
*spec.ExpModelviacreateExpModelincli/cmd/exp.go. - Framework looks up the executor using
createExecutorKey("redis", "", "network"). - Your
Execmethod receives the UID, context, and model, then dispatches tocreate()ordestroy(). - Response is encoded and returned to the console.
Summary
- Implement the
spec.Executorinterface withName(),Exec(), andSetChannel()methods in a new package underexec/<target>/. - Expose a
NewExecutor()factory function that returns your implementation. - Register the executor in
cli/cmd/exp.goby callingspecutil.ParseSpecsToModel()with your YAML spec file and factory function. - Define a
<target>-spec-<ver>.yamlfile declaring actions, flags, and matchers for your target type. - Handle both create and destroy modes in
Exec()usingspec.IsDestroy(ctx)to return appropriate*spec.Responseobjects.
Frequently Asked Questions
What interface must a custom executor implement?
Your executor must implement the spec.Executor interface defined in chaosblade-spec-go/spec/executor.go. This requires implementing Name() string, SetChannel(ch spec.Channel), and Exec(uid string, ctx context.Context, model *spec.ExpModel) *spec.Response. The Exec method is the entry point where your chaos logic runs.
Where do I register a new executor in the ChaosBlade codebase?
Register your executor in cli/cmd/exp.go by creating a registration function (e.g., registerRedisExpCommands()) that calls specutil.ParseSpecsToModel() with your spec YAML path and NewExecutor() factory. Add this function to registerSubCommands() to ensure it loads when the CLI starts.
How does the CLI map commands to my executor?
The CLI uses specutil.ParseSpecsToModel() to read your YAML spec and build command models. It stores your executor in an internal map keyed by target and action names. When a user runs blade create <target> <action>, the framework retrieves your executor via createExecutorKey() and invokes its Exec method with the parsed experiment model.
Can I write unit tests for my custom executor?
Yes. Create an executor_test.go file in your target package (e.g., exec/redis/executor_test.go) and test the Name() method and Exec() behavior using mock contexts. Follow the testing patterns in exec/os/executor_test.go, which demonstrates how to verify correct *spec.Response returns and flag handling.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →