diff --git a/docs/modules/k3s.md b/docs/modules/k3s.md new file mode 100644 index 00000000000..a3c2c0fa754 --- /dev/null +++ b/docs/modules/k3s.md @@ -0,0 +1,53 @@ +# K3s Module + +!!! note + This module is INCUBATING. While it is ready for use and operational in the current version of Testcontainers, it is possible that it may receive breaking changes in the future. See [our contributing guidelines](/contributing/#incubating-modules) for more information on our incubating modules policy. + +Testcontainers module for Rancher's [K3s](https://rancher.com/products/k3s/) lightweight Kubernetes. +This module is intended to be used for testing components that interact with Kubernetes APIs - for example, operators. + +## Usage example + +Start a K3s server as follows: + + +[Starting a K3S server](../../modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java) inside_block:starting_k3s + + +### Connecting to the server + +`K3sContainer` exposes a working Kubernetes client configuration, as a YAML String, via the `getKubeConfigYaml()` method. + +This may be used with Kubernetes clients - e.g. for the [official Java client](connecting_with_k8sio) and +[the Fabric8 Kubernetes client](https://github.com/fabric8io/kubernetes-client): + + +[Official Java client](../../modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java) inside_block:connecting_with_k8sio +[Fabric8 Kubernetes client](../../modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java) inside_block:connecting_with_fabric8 + + +## Known limitations + +!!! warning + * K3sContainer runs as a privileged container and needs to be able to spawn its own containers. For these reasons, + K3sContainer will not work in certain rootless Docker, Docker-in-Docker, or other environments where privileged + containers are disallowed. + + * k3s containers may be unable to run on host machines where `/var/lib/docker` is on a BTRFS filesystem. See [k3s-io/k3s#4863](https://github.com/k3s-io/k3s/issues/4863) for an example. + +## Adding this module to your project dependencies + +Add the following dependency to your `pom.xml`/`build.gradle` file: + +```groovy tab='Gradle' +testImplementation "org.testcontainers:k3s:{{latest_version}}" +``` + +```xml tab='Maven' + + org.testcontainers + k3s + {{latest_version}} + test + +``` diff --git a/mkdocs.yml b/mkdocs.yml index be30f26a964..827ef6a836d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -65,6 +65,7 @@ nav: - modules/docker_compose.md - modules/elasticsearch.md - modules/gcloud.md + - modules/k3s.md - modules/kafka.md - modules/localstack.md - modules/mockserver.md diff --git a/modules/k3s/build.gradle b/modules/k3s/build.gradle new file mode 100644 index 00000000000..85ee36652b3 --- /dev/null +++ b/modules/k3s/build.gradle @@ -0,0 +1,11 @@ +description = "Testcontainers :: K3S" + +dependencies { + api project(":testcontainers") + + shaded 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.1' + + testImplementation 'io.fabric8:kubernetes-client:5.11.0' + testImplementation 'io.kubernetes:client-java:14.0.0' + testImplementation 'org.assertj:assertj-core:3.21.0' +} diff --git a/modules/k3s/src/main/java/org/testcontainers/k3s/K3sContainer.java b/modules/k3s/src/main/java/org/testcontainers/k3s/K3sContainer.java new file mode 100644 index 00000000000..1080563226a --- /dev/null +++ b/modules/k3s/src/main/java/org/testcontainers/k3s/K3sContainer.java @@ -0,0 +1,64 @@ +package org.testcontainers.k3s; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.github.dockerjava.api.command.InspectContainerResponse; +import lombok.SneakyThrows; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.utility.DockerImageName; + +import java.util.HashMap; +import java.util.Map; + +public class K3sContainer extends GenericContainer { + + private String kubeConfigYaml; + + public K3sContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DockerImageName.parse("rancher/k3s")); + + addExposedPorts(6443, 8443); + setPrivilegedMode(true); + addFileSystemBind("/sys/fs/cgroup", "/sys/fs/cgroup", BindMode.READ_WRITE); + + Map tmpFsMapping = new HashMap<>(); + tmpFsMapping.put("/run", ""); + tmpFsMapping.put("/var/run", ""); + setTmpFsMapping(tmpFsMapping); + + setCommand( + "server", + "--no-deploy=traefik", + "--tls-san=" + this.getHost() + ); + setWaitStrategy(new LogMessageWaitStrategy().withRegEx(".*Node controller sync successful.*")); + } + + @SneakyThrows + @Override + protected void containerIsStarted(InspectContainerResponse containerInfo) { + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + + ObjectNode rawKubeConfig = copyFileFromContainer( + "/etc/rancher/k3s/k3s.yaml", + is -> objectMapper.readValue(is, ObjectNode.class) + ); + + ObjectNode clusterConfig = rawKubeConfig.at("/clusters/0/cluster").require(); + clusterConfig.replace("server", new TextNode("https://" + this.getHost() + ":" + this.getMappedPort(6443))); + + rawKubeConfig.set("current-context", new TextNode("default")); + + kubeConfigYaml = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(rawKubeConfig); + } + + public String getKubeConfigYaml() { + return kubeConfigYaml; + } +} diff --git a/modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java b/modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java new file mode 100644 index 00000000000..8e7b601c5b0 --- /dev/null +++ b/modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java @@ -0,0 +1,43 @@ +package org.testcontainers.k3s; + +import io.fabric8.kubernetes.api.model.Node; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; + +import java.util.List; + +@Slf4j +public class Fabric8K3sContainerTest { + + @Test + public void shouldStartAndHaveListableNode() { + try ( + // starting_k3s { + K3sContainer k3s = new K3sContainer(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1")) + .withLogConsumer(new Slf4jLogConsumer(log)) + // } + ) { + k3s.start(); + + // connecting_with_fabric8 { + // obtain a kubeconfig file which allows us to connect to k3s + String kubeConfigYaml = k3s.getKubeConfigYaml(); + + // requires io.fabric8:kubernetes-client:5.11.0 or higher + Config config = Config.fromKubeconfig(kubeConfigYaml); + + DefaultKubernetesClient client = new DefaultKubernetesClient(config); + + // interact with the running K3s server, e.g.: + List nodes = client.nodes().list().getItems(); + // } + + Assertions.assertThat(nodes).hasSize(1); + } + } +} diff --git a/modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java b/modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java new file mode 100644 index 00000000000..970e2390457 --- /dev/null +++ b/modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java @@ -0,0 +1,43 @@ +package org.testcontainers.k3s; + +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1NodeList; +import io.kubernetes.client.util.Config; +import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; + +import java.io.IOException; +import java.io.StringReader; + +@Slf4j +public class OfficialClientK3sContainerTest { + + @Test + public void shouldStartAndHaveListableNode() throws IOException, ApiException { + try ( + // starting_k3s { + K3sContainer k3s = new K3sContainer(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1")) + .withLogConsumer(new Slf4jLogConsumer(log)) + // } + ) { + k3s.start(); + + // connecting_with_k8sio { + String kubeConfigYaml = k3s.getKubeConfigYaml(); + + ApiClient client = Config.fromConfig(new StringReader(kubeConfigYaml)); + CoreV1Api api = new CoreV1Api(client); + + // interact with the running K3s server, e.g.: + V1NodeList nodes = api.listNode(null, null, null, null, null, null, null, null, null, null); + // } + + Assertions.assertThat(nodes.getItems()).hasSize(1); + } + } +} diff --git a/modules/k3s/src/test/resources/logback-test.xml b/modules/k3s/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..535e406fc13 --- /dev/null +++ b/modules/k3s/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + +