diff --git a/kubernetes/k8s-admission-controller/Dockerfile b/kubernetes/k8s-admission-controller/Dockerfile
new file mode 100644
index 0000000000..f8939ee7c8
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/Dockerfile
@@ -0,0 +1,11 @@
+FROM adoptopenjdk:11-jre-hotspot as builder
+ARG JAR_FILE=target/*.jar
+COPY ${JAR_FILE} application.jar
+RUN java -Djarmode=layertools -jar application.jar extract
+
+FROM adoptopenjdk:11-jre-hotspot
+COPY --from=builder dependencies/ ./
+COPY --from=builder snapshot-dependencies/ ./
+COPY --from=builder spring-boot-loader/ ./
+COPY --from=builder application/ ./
+ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
\ No newline at end of file
diff --git a/kubernetes/k8s-admission-controller/pom.xml b/kubernetes/k8s-admission-controller/pom.xml
new file mode 100644
index 0000000000..fbee9ceba6
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/pom.xml
@@ -0,0 +1,86 @@
+
+
+ 4.0.0
+
+
+ com.baeldung
+ parent-boot-2
+ 0.0.1-SNAPSHOT
+ ./../../parent-boot-2
+
+
+ k8s-admission-controller
+ k8s-admission-controller
+ Demo project for Spring Boot
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ repackage
+
+
+
+
+
+
+ org.projectlombok
+ lombok
+
+
+ com.baeldung.kubernetes.admission.Application
+
+ true
+
+
+
+
+
+
+
diff --git a/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/Application.java b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/Application.java
new file mode 100644
index 0000000000..1260ec0af9
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/Application.java
@@ -0,0 +1,17 @@
+package com.baeldung.kubernetes.admission;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+
+import com.baeldung.kubernetes.admission.config.AdmissionControllerProperties;
+
+@SpringBootApplication
+@EnableConfigurationProperties(AdmissionControllerProperties.class)
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/config/AdmissionControllerProperties.java b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/config/AdmissionControllerProperties.java
new file mode 100644
index 0000000000..2352068ba1
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/config/AdmissionControllerProperties.java
@@ -0,0 +1,22 @@
+/**
+ *
+ */
+package com.baeldung.kubernetes.admission.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import lombok.Data;
+
+/**
+ * @author lighthouse.psevestre
+ *
+ */
+@ConfigurationProperties(prefix = "admission-controller")
+@Data
+public class AdmissionControllerProperties {
+
+ private boolean disabled;
+ private String annotation = "com.baeldung/wait-for-it";
+ private String waitForItImage = "willwill/wait-for-it";
+
+}
diff --git a/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/controller/AdmissionReviewController.java b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/controller/AdmissionReviewController.java
new file mode 100644
index 0000000000..a73aa3d89b
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/controller/AdmissionReviewController.java
@@ -0,0 +1,30 @@
+/**
+ *
+ */
+package com.baeldung.kubernetes.admission.controller;
+
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.baeldung.kubernetes.admission.dto.AdmissionReviewResponse;
+import com.baeldung.kubernetes.admission.service.AdmissionService;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import lombok.RequiredArgsConstructor;
+import reactor.core.publisher.Mono;
+
+/**
+ *
+ */
+@RestController
+@RequiredArgsConstructor
+public class AdmissionReviewController {
+
+ private final AdmissionService admissionService;
+
+ @PostMapping(path = "/mutate")
+ public Mono processAdmissionReviewRequest(@RequestBody Mono request) {
+ return request.map((body) -> admissionService.processAdmission(body));
+ }
+}
diff --git a/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/dto/AdmissionReviewData.java b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/dto/AdmissionReviewData.java
new file mode 100644
index 0000000000..6d590ff408
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/dto/AdmissionReviewData.java
@@ -0,0 +1,31 @@
+/**
+ *
+ */
+package com.baeldung.kubernetes.admission.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * Result sent to the API server after reviewing and, possibly
+ * modifying the incoming request
+ */
+@Builder
+@Data
+public class AdmissionReviewData {
+
+ final String uid;
+ final boolean allowed;
+
+ @JsonInclude(Include.NON_NULL)
+ final String patchType;
+
+ @JsonInclude(Include.NON_NULL)
+ final String patch;
+
+ @JsonInclude(Include.NON_NULL)
+ final AdmissionStatus status;
+}
diff --git a/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/dto/AdmissionReviewException.java b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/dto/AdmissionReviewException.java
new file mode 100644
index 0000000000..a788eca461
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/dto/AdmissionReviewException.java
@@ -0,0 +1,28 @@
+package com.baeldung.kubernetes.admission.dto;
+
+/**
+ * Exceção utilizada para reportar erros de validação no manifesto recebido para admissão
+ * @author lighthouse.psevestre
+ *
+ */
+public class AdmissionReviewException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+ private final int code;
+
+ public AdmissionReviewException(int code, String message) {
+ super(message);
+ this.code = code;
+ }
+
+ public AdmissionReviewException(String message) {
+ super(message);
+ this.code = 400;
+
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+}
diff --git a/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/dto/AdmissionReviewResponse.java b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/dto/AdmissionReviewResponse.java
new file mode 100644
index 0000000000..1780f52540
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/dto/AdmissionReviewResponse.java
@@ -0,0 +1,25 @@
+/**
+ *
+ */
+package com.baeldung.kubernetes.admission.dto;
+
+import lombok.Builder;
+import lombok.Builder.Default;
+import lombok.Data;
+
+/**
+ * Response "envelope" sent back to the API Server
+ */
+@Builder
+@Data
+public class AdmissionReviewResponse {
+
+ @Default
+ final String apiVersion = "admission.k8s.io/v1";
+
+ @Default
+ final String kind = "AdmissionReview";
+
+ final AdmissionReviewData response;
+
+}
diff --git a/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/dto/AdmissionStatus.java b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/dto/AdmissionStatus.java
new file mode 100644
index 0000000000..ccab7ac958
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/dto/AdmissionStatus.java
@@ -0,0 +1,13 @@
+package com.baeldung.kubernetes.admission.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Builder
+@Data
+public class AdmissionStatus {
+
+ int code;
+ String message;
+
+}
diff --git a/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/service/AdmissionService.java b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/service/AdmissionService.java
new file mode 100644
index 0000000000..814bafbae7
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/main/java/com/baeldung/kubernetes/admission/service/AdmissionService.java
@@ -0,0 +1,218 @@
+/**
+ *
+ */
+package com.baeldung.kubernetes.admission.service;
+
+import java.util.Base64;
+import java.util.UUID;
+
+import org.springframework.stereotype.Component;
+
+import com.baeldung.kubernetes.admission.config.AdmissionControllerProperties;
+import com.baeldung.kubernetes.admission.dto.AdmissionReviewData;
+import com.baeldung.kubernetes.admission.dto.AdmissionReviewException;
+import com.baeldung.kubernetes.admission.dto.AdmissionReviewResponse;
+import com.baeldung.kubernetes.admission.dto.AdmissionStatus;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Process an incoming admission request and add the "wait-for-it" init container
+ * if there's an appropriate annotation
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class AdmissionService {
+
+ private final AdmissionControllerProperties admissionControllerProperties;
+ private final ObjectMapper om;
+
+ public AdmissionReviewResponse processAdmission(ObjectNode body) {
+
+ String uid = body.path("request")
+ .required("uid")
+ .asText();
+
+ log.info("[I42] processAdmission: uid={}",uid);
+ if ( log.isDebugEnabled()) {
+ log.debug("processAdmission: body={}", body.toPrettyString());
+ }
+
+ // Get request annotations
+ JsonNode annotations = body.path("request")
+ .path("object")
+ .path("metadata")
+ .path("annotations");
+ log.info("processAdmision: annotations={}", annotations.toString());
+
+ AdmissionReviewData data;
+ try {
+ if (admissionControllerProperties.isDisabled()) {
+ log.info("[I58] 'disabled' option in effect. No changes to current request will be made");
+ data = createSimpleAllowedReview(body);
+ } else if (annotations.isMissingNode()) {
+ log.info("[I68] No annotations found in request. No changes will be made");
+ data = createSimpleAllowedReview(body);
+ } else {
+ data = processAnnotations(body, annotations);
+ }
+
+ log.info("[I65] Review result: isAllowed=" + data.isAllowed());
+ log.info("[I64] AdmissionReviewData= {}", data);
+
+ return AdmissionReviewResponse.builder()
+ .apiVersion(body.required("apiVersion").asText())
+ .kind(body.required("kind").asText())
+ .response(data)
+ .build();
+ } catch (AdmissionReviewException ex) {
+ log.error("[E72] Error processing AdmissionRequest: code={}, message={}", ex.getCode(), ex.getMessage());
+ data = createRejectedAdmissionReview(body, ex.getCode(), ex.getMessage());
+
+ return AdmissionReviewResponse.builder()
+ .apiVersion(body.required("apiVersion").asText())
+ .kind(body.required("kind").asText())
+ .response(data)
+ .build();
+ } catch (Exception ex) {
+ log.error("[E72] Unable to process AdmissionRequest: " + ex.getMessage(), ex);
+ data = createRejectedAdmissionReview(body, 500, ex.getMessage());
+ return AdmissionReviewResponse.builder()
+ .apiVersion(body.required("apiVersion").asText())
+ .kind(body.required("kind").asText())
+ .response(data)
+ .build();
+ }
+ }
+
+ /**
+ * @param body
+ * @return
+ */
+ protected AdmissionReviewData createSimpleAllowedReview(ObjectNode body) {
+ AdmissionReviewData data;
+ String requestId = body.path("request")
+ .required("uid")
+ .asText();
+
+ data = AdmissionReviewData.builder()
+ .allowed(true)
+ .uid(requestId)
+ .build();
+
+ return data;
+
+ }
+
+ /**
+ * @param body
+ * @return
+ */
+ protected AdmissionReviewData createRejectedAdmissionReview(ObjectNode body, int code, String message) {
+ AdmissionReviewData data;
+ String requestId = body.path("request")
+ .required("uid")
+ .asText();
+
+ AdmissionStatus status = AdmissionStatus.builder()
+ .code(code)
+ .message(message)
+ .build();
+
+ data = AdmissionReviewData.builder()
+ .allowed(false)
+ .uid(requestId)
+ .status(status)
+ .build();
+
+ return data;
+
+ }
+
+ /**
+ * Processa anotações incluídas no deployment
+ * @param annotations
+ * @return
+ */
+ protected AdmissionReviewData processAnnotations(ObjectNode body, JsonNode annotations) {
+
+ if (annotations.path(admissionControllerProperties.getAnnotation())
+ .isMissingNode()) {
+ log.info("[I78] processAnnotations: Annotation {} not found in deployment deployment.", admissionControllerProperties.getAnnotation());
+ return createSimpleAllowedReview(body);
+ }
+ else {
+ log.info("[I163] annotation found: {}", annotations.path(admissionControllerProperties.getAnnotation()));
+ }
+
+ // Get wait-for-it arguments from the annotation
+ String waitForArgs = annotations.path(admissionControllerProperties.getAnnotation())
+ .asText();
+
+ log.info("[I169] waitForArgs={}", waitForArgs);
+ // Create a PATCH object
+ String patch = injectInitContainer(body, waitForArgs);
+
+ return AdmissionReviewData.builder()
+ .allowed(true)
+ .uid(body.path("request")
+ .required("uid")
+ .asText())
+ .patch(Base64.getEncoder()
+ .encodeToString(patch.getBytes()))
+ .patchType("JSONPatch")
+ .build();
+
+ }
+
+ /**
+ * Creates the JSONPatch to be included in the admission response
+ * @param body
+ * @param waitForArgs
+ * @return JSONPatch string
+ */
+ protected String injectInitContainer(ObjectNode body, String waitForArgs) {
+
+ // Recover original init containers from the request
+ JsonNode originalSpec = body.path("request")
+ .path("object")
+ .path("spec")
+ .path("template")
+ .path("spec")
+ .require();
+
+ JsonNode maybeInitContainers = originalSpec.path("initContainers");
+ ArrayNode initContainers =
+ maybeInitContainers.isMissingNode()?
+ om.createArrayNode():(ArrayNode) maybeInitContainers;
+
+ // Create the patch array
+ ArrayNode patchArray = om.createArrayNode();
+ ObjectNode addNode = patchArray.addObject();
+
+ addNode.put("op", "add");
+ addNode.put("path", "/spec/template/spec/initContainers");
+ ArrayNode values = addNode.putArray("value");
+
+ // Preserve original init containers
+ values.addAll(initContainers);
+
+ // append the "wait-for-it" container
+ ObjectNode wfi = values.addObject();
+ wfi.put("name", "wait-for-it-" + UUID.randomUUID()); // Create an unique name, JIC
+ wfi.put("image", admissionControllerProperties.getWaitForItImage());
+
+ ArrayNode args = wfi.putArray("args");
+ for (String s : waitForArgs.split("\\s")) {
+ args.add(s);
+ }
+
+ return patchArray.toString();
+ }
+}
diff --git a/kubernetes/k8s-admission-controller/src/test/java/com/baeldung/kubernetes/admission/service/AdmissionServiceUnitTest.java b/kubernetes/k8s-admission-controller/src/test/java/com/baeldung/kubernetes/admission/service/AdmissionServiceUnitTest.java
new file mode 100644
index 0000000000..544b48a9d4
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/test/java/com/baeldung/kubernetes/admission/service/AdmissionServiceUnitTest.java
@@ -0,0 +1,65 @@
+package com.baeldung.kubernetes.admission.service;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Base64;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+import com.baeldung.kubernetes.admission.config.AdmissionControllerProperties;
+import com.baeldung.kubernetes.admission.dto.AdmissionReviewResponse;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+@SpringBootTest
+@ActiveProfiles("test")
+@EnableConfigurationProperties(AdmissionControllerProperties.class)
+class AdmissionServiceUnitTest {
+
+ @Autowired
+ private ObjectMapper mapper;
+
+ @Autowired
+ private AdmissionService admissionService;
+
+ @Test
+ void whenAnnotationPresent_thenAddContainer() throws Exception {
+
+ InputStream is = this.getClass()
+ .getClassLoader()
+ .getResourceAsStream("test1.json");
+ JsonNode body = mapper.readTree(is);
+ AdmissionReviewResponse response = admissionService.processAdmission((ObjectNode) body);
+ assertNotNull(response);
+ assertNotNull(response.getResponse());
+ assertNotNull(response.getResponse());
+ assertTrue(response.getResponse()
+ .isAllowed());
+
+ String jsonResponse = mapper.writeValueAsString(response);
+ System.out.println(jsonResponse);
+
+ // Decode Patch data
+ String b64patch = response.getResponse()
+ .getPatch();
+ assertNotNull(b64patch);
+ byte[] patch = Base64.getDecoder()
+ .decode(b64patch);
+
+ JsonNode root = mapper.reader()
+ .readTree(new ByteArrayInputStream(patch));
+ assertTrue(root instanceof ArrayNode);
+
+ assertEquals(1, ((ArrayNode) root).size());
+
+ }
+
+}
diff --git a/kubernetes/k8s-admission-controller/src/test/k8s/nginx.yaml b/kubernetes/k8s-admission-controller/src/test/k8s/nginx.yaml
new file mode 100644
index 0000000000..0c601660d1
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/test/k8s/nginx.yaml
@@ -0,0 +1,23 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: frontend
+ labels:
+ app: nginx
+ annotations:
+ com.baeldung/wait-for-it: "www.google.com:80"
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: nginx
+ template:
+ metadata:
+ labels:
+ app: nginx
+ spec:
+ containers:
+ - name: nginx
+ image: nginx:1.14.2
+ ports:
+ - containerPort: 80
diff --git a/kubernetes/k8s-admission-controller/src/test/resources/test1.json b/kubernetes/k8s-admission-controller/src/test/resources/test1.json
new file mode 100644
index 0000000000..0ad42ae133
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/test/resources/test1.json
@@ -0,0 +1,84 @@
+{
+ "kind": "AdmissionReview",
+ "apiVersion": "admission.k8s.io/v1",
+ "request": {
+ "uid": "c46a6607-129d-425b-af2f-c6f87a0756da",
+ "kind": {
+ "group": "apps",
+ "version": "v1",
+ "kind": "Deployment"
+ },
+ "resource": {
+ "group": "apps",
+ "version": "v1",
+ "resource": "deployments"
+ },
+ "requestKind": {
+ "group": "apps",
+ "version": "v1",
+ "kind": "Deployment"
+ },
+ "requestResource": {
+ "group": "apps",
+ "version": "v1",
+ "resource": "deployments"
+ },
+ "name": "test-deployment",
+ "namespace": "test-namespace",
+ "operation": "CREATE",
+ "object": {
+ "kind": "Deployment",
+ "apiVersion": "apps/v1",
+ "metadata": {
+ "name": "test-deployment",
+ "namespace": "test-namespace",
+ "annotations": {
+ "com.baeldung/wait-for-it": "www.google.com:80"
+ }
+ },
+ "spec": {
+ "replicas": 1,
+ "selector": {
+ "matchLabels": {
+ "app": "test-app"
+ }
+ },
+ "template": {
+ "metadata": {
+ "name": "test-app",
+ "creationTimestamp": null,
+ "labels": {
+ "app": "test-app"
+ }
+ },
+ "spec": {
+ "containers": [
+ {
+ "name": "app",
+ "image": "test-app-image:latest",
+ "ports": [
+ {
+ "name": "http",
+ "containerPort": 8080,
+ "protocol": "TCP"
+ }
+ ],
+ "resources": {},
+ "terminationMessagePath": "/dev/termination-log",
+ "terminationMessagePolicy": "File",
+ "imagePullPolicy": "Always"
+ }
+ ]
+ }
+ }
+ },
+ "status": {}
+ },
+ "oldObject": null,
+ "dryRun": false,
+ "options": {
+ "kind": "CreateOptions",
+ "apiVersion": "meta.k8s.io/v1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/kubernetes/k8s-admission-controller/src/test/resources/test2.json b/kubernetes/k8s-admission-controller/src/test/resources/test2.json
new file mode 100644
index 0000000000..5b8a6ca243
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/test/resources/test2.json
@@ -0,0 +1,85 @@
+{
+ "kind": "AdmissionReview",
+ "apiVersion": "admission.k8s.io/v1",
+ "request": {
+ "uid": "c46a6607-129d-425b-af2f-c6f87a0756da",
+ "kind": {
+ "group": "apps",
+ "version": "v1",
+ "kind": "Deployment"
+ },
+ "resource": {
+ "group": "apps",
+ "version": "v1",
+ "resource": "deployments"
+ },
+ "requestKind": {
+ "group": "apps",
+ "version": "v1",
+ "kind": "Deployment"
+ },
+ "requestResource": {
+ "group": "apps",
+ "version": "v1",
+ "resource": "deployments"
+ },
+ "name": "test-deployment",
+ "namespace": "test-namespace",
+ "operation": "CREATE",
+ "object": {
+ "kind": "Deployment",
+ "apiVersion": "apps/v1",
+ "metadata": {
+ "name": "test-deployment",
+ "namespace": "test-namespace",
+ "annotations": {
+ "com.baeldung/wait-for-it": "www.google.com:80"
+ }
+ },
+ "spec": {
+ "replicas": 1,
+ "selector": {
+ "matchLabels": {
+ "app": "test-app"
+ }
+ },
+ "template": {
+ "metadata": {
+ "name": "test-app",
+ "creationTimestamp": null,
+ "labels": {
+ "app": "test-app"
+ }
+ },
+ "spec": {
+ "initContainers": [],
+ "containers": [
+ {
+ "name": "app",
+ "image": "test-app-image:latest",
+ "ports": [
+ {
+ "name": "http",
+ "containerPort": 8080,
+ "protocol": "TCP"
+ }
+ ],
+ "resources": {},
+ "terminationMessagePath": "/dev/termination-log",
+ "terminationMessagePolicy": "File",
+ "imagePullPolicy": "Always"
+ }
+ ]
+ }
+ }
+ },
+ "status": {}
+ },
+ "oldObject": null,
+ "dryRun": false,
+ "options": {
+ "kind": "CreateOptions",
+ "apiVersion": "meta.k8s.io/v1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/kubernetes/k8s-admission-controller/src/test/resources/test3.json b/kubernetes/k8s-admission-controller/src/test/resources/test3.json
new file mode 100644
index 0000000000..9cb4d516f9
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/test/resources/test3.json
@@ -0,0 +1,94 @@
+{
+ "kind": "AdmissionReview",
+ "apiVersion": "admission.k8s.io/v1",
+ "request": {
+ "uid": "c46a6607-129d-425b-af2f-c6f87a0756da",
+ "kind": {
+ "group": "apps",
+ "version": "v1",
+ "kind": "Deployment"
+ },
+ "resource": {
+ "group": "apps",
+ "version": "v1",
+ "resource": "deployments"
+ },
+ "requestKind": {
+ "group": "apps",
+ "version": "v1",
+ "kind": "Deployment"
+ },
+ "requestResource": {
+ "group": "apps",
+ "version": "v1",
+ "resource": "deployments"
+ },
+ "name": "test-deployment",
+ "namespace": "test-namespace",
+ "operation": "CREATE",
+ "object": {
+ "kind": "Deployment",
+ "apiVersion": "apps/v1",
+ "metadata": {
+ "name": "test-deployment",
+ "namespace": "test-namespace",
+ "annotations": {
+ "com.baeldung/wait-for-it": "www.google.com:80"
+ }
+ },
+ "spec": {
+ "replicas": 1,
+ "selector": {
+ "matchLabels": {
+ "app": "test-app"
+ }
+ },
+ "template": {
+ "metadata": {
+ "name": "test-app",
+ "creationTimestamp": null,
+ "labels": {
+ "app": "test-app"
+ }
+ },
+ "spec": {
+ "initContainers": [
+ {
+ "name": "init1",
+ "image": "test-app-image:latest",
+ "resources": {},
+ "terminationMessagePath": "/dev/termination-log",
+ "terminationMessagePolicy": "File",
+ "imagePullPolicy": "Always"
+ }
+ ],
+ "containers": [
+ {
+ "name": "app",
+ "image": "test-app-image:latest",
+ "ports": [
+ {
+ "name": "http",
+ "containerPort": 8080,
+ "protocol": "TCP"
+ }
+ ],
+ "resources": {},
+ "terminationMessagePath": "/dev/termination-log",
+ "terminationMessagePolicy": "File",
+ "imagePullPolicy": "Always"
+ }
+ ]
+ }
+ }
+ },
+ "status": {}
+ },
+ "oldObject": null,
+ "dryRun": false,
+ "options": {
+ "kind": "CreateOptions",
+ "apiVersion": "meta.k8s.io/v1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/kubernetes/k8s-admission-controller/src/test/resources/test4.json b/kubernetes/k8s-admission-controller/src/test/resources/test4.json
new file mode 100644
index 0000000000..1e3be90a93
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/test/resources/test4.json
@@ -0,0 +1,48 @@
+{
+ "kind": "AdmissionReview",
+ "apiVersion": "admission.k8s.io/v1",
+ "request": {
+ "uid": "26beb334-739a-48d2-b04d-25f6e5e7c106",
+ "kind": {
+ "group": "apps",
+ "version": "v1",
+ "kind": "Deployment"
+ },
+ "resource": {},
+ "f:type": {}
+ },
+ "f:template": {
+ "f:metadata": {
+ "f:labels": {
+ ".": {},
+ "f:app": {}
+ }
+ },
+ "f:spec": {
+ "f:containers": {
+ "k:{\"name\":\"nginx\"}": {
+ ".": {},
+ "f:image": {},
+ "f:imagePullPolicy": {},
+ "f:name": {},
+ "f:ports": {
+ ".": {},
+ "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": {
+ ".": {},
+ "f:containerPort": {},
+ "f:protocol": {}
+ }
+ },
+ "f:resources": {},
+ "f:terminationMessagePath": {},
+ "f:terminationMessagePolicy": {}
+ }
+ },
+ "f:dnsPolicy": {},
+ "f:restartPolicy": {},
+ "f:schedulerName": {},
+ "f:securityContext": {},
+ "f:terminationGracePeriodSeconds": {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/kubernetes/k8s-admission-controller/src/test/terraform/.gitignore b/kubernetes/k8s-admission-controller/src/test/terraform/.gitignore
new file mode 100644
index 0000000000..4c305a080a
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/test/terraform/.gitignore
@@ -0,0 +1,3 @@
+.terraform
+terraform.tfstate
+terraform.tfstate.backup
diff --git a/kubernetes/k8s-admission-controller/src/test/terraform/.terraform.lock.hcl b/kubernetes/k8s-admission-controller/src/test/terraform/.terraform.lock.hcl
new file mode 100644
index 0000000000..e13c37b4ec
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/test/terraform/.terraform.lock.hcl
@@ -0,0 +1,57 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/kubernetes" {
+ version = "2.3.2"
+ constraints = "2.3.2"
+ hashes = [
+ "h1:D8HWX3vouTPI3Jicq43xOQyoYWtSsVua92cBVrJ3ZMs=",
+ "zh:10f71c170be13538374a4b9553fcb3d98a6036bcd1ca5901877773116c3f828e",
+ "zh:11d2230e531b7480317e988207a73cb67b332f225b0892304983b19b6014ebe0",
+ "zh:3317387a9a6cc27fd7536b8f3cad4b8a9285e9461f125c5a15d192cef3281856",
+ "zh:458a9858362900fbe97e00432ae8a5bef212a4dacf97a57ede7534c164730da4",
+ "zh:50ea297007d9fe53e5411577f87a4b13f3877ce732089b42f938430e6aadff0d",
+ "zh:56705c959e4cbea3b115782d04c62c68ac75128c5c44ee7aa4043df253ffbfe3",
+ "zh:7eb3722f7f036e224824470c3e0d941f1f268fcd5fa2f8203e0eee425d0e1484",
+ "zh:9f408a6df4d74089e6ce18f9206b06b8107ddb57e2bc9b958a6b7dc352c62980",
+ "zh:aadd25ccc3021040808feb2645779962f638766eb583f586806e59f24dde81bb",
+ "zh:b101c3456e4309b09aab129b0118561178c92cb4be5d96dec553189c3084dca1",
+ "zh:ec08478573b4953764099fbfd670fae81dc24b60e467fb3b023e6fab50b70a9e",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/null" {
+ version = "3.1.0"
+ hashes = [
+ "h1:SFT7X3zY18CLWjoH2GfQyapxsRv6GDKsy9cF1aRwncc=",
+ "zh:02a1675fd8de126a00460942aaae242e65ca3380b5bb192e8773ef3da9073fd2",
+ "zh:53e30545ff8926a8e30ad30648991ca8b93b6fa496272cd23b26763c8ee84515",
+ "zh:5f9200bf708913621d0f6514179d89700e9aa3097c77dac730e8ba6e5901d521",
+ "zh:9ebf4d9704faba06b3ec7242c773c0fbfe12d62db7d00356d4f55385fc69bfb2",
+ "zh:a6576c81adc70326e4e1c999c04ad9ca37113a6e925aefab4765e5a5198efa7e",
+ "zh:a8a42d13346347aff6c63a37cda9b2c6aa5cc384a55b2fe6d6adfa390e609c53",
+ "zh:c797744d08a5307d50210e0454f91ca4d1c7621c68740441cf4579390452321d",
+ "zh:cecb6a304046df34c11229f20a80b24b1603960b794d68361a67c5efe58e62b8",
+ "zh:e1371aa1e502000d9974cfaff5be4cfa02f47b17400005a16f14d2ef30dc2a70",
+ "zh:fc39cc1fe71234a0b0369d5c5c7f876c71b956d23d7d6f518289737a001ba69b",
+ "zh:fea4227271ebf7d9e2b61b89ce2328c7262acd9fd190e1fd6d15a591abfa848e",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/tls" {
+ version = "3.1.0"
+ hashes = [
+ "h1:ekOxs6MjdIElt8h9crEVaOwWbEqtfUUfArtA13Jkk6A=",
+ "zh:3d46616b41fea215566f4a957b6d3a1aa43f1f75c26776d72a98bdba79439db6",
+ "zh:623a203817a6dafa86f1b4141b645159e07ec418c82fe40acd4d2a27543cbaa2",
+ "zh:668217e78b210a6572e7b0ecb4134a6781cc4d738f4f5d09eb756085b082592e",
+ "zh:95354df03710691773c8f50a32e31fca25f124b7f3d6078265fdf3c4e1384dca",
+ "zh:9f97ab190380430d57392303e3f36f4f7835c74ea83276baa98d6b9a997c3698",
+ "zh:a16f0bab665f8d933e95ca055b9c8d5707f1a0dd8c8ecca6c13091f40dc1e99d",
+ "zh:be274d5008c24dc0d6540c19e22dbb31ee6bfdd0b2cddd4d97f3cd8a8d657841",
+ "zh:d5faa9dce0a5fc9d26b2463cea5be35f8586ab75030e7fa4d4920cd73ee26989",
+ "zh:e9b672210b7fb410780e7b429975adcc76dd557738ecc7c890ea18942eb321a5",
+ "zh:eb1f8368573d2370605d6dbf60f9aaa5b64e55741d96b5fb026dbfe91de67c0d",
+ "zh:fc1e12b713837b85daf6c3bb703d7795eaf1c5177aebae1afcf811dd7009f4b0",
+ ]
+}
diff --git a/kubernetes/k8s-admission-controller/src/test/terraform/component.auto.tfvars b/kubernetes/k8s-admission-controller/src/test/terraform/component.auto.tfvars
new file mode 100644
index 0000000000..eb1cdaea53
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/test/terraform/component.auto.tfvars
@@ -0,0 +1,10 @@
+#
+# Sample variable values.
+#
+namespace="default"
+deployment_name="wait-for-it-admission-controller"
+replicas=1
+image="psevestre/wait-for-it-admission-controller"
+image_prefix=""
+image_version="latest"
+k8s_config_context="minikube"
diff --git a/kubernetes/k8s-admission-controller/src/test/terraform/main.tf b/kubernetes/k8s-admission-controller/src/test/terraform/main.tf
new file mode 100644
index 0000000000..a0717c4013
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/test/terraform/main.tf
@@ -0,0 +1,277 @@
+
+locals {
+ prefix = var.image_prefix != "" ? "${var.image_prefix}/":""
+ image = "${local.prefix}${var.image}:${var.image_version}"
+ cloud_sdk_image = "${local.prefix}frapsoft/openssl"
+ ns = data.kubernetes_namespace.ns.metadata[0].name
+
+ # Spring SSL Configuration
+ webhook_config_json = jsonencode({
+ server = {
+ port = 443
+ ssl = {
+ "key-store" = "/shared-config/webhook.p12"
+ "key-store-type" = "PKCS12"
+ "key-alias" = "webhook"
+ "key-store-password" = ""
+ }
+ }
+
+ admission-controller = {
+ disabled = false
+ image-prefix = "gcr.io/sandboxbv-01"
+ }
+ })
+
+}
+
+
+# Resource namespace
+data "kubernetes_namespace" "ns" {
+ metadata {
+ name = var.namespace
+ }
+}
+
+# TLS Key
+resource "tls_private_key" "tls" {
+ algorithm = "RSA"
+}
+
+# CSR
+resource "tls_cert_request" "tls" {
+ key_algorithm = "RSA"
+ private_key_pem = tls_private_key.tls.private_key_pem
+ subject {
+ common_name = "${var.deployment_name}.${var.namespace}.svc"
+ }
+
+ dns_names = [
+ var.deployment_name,
+ "${var.deployment_name}.${var.namespace}",
+ "${var.deployment_name}.${var.namespace}.svc",
+ "${var.deployment_name}.${var.namespace}.svc.cluster.local"
+ ]
+
+}
+
+# HTTPS Certificate
+resource "tls_self_signed_cert" "tls" {
+ key_algorithm = tls_private_key.tls.algorithm
+ private_key_pem = tls_private_key.tls.private_key_pem
+
+ subject {
+ common_name = "${var.deployment_name}.${local.ns}"
+ }
+
+ validity_period_hours = 24*365*20
+
+ dns_names = [
+ var.deployment_name,
+ "${var.deployment_name}.${var.namespace}",
+ "${var.deployment_name}.${var.namespace}.svc",
+ "${var.deployment_name}.${var.namespace}.svc.cluster.local"
+ ]
+
+ allowed_uses = [
+ "key_encipherment",
+ "digital_signature",
+ "server_auth"
+ ]
+}
+
+# Certificado
+# Obs: Desativado pois o certificado fica preso no estado "Issued"
+resource "kubernetes_certificate_signing_request" "tls" {
+ count = 0
+ metadata {
+ name = "${var.deployment_name}.${var.namespace}"
+ }
+
+ auto_approve = true
+
+ spec {
+ usages = [
+ "key encipherment",
+ "digital signature",
+ "server auth"
+ ]
+
+ signer_name = "kubernetes.io/kubelet-serving"
+
+ request = tls_cert_request.tls.cert_request_pem
+ }
+
+}
+
+# Secret to store TLS key/cert
+resource "kubernetes_secret" "tls" {
+ metadata {
+ namespace = local.ns
+ name = var.deployment_name
+ }
+
+ data = {
+ "webhook-key.pem" = tls_private_key.tls.private_key_pem
+ "webhook-cert.pem" = tls_self_signed_cert.tls.cert_pem
+ }
+
+}
+
+# Deployment
+resource "kubernetes_deployment" "main" {
+ metadata {
+ name = var.deployment_name
+ namespace = local.ns
+ }
+
+ spec {
+ replicas = var.replicas
+ selector {
+ match_labels = {
+ app = var.deployment_name
+ }
+ }
+
+ template {
+ metadata {
+ labels = {
+ app = var.deployment_name
+ }
+ }
+
+ spec {
+ container {
+ image = local.image
+ name = var.deployment_name
+ volume_mount {
+ mount_path = "/shared-config"
+ name = "shared-config"
+ }
+
+ env {
+ name = "SPRING_APPLICATION_JSON"
+ value = local.webhook_config_json
+ }
+
+ }
+
+ init_container {
+ name = "setup-keystore"
+ image = local.cloud_sdk_image
+
+ args = [
+ "pkcs12", "-export",
+ "-in", "/secret/webhook-cert.pem",
+ "-inkey", "/secret/webhook-key.pem",
+ "-name", "webhook",
+ "-out", "/shared-config/webhook.p12",
+ "-passout", "pass:"
+ ]
+
+ volume_mount {
+ mount_path = "/shared-config"
+ name = "shared-config"
+ }
+
+ volume_mount {
+ mount_path = "/secret/webhook-cert.pem"
+ name = "webhook-secret"
+ sub_path = "webhook-cert.pem"
+ }
+
+ volume_mount {
+ mount_path = "/secret/webhook-key.pem"
+ name = "webhook-secret"
+ sub_path = "webhook-key.pem"
+ }
+
+ }
+
+ volume {
+ name = "shared-config"
+ empty_dir {}
+ }
+
+ volume {
+ name = "webhook-secret"
+ secret {
+ secret_name = kubernetes_secret.tls.metadata[0].name
+ items {
+ key = "webhook-cert.pem"
+ path = "webhook-cert.pem"
+ }
+ items {
+ key = "webhook-key.pem"
+ path = "webhook-key.pem"
+ }
+ }
+ }
+
+ }
+ }
+ }
+}
+
+# Service
+resource "kubernetes_service" "svc" {
+ metadata {
+ name = var.deployment_name
+ namespace = local.ns
+ }
+
+ spec {
+ selector = {
+ "app" = var.deployment_name
+ }
+
+ port {
+ name = "https"
+ port = 443
+ protocol = "TCP"
+ target_port = 443
+ }
+
+ type = "ClusterIP"
+ }
+}
+
+# Admission Controller
+resource "kubernetes_mutating_webhook_configuration" "waitforit" {
+ metadata {
+ name = var.deployment_name
+ }
+
+ webhook {
+ name = var.admission_controller_name
+ admission_review_versions = [ "v1", "v1beta1" ]
+
+ #failure_policy = "Ignore" #
+
+ client_config {
+
+ service {
+ name = kubernetes_service.svc.metadata[0].name
+ namespace = local.ns
+ path = "/mutate"
+ port = 443
+ }
+
+ # IMPORTANT: CA_BUNDLE must be Base64-encoded
+ ca_bundle = tls_self_signed_cert.tls.cert_pem
+ }
+
+ rule {
+ api_groups = [ "*" ]
+ api_versions = [ "*" ]
+ operations = [ "CREATE", "UPDATE" ]
+ resources = [ "deployments", "statefulsets" ]
+ }
+
+ side_effects = "None" #
+ }
+
+ depends_on = [
+ kubernetes_deployment.main
+ ]
+}
diff --git a/kubernetes/k8s-admission-controller/src/test/terraform/providers.tf b/kubernetes/k8s-admission-controller/src/test/terraform/providers.tf
new file mode 100644
index 0000000000..eb095caa0e
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/test/terraform/providers.tf
@@ -0,0 +1,14 @@
+terraform {
+ required_providers {
+ kubernetes = {
+ source = "hashicorp/kubernetes"
+ version = "2.3.2"
+ }
+ }
+}
+
+# Use standard kubectl environment to get connection details
+provider "kubernetes" {
+ config_context = var.k8s_config_context
+ config_path = var.k8s_config_path
+}
\ No newline at end of file
diff --git a/kubernetes/k8s-admission-controller/src/test/terraform/variables.tf b/kubernetes/k8s-admission-controller/src/test/terraform/variables.tf
new file mode 100644
index 0000000000..33ca735f0d
--- /dev/null
+++ b/kubernetes/k8s-admission-controller/src/test/terraform/variables.tf
@@ -0,0 +1,50 @@
+variable "namespace" {
+ type = string
+ description = "Namespace where the Admission Controller will be deploymed"
+}
+
+variable "deployment_name" {
+ type = string
+ description = "Admission Controller Deployment Name"
+}
+
+variable "replicas" {
+ type = number
+ description = "Number of replicas used in the deployment"
+ default = 3
+}
+
+variable "image" {
+ type = string
+ description = "Admission Controller image name"
+}
+
+variable "image_version" {
+ type = string
+ description = "Admission Controller image version name"
+ default = "latest"
+}
+
+variable "image_prefix" {
+ type = string
+ description = "Image repository prefix"
+ default = "gcr.io/baeldung"
+}
+
+variable "admission_controller_name" {
+ type = string
+ description = "Admission Controller name"
+ default = "wait-for-it.service.local"
+}
+
+variable "k8s_config_context" {
+ type = string
+ description = "Name of the K8S config context"
+}
+
+variable "k8s_config_path" {
+ type = string
+ description = "Location of the standard K8S configuration"
+ default = "~/.kube/config"
+
+}
\ No newline at end of file
diff --git a/kubernetes/pom.xml b/kubernetes/pom.xml
index 72cb1577a4..57ae4fd596 100644
--- a/kubernetes/pom.xml
+++ b/kubernetes/pom.xml
@@ -1,6 +1,5 @@
-
+
+
4.0.0
kubernetes
pom
@@ -13,5 +12,6 @@
k8s-intro
-
+ k8s-admission-controller
+
\ No newline at end of file