[BAEL-5014] Kubernetes Admission Controller (#11044)

* [BAEL-4849] Article code

* [BAEL-4968] Article code

* [BAEL-4968] Article code

* [BAEL-4968] Article code
This commit is contained in:
psevestre 2021-07-18 12:19:13 -03:00 committed by GitHub
parent 3f18d3ae53
commit d2035e86af
23 changed files with 1295 additions and 4 deletions

View File

@ -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"]

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>./../../parent-boot-2</relativePath>
</parent>
<artifactId>k8s-admission-controller</artifactId>
<name>k8s-admission-controller</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<mainClass>com.baeldung.kubernetes.admission.Application</mainClass>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}

View File

@ -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";
}

View File

@ -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<AdmissionReviewResponse> processAdmissionReviewRequest(@RequestBody Mono<ObjectNode> request) {
return request.map((body) -> admissionService.processAdmission(body));
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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": {}
}
}
}

View File

@ -0,0 +1,3 @@
.terraform
terraform.tfstate
terraform.tfstate.backup

View File

@ -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",
]
}

View File

@ -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"

View File

@ -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
]
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -1,6 +1,5 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>kubernetes</artifactId>
<packaging>pom</packaging>
@ -13,5 +12,6 @@
<modules>
<module>k8s-intro</module>
</modules>
<module>k8s-admission-controller</module>
</modules>
</project>