Skip to content
This repository was archived by the owner on Apr 10, 2024. It is now read-only.

Commit 1e4f117

Browse files
authored
dynamic admission hook sample (#24)
1 parent 98bb04b commit 1e4f117

File tree

8 files changed

+255
-4
lines changed

8 files changed

+255
-4
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ class JUnitExtensionTest {
4040
}
4141
```
4242

43+
### Testing Mutation and Validation Webhooks
44+
45+
An additional benefits os running K8S API Server this way, is that it makes easy to test
46+
[Conversion Hooks]()https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#webhook-conversion
47+
and/or
48+
[Dynamic Admission Controllers](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)
49+
50+
You would probably use some additional framework to implement those hooks, like [kubernetes-webooks-framework](https://github.com/java-operator-sdk/kubernetes-webooks-framework)
51+
with Quarkus or Spring. However, we demonstrate how it works in [this test](https://github.com/csviri/jenvtest/blob/main/src/test/java/com/csviri/jenvtest/KubernetesMutationHookHandlingTest.java)
52+
4353
### Download binaries
4454

4555
Binaries are downloaded automatically under $JENVTEST_DIR/k8s/[target-platform-and-version].

pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
<impsort-maven-plugin.version>1.8.0</impsort-maven-plugin.version>
3333
<formatter-maven-plugin.version>2.22.0</formatter-maven-plugin.version>
3434
<directory-maven-plugin.version>1.0</directory-maven-plugin.version>
35+
<jetty.version>11.0.14</jetty.version>
36+
<kubernetes.webhooks.framework.version>1.0.0</kubernetes.webhooks.framework.version>
3537
</properties>
3638

3739
<dependencyManagement>
@@ -112,11 +114,23 @@
112114
<version>${assertj.version}</version>
113115
<scope>test</scope>
114116
</dependency>
117+
<dependency>
118+
<groupId>org.eclipse.jetty</groupId>
119+
<artifactId>jetty-server</artifactId>
120+
<version>${jetty.version}</version>
121+
<scope>test</scope>
122+
</dependency>
115123
<dependency>
116124
<groupId>org.bouncycastle</groupId>
117125
<artifactId>bcpkix-jdk18on</artifactId>
118126
<version>${bouncycastle.version}</version>
119127
</dependency>
128+
<dependency>
129+
<groupId>io.javaoperatorsdk</groupId>
130+
<artifactId>kubernetes-webhooks-framework-core</artifactId>
131+
<version>${kubernetes.webhooks.framework.version}</version>
132+
<scope>test</scope>
133+
</dependency>
120134
</dependencies>
121135

122136

src/main/java/com/csviri/jenvtest/CertManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ private void generateUserCertificates() {
8080
}
8181

8282

83-
private static void generateKeyAndCertificate(String dirName, File keyFile, File certFile,
83+
public static void generateKeyAndCertificate(String dirName, File keyFile, File certFile,
8484
GeneralName... generalNames) {
8585
try {
8686
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");

src/main/java/com/csviri/jenvtest/process/EtcdProcess.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class EtcdProcess {
1515

1616
private static final Logger log = LoggerFactory.getLogger(EtcdProcess.class);
1717
private static final Logger etcdLog =
18-
LoggerFactory.getLogger(EtcdProcess.class.getName() + ".etcdProcess");
18+
LoggerFactory.getLogger(EtcdProcess.class.getName() + ".EtcdProcessLogs");
1919

2020
private final BinaryManager binaryManager;
2121

src/main/java/com/csviri/jenvtest/process/KubeAPIServerProcess.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class KubeAPIServerProcess {
1414

1515
private static final Logger log = LoggerFactory.getLogger(KubeAPIServerProcess.class);
1616
private static final Logger apiLog = LoggerFactory.getLogger(KubeAPIServerProcess.class
17-
.getName() + ".apiServerProcess");
17+
.getName() + ".APIServerProcessLogs");
1818

1919
private final CertManager certManager;
2020
private final BinaryManager binaryManager;
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package com.csviri.jenvtest;
2+
3+
import java.io.*;
4+
import java.nio.charset.StandardCharsets;
5+
import java.security.KeyStore;
6+
import java.security.KeyStoreException;
7+
import java.security.NoSuchAlgorithmException;
8+
import java.security.PrivateKey;
9+
import java.security.cert.Certificate;
10+
import java.security.cert.CertificateException;
11+
import java.security.cert.X509Certificate;
12+
import java.util.Base64;
13+
import java.util.HashMap;
14+
15+
import org.apache.commons.io.FileUtils;
16+
import org.bouncycastle.asn1.x509.GeneralName;
17+
import org.bouncycastle.cert.X509CertificateHolder;
18+
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
19+
import org.bouncycastle.openssl.PEMKeyPair;
20+
import org.bouncycastle.openssl.PEMParser;
21+
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
22+
import org.eclipse.jetty.server.*;
23+
import org.eclipse.jetty.server.handler.AbstractHandler;
24+
import org.eclipse.jetty.util.ssl.SslContextFactory;
25+
import org.junit.jupiter.api.AfterAll;
26+
import org.junit.jupiter.api.BeforeAll;
27+
import org.junit.jupiter.api.Test;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
32+
import io.fabric8.kubernetes.api.model.admissionregistration.v1.MutatingWebhookConfiguration;
33+
import io.fabric8.kubernetes.api.model.networking.v1.*;
34+
import io.fabric8.kubernetes.client.KubernetesClient;
35+
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
36+
import io.fabric8.kubernetes.client.utils.Serialization;
37+
import io.javaoperatorsdk.webhook.admission.AdmissionController;
38+
39+
import com.csviri.jenvtest.junit.EnableKubeAPIServer;
40+
41+
import jakarta.servlet.http.HttpServletRequest;
42+
import jakarta.servlet.http.HttpServletResponse;
43+
44+
import static org.assertj.core.api.Assertions.assertThat;
45+
46+
/**
47+
* Test demonstrates how to test locally Mutating Webhooks, in real like you implementation such an
48+
* endpoint would be simpler, using frameworks like
49+
* <a href="https://github.com/java-operator-sdk/kubernetes-webooks-framework">Kubernetes Webhook
50+
* Framework</a> with combination of Quarkus or Spring.
51+
*/
52+
@EnableKubeAPIServer
53+
class KubernetesMutationHookHandlingTest {
54+
55+
private static final Logger log =
56+
LoggerFactory.getLogger(KubernetesMutationHookHandlingTest.class);
57+
58+
public static final String PASSWORD = "secret";
59+
public static final String TEST_LABEL_KEY = "test-label";
60+
public static final String TEST_LABEL_VALUE = "mutation-test";
61+
62+
static File certFile = new File("target", "mutation.crt");
63+
// server that handles mutation hooks
64+
static Server server = new Server();
65+
66+
// using https://github.com/java-operator-sdk/kubernetes-webooks-framework framework to implement
67+
// the response
68+
static AdmissionController<Ingress> mutationController =
69+
new AdmissionController<>((resource, operation) -> {
70+
if (resource.getMetadata().getLabels() == null) {
71+
resource.getMetadata().setLabels(new HashMap<>());
72+
}
73+
resource.getMetadata().getLabels().putIfAbsent(TEST_LABEL_KEY, TEST_LABEL_VALUE);
74+
return resource;
75+
});
76+
77+
@Test
78+
void handleMutatingWebhook() {
79+
var client = new KubernetesClientBuilder().build();
80+
applyConfig(client);
81+
82+
var ingress = client.resource(testIngress()).create();
83+
84+
assertThat(ingress.getMetadata().getLabels()).containsEntry(TEST_LABEL_KEY, TEST_LABEL_VALUE);
85+
}
86+
87+
@BeforeAll
88+
static void startWebhookServer() throws Exception {
89+
initServerConfigs();
90+
server.setHandler(new AbstractHandler() {
91+
@Override
92+
public void handle(String s, Request request, HttpServletRequest httpServletRequest,
93+
HttpServletResponse httpServletResponse) {
94+
try {
95+
request.setHandled(true);
96+
AdmissionReview admissionReview =
97+
Serialization.unmarshal(httpServletRequest.getInputStream());
98+
99+
var response = mutationController.handle(admissionReview);
100+
101+
var out = httpServletResponse.getWriter();
102+
httpServletResponse.setContentType("application/json");
103+
httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.toString());
104+
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
105+
out.println(Serialization.asJson(response));
106+
} catch (Exception e) {
107+
log.error("Error in webhook", e);
108+
throw new RuntimeException(e);
109+
}
110+
}
111+
});
112+
server.start();
113+
}
114+
115+
@AfterAll
116+
static void stopWebhookServer() throws Exception {
117+
server.stop();
118+
}
119+
120+
private void applyConfig(KubernetesClient client) {
121+
try (var resource =
122+
KubernetesMutationHookHandlingTest.class
123+
.getResourceAsStream("/MutatingWebhookConfig.yaml")) {
124+
MutatingWebhookConfiguration hook =
125+
(MutatingWebhookConfiguration) client.load(resource).items().get(0);
126+
String cert = FileUtils.readFileToString(certFile, StandardCharsets.UTF_8);
127+
128+
hook.getWebhooks().get(0).getClientConfig()
129+
.setCaBundle(new String(Base64.getEncoder().encode(cert.getBytes())));
130+
131+
client.resource(hook).create();
132+
} catch (IOException e) {
133+
throw new RuntimeException(e);
134+
}
135+
}
136+
137+
private static void initServerConfigs() {
138+
HttpConfiguration httpConfig = new HttpConfiguration();
139+
httpConfig.addCustomizer(new SecureRequestCustomizer());
140+
HttpConnectionFactory http11 = new HttpConnectionFactory(httpConfig);
141+
142+
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
143+
var keyFile = new File("target", "mutation.key");
144+
145+
// certificates are generated in pem format, since are used also as an input for
146+
// MutatingWebhookConfiguration CA Bundle
147+
CertManager.generateKeyAndCertificate("CN=example.org", keyFile, certFile,
148+
new GeneralName(GeneralName.iPAddress, "127.0.0.1"));
149+
sslContextFactory.setKeyStore(createKeyStoreFrom(keyFile, certFile));
150+
sslContextFactory.setKeyStorePassword(PASSWORD);
151+
SslConnectionFactory tls = new SslConnectionFactory(sslContextFactory, http11.getProtocol());
152+
153+
ServerConnector connector = new ServerConnector(server, tls, http11);
154+
connector.setPort(8443);
155+
server.addConnector(connector);
156+
}
157+
158+
/** Creates Keystore from generated Certificate/Key Pem files */
159+
private static KeyStore createKeyStoreFrom(File keyPem, File certPem) {
160+
try (var certReader = new PEMParser(new InputStreamReader(new FileInputStream(certPem)));
161+
var keyReader = new PEMParser(new InputStreamReader(new FileInputStream(keyPem)))) {
162+
var certConverter = new JcaX509CertificateConverter();
163+
X509Certificate cert =
164+
certConverter.getCertificate((X509CertificateHolder) certReader.readObject());
165+
166+
JcaPEMKeyConverter keyConverter = new JcaPEMKeyConverter();
167+
PrivateKey key =
168+
keyConverter.getPrivateKey(((PEMKeyPair) keyReader.readObject()).getPrivateKeyInfo());
169+
170+
KeyStore keystore = KeyStore.getInstance("JKS");
171+
keystore.load(null);
172+
keystore.setCertificateEntry("cert-alias", cert);
173+
keystore.setKeyEntry("key-alias", key, PASSWORD.toCharArray(), new Certificate[] {cert});
174+
175+
return keystore;
176+
} catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) {
177+
throw new RuntimeException(e);
178+
}
179+
}
180+
181+
public static Ingress testIngress() {
182+
return new IngressBuilder()
183+
.withNewMetadata()
184+
.withName("test1")
185+
.endMetadata()
186+
.withSpec(new IngressSpecBuilder()
187+
.withIngressClassName("sample")
188+
.withRules(new IngressRuleBuilder()
189+
.withHttp(new HTTPIngressRuleValueBuilder()
190+
.withPaths(new HTTPIngressPathBuilder()
191+
.withPath("/test")
192+
.withPathType("Prefix")
193+
.withBackend(new IngressBackendBuilder()
194+
.withService(new IngressServiceBackendBuilder()
195+
.withName("service")
196+
.withPort(new ServiceBackendPortBuilder()
197+
.withNumber(80)
198+
.build())
199+
.build())
200+
.build())
201+
.build())
202+
.build())
203+
.build())
204+
.build())
205+
.build();
206+
}
207+
208+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: admissionregistration.k8s.io/v1
2+
kind: MutatingWebhookConfiguration
3+
metadata:
4+
name: "mutating.jenvtest.example.com"
5+
webhooks:
6+
- name: "mutating.jenvtest.example.com"
7+
rules:
8+
- apiGroups: ["networking.k8s.io"]
9+
apiVersions: ["v1"]
10+
operations: ["*"]
11+
resources: ["ingresses"]
12+
scope: "Namespaced"
13+
clientConfig:
14+
url: "https://127.0.0.1:8443/mutate"
15+
admissionReviewVersions: ["v1"]
16+
sideEffects: None
17+
timeoutSeconds: 10

src/test/resources/log4j2.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
</Console>
77
</Appenders>
88
<Loggers>
9-
<Root level="debug">
9+
<Logger name="com.csviri.jenvtest" level="debug">
10+
</Logger>
11+
<Root level="error">
1012
<AppenderRef ref="Console"/>
1113
</Root>
1214
</Loggers>

0 commit comments

Comments
 (0)