A Java-based Testcontainers container implementation that provides ephemeral Kubernetes clusters for integration testing.
The Kindcontainer libraries offers three different Kubernetes container implementations:
ApiServerContainer
K3sContainer
KindContainer
While ApiServerContainer
(as the name suggests) starts only a Kubernetes API Server (plus the required etcd),
both K3sContainer
and KindContainer
are feature rich Kubernetes containers that can e.g. spin up Pods
and even provision PersistentVolumes
.
First you need to add the Kindcontainer dependency to your build. Kindcontainer is available on maven central.
Add the Kindcontainer dependency:
<project>
<dependencies>
<dependency>
<groupId>com.dajudge.kindcontainer</groupId>
<artifactId>kindcontainer</artifactId>
<version>1.4.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Add the Kindcontainer dependency:
repositories {
mavenCentral()
}
dependencies {
testImplementation "com.dajudge.kindcontainer:kindcontainer:1.4.0"
}
Once you have the Kindcontainer dependency configured you can create JUnit test case easily.
public class SomeKindTest {
@ClassRule
public static final KindContainer<?> KUBE = new KindContainer<>();
@Test
public void verify_node_is_present() {
// Create a fabric8 client and use it!
try (KubernetesClient client = new DefaultKubernetesClient(fromKubeconfig(KUBE.getKubeconfig()))) {
assertEquals(1, client.nodes().list().getItems().size());
}
}
}
Look here for the reference test.
public class SomeK3sTest {
@ClassRule
public static final K3sContainer<?> K3S = new K3sContainer<>();
@Test
public void verify_node_is_present() {
// Create a fabric8 client and use it!
try (KubernetesClient client = new DefaultKubernetesClient(fromKubeconfig(K3S.getKubeconfig()))) {
assertEquals(1, client.nodes().list().getItems().size());
}
}
}
Look here for the reference test.
If you don't need a full-fledged Kubernetes distribution for your testing, using the ApiServerContainer
might be an option for you that shaves off a lot of the startup overhead of the KindContainer
. The
ApiServerContainer
only starts a Kubernetes API-Server (and the required etcd), which can already be enough
if all you want to test is if your custom controller/operator handles its CRDs properly or creates the required
objects in the control plane.
public class SomeApiServerTest {
@ClassRule
public static final ApiServerContainer<?> KUBE = new ApiServerContainer<>();
@Test
public void verify_no_node_is_present() {
// Create a fabric8 client and use it!
try (KubernetesClient client = new DefaultKubernetesClient(fromKubeconfig(KUBE.getKubeconfig()))) {
assertTrue(client.nodes().list().getItems().isEmpty());
}
}
}
Look here for the reference test.
Here's a couple challenges frequently seen in the wild and how you can solve them with Kindcontainer.
Kindcontainer supports running different (selected and tested) versions of Kubernetes. The default version is the
latest supported stable version for each container. You can change the version by passing a version enum to the
constructor. The following example illustrates this process for the KindContainer
implementation, but it works
analogous for the other two containers as well.
KindContainer<?> container=new KindContainer<>(KindContainerVersion.VERSION_1_24_1);
Kindcontainer makes it easy to perform common tasks either during setup of the container
or later on during the test by offering fluent APIs to the kubectl
and helm
commands.
If you're acquainted with the kubectl
and helm
commands, you'll feel right at home with
the fluent APIs in no time.
You can use them directly during container instantiation like this:
// Kubectl example
public class SomeKubectlTest {
@ClassRule
public static final ApiServerContainer<?> KUBE = new ApiServerContainer<>()
.withKubectl(kubectl -> {
kubectl.apply
.fileFromClasspath("manifests/serviceaccount1.yaml")
.run();
});
}
// Helm3 example
public class SomeHelmTest {
@ClassRule
public static final KindContainer<?> KUBE = new KindContainer<>()
.withHelm3(helm -> {
helm.repo.add.run("mittwald", "https://helm.mittwald.de");
helm.repo.update.run();
helm.install
.namespace("kubernetes-replicator")
.createNamespace()
.run("kubernetes-replicator", "mittwald/kubernetes-replicator");
});
}
The fluent APIs are far from complete, but they cover the most common use cases. If you're missing a command, feel free to open an issue or even better, a pull request.
In some environments it might be necessary to use custom docker images for the containers Kindcontainer starts.
Attention: You need to make sure that the images you are using are compatible with the images used by kindcontainer by default. These are the images used by Kindcontainer if you don't override them:
Purpose | Image | Version |
---|---|---|
ApiServerContainer |
registry.k8s.io/kube-apiserver |
v${major}.${minor}.${patch} |
K3sContainer |
rancher/k3s |
v${major}.${minor}.${patch}-k3s1 |
KindContainer |
kindest/node |
v${major}.${minor}.${patch} |
etcd |
registry.k8s.io/etcd |
3.4.13-0 |
Fluent API helm |
alpine/helm |
3.7.2 |
Fluent API kubectl |
bitnami/kubectl |
1.21.9-debian-10-r10 |
Webhooks nginx |
nginx |
1.23.3 |
Webhooks OpenSSH Server | linuxserver/openssh-server |
9.0_p1-r2-ls99 |
You can customize the docker image of the Kubernetes container you're starting. This can be
by suffixing the kubernetes version you want to run with a call to withImage()
like this:
KindContainer<?> container = new KindContainer<>(KindContainerVersion.VERSION_1_24_1.withImage("my-registry.com/kind:1.24.1"));
The fluent APIs for helm
and kubectl
are implemented using support containers. To customize which images are being
used to start those support containers you can use the withKubectlImage()
and withHelm3Image()
methods:
K3sContainer<?> container = new K3sContainer<>()
.withKubectlImage(DockerImageName.parse("my-registry/kubectl:1.21.9-debian-10-r10"))
.withHelm3Image(DockerImageName.parse("my-registry/helm:3.7.2"));
ApiServerContainer
has a hard dependency on etcd
that's started in a separate container. To customize which image is
being used to start that support container use method withEtcdImage()
:
ApiServerContainer<?> container = new ApiServerContainer<>().withEtcdImage(DockerImageName.parse("my-registry.com/etcd:.4.13-0"));
Testing dynamic admission control webhooks requires support containers with nginx
and sshd
. To customize which
images
are being used to start those support containers use the withNginxImage()
and withOpensshServerImage()
methods.
ApiServerContainer<?> container = new ApiServerContainer()
.withNginxImage(DockerImageName.parse("my-registry/nginx:1.23.3"))
.withOpensshServerImage(DockerImageName.parse("my-registry/openssh-server:9.0_p1-r2-ls99"));
You can use Kindcontainer to test your admission controllers.
- Make sure you start your webhooks before you start the Kindcontainer
- Start your webhooks without HTTPS/TLS
- Make sure your webhooks listen at
http://localhost:<port>
- Register each webhook with
withAdmissionController()
Example:
ApiServerContainer<?> container = new ApiServerContainer().withAdmissionController(admission -> {
admission.validating() // use mutating() for a mutating admission controller
.withNewWebhook("validating.kindcontainer.dajudge.com")
.atPort(webhookPort)
.withNewRule()
.withApiGroups("")
.withApiVersions("v1")
.withOperations("CREATE", "UPDATE")
.withResources("configmaps")
.withScope("Namespaced")
.endRule()
.endWebhook()
.build();
})
You can find examples in the kindcontainer-examples repository.