Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic K3S module #4582

Merged
merged 12 commits into from Jan 13, 2022
53 changes: 53 additions & 0 deletions 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:

<!--codeinclude-->
[Starting a K3S server](../../modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java) inside_block:starting_k3s
<!--/codeinclude-->

### 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):

<!--codeinclude-->
[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
<!--/codeinclude-->

## 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'
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>k3s</artifactId>
<version>{{latest_version}}</version>
<scope>test</scope>
</dependency>
```
1 change: 1 addition & 0 deletions mkdocs.yml
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions 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'
}
64 changes: 64 additions & 0 deletions 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<K3sContainer> {

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<String, String> 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;
}
}
@@ -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);
bsideup marked this conversation as resolved.
Show resolved Hide resolved

DefaultKubernetesClient client = new DefaultKubernetesClient(config);

// interact with the running K3s server, e.g.:
List<Node> nodes = client.nodes().list().getItems();
// }

Assertions.assertThat(nodes).hasSize(1);
}
}
}
@@ -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);
}
}
}
16 changes: 16 additions & 0 deletions modules/k3s/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>

<logger name="org.testcontainers" level="DEBUG"/>
</configuration>