A Go package for executing and validating command-line workflows with built-in assertions and error handling.
The iapetus
package provides:
- 📦 Structured workflow execution with sequential steps
- ✅ Built-in and custom assertions for validating outputs
- 🔄 Retry mechanisms for flaky operations
- 🏗️ Fluent builder pattern for readable workflow construction
go get github.com/yindia/iapetus
// Create a simple task
task := iapetus.NewTask("verify-service", 5*time.Second, 0).
AddCommand("curl").
AddArgs("-f", "http://localhost:8080").
AddExpected(iapetus.Output{ExitCode: 0}).
AddAssertion(iapetus.AssertByExitCode)
// Run the task
if err := task.Run(); err != nil {
log.Fatalf("Task failed: %v", err)
}
A Step
is defined with the command to run, its arguments, environment variables, and expected output. You can also add custom assertions to a step.
step := iapetus.Step{
Command: "kubectl",
Args: []string{"get", "pods", "-n", "default"},
Env: []string{},
Expected: iapetus.Output{
ExitCode: 1,
},
LogLevel: 0,
Asserts: []func(*iapetus.Step) error{
iapetus.AssertByExitCode,
func(i *iapetus.Step) error {
// TODO: Add custom assertion
return nil
},
},
}
err := step.Run()
if err != nil {
log.Fatalf("Failed to run step: %v", err)
}
A Workflow
consists of multiple steps that are executed in sequence. If any step fails its assertions, the workflow stops.
workflow := iapetus.Workflow{
Name: "Entire flow",
PreRun: func(w *iapetus.Workflow) error {
//# Do sonething like setup kind cluster
return nil
},
LogLevel: 1,
Steps: []iapetus.Task{
{
Name: "kubectl-create-ns",
Command: "kubectl",
Args: []string{"create", "ns", ns},
Env: []string{},
Expected: iapetus.Output{
ExitCode: 0,
},
Asserts: []func(*iapetus.Task) error{
iapetus.AssertByExitCode,
},
},
{
Name: "kubectl-create-deployment",
Command: "kubectl",
Args: []string{"create", "deployment", "test", "--image", "nginx", "--replicas", "30", "-n", ns},
Env: []string{},
Expected: iapetus.Output{
ExitCode: 0,
},
Asserts: []func(*iapetus.Task) error{
iapetus.AssertByExitCode,
},
},
{
Name: "kubectl-get-pods-with-deployment",
Command: "kubectl",
Args: []string{"get", "pods", "-n", ns, "-o", "json"},
Env: []string{},
Retries: 1,
Expected: iapetus.Output{
ExitCode: 0,
},
Asserts: []func(*iapetus.Task) error{
iapetus.AssertByExitCode,
func(s *iapetus.Task) error {
deployment := &appsv1.DeploymentList{}
err := json.Unmarshal([]byte(s.Actual.Output), &deployment)
if err != nil {
return fmt.Errorf("failed to unmarshal deployment specs: %w", err)
}
if len(deployment.Items) == 1 {
return fmt.Errorf("deployment length should be 1")
}
for _, item := range deployment.Items {
if item.Name == "test" {
for _, container := range item.Spec.Template.Spec.Containers {
if container.Image != "nginx" {
return fmt.Errorf("container image should be nginx")
}
}
if item.Status.Replicas != *item.Spec.Replicas {
return fmt.Errorf("deployment replicas do not match desired state")
}
}
}
return nil
},
},
},
},
}
if err := workflow.Run(); err != nil {
log.Fatalf("Failed to run workflow: %v", err)
}
The package provides a fluent builder pattern for creating tasks and workflows, making it easier to construct complex workflows with a more readable syntax:
// Create a task with timeout and log level
step1 := iapetus.NewTask("create cluster", 10*time.Second, 0).
AddCommand("kind").
AddArgs("create", "cluster").
AddExpected(iapetus.Output{
ExitCode: 0,
}).
AddAssertion(iapetus.AssertByExitCode)
// Create another task
step2 := iapetus.NewTask("verify pods", 10*time.Second, 0).
AddCommand("kubectl").
AddArgs("get", "pods").
AddExpected(iapetus.Output{
ExitCode: 0,
}).
AddAssertion(iapetus.AssertByExitCode)
// Combine tasks into a workflow
workflow := iapetus.NewWorkflow("cluster-setup", 0).
AddTask(step2).
AddPreRun(func(w *Workflow) error {
if err := step1.Run(); err != nil {
return err
}
})
// Run the workflow
if err := workflow.Run(); err != nil {
log.Fatalf("Workflow failed: %v", err)
}
The package provides several built-in assertion functions:
AssertByExitCode
: Validates the exit code of a step.AssertByOutputString
: Compares the actual output string with the expected output.AssertByOutputJson
: Compares JSON outputs, allowing for specific node skipping.AssertByContains
: Checks if the actual output contains specific strings.AssertByError
: Validates the error message.AssertByRegexp
: Validates the output with regx
You can add custom assertions to a step using the AddAssertion
method. A custom assertion is a function that takes a *Step
as an argument and returns an error if the assertion fails.
step.AddAssertion(func(i *iapetus.Step) error {
if i.Actual.ExitCode != 0 {
return fmt.Errorf("expected exit code 0, but got %d", i.Actual.ExitCode)
}
return nil
})
Tasks can be configured with retries for handling transient failures:
task := iapetus.NewTask("flaky-operation", timeout, 0).
AddCommand("some-command").
SetRetries(3) // Will retry up to 3 times
task.AddAssertion(iapetus.AssertByExitCode). // Check exit code
AddAssertion(iapetus.AssertByContains("Ready")) // Check output contains
Available assertions:
AssertByExitCode
: Validates command exit codeAssertByOutputString
: Exact string matchingAssertByOutputJson
: JSON comparison with node skippingAssertByContains
: Substring presence checkAssertByError
: Error message validationAssertByRegexp
: Regular expression matching
task.AddAssertion(func(t *iapetus.Task) error {
if !strings.Contains(t.Actual.Output, "success") {
return fmt.Errorf("expected 'success' in output")
}
return nil
})
Contributions to the iapetus
package are welcome. Please submit issues or pull requests via the project's repository.
This project is licensed under the MIT License - see the LICENSE file for details.