Skip to Content

Testing a Kubernetes Cluster with Sonobuoy

If you are constantly building Kubernetes clusters as part of your job, you may want to consider bringing Sonobuoy into your workflow. It’s a good way to build out a standard suite of validation tests that can ensure that a cluster is configured in a standardized fashion. Sonobuoy has a concept called plugins. They produce several that you can use to run things like upstream E2E tests, CIS Benchmarks, or RBAC auditing. In addition to these, you can also build your own custom plugins, which can use the language of your choice using the testing framework of your choice. Let’s find out what we can achieve using one of the included plugins and a custom plugin.

Installing Sonobuoy

Sonobuoy can be installed for Windows or Linux by doing a wget and extracting using tar.

# Windows
wget https://github.com/vmware-tanzu/sonobuoy/releases/download/v0.56.0/sonobuoy_0.56.0_windows_amd64.tar.gz
tar.exe -xvf sonobuoy_0.56.0_windows_amd64.tar.gz

# Linux
wget https://github.com/vmware-tanzu/sonobuoy/releases/download/v0.56.0/sonobuoy_0.56.0_linux_amd64.tar.gz
tar -xvf sonobuoy_0.56.0_linux_amd64.tar.gz

Move the executable somewhere on your path.

Setup

You will need an admin kubeconfig and it will need to be added to your KUBECONFIG environment variable. That is all you need outside of having Sonobuoy installed. If you are using Rancher, here is how you can get it for your cluster. Now we can verify that it’s all configured correctly by running the conformance test in quick mode. This should complete in about 1-2 minutes.

$ sonobuoy run --wait --mode quick

21:44:40          PLUGIN        NODE    STATUS   RESULT   PROGRESS
21:44:40             e2e      global   running                    
21:44:40    systemd-logs   rke2agent   running                    
21:44:40    systemd-logs    rke2main   running                    
21:44:40 
21:44:40 Sonobuoy is still running. Runs can take 60 minutes or more depending on cluster and plugin configuration.
21:45:00             e2e      global   complete   passed   1/1 (0 failures)
21:45:00    systemd-logs   rke2agent   complete   passed                   
21:45:00    systemd-logs    rke2main   complete   passed                   
21:45:00 Sonobuoy plugins have completed. Preparing results for download.
21:45:20 Sonobuoy has completed. Use `sonobuoy retrieve` to get results.

It looks like everything is set up correctly. We can start running plugins and don’t forget to make sure everything has been cleaned up.

sonobuoy delete

Cluster Inventory Plugin

We are going to start with using the cluster inventory plugin. We can take the report return by that and parse it looking for the information that we expect. We can assert things about our cluster by just using the report generated from this plugin. Let’s get the report by running against a cluster.

sonobuoy run --plugin https://raw.githubusercontent.com/vmware-tanzu/sonobuoy-plugins/master/cluster-inventory/cluster-inventory.yaml

Once it has been completed we can pull down the results in a YAML format.

results=$(sonobuoy retrieve)
sonobuoy results $results --mode dump > inventory.yaml

Now we can parse through our inventory.yaml and assert some things about our cluster. We are going to check our total node count and assert that we have at least one node that is Windows running as an RKE2 worker node. We will also check that our status is Ready. We also have some node info so we can verify the operating system being used along with our Kubelet and ContainerD versions. Here is a list of what we are going to check.

  • status
  • labels
    • kubernetes.io/os
    • node-role.kubernetes.io/worker
    • node.kubernetes.io/instance-type
  • nodeInfo
    • osimage
    • containerruntimeversion
    • kubeletversion
  • node count

Let’s start writing our checks. We could convert or alter the plugin to only return the JSON report, we are just going to roll with what we have with minimum effort. There are a couple of approaches, you can use yq and if you use PowerShell, then the PowerShell YAML module is a good choice. I am going to use Go along with testify for ease of use. Let’s create our sonobuoy_test.go and add some structs to use to parse the results.

type NodeInfo struct {
	ContainerRuntimeVersion string `yaml:"containerruntimeversion,omitempty"`
	KubeletVersion          string `yaml:"kubeletversion,omitempty"`
	KubeProxyVersion        string `yaml:"kubeproxyversion,omitempty"`
	OperatingSystem         string `yaml:"operatingsystem,omitempty"`
	OSImage                 string `yaml:"osimage,omitempty"`
}

type Details struct {
	Addresses  []map[string]string `yaml:"addresses,omitempty"`
	Conditions []map[string]string `yaml:"conditions,omitempty"`
	Labels     map[string]string   `yaml:"labels,omitempty"`
	NodeInfo   NodeInfo            `yaml:"nodeInfo,omitempty"`
	Taints     []map[string]string `yaml:"taints,omitempty"`
}

type Item struct {
	Name    string  `yaml:"name,omitempty"`
	Status  string  `yaml:"status,omitempty"`
	Items   []Item  `yaml:"items,omitempty"`
	Details Details `yaml:"details,omitempty"`
}

This only represents the information that I want to check and is enough to demonstrate the use case. Next, we can create our test function. We will start off by setting up a struct to represent what we expect the results to be. Then we can read in and parse our YAML file. Finally, we will start asserting as we parse through the results to select the information we want to check.

func TestCluster(t *testing.T) {
	expected := struct {
		count                                    int
		status, windowsK8s, linuxK8s, rkeVersion string
	}{
		count:      3,
		status:     "Ready",
		windowsK8s: "v1.22.5",
		linuxK8s:   "v1.22.5+rke2r2",
		rkeVersion: "rke2",
	}

	a := assert.New(t)

	data, err := ioutil.ReadFile("inventory.yaml")
	if err != nil {
		log.Fatal(err)
	}

	var results Item
	if err := yaml.Unmarshal(data, &results); err != nil {
		log.Fatal(err)
	}

	a.NotNil(results)
	for _, i := range results.Items[0].Items {
		if i.Name == "Cluster Components" {
			for _, i2 := range i.Items {
				if i2.Name == "Nodes" {
					a.Exactly(expected.count, len(i2.Items))
					for _, node := range i2.Items {
						a.Contains(node.Status, expected.status)

						nodeInfo := node.Details.NodeInfo
						a.NotNil(nodeInfo)

						labels := node.Details.Labels

						a.NotNil(labels)
						a.Contains(labels, "cattle.io/os")
						a.Contains(labels, "kubernetes.io/os")
						a.Contains(labels, "rke.cattle.io/machine")
						a.Contains(labels, "node.kubernetes.io/instance-type")
						a.Equal(expected.rkeVersion, labels["node.kubernetes.io/instance-type"])

						if node.Details.NodeInfo.OperatingSystem == "windows" {
							a.Equal(expected.windowsK8s, nodeInfo.KubeletVersion)
							a.Equal(expected.windowsK8s, nodeInfo.KubeProxyVersion)
							a.Equal("windows", labels["cattle.io/os"])
							a.Equal("windows", labels["kubernetes.io/os"])
						}
						if node.Details.NodeInfo.OperatingSystem == "linux" {
							a.Equal(expected.linuxK8s, nodeInfo.KubeletVersion)
							a.Equal(expected.linuxK8s, nodeInfo.KubeProxyVersion)
							a.Equal("linux", labels["cattle.io/os"])
							a.Equal("linux", labels["kubernetes.io/os"])
						}
					}
				}
			}
		}
	}
}

Bringing it all together including the imports.

package main

import (
	"io/ioutil"
	"log"
	"testing"

	"github.com/stretchr/testify/assert"
	"gopkg.in/yaml.v3"
)

type NodeInfo struct {
	ContainerRuntimeVersion string `yaml:"containerruntimeversion,omitempty"`
	KubeletVersion          string `yaml:"kubeletversion,omitempty"`
	KubeProxyVersion        string `yaml:"kubeproxyversion,omitempty"`
	OperatingSystem         string `yaml:"operatingsystem,omitempty"`
	OSImage                 string `yaml:"osimage,omitempty"`
}

type Details struct {
	Addresses  []map[string]string `yaml:"addresses,omitempty"`
	Conditions []map[string]string `yaml:"conditions,omitempty"`
	Labels     map[string]string   `yaml:"labels,omitempty"`
	NodeInfo   NodeInfo            `yaml:"nodeInfo,omitempty"`
	Taints     []map[string]string `yaml:"taints,omitempty"`
}

type Item struct {
	Name    string  `yaml:"name,omitempty"`
	Status  string  `yaml:"status,omitempty"`
	Items   []Item  `yaml:"items,omitempty"`
	Details Details `yaml:"details,omitempty"`
}

func TestCluster(t *testing.T) {
	expected := struct {
		count                                    int
		status, windowsK8s, linuxK8s, rkeVersion string
	}{
		count:      3,
		status:     "Ready",
		windowsK8s: "v1.22.5",
		linuxK8s:   "v1.22.5+rke2r2",
		rkeVersion: "rke2",
	}

	a := assert.New(t)

	data, err := ioutil.ReadFile("inventory.yaml")
	if err != nil {
		log.Fatal(err)
	}

	var results Item
	if err := yaml.Unmarshal(data, &results); err != nil {
		log.Fatal(err)
	}

	a.NotNil(results)
	for _, i := range results.Items[0].Items {
		if i.Name == "Cluster Components" {
			for _, i2 := range i.Items {
				if i2.Name == "Nodes" {
					a.Exactly(expected.count, len(i2.Items))
					for _, node := range i2.Items {
						a.Contains(node.Status, expected.status)

						nodeInfo := node.Details.NodeInfo
						a.NotNil(nodeInfo)

						labels := node.Details.Labels

						a.NotNil(labels)
						a.Contains(labels, "cattle.io/os")
						a.Contains(labels, "kubernetes.io/os")
						a.Contains(labels, "rke.cattle.io/machine")
						a.Contains(labels, "node.kubernetes.io/instance-type")
						a.Equal(expected.rkeVersion, labels["node.kubernetes.io/instance-type"])

						if node.Details.NodeInfo.OperatingSystem == "windows" {
							a.Equal(expected.windowsK8s, nodeInfo.KubeletVersion)
							a.Equal(expected.windowsK8s, nodeInfo.KubeProxyVersion)
							a.Equal("windows", labels["cattle.io/os"])
							a.Equal("windows", labels["kubernetes.io/os"])
						}
						if node.Details.NodeInfo.OperatingSystem == "linux" {
							a.Equal(expected.linuxK8s, nodeInfo.KubeletVersion)
							a.Equal(expected.linuxK8s, nodeInfo.KubeProxyVersion)
							a.Equal("linux", labels["cattle.io/os"])
							a.Equal("linux", labels["kubernetes.io/os"])
						}
					}
				}
			}
		}
	}
}

Now we can execute our tests.

$ go test
PASS
ok      github.com/example/sonobuoy       0.013s

All of our tests passed. We have successfully validated a cluster that we deployed using the Cluster Inventory Sonobuoy plugin.

Wrapping up

This is just an example of how to leverage Sonobuoy to perform additional testing outside of just conformance tests. Sonobuoy is extensible with helpers that allow you to create your own plugins.

Thanks for reading,

Jamie

If you enjoy the content, then consider buying me a coffee.