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 in exec/os/executor.go.
  • Access CLI flags via model.ActionFlags, which is populated by the YAML spec parser in build/spec/spec.go.
  • Return spec.ReturnFail() for errors or spec.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:

  1. CLI parses flags and builds *spec.ExpModel via createExpModel in cli/cmd/exp.go.
  2. Framework looks up the executor using createExecutorKey("redis", "", "network").
  3. Your Exec method receives the UID, context, and model, then dispatches to create() or destroy().
  4. Response is encoded and returned to the console.

Summary

  • Implement the spec.Executor interface with Name(), Exec(), and SetChannel() methods in a new package under exec/<target>/.
  • Expose a NewExecutor() factory function that returns your implementation.
  • Register the executor in cli/cmd/exp.go by calling specutil.ParseSpecsToModel() with your YAML spec file and factory function.
  • Define a <target>-spec-<ver>.yaml file declaring actions, flags, and matchers for your target type.
  • Handle both create and destroy modes in Exec() using spec.IsDestroy(ctx) to return appropriate *spec.Response objects.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →