Skip to content

Commit ff8836d

Browse files
authored
Use Dekorate in the Spring Boot example and add documentation (#27)
1 parent ce64f6e commit ff8836d

File tree

10 files changed

+307
-4
lines changed

10 files changed

+307
-4
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ hs_err_pid*
2424

2525
admission-controller-framework.iml
2626
.idea
27+
target
28+
.cache

samples/quarkus/src/main/java/io/javaoperatorsdk/webhook/admission/sample/quarkus/AdmissionControllerConfig.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.javaoperatorsdk.webhook.admission.sample.quarkus;
22

3+
import java.util.HashMap;
34
import java.util.concurrent.CompletableFuture;
45

56
import javax.enterprise.context.Dependent;
@@ -33,6 +34,10 @@ public class AdmissionControllerConfig {
3334
@Named(MUTATING_CONTROLLER)
3435
public AdmissionController<Pod> mutatingController() {
3536
return new AdmissionController<>((resource, operation) -> {
37+
if (resource.getMetadata().getLabels() == null) {
38+
resource.getMetadata().setLabels(new HashMap<>());
39+
}
40+
3641
resource.getMetadata().getLabels().putIfAbsent(APP_NAME_LABEL_KEY, "mutation-test");
3742
return resource;
3843
});
@@ -42,7 +47,8 @@ public AdmissionController<Pod> mutatingController() {
4247
@Named(VALIDATING_CONTROLLER)
4348
public AdmissionController<Pod> validatingController() {
4449
return new AdmissionController<>((resource, operation) -> {
45-
if (resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null) {
50+
if (resource.getMetadata().getLabels() == null
51+
|| resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null) {
4652
throw new NotAllowedException("Missing label: " + APP_NAME_LABEL_KEY);
4753
}
4854
});
@@ -53,6 +59,10 @@ public AdmissionController<Pod> validatingController() {
5359
public AsyncAdmissionController<Pod> asyncMutatingController() {
5460
return new AsyncAdmissionController<>(
5561
(AsyncMutator<Pod>) (resource, operation) -> CompletableFuture.supplyAsync(() -> {
62+
if (resource.getMetadata().getLabels() == null) {
63+
resource.getMetadata().setLabels(new HashMap<>());
64+
}
65+
5666
resource.getMetadata().getLabels().putIfAbsent(APP_NAME_LABEL_KEY, "mutation-test");
5767
return resource;
5868
}));
@@ -62,7 +72,8 @@ public AsyncAdmissionController<Pod> asyncMutatingController() {
6272
@Named(ASYNC_VALIDATING_CONTROLLER)
6373
public AsyncAdmissionController<Pod> asyncValidatingController() {
6474
return new AsyncAdmissionController<>((resource, operation) -> {
65-
if (resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null) {
75+
if (resource.getMetadata().getLabels() == null
76+
|| resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null) {
6677
throw new NotAllowedException("Missing label: " + APP_NAME_LABEL_KEY);
6778
}
6879
});

samples/spring-boot/README.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
## Admission Control for Pod resources in Spring Boot
2+
3+
The idea of this example is to demonstrate how we can alter resources and, then, validate them using [Admission Control webhooks](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers) webhooks in Spring Boot.
4+
5+
To be precise, this example will install two admission webhooks that watch and manage Pod resources:
6+
(1) a Mutating admission webhook that will add the label "app.kubernetes.io/name" with value "mutation-test" to the Pod resource
7+
(2) a Validation admission webhook that will verify all the new Pod resources have the label "app.kubernetes.io/name"
8+
9+
**Note:** Kubernetes will first invoke mutating webhooks and then will invoke validation webhooks at this order.
10+
11+
### Introduction
12+
13+
The admission controllers are installed via either [the ValidatingWebhookConfiguration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#validatingwebhookconfiguration-v1-admissionregistration-k8s-io) resource for validation admission webhooks or [the MutatingWebhooksConfiguration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#mutatingwebhookconfiguration-v1-admissionregistration-k8s-io) resource for mutating admission webhooks. For example, the ValidatingWebhookConfiguration resource looks like:
14+
15+
```yaml
16+
apiVersion: admissionregistration.k8s.io/v1
17+
kind: ValidatingWebhookConfiguration
18+
metadata:
19+
name: "my-validation-webhook"
20+
webhooks:
21+
- name: "my-validation-webhook"
22+
rules: ## 1
23+
- apiGroups: [""]
24+
apiVersions: ["v1"]
25+
operations: ["CREATE"]
26+
resources: ["pods"]
27+
scope: "Namespaced"
28+
clientConfig:
29+
service:
30+
namespace: "test"
31+
name: "spring-boot-sample" ## 2
32+
path: "/validate"
33+
caBundle: "<CA_BUNDLE>" ## 3
34+
admissionReviewVersions: ["v1"]
35+
```
36+
37+
The above admission webhook configuration is going to watch "CREATE" events of POD resources (see ## 1). When new POD resources are installed into our Kubernetes cluster, then Kubernetes will invoke the POST resource with path "/validate" from the service name "spring-boot-sample" and Kubernetes namespace "test" (see ## 2). The complete URL that Kubernetes will do is "https://spring-boot-sample.test.svc:443/validate", so our application needs to be configured with SSL enabled and the webhook needs to be provided with the right CA bundle (see ## 3) that is accepted by the application.
38+
39+
### Requirements
40+
41+
Before getting started, you need to:
42+
43+
1. have connected to your Kubernetes cluster using `kubectl/oc`.
44+
2. have installed [the Cert Manager operator](https://cert-manager.io/) into your Kubernetes cluster.
45+
3. have logged into a container registry such as `quay.io` at your choice.
46+
47+
### Getting Started
48+
49+
The first thing we need is to add the [Dekorate Spring Boot](https://dekorate.io/docs/spring-boot-integration) extension to generate the `Deployment` and `Service` Kubernetes resources into our Maven pom file:
50+
51+
```xml
52+
<dependency>
53+
<groupId>io.dekorate</groupId>
54+
<artifactId>kubernetes-spring-starter</artifactId>
55+
</dependency>
56+
```
57+
58+
The second thing we need is to enable SSL in Spring Boot. In this example, we're going to use the [Dekorate Cert-Manager](https://dekorate.io/docs/cert-manager) extension to generate the Cert-Manager resources and map the volumes where the certifications will be installed. See more information in [the Spring Boot with Cert-Manager example](https://github.com/dekorateio/dekorate/tree/main/examples/spring-boot-with-certmanager-example#enable-https-transport) from Dekorate. So, let's add this extension to our Maven pom file as well:
59+
60+
```xml
61+
<dependency>
62+
<groupId>io.dekorate</groupId>
63+
<artifactId>certmanager-annotations</artifactId>
64+
</dependency>
65+
```
66+
67+
Finally, update the application properties as suggested in [the Spring Boot with Cert-Manager example](https://github.com/dekorateio/dekorate/tree/main/examples/spring-boot-with-certmanager-example#enable-https-transport).
68+
69+
### Deployment in Kubernetes
70+
71+
Let's start by creating the Kubernetes namespace `test` where we'll install all the resources:
72+
73+
```
74+
kubectl create namespace test
75+
kubectl config set-context --current --namespace=test
76+
```
77+
78+
Next, let's generate all the manifests and push the container image into your container registry. This example uses [Dekorate](https://dekorate.io/) to generate all the Kubernetes resources and the [Dekorate JIB](https://dekorate.io/docs/jib-build-hook) extension to build/push the image. Therefore, we can generate the manifests and build/push the image at once simply by running the following Maven command:
79+
80+
```
81+
mvn clean install \
82+
-Ddekorate.jib.registry=<CONTAINER REGISTRY URL> \
83+
-Ddekorate.jib.group=<CONTAINER REGISTRY USER> \
84+
-Ddekorate.jib.version=<VERSION> \
85+
-Ddekorate.jib.autoPushEnabled=true
86+
```
87+
88+
For example, if our container registry url is `quay.io`, the group is `jcarvaja` and the version is `latest`; the previous Maven command will build and push the image `quay.io/jcarvaja/spring-boot-sample:latest`.
89+
90+
Moreover, the above command will also generate the following resources in the `target/classes/META-INF/kubernetes.yaml` file:
91+
- `Deployment` and `Service` resources
92+
- `Certificate` and `Issuer` resources to configure the Cert-Manager operator
93+
94+
So, let's install all the generated resources into our Kubernetes cluster:
95+
96+
```
97+
kubectl apply -f target/classes/META-INF/dekorate/kubernetes.yml
98+
```
99+
100+
When installed, the Cert-Manager operator will watch the `Certificate` and `Issuer` resources and will generate a secret named `tls-secret`. We can see the content by running the following command:
101+
102+
```
103+
kubectl -n test get secret tls-secret -o yaml
104+
```
105+
106+
Next, we need to configure and install the admission webhook with the proper CA certificate (key `ca.crt` from the secret). Luckily, we can leverage this configuration to the Cert-Manager operator by using the annotation `cert-manager.io/inject-ca-from` (more information in [here](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-certificate-resource)).
107+
108+
Let's install the admission webhook with the annotation `cert-manager.io/inject-ca-from=test/spring-boot-sample`:
109+
110+
```
111+
kubectl apply -f k8s/validating-webhook-configuration.yml
112+
```
113+
114+
And let's verify that the `Cert-Manager` operator has populated the `caBundle` field thanks to the annotation `cert-manager.io/inject-ca-from`:
115+
116+
```
117+
kubectl get validatingwebhookconfigurations.admissionregistration.k8s.io pod-policy.spring-boot.example.com -o yaml | grep caBundle
118+
```
119+
120+
**Note:** The content of the caBundle should be the same as in `kubectl -n test get secret tls-secret -o yaml | grep ca.crt`.
121+
122+
So far, we have installed the validating webhook! Let's see it in action by creating a new POD resource:
123+
124+
```yaml
125+
apiVersion: v1
126+
kind: Pod
127+
metadata:
128+
name: pod-with-missing-label
129+
spec:
130+
containers:
131+
- image: any
132+
imagePullPolicy: IfNotPresent
133+
name: spring-boot-sample
134+
```
135+
136+
This POD resource should not pass the validation as the label "app.kubernetes.io/name" does not exist.
137+
138+
When we install it, we should get the following error:
139+
140+
```
141+
> kubectl apply -f k8s/create-pod-with-missing-label-example.yml
142+
Error from server: error when creating "k8s/create-pod-with-missing-label-example.yaml": admission webhook "pod-policy.spring-boot.example.com" denied the request: Missing label: app.kubernetes.io/name
143+
```
144+
145+
So good so far!
146+
147+
Let's now install the mutating webhook:
148+
149+
```
150+
kubectl apply -f k8s/mutating-webhook-configuration.yml
151+
```
152+
153+
And again, let's install the same Pod without annotations:
154+
155+
```
156+
> kubectl apply -f k8s/create-pod-with-missing-label-example.yml
157+
pod/pod-with-missing-label created
158+
```
159+
160+
Now, the pod resource passed the validation because the mutating webhook added the missing label. We can see the installed Pod resource:
161+
162+
```
163+
> kubectl get pod pod-with-missing-label -o yaml | grep app.kubernetes.io/name
164+
app.kubernetes.io/name: mutation-test
165+
```
166+
167+
This label was added by our mutate webhook (see logic in [here](https://github.com/java-operator-sdk/admission-controller-framework/blob/ce64f6e2a1a11a538d73acf6c49af96c04ed484d/samples/spring-boot/src/main/java/io/javaoperatorsdk/webhook/sample/springboot/Config.java#L57)).
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: v1
2+
kind: Pod
3+
metadata:
4+
name: pod-with-missing-label
5+
spec:
6+
containers:
7+
- image: any
8+
imagePullPolicy: IfNotPresent
9+
name: spring-boot-sample
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
apiVersion: admissionregistration.k8s.io/v1
2+
kind: MutatingWebhookConfiguration
3+
metadata:
4+
name: "pod-mutating.spring-boot.example.com"
5+
annotations:
6+
cert-manager.io/inject-ca-from: test/spring-boot-sample
7+
webhooks:
8+
- name: "pod-mutating.spring-boot.example.com"
9+
rules:
10+
- apiGroups: [""]
11+
apiVersions: ["v1"]
12+
operations: ["CREATE"]
13+
resources: ["pods"]
14+
scope: "Namespaced"
15+
clientConfig:
16+
service:
17+
namespace: "test"
18+
name: "spring-boot-sample"
19+
path: "/mutate"
20+
admissionReviewVersions: ["v1"]
21+
sideEffects: None
22+
timeoutSeconds: 5
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
apiVersion: admissionregistration.k8s.io/v1
2+
kind: ValidatingWebhookConfiguration
3+
metadata:
4+
name: "pod-policy.spring-boot.example.com"
5+
annotations:
6+
cert-manager.io/inject-ca-from: test/spring-boot-sample
7+
webhooks:
8+
- name: "pod-policy.spring-boot.example.com"
9+
rules:
10+
- apiGroups: [""]
11+
apiVersions: ["v1"]
12+
operations: ["CREATE"]
13+
resources: ["pods"]
14+
scope: "Namespaced"
15+
clientConfig:
16+
service:
17+
namespace: "test"
18+
name: "spring-boot-sample"
19+
path: "/validate"
20+
admissionReviewVersions: ["v1"]
21+
sideEffects: None
22+
timeoutSeconds: 5

samples/spring-boot/pom.xml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<properties>
1717
<java.version>11</java.version>
1818
<spring-boot-dependencies.version>2.6.6</spring-boot-dependencies.version>
19+
<dekorate.version>2.11.1</dekorate.version>
1920
</properties>
2021

2122
<dependencyManagement>
@@ -39,6 +40,21 @@
3940
<groupId>org.springframework.boot</groupId>
4041
<artifactId>spring-boot-starter-webflux</artifactId>
4142
</dependency>
43+
<dependency>
44+
<groupId>io.dekorate</groupId>
45+
<artifactId>kubernetes-spring-starter</artifactId>
46+
<version>${dekorate.version}</version>
47+
</dependency>
48+
<dependency>
49+
<groupId>io.dekorate</groupId>
50+
<artifactId>certmanager-annotations</artifactId>
51+
<version>${dekorate.version}</version>
52+
</dependency>
53+
<dependency>
54+
<groupId>io.dekorate</groupId>
55+
<artifactId>jib-annotations</artifactId>
56+
<version>${dekorate.version}</version>
57+
</dependency>
4258
<dependency>
4359
<groupId>org.springframework.boot</groupId>
4460
<artifactId>spring-boot-starter-test</artifactId>
@@ -62,6 +78,13 @@
6278
<groupId>org.springframework.boot</groupId>
6379
<artifactId>spring-boot-maven-plugin</artifactId>
6480
<version>${spring-boot-dependencies.version}</version>
81+
<executions>
82+
<execution>
83+
<goals>
84+
<goal>repackage</goal>
85+
</goals>
86+
</execution>
87+
</executions>
6588
</plugin>
6689
</plugins>
6790
</build>

samples/spring-boot/src/main/java/io/javaoperatorsdk/webhook/sample/springboot/Config.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.javaoperatorsdk.webhook.sample.springboot;
22

3+
import java.util.HashMap;
34
import java.util.concurrent.CompletableFuture;
45

56
import org.springframework.context.annotation.Bean;
@@ -22,6 +23,10 @@ public class Config {
2223
@Bean
2324
public AdmissionController<Pod> mutatingController() {
2425
return new AdmissionController<>((resource, operation) -> {
26+
if (resource.getMetadata().getLabels() == null) {
27+
resource.getMetadata().setLabels(new HashMap<>());
28+
}
29+
2530
resource.getMetadata().getLabels().putIfAbsent(APP_NAME_LABEL_KEY, "mutation-test");
2631
return resource;
2732
});
@@ -30,7 +35,8 @@ public AdmissionController<Pod> mutatingController() {
3035
@Bean
3136
public AdmissionController<Pod> validatingController() {
3237
return new AdmissionController<>((resource, operation) -> {
33-
if (resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null) {
38+
if (resource.getMetadata().getLabels() == null
39+
|| resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null) {
3440
throw new NotAllowedException("Missing label: " + APP_NAME_LABEL_KEY);
3541
}
3642
});
@@ -54,6 +60,10 @@ public AdmissionController<Pod> errorValidatingController() {
5460
public AsyncAdmissionController<Pod> asyncMutatingController() {
5561
return new AsyncAdmissionController<>(
5662
(AsyncMutator<Pod>) (resource, operation) -> CompletableFuture.supplyAsync(() -> {
63+
if (resource.getMetadata().getLabels() == null) {
64+
resource.getMetadata().setLabels(new HashMap<>());
65+
}
66+
5767
resource.getMetadata().getLabels().putIfAbsent(APP_NAME_LABEL_KEY, "mutation-test");
5868
return resource;
5969
}));
@@ -62,7 +72,8 @@ public AsyncAdmissionController<Pod> asyncMutatingController() {
6272
@Bean
6373
public AsyncAdmissionController<Pod> asyncValidatingController() {
6474
return new AsyncAdmissionController<>((resource, operation) -> {
65-
if (resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null) {
75+
if (resource.getMetadata().getLabels() == null
76+
|| resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null) {
6677
throw new NotAllowedException("Missing label: " + APP_NAME_LABEL_KEY);
6778
}
6879
});

0 commit comments

Comments
 (0)