Skip to content

Commit a791f41

Browse files
authored
async support (#15)
1 parent 66f716f commit a791f41

File tree

16 files changed

+605
-119
lines changed

16 files changed

+605
-119
lines changed

README.md

Lines changed: 4 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,16 @@
11
# admission-controller-framework
22

3-
Framework and tooling to support implementing admission controllers for Kubernetes in Java
4-
5-
Currently, project is early phases.
6-
3+
Framework and tooling to support implementing admission controllers for Kubernetes in Java.
4+
Supports both **quarkus** and **spring boot**. Both Sync and Async programing model.
75

86
## Sample Usage
97

108
Defining a mutation or validation controller should be simple as:
119

12-
```java
13-
14-
@Dependent
15-
public class AdmissionControllerConfig {
16-
17-
public static final String APP_NAME_LABEL_KEY = "app.kubernetes.io/name";
18-
19-
@Singleton
20-
@Named("mutatingController")
21-
public AdmissionController<Pod> mutatingController() {
22-
return new AdmissionController<>((resource, operation) -> {
23-
resource.getMetadata().getLabels().putIfAbsent(APP_NAME_LABEL_KEY, "mutation-test");
24-
return resource;
25-
});
26-
}
27-
28-
@Singleton
29-
@Named("validatingController")
30-
public AdmissionController<Pod> validatingController() {
31-
return new AdmissionController<>((resource, operation) -> {
32-
if (resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null) {
33-
throw new NotAllowedException("Missing label: " + APP_NAME_LABEL_KEY);
34-
}
35-
});
36-
}
37-
}
38-
39-
```
10+
https://github.com/java-operator-sdk/admission-controller-framework/blob/0946595d941b789caef6a69b34c2e5be8c6b59cf/samples/quarkus/src/main/java/io/javaoperatorsdk/admissioncontroller/sample/quarkus/AdmissionControllerConfig.java#L31-L68
4011

4112
What can be then simple used in an endpoint:
4213

43-
```java
44-
45-
@Path("/")
46-
public class AdmissionEndpoint {
47-
48-
private AdmissionController<Pod> mutationController;
49-
private AdmissionController<Pod> validationController;
50-
51-
@Inject
52-
public AdmissionEndpoint(@Named("mutatingController") AdmissionController<Pod> mutationController,
53-
@Named("validatingController") AdmissionController<Pod> validationController) {
54-
this.mutationController = mutationController;
55-
this.validationController = validationController;
56-
}
57-
58-
@POST
59-
@Path("mutate")
60-
@Consumes(MediaType.APPLICATION_JSON)
61-
@Produces(MediaType.APPLICATION_JSON)
62-
public AdmissionReview mutate(AdmissionReview admissionReview) {
63-
return mutationController.handle(admissionReview);
64-
}
65-
66-
@POST
67-
@Path("validate")
68-
@Consumes(MediaType.APPLICATION_JSON)
69-
@Produces(MediaType.APPLICATION_JSON)
70-
public AdmissionReview hello(AdmissionReview admissionReview) {
71-
return validationController.handle(admissionReview);
72-
}
73-
74-
}
75-
76-
```
77-
14+
https://github.com/java-operator-sdk/admission-controller-framework/blob/0946595d941b789caef6a69b34c2e5be8c6b59cf/samples/quarkus/src/main/java/io/javaoperatorsdk/admissioncontroller/sample/quarkus/AdmissionEndpoint.java#L57-L89
7815

7916
See samples also for details.

core/src/main/java/io/javaoperatorsdk/admissioncontroller/AdmissionController.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ public AdmissionReview handle(AdmissionReview admissionReview) {
3030
response.setUid(admissionReview.getRequest().getUid());
3131
return responseAdmissionReview;
3232
}
33+
3334
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.javaoperatorsdk.admissioncontroller;
2+
3+
import java.util.concurrent.CompletionStage;
4+
5+
import io.fabric8.kubernetes.api.model.KubernetesResource;
6+
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
7+
import io.javaoperatorsdk.admissioncontroller.mutation.AsyncDefaultRequestMutator;
8+
import io.javaoperatorsdk.admissioncontroller.mutation.AsyncMutator;
9+
import io.javaoperatorsdk.admissioncontroller.validation.AsyncDefaultRequestValidator;
10+
import io.javaoperatorsdk.admissioncontroller.validation.Validator;
11+
12+
public class AsyncAdmissionController<T extends KubernetesResource> {
13+
14+
private final AsyncRequestHandler requestHandler;
15+
16+
public AsyncAdmissionController(AsyncMutator<T> mutator) {
17+
this(new AsyncDefaultRequestMutator<>(mutator));
18+
}
19+
20+
public AsyncAdmissionController(Validator<T> validator) {
21+
this(new AsyncDefaultRequestValidator<>(validator));
22+
}
23+
24+
public AsyncAdmissionController(AsyncRequestHandler requestHandler) {
25+
this.requestHandler = requestHandler;
26+
}
27+
28+
public CompletionStage<AdmissionReview> handle(AdmissionReview admissionReview) {
29+
return requestHandler.handle(admissionReview.getRequest())
30+
.thenApply(r -> {
31+
AdmissionReview responseAdmissionReview = new AdmissionReview();
32+
responseAdmissionReview.setResponse(r);
33+
r.setUid(admissionReview.getRequest().getUid());
34+
return responseAdmissionReview;
35+
});
36+
}
37+
38+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.javaoperatorsdk.admissioncontroller;
2+
3+
import java.util.concurrent.CompletionStage;
4+
5+
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
6+
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponse;
7+
8+
public interface AsyncRequestHandler {
9+
10+
CompletionStage<AdmissionResponse> handle(AdmissionRequest admissionRequest);
11+
12+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package io.javaoperatorsdk.admissioncontroller.mutation;
2+
3+
import java.util.concurrent.CompletableFuture;
4+
import java.util.concurrent.CompletionStage;
5+
6+
import io.fabric8.kubernetes.api.model.KubernetesResource;
7+
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
8+
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponse;
9+
import io.javaoperatorsdk.admissioncontroller.*;
10+
import io.javaoperatorsdk.admissioncontroller.clone.Cloner;
11+
import io.javaoperatorsdk.admissioncontroller.clone.ObjectMapperCloner;
12+
13+
import static io.javaoperatorsdk.admissioncontroller.AdmissionUtils.admissionResponseFromMutation;
14+
import static io.javaoperatorsdk.admissioncontroller.AdmissionUtils.getTargetResource;
15+
16+
public class AsyncDefaultRequestMutator<T extends KubernetesResource>
17+
implements AsyncRequestHandler {
18+
19+
private final AsyncMutator<T> mutator;
20+
private final Cloner<T> cloner;
21+
22+
public AsyncDefaultRequestMutator(AsyncMutator<T> mutator) {
23+
this(mutator, new ObjectMapperCloner<>());
24+
}
25+
26+
public AsyncDefaultRequestMutator(AsyncMutator<T> mutator, Cloner<T> cloner) {
27+
this.mutator = mutator;
28+
this.cloner = cloner;
29+
}
30+
31+
@Override
32+
public CompletionStage<AdmissionResponse> handle(AdmissionRequest admissionRequest) {
33+
Operation operation = Operation.valueOf(admissionRequest.getOperation());
34+
var originalResource = (T) getTargetResource(admissionRequest, operation);
35+
var clonedResource = cloner.clone(originalResource);
36+
CompletionStage<AdmissionResponse> admissionResponse;
37+
try {
38+
var mutatedResource = mutator.mutate(clonedResource, operation);
39+
admissionResponse =
40+
mutatedResource.thenApply(mr -> admissionResponseFromMutation(originalResource, mr));
41+
} catch (NotAllowedException e) {
42+
admissionResponse = CompletableFuture
43+
.supplyAsync(() -> AdmissionUtils.notAllowedExceptionToAdmissionResponse(e));
44+
}
45+
return admissionResponse;
46+
}
47+
48+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.javaoperatorsdk.admissioncontroller.mutation;
2+
3+
import java.util.concurrent.CompletionStage;
4+
5+
import io.fabric8.kubernetes.api.model.KubernetesResource;
6+
import io.javaoperatorsdk.admissioncontroller.NotAllowedException;
7+
import io.javaoperatorsdk.admissioncontroller.Operation;
8+
9+
public interface AsyncMutator<T extends KubernetesResource> {
10+
11+
CompletionStage<T> mutate(T resource, Operation operation) throws NotAllowedException;
12+
13+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.javaoperatorsdk.admissioncontroller.validation;
2+
3+
import java.util.concurrent.CompletableFuture;
4+
import java.util.concurrent.CompletionException;
5+
import java.util.concurrent.CompletionStage;
6+
7+
import io.fabric8.kubernetes.api.model.KubernetesResource;
8+
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
9+
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponse;
10+
import io.javaoperatorsdk.admissioncontroller.*;
11+
12+
import static io.javaoperatorsdk.admissioncontroller.AdmissionUtils.getTargetResource;
13+
14+
public class AsyncDefaultRequestValidator<T extends KubernetesResource>
15+
implements AsyncRequestHandler {
16+
17+
private final Validator<T> validator;
18+
19+
public AsyncDefaultRequestValidator(Validator<T> validator) {
20+
this.validator = validator;
21+
}
22+
23+
@Override
24+
@SuppressWarnings("unchecked")
25+
public CompletionStage<AdmissionResponse> handle(AdmissionRequest admissionRequest) {
26+
Operation operation = Operation.valueOf(admissionRequest.getOperation());
27+
var originalResource = (T) getTargetResource(admissionRequest, operation);
28+
29+
var asyncValidate =
30+
CompletableFuture.runAsync(() -> validator.validate(originalResource, operation));
31+
return asyncValidate
32+
.thenApply(v -> allowedAdmissionResponse())
33+
.exceptionally(e -> {
34+
if (e instanceof CompletionException) {
35+
if (e.getCause() instanceof NotAllowedException) {
36+
return AdmissionUtils.notAllowedExceptionToAdmissionResponse(
37+
(NotAllowedException) e.getCause());
38+
} else {
39+
throw new IllegalStateException(e.getCause());
40+
}
41+
} else {
42+
throw new IllegalStateException(e);
43+
}
44+
});
45+
}
46+
47+
private AdmissionResponse allowedAdmissionResponse() {
48+
AdmissionResponse admissionResponse = new AdmissionResponse();
49+
admissionResponse.setAllowed(true);
50+
return admissionResponse;
51+
}
52+
}

samples/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<artifactId>admission-controller-framework</artifactId>
88
<version>0.1.1-SNAPSHOT</version>
99
</parent>
10-
<artifactId>operator-framework-framework-samples</artifactId>
10+
<artifactId>operator-framework-samples</artifactId>
1111
<packaging>pom</packaging>
1212
<name>Admission Controller Framework - Samples</name>
1313
<modules>

samples/quarkus/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<modelVersion>4.0.0</modelVersion>
55
<parent>
66
<groupId>io.javaoperatorsdk</groupId>
7-
<artifactId>operator-framework-framework-samples</artifactId>
7+
<artifactId>operator-framework-samples</artifactId>
88
<version>0.1.1-SNAPSHOT</version>
99
</parent>
1010
<groupId>io.javaoperatorsdk.admissioncontroller.sample</groupId>
@@ -38,7 +38,7 @@
3838
</dependency>
3939
<dependency>
4040
<groupId>io.quarkus</groupId>
41-
<artifactId>quarkus-resteasy</artifactId>
41+
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
4242
</dependency>
4343
<dependency>
4444
<groupId>io.javaoperatorsdk</groupId>
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
11
package io.javaoperatorsdk.admissioncontroller.sample.quarkus;
22

3+
import java.util.concurrent.CompletableFuture;
4+
35
import javax.enterprise.context.Dependent;
46
import javax.inject.Named;
57
import javax.inject.Singleton;
68

79
import io.fabric8.kubernetes.api.model.Pod;
810
import io.javaoperatorsdk.admissioncontroller.AdmissionController;
11+
import io.javaoperatorsdk.admissioncontroller.AsyncAdmissionController;
912
import io.javaoperatorsdk.admissioncontroller.NotAllowedException;
13+
import io.javaoperatorsdk.admissioncontroller.mutation.AsyncMutator;
14+
import io.javaoperatorsdk.admissioncontroller.mutation.Mutator;
15+
import io.javaoperatorsdk.admissioncontroller.validation.Validator;
1016

1117
@Dependent
1218
public class AdmissionControllerConfig {
1319

1420
public static final String APP_NAME_LABEL_KEY = "app.kubernetes.io/name";
1521

22+
public static final String MUTATING_CONTROLLER = "mutatingController";
23+
public static final String VALIDATING_CONTROLLER = "validatingController";
24+
public static final String ERROR_MUTATING_CONTROLLER = "errorMutatingController";
25+
public static final String ERROR_VALIDATING_CONTROLLER = "errorValidatingController";
26+
public static final String ASYNC_MUTATING_CONTROLLER = "asyncMutatingController";
27+
public static final String ASYNC_VALIDATING_CONTROLLER = "asyncValidatingController";
28+
public static final String ERROR_ASYNC_MUTATING_CONTROLLER = "errorAsyncMutatingController";
29+
public static final String ERROR_ASYNC_VALIDATING_CONTROLLER = "errorAsyncValidatingController";
30+
public static final String ERROR_MESSAGE = "Some error happened";
31+
1632
@Singleton
17-
@Named("mutatingController")
33+
@Named(MUTATING_CONTROLLER)
1834
public AdmissionController<Pod> mutatingController() {
1935
return new AdmissionController<>((resource, operation) -> {
2036
resource.getMetadata().getLabels().putIfAbsent(APP_NAME_LABEL_KEY, "mutation-test");
@@ -23,7 +39,7 @@ public AdmissionController<Pod> mutatingController() {
2339
}
2440

2541
@Singleton
26-
@Named("validatingController")
42+
@Named(VALIDATING_CONTROLLER)
2743
public AdmissionController<Pod> validatingController() {
2844
return new AdmissionController<>((resource, operation) -> {
2945
if (resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null) {
@@ -32,4 +48,56 @@ public AdmissionController<Pod> validatingController() {
3248
});
3349
}
3450

51+
@Singleton
52+
@Named(ASYNC_MUTATING_CONTROLLER)
53+
public AsyncAdmissionController<Pod> asyncMutatingController() {
54+
return new AsyncAdmissionController<>(
55+
(AsyncMutator<Pod>) (resource, operation) -> CompletableFuture.supplyAsync(() -> {
56+
resource.getMetadata().getLabels().putIfAbsent(APP_NAME_LABEL_KEY, "mutation-test");
57+
return resource;
58+
}));
59+
}
60+
61+
@Singleton
62+
@Named(ASYNC_VALIDATING_CONTROLLER)
63+
public AsyncAdmissionController<Pod> asyncValidatingController() {
64+
return new AsyncAdmissionController<>((resource, operation) -> {
65+
if (resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null) {
66+
throw new NotAllowedException("Missing label: " + APP_NAME_LABEL_KEY);
67+
}
68+
});
69+
}
70+
71+
@Singleton
72+
@Named(ERROR_MUTATING_CONTROLLER)
73+
public AdmissionController<Pod> errorMutatingController() {
74+
return new AdmissionController<>((Validator<Pod>) (resource, operation) -> {
75+
throw new IllegalStateException(ERROR_MESSAGE);
76+
});
77+
}
78+
79+
@Singleton
80+
@Named(ERROR_VALIDATING_CONTROLLER)
81+
public AdmissionController<Pod> errorValidatingController() {
82+
return new AdmissionController<>((Mutator<Pod>) (resource, operation) -> {
83+
throw new IllegalStateException(ERROR_MESSAGE);
84+
});
85+
}
86+
87+
@Singleton
88+
@Named(ERROR_ASYNC_MUTATING_CONTROLLER)
89+
public AsyncAdmissionController<Pod> errorAsyncMutatingController() {
90+
return new AsyncAdmissionController<>((AsyncMutator<Pod>) (resource, operation) -> {
91+
throw new IllegalStateException(ERROR_MESSAGE);
92+
});
93+
}
94+
95+
@Singleton
96+
@Named(ERROR_ASYNC_VALIDATING_CONTROLLER)
97+
public AsyncAdmissionController<Pod> errorAsyncValidatingController() {
98+
return new AsyncAdmissionController<>((Validator<Pod>) (resource, operation) -> {
99+
throw new IllegalStateException(ERROR_MESSAGE);
100+
});
101+
}
102+
35103
}

0 commit comments

Comments
 (0)