# How to Write Custom Executors for Adding New Experiment Target Types in ChaosBlade

> Learn to write custom executors for new experiment target types in ChaosBlade. Implement the spec.Executor interface, create a factory function, and register your executor to extend ChaosBlade's capabilities.

- Repository: [ChaosBlade/chaosblade](https://github.com/chaosblade-io/chaosblade)
- Tags: how-to-guide
- Published: 2026-02-27

---

**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`](https://github.com/chaosblade-io/chaosblade/blob/main/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`](https://github.com/chaosblade-io/chaosblade/blob/main/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`](https://github.com/chaosblade-io/chaosblade/blob/main/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:

```text
exec/redis/
    executor.go
    executor_test.go

```

### Writing the Executor Boilerplate

Implement the interface in [`exec/redis/executor.go`](https://github.com/chaosblade-io/chaosblade/blob/main/exec/redis/executor.go). The `Exec` method must handle both create and destroy operations by checking the context:

```go
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`](https://github.com/chaosblade-io/chaosblade/blob/main/exec/os/executor.go).
- Access CLI flags via `model.ActionFlags`, which is populated by the YAML spec parser in [`build/spec/spec.go`](https://github.com/chaosblade-io/chaosblade/blob/main/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`](https://github.com/chaosblade-io/chaosblade/blob/main/cli/cmd/exp.go) that calls `specutil.ParseSpecsToModel()`:

```go
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`](https://github.com/chaosblade-io/chaosblade/blob/main/cli/cmd/exp.go):

```go
func (ec *baseExpCommandService) registerSubCommands() {
    // … existing registrations …
    ec.registerRedisExpCommands()
}

```

The `ParseSpecsToModel` function (located in [`build/spec/spec.go`](https://github.com/chaosblade-io/chaosblade/blob/main/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):

```yaml

# 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:

```bash

# 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`](https://github.com/chaosblade-io/chaosblade/blob/main/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`](https://github.com/chaosblade-io/chaosblade/blob/main/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`](https://github.com/chaosblade-io/chaosblade/blob/main/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`](https://github.com/chaosblade-io/chaosblade/blob/main/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`](https://github.com/chaosblade-io/chaosblade/blob/main/executor_test.go) file in your target package (e.g., [`exec/redis/executor_test.go`](https://github.com/chaosblade-io/chaosblade/blob/main/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`](https://github.com/chaosblade-io/chaosblade/blob/main/exec/os/executor_test.go), which demonstrates how to verify correct `*spec.Response` returns and flag handling.