Helmit
Safety first!

Helmit is a Golang framework and tool for end-to-end testing of Kubernetes and Helm applications.
Helmit supports testing, benchmarking, and simulation inside Kubernetes clusters.
- Installation
- User Guide
- Examples
Installation
Helmit uses Go modules for dependency management. When installing the
helmit command, ensure Go modules are enabled:
GO111MODULE=on go get github.com/onosproject/helmit/cmd/helmit
User Guide
Helm API
Helmit provides a Go API for managing Helm charts within a Kubernetes cluster. Tests, benchmarks, and simulations
can use the Helmit Helm API to configure and install charts to test and query resources within releases.
The Helm API is provided by the github.com/onosproject/helmit/pkg/helm package:
import "github.com/onosproject/helmit/pkg/helm"
chart := helm.Chart("my-chart")
release := chart.Release("my-release")
The Helmit API supports installation of local or remote charts. To install a local chart, use the path
as the chart name:
helm.Chart("atomix-controller").
Release("atomix-controller").
Install(true)
To install a remote chart, simply use the chart name:
helm.Chart("kafka").
Release("kafka").
Install(true)
If the chart repository is not accessible from within the test container, you can optionally specify a repository
URL when creating the chart:
helm.Chart("kafka", "http://storage.googleapis.com/kubernetes-charts-incubator").
Release("kafka").
Install(true)
The Install method installs the chart in the same was as the helm install command does. The boolean flags to the
Install method indicates whether to block until the chart's resources are ready.
Release values can be set programmatically using the Set receiver:
helm.Chart("kafka", "http://storage.googleapis.com/kubernetes-charts-incubator").
Release("kafka").
Set("replicas", 2).
Set("zookeeper.replicaCount", 3).
Install(true)
Note that values set via command line flags take precedence over programmatically configured values.
Kubernetes Client
Tests often need to query the resources created by a Helm chart that has been installed. Helmit provides a
custom Kubernetes client designed to query Helm chart resources. The Helmit Kubernetes client looks similar
to the standard Go client but can limit the scope of API calls to
resources transitively owned by a Helm chart release.
To create a Kubernetes client for a release, call NewForRelease:
// Create an atomix-controller release
release := helm.Chart("atomix-controller").Release("atomix-controller")
// Create a Kubernetes client scoped for the atomix-controller release
client := kubernetes.NewForReleaseOrDie(release)
The release scoped client can be used to list resources created by the release. This can be helpful for e.g.
injecting failures into the cluster during tests:
// Get a list of pods created by the atomix-controller
pods, err := client.CoreV1().Pods().List()
assert.NoError(t, err)
// Get the Atomix controller pod
pod := pods[0]
// Delete the pod
err := pod.Delete()
assert.NoError(t, err)
Additionally, Kubernetes objects that create and own other Kubernetes resources -- like Deployment, StatefulSet,
Job, etc -- provide scoped clients that can be used to query the resources they own as well:
// Get the atomix-controller deployment
deps, err := client.AppsV1().Deployments().List()
assert.NoError(t, err)
assert.Len(t, deps, 1)
dep := deps[0]
// Get the pods created by the controller deployment
pods, err := dep.CoreV1().Pods().List()
assert.NoError(t, err)
assert.Len(t, pods, 1)
pod := pods[0]
// Delete the controller pod
err = pod.Delete()
assert.NoError(t, err)
// Wait a minute for the controller deployment to recover
err = dep.Wait(1 * time.Minute)
assert.NoError(t, err)
// Verify the pod was recovered
pods, err := dep.CoreV1().Pods().List()
assert.NoError(t, err)
assert.Len(t, pods, 1)
assert.NotEqual(t, pod.Name, pods[0].Name)
Code Generation
Like other Kubernetes clients, the Helmit Kubernetes client is generated from a set of templates and Kubernetes
resource metadata using the helmit-generate tool.
go run github.com/onosproject/helmit/cmd/helmit-generate ...
Given a YAML file defining the client's resources, the helmit-generate tool
generates the scoped client code. To generate the base Helmit Kubernetes client, run make generate:
make generate
To generate a client with additional resources that are not supported by the base client, define your own
client configuration and run the tool:
go run github.com/onosproject/helmit/cmd/helmit-generate ./my-client.yaml ./path/to/my/package
The helmit command-line tool is used to run tests, benchmarks, and simulations inside a Kubernetes cluster. To
install the helmit CLI, use go get with Go modules enabled:
GO111MODULE=on go get github.com/onosproject/helmit/cmd/helmit
To use the Helmit CLI, you must have kubectl installed and
configured. Helmit will use the Kubernetes configuration to connect to the cluster to deploy and run tests.
The Helmit CLI consists of only three commands:
Each command deploys and runs pods which can deploy Helm charts from within the Kubernetes cluster using the
Helm API. Each Helmit command supports configuring Helm values in the same way the helm command
itself does.
The helmit sub-commands support an optional context within which to run tests. When the --context flag is set,
the specified context directory will be copied to the Helmit pod running inside Kubernetes and set as the current
working directory during runs:
helmit test ./cmd/tests --context ./deploy/charts
This allows suites to reference charts by path from within Helmit containers deployed inside Kubernetes:
helm.Chart("./atomix-controller").
Release("atomix-controller-1").
Install(true)
As with Helm, the helmit commands also support values files and flags:
helmit test ./cmd/tests -f atomix-controller-1=atomix-values.yaml --set atomix-controller-1.replicas=2
Because suites may install multiple Helm releases, values files and flags must be prefixed by the release name.
For example, -f my-release=values.yaml will add a values file to the release named my-release, and
--set my-release.replicas=3 will set the replicas value for the release named my-release.
Testing
Helmit supports testing of Kubernetes resources and Helm charts using a custom test framework and
command line tool. To test a Kubernetes application, simply write a Golang test suite
and then run the suite using the helmit test tool:
helmit test ./cmd/tests
Writing Tests
Helmit tests are written as suites. When tests are run, each test suite will be deployed and run in its own namespace.
Test suite functions are executed serially.
import "github.com/onosproject/helmit/pkg/test"
type AtomixTestSuite struct {
test.Suite
}
The SetupTestSuite interface can be implemented to set up the test namespace prior to running tests:
func (s *AtomixTestSuite) SetupTestSuite() error {
err := helm.Chart("atomix-controller").
Release("atomix-controller").
Set("scope", "Namespace").
Install(true)
if err != nil {
return err
}
err = helm.Chart("atomix-database").
Release("atomix-raft").
Set("clusters", 3).
Set("partitions", 10).
Set("backend.replicas", 3).
Set("backend.image", "atomix/raft-replica:latest").
Install(true)
if err != nil {
return err
}
return nil
}
Tests are receivers on the test suite that follow the pattern Test*. The standard Golang testing library is
used, so all your favorite assertion libraries can be used as well:
import "testing"
func (s *AtomixTestSuite) TestMap(t *testing.T) {
address, err := s.getController()
assert.NoError(t, err)
client, err := atomix.New(address)
assert.NoError(t, err)
database, err := client.GetDatabase(context.Background(), "atomix-raft")
assert.NoError(t, err)
m, err := database.GetMap(context.Background(), "TestMap")
assert.NoError(t, err)
}
Helmit also supports TearDownTestSuite and TearDownTest functions for tearing down test suites and tests
respectively:
func (s *AtomixTestSuite) TearDownTest() error {
return helm.Chart("atomix-database").
Release("atomix-database").
Uninstall()
}
Registering Test Suites
In order to run tests, a main must be provided that registers and names test suites.
import "github.com/onosproject/helmit/pkg/registry"
func init() {
registry.RegisterTestSuite("atomix", &tests.AtomixTestSuite{})
}
Once the tests have been registered, the main should call test.Main() to run the tests:
import (
"github.com/onosproject/helmit/pkg/registry"
"github.com/onosproject/helmit/pkg/test"
tests "github.com/onosproject/helmit/test"
)
func main() {
registry.RegisterTestSuite("atomix", &tests.AtomixTestSuite{})
test.Main()
}
Running Tests
Once a test suite has been written and registered, running the tests on Kubernetes is simply a matter of running
the helmit test command and pointing to the test main:
helmit test ./cmd/tests
When helmit test is run with no additional arguments, the test coordinator will run all registered test suites
in parallel and each within its own namespace. To run a specific test suite, use the --suite flag:
helmit test ./cmd/tests --suite my-tests
The helmit test command also supports configuring tested Helm charts from the command-line. See the
command-line tools documentation for more info.
Benchmarking
Helmit supports benchmarking of Kubernetes resources and Helm charts using a custom benchmarking framework and
command line tool. To benchmark a Kubernetes application, simply write a Golang benchmark suite
and then run the suite using the helmit bench tool:
helmit bench ./cmd/benchmarks
The benchmark coordinator supports benchmarking a single function on a single node or scaling benchmarks across multiple
containers running inside a Kubernetes cluster.
Writing Benchmark Suites
To run a benchmark you must first define a benchmark suite. Benchmark suites are Golang structs containing a series
of receivers to benchmark:
import "github.com/onosproject/helmit/pkg/benchmark"
type AtomixBenchSuite struct {
benchmark.Suite
}
Helmit runs each suite within its own namespace, and each benchmark consists of a Benchmark* receivers on the suite.
Prior to running benchmarks, benchmarks suites typically need to set up resources within the Kubernetes namespace.
Benchmarks can implement the following interfaces to manage the namespace:
SetupBenchmarkSuite - Called on a single worker prior to running benchmarks
SetupBenchmarkWorker - Called on each worker pod prior to running benchmarks
SetupBenchmark - Called on each worker pod prior to running each benchmark
Typically, benchmark suites should implement the SetupBenchmarkSuite interface to install Helm charts:
func (s *AtomixBenchSuite) SetupBenchmarkSuite(c *benchmark.Context) error {
err := helm.Chart("atomix-controller").
Release("atomix-controller").
Set("scope", "Namespace").
Install(true)
if err != nil {
return err
}
err = helm.Chart("atomix-database").
Release("atomix-raft").
Set("clusters", 3).
Set("partitions", 10).
Set("backend.replicas", 3).
Set("backend.image", "atomix/raft-replica:latest").
Install(true)
if err != nil {
return err
}
return nil
}
Benchmarks are written as Benchmark* receivers:
func (s *AtomixBenchSuite) BenchmarkMapPut(b *benchmark.Benchmark) error {
...
}
Each benchmark receiver will be called repeatedly for a configured duration of number of iterations. To generate
randomized benchmark input, the input package provides input utilities:
import "github.com/onosproject/helmit/pkg/input"
var keys = input.RandomChoice(input.SetOf(input.RandomString(8), 1000))
var values = input.RandomBytes(128)
func (s *AtomixBenchSuite) BenchmarkMapPut(b *benchmark.Benchmark) error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
_, err := s.m.Put(ctx, keys.Next().String(), values.Next().Bytes())
return err
}
Registering Benchmarks
In order to run benchmarks, a main must be provided that registers and names benchmark suites.
import "github.com/onosproject/helmit/pkg/registry"
func init() {
registry.RegisterBenchmarkSuite("atomix", &tests.AtomixBenchSuite{})
}
Once the benchmarks have been registered, the main should call benchmark.Main() to run the benchmarks:
import (
"github.com/onosproject/helmit/pkg/registry"
"github.com/onosproject/helmit/pkg/benchmark"
benchmarks "github.com/onosproject/helmit/benchmark"
)
func main() {
registry.RegisterBenchmarkSuite("atomix", &tests.AtomixBenchSuite{})
benchmark.Main()
}
Running Benchmarks
Benchmarks are run using the helmit bench command. To run a benchmark, run helmit bench with the path to
the command in which benchmarks are registered:
helmit bench ./cmd/benchmarks
By default, the helmit bench command will run every benchmark suite registered in the provided main.
To run a specific benchmark suite, use the --suite flag:
helmit bench ./cmd/benchmarks --suite atomix
To run a specific benchmark function, use the --benchmark flag:
helmit bench ./cmd/benchmarks --suite atomix --benchmark BenchmarkMapPut
Benchmarks can either be run for a specific number of iterations:
helmit bench ./cmd/benchmarks --requests 10000
Or for a duration of time:
helmit bench ./cmd/benchmarks --duration 10m
By default, benchmarks are run with a single benchmark goroutine on a single client pod. Benchmarks can be scaled
across many client pods by setting the --workers flag:
helmit bench ./cmd/benchmarks --duration 10m --workers 10
To scale the number of goroutines within each benchmark worker, set the --parallel flag:
helmit bench ./cmd/benchmarks --duration 10m --parallel 10
As with all Helmit commands, the helmit bench command supports contexts and Helm values and value files:
helmit bench ./cmd/benchmarks -c . -f kafka=kafka-values.yaml --set kafka.replicas=2 --duration 10m
Simulation
Helmit supports simulation of Kubernetes and Helm applications using a custom simulation framework and command
line tool. Simulations are collections of operations on a Kubernetes application that are randomized by the Helmit
simulator. To run a simulation, write a Golang simulation suite and then run the suite using
the helmit sim tool:
helmit sim ./cmd/sims
Writing Simulations
To run a simulation you must first define a simulation suite. Simulation suites are Golang structs containing a series
of receivers that simulate operations on Kubernetes applications:
import "github.com/onosproject/helmit/pkg/simulation"
type AtomixSimSuite struct {
simulation.Suite
}
Helmit runs each suite within its own namespace, and each simulation consists of a set of simulator functions to run.
Prior to running simulations, simulation suites typically need to set up resources within the Kubernetes namespace.
Simulations can implement the following interfaces to manage the namespace:
SetupSimulation - Called on a single simulation pod prior to running a simulation
SetupSimulator - Called on each simulator pod prior to running a simulation
Typically, simulation suites should implement the SetupSimulation interface to install Helm charts:
func (s *AtomixSimSuite) SetupSimulation(c *simulation.Simulator) error {
err := helm.Chart("atomix-controller").
Release("atomix-controller").
Set("scope", "Namespace").
Install(true)
if err != nil {
return err
}
err = helm.Chart("atomix-database").
Release("atomix-raft").
Set("clusters", 3).
Set("partitions", 10).
Set("backend.replicas", 3).
Set("backend.image", "atomix/raft-replica:latest").
Install(true)
if err != nil {
return err
}
return nil
}
Simulator functions can be written with any name pattern:
import "github.com/onosproject/helmit/pkg/input"
var keys = input.RandomChoice(input.SetOf(input.RandomString(8), 1000))
var values = input.RandomBytes(128)
func (s *AtomixSimSuite) SimulateMapPut(c *simulation.Simulator) error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
_, err := s.m.Put(ctx, keys.Next().String(), values.Next().Bytes())
return err
}
Simulations must schedule their simulator functions by implementing the ScheduleSimulator interface:
func (s *AtomixSimSuite) ScheduleSimulator(sim *simulation.Simulator) {
sim.Schedule("get", s.SimulateMapGet, 1*time.Second, 1)
sim.Schedule("put", s.SimulateMapPut, 5*time.Second, 1)
sim.Schedule("remove", s.SimulateMapRemove, 30*time.Second, 1)
}
When scheduling simulators, the simulation specifies a default rate at which the simulators are executed. Note
that simulator rates can be overridden from the simulator command line
Registering Simulation Suites
In order to run simulations, a main must be provided that registers and names simulation suites.
import "github.com/onosproject/helmit/pkg/registry"
func init() {
registry.RegisterSimulationSuite("atomix", &sims.AtomixSimulationSuite{})
}
Once the simulations have been registered, the main should call simulation.Main() to run the simulations:
import (
"github.com/onosproject/helmit/pkg/registry"
"github.com/onosproject/helmit/pkg/simulation"
simulations "github.com/onosproject/helmit/simulation"
)
func main() {
registry.RegisterSimulationSuite("atomix", &sims.AtomixSimulationSuite{})
simulation.Main()
}
Running Simulations
Simulations are run using the helmit sim command. To run a simulation, run helmit sim with the path to
the command in which simulations are registered:
helmit sim ./cmd/simulations
By default, the helmit sim command will run every simulation registered in the provided main.
To run a specific simulation, use the --simulation flag:
helmit sim ./cmd/simulations --suite atomix
Simulations can either be run for a configurable duration of time:
helmit sim ./cmd/simulations --duration 10m
By default, simulations are run on a single client pod. Simulations can be scaled across many client pods by
setting the --simulators flag:
helmit sim ./cmd/simulations --duration 10m --simulators 10
As with all Helmit commands, the helmit sim command supports contexts and Helm values and value files:
helmit sim ./cmd/simulations -c . -f kafka=kafka-values.yaml --set kafka.replicas=2 --duration 10m
Examples