├── contributing └── eclipse.importorder ├── samples ├── spring-boot │ ├── .mvn │ │ └── wrapper │ │ │ ├── maven-wrapper.jar │ │ │ ├── maven-wrapper.properties │ │ │ └── MavenWrapperDownloader.java │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ ├── kubernetes │ │ │ │ │ └── common.yaml │ │ │ │ └── application.properties │ │ │ └── java │ │ │ │ └── io │ │ │ │ └── javaoperatorsdk │ │ │ │ └── webhook │ │ │ │ └── sample │ │ │ │ └── springboot │ │ │ │ ├── SpringBootSampleApplication.java │ │ │ │ ├── conversion │ │ │ │ ├── ConversionConfig.java │ │ │ │ └── ConversionEndpoint.java │ │ │ │ └── admission │ │ │ │ ├── AdmissionConfig.java │ │ │ │ └── AdmissionEndpoint.java │ │ └── test │ │ │ └── java │ │ │ └── io │ │ │ └── javaoperatorsdk │ │ │ └── webhook │ │ │ └── sample │ │ │ └── springboot │ │ │ ├── admission │ │ │ ├── AdditionalAdmissionConfig.java │ │ │ ├── AdmissionAdditionalTestEndpoint.java │ │ │ └── AdmissionEndpointTest.java │ │ │ ├── SpringBootWebhooksE2E.java │ │ │ └── conversion │ │ │ └── ConversionEndpointTest.java │ ├── k8s │ │ ├── create-pod-with-missing-label-example.yml │ │ ├── mutating-webhook-configuration.yml │ │ ├── validating-webhook-configuration.yml │ │ └── kubernetes.yml │ ├── .gitignore │ └── pom.xml ├── quarkus │ ├── src │ │ ├── main │ │ │ ├── kubernetes │ │ │ │ └── common.yml │ │ │ ├── docker │ │ │ │ ├── Dockerfile.native-distroless │ │ │ │ ├── Dockerfile.native │ │ │ │ ├── Dockerfile.legacy-jar │ │ │ │ └── Dockerfile.jvm │ │ │ ├── java │ │ │ │ └── io │ │ │ │ │ └── javaoperatorsdk │ │ │ │ │ └── webhook │ │ │ │ │ └── sample │ │ │ │ │ ├── conversion │ │ │ │ │ ├── ConversionControllerConfig.java │ │ │ │ │ └── ConversionEndpoint.java │ │ │ │ │ └── admission │ │ │ │ │ ├── AdmissionControllerConfig.java │ │ │ │ │ └── AdmissionEndpoint.java │ │ │ └── resources │ │ │ │ ├── application.properties │ │ │ │ └── META-INF │ │ │ │ └── resources │ │ │ │ └── index.html │ │ └── test │ │ │ └── java │ │ │ └── io │ │ │ └── javaoperatorsdk │ │ │ └── webhook │ │ │ └── sample │ │ │ ├── conversion │ │ │ ├── NativeConversionEndpointIT.java │ │ │ └── ConversionEndpointTest.java │ │ │ ├── admission │ │ │ ├── NativeAdmissionEndpointIT.java │ │ │ ├── AdditionalAdmissionConfig.java │ │ │ ├── AdmissionAdditionalTestEndpoint.java │ │ │ └── AdmissionEndpointTest.java │ │ │ └── QuarkusWebhooksE2E.java │ ├── .mvn │ │ └── wrapper │ │ │ ├── maven-wrapper.properties │ │ │ └── MavenWrapperDownloader.java │ ├── k8s │ │ ├── mutating-webhook-configuration.yml │ │ └── validating-webhook-configuration.yml │ └── README.md ├── commons │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ ├── META-INF │ │ │ │ │ └── services │ │ │ │ │ │ └── io.fabric8.kubernetes.api.model.KubernetesResource │ │ │ │ ├── conversion-error-request.json │ │ │ │ ├── conversion-request.json │ │ │ │ └── admission-request.json │ │ │ └── java │ │ │ │ └── io │ │ │ │ └── javaoperatorsdk │ │ │ │ └── webhook │ │ │ │ └── sample │ │ │ │ └── commons │ │ │ │ ├── customresource │ │ │ │ ├── MultiVersionCustomResourceSpec.java │ │ │ │ ├── MultiVersionCustomResourceSpecV2.java │ │ │ │ ├── MultiVersionCustomResourceV2.java │ │ │ │ └── MultiVersionCustomResource.java │ │ │ │ ├── mapper │ │ │ │ ├── MultiVersionHub.java │ │ │ │ ├── AsyncV1Mapper.java │ │ │ │ ├── AsyncV2Mapper.java │ │ │ │ ├── V1Mapper.java │ │ │ │ └── V2Mapper.java │ │ │ │ ├── ConversionControllers.java │ │ │ │ ├── AdmissionControllers.java │ │ │ │ └── Utils.java │ │ └── test │ │ │ └── java │ │ │ └── io │ │ │ └── javaoperatorsdk │ │ │ └── webhook │ │ │ └── sample │ │ │ └── AbstractEndToEndTest.java │ └── pom.xml ├── pom.xml └── manual-test.http ├── core ├── src │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── javaoperatorsdk │ │ │ └── webhook │ │ │ ├── admission │ │ │ ├── Operation.java │ │ │ ├── AdmissionRequestHandler.java │ │ │ ├── AsyncAdmissionRequestHandler.java │ │ │ ├── validation │ │ │ │ ├── Validator.java │ │ │ │ ├── DefaultAdmissionRequestValidator.java │ │ │ │ └── AsyncDefaultAdmissionRequestValidator.java │ │ │ ├── mutation │ │ │ │ ├── AsyncMutator.java │ │ │ │ ├── Mutator.java │ │ │ │ ├── DefaultAdmissionRequestMutator.java │ │ │ │ └── AsyncDefaultAdmissionRequestMutator.java │ │ │ ├── AdmissionControllerException.java │ │ │ ├── AdmissionController.java │ │ │ ├── AsyncAdmissionController.java │ │ │ ├── NotAllowedException.java │ │ │ └── AdmissionUtils.java │ │ │ ├── conversion │ │ │ ├── Mapper.java │ │ │ ├── ConversionRequestHandler.java │ │ │ ├── MissingConversionMapperException.java │ │ │ ├── AsyncMapper.java │ │ │ ├── AsyncConversionRequestHandler.java │ │ │ ├── Utils.java │ │ │ ├── ConversionException.java │ │ │ ├── TargetVersion.java │ │ │ ├── Commons.java │ │ │ ├── ConversionController.java │ │ │ └── AsyncConversionController.java │ │ │ └── clone │ │ │ ├── Cloner.java │ │ │ └── ObjectMapperCloner.java │ └── test │ │ ├── java │ │ └── io │ │ │ └── javaoperatorsdk │ │ │ └── webhook │ │ │ ├── conversion │ │ │ ├── crd │ │ │ │ ├── CustomResourceV1Spec.java │ │ │ │ ├── CustomResourceV1Status.java │ │ │ │ ├── CustomResourceV2Status.java │ │ │ │ ├── CustomResourceV3Status.java │ │ │ │ ├── CustomResourceV2Spec.java │ │ │ │ ├── CustomResourceV1.java │ │ │ │ ├── CustomResourceV2.java │ │ │ │ ├── CustomResourceV3.java │ │ │ │ └── CustomResourceV3Spec.java │ │ │ ├── UtilsTest.java │ │ │ ├── mapper │ │ │ │ ├── CustomResourceV3Mapper.java │ │ │ │ ├── AsyncV3Mapper.java │ │ │ │ ├── AsyncV1Mapper.java │ │ │ │ ├── AsyncV2Mapper.java │ │ │ │ ├── CustomResourceV1Mapper.java │ │ │ │ └── CustomResourceV2Mapper.java │ │ │ ├── ConversionControllerTest.java │ │ │ ├── AsyncConversionControllerTest.java │ │ │ └── ConversionTestSupport.java │ │ │ └── admission │ │ │ ├── AdmissionControllerTest.java │ │ │ ├── AdmissionTestSupport.java │ │ │ └── AsyncAdmissionControllerTest.java │ │ └── resources │ │ └── io │ │ └── javaoperatorsdk │ │ └── webhook │ │ └── admission │ │ └── deployment.yaml └── pom.xml ├── .mvn └── wrapper │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── master-snapshot-release.yml │ ├── sonar.yml │ ├── release.yml │ └── pr.yml ├── README.md └── CONTRIBUTING.md /contributing/eclipse.importorder: -------------------------------------------------------------------------------- 1 | 0=java 2 | 1=javax 3 | 2=org 4 | 2=io 5 | 4=com 6 | 5= 7 | 6=\# 8 | -------------------------------------------------------------------------------- /samples/spring-boot/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operator-framework/josdk-webhooks/HEAD/samples/spring-boot/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /samples/quarkus/src/main/kubernetes/common.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: pkcs12-pass 6 | data: 7 | password: c3VwZXJzZWNyZXQ= 8 | type: Opaque -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/Operation.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission; 2 | 3 | public enum Operation { 4 | CREATE, UPDATE, DELETE, CONNECT 5 | } 6 | -------------------------------------------------------------------------------- /samples/spring-boot/src/main/resources/kubernetes/common.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: pkcs12-pass 6 | data: 7 | password: c3VwZXJzZWNyZXQ= 8 | type: Opaque -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.2/apache-maven-3.8.2-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /samples/spring-boot/k8s/create-pod-with-missing-label-example.yml: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /samples/commons/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource: -------------------------------------------------------------------------------- 1 | io.javaoperatorsdk.webhook.sample.commons.customresource.MultiVersionCustomResource 2 | io.javaoperatorsdk.webhook.sample.commons.customresource.MultiVersionCustomResourceV2 -------------------------------------------------------------------------------- /samples/quarkus/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /samples/spring-boot/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/conversion/Mapper.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | import io.fabric8.kubernetes.api.model.HasMetadata; 4 | 5 | public interface Mapper { 6 | 7 | HUB toHub(R resource); 8 | 9 | R fromHub(HUB hub); 10 | } 11 | -------------------------------------------------------------------------------- /samples/quarkus/src/test/java/io/javaoperatorsdk/webhook/sample/conversion/NativeConversionEndpointIT.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.conversion; 2 | 3 | import io.quarkus.test.junit.QuarkusIntegrationTest; 4 | 5 | @QuarkusIntegrationTest 6 | public class NativeConversionEndpointIT extends ConversionEndpointTest { 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/conversion/ConversionRequestHandler.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionReview; 4 | 5 | public interface ConversionRequestHandler { 6 | 7 | ConversionReview handle(ConversionReview admissionRequest); 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/conversion/MissingConversionMapperException.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | public class MissingConversionMapperException extends ConversionException { 4 | 5 | public MissingConversionMapperException() {} 6 | 7 | public MissingConversionMapperException(String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/quarkus/src/test/java/io/javaoperatorsdk/webhook/sample/admission/NativeAdmissionEndpointIT.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.admission; 2 | 3 | import io.quarkus.test.junit.QuarkusIntegrationTest; 4 | 5 | @QuarkusIntegrationTest 6 | public class NativeAdmissionEndpointIT extends AdmissionEndpointTest { 7 | 8 | // Execute the same tests but in native mode. 9 | } 10 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/crd/CustomResourceV1Spec.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.crd; 2 | 3 | public class CustomResourceV1Spec { 4 | 5 | private int value; 6 | 7 | public int getValue() { 8 | return value; 9 | } 10 | 11 | public CustomResourceV1Spec setValue(int value) { 12 | this.value = value; 13 | return this; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/AdmissionRequestHandler.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission; 2 | 3 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest; 4 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponse; 5 | 6 | public interface AdmissionRequestHandler { 7 | 8 | AdmissionResponse handle(AdmissionRequest admissionRequest); 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/conversion/AsyncMapper.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | import java.util.concurrent.CompletionStage; 4 | 5 | import io.fabric8.kubernetes.api.model.HasMetadata; 6 | 7 | public interface AsyncMapper { 8 | 9 | CompletionStage toHub(R resource); 10 | 11 | CompletionStage fromHub(HUB hub); 12 | } 13 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/crd/CustomResourceV1Status.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.crd; 2 | 3 | public class CustomResourceV1Status { 4 | 5 | private int value1; 6 | 7 | public int getValue1() { 8 | return value1; 9 | } 10 | 11 | public CustomResourceV1Status setValue1(int value1) { 12 | this.value1 = value1; 13 | return this; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/crd/CustomResourceV2Status.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.crd; 2 | 3 | public class CustomResourceV2Status { 4 | 5 | private int value1; 6 | 7 | public int getValue1() { 8 | return value1; 9 | } 10 | 11 | public CustomResourceV2Status setValue1(int value1) { 12 | this.value1 = value1; 13 | return this; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/crd/CustomResourceV3Status.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.crd; 2 | 3 | public class CustomResourceV3Status { 4 | 5 | private int value1; 6 | 7 | public int getValue1() { 8 | return value1; 9 | } 10 | 11 | public CustomResourceV3Status setValue1(int value1) { 12 | this.value1 = value1; 13 | return this; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/conversion/AsyncConversionRequestHandler.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | import java.util.concurrent.CompletionStage; 4 | 5 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionReview; 6 | 7 | public interface AsyncConversionRequestHandler { 8 | 9 | CompletionStage handle(ConversionReview conversionReview); 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/clone/Cloner.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.clone; 2 | 3 | public interface Cloner { 4 | 5 | /** 6 | * Returns a deep copy of the given object if not {@code null} or {@code null} otherwise. 7 | * 8 | * @param object the object to be cloned 9 | * @return a deep copy of the given object if it isn't {@code null}, {@code null} otherwise 10 | */ 11 | R clone(R object); 12 | } 13 | -------------------------------------------------------------------------------- /samples/commons/src/main/java/io/javaoperatorsdk/webhook/sample/commons/customresource/MultiVersionCustomResourceSpec.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.commons.customresource; 2 | 3 | public class MultiVersionCustomResourceSpec { 4 | 5 | private int value; 6 | 7 | public int getValue() { 8 | return value; 9 | } 10 | 11 | public MultiVersionCustomResourceSpec setValue(int value) { 12 | this.value = value; 13 | return this; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | kubernetes-webhooks-framework.iml 26 | .idea 27 | target 28 | .cache 29 | 30 | *.iml -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/AsyncAdmissionRequestHandler.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission; 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 AsyncAdmissionRequestHandler { 9 | 10 | CompletionStage handle(AdmissionRequest admissionRequest); 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/validation/Validator.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission.validation; 2 | 3 | import io.fabric8.kubernetes.api.model.KubernetesResource; 4 | import io.javaoperatorsdk.webhook.admission.NotAllowedException; 5 | import io.javaoperatorsdk.webhook.admission.Operation; 6 | 7 | public interface Validator { 8 | 9 | void validate(T resource, T oldResource, Operation operation) throws NotAllowedException; 10 | } 11 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/UtilsTest.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class UtilsTest { 8 | 9 | @Test 10 | void getsVersionFromApiVersion() { 11 | assertThat(Utils.versionOfApiVersion("apiextensions.k8s.io/v1")).isEqualTo("v1"); 12 | assertThat(Utils.versionOfApiVersion("extensions/v1beta1")).isEqualTo("v1beta1"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/conversion/Utils.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | public class Utils { 4 | 5 | private Utils() {} 6 | 7 | /** 8 | * @param apiVersion like "apiextensions.k8s.io/v1" 9 | * @return version suffix; "v1" from the example above 10 | */ 11 | public static String versionOfApiVersion(String apiVersion) { 12 | var lastDelimiter = apiVersion.lastIndexOf("/"); 13 | return apiVersion.substring(lastDelimiter + 1); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/test/resources/io/javaoperatorsdk/webhook/admission/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | labels: 6 | app: nginx 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: nginx 12 | template: 13 | metadata: 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx 19 | image: nginx:1.14.2 20 | ports: 21 | - containerPort: 80 22 | -------------------------------------------------------------------------------- /samples/spring-boot/src/main/java/io/javaoperatorsdk/webhook/sample/springboot/SpringBootSampleApplication.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.springboot; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SpringBootSampleApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(SpringBootSampleApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /samples/commons/src/main/java/io/javaoperatorsdk/webhook/sample/commons/customresource/MultiVersionCustomResourceSpecV2.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.commons.customresource; 2 | 3 | public class MultiVersionCustomResourceSpecV2 { 4 | 5 | private String alteredValue; 6 | 7 | public String getAlteredValue() { 8 | return alteredValue; 9 | } 10 | 11 | public MultiVersionCustomResourceSpecV2 setAlteredValue(String alteredValue) { 12 | this.alteredValue = alteredValue; 13 | return this; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/mutation/AsyncMutator.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission.mutation; 2 | 3 | import java.util.concurrent.CompletionStage; 4 | 5 | import io.fabric8.kubernetes.api.model.KubernetesResource; 6 | import io.javaoperatorsdk.webhook.admission.NotAllowedException; 7 | import io.javaoperatorsdk.webhook.admission.Operation; 8 | 9 | public interface AsyncMutator { 10 | 11 | CompletionStage mutate(T resource, Operation operation) throws NotAllowedException; 12 | } 13 | -------------------------------------------------------------------------------- /samples/spring-boot/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/mutation/Mutator.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission.mutation; 2 | 3 | import io.fabric8.kubernetes.api.model.KubernetesResource; 4 | import io.javaoperatorsdk.webhook.admission.NotAllowedException; 5 | import io.javaoperatorsdk.webhook.admission.Operation; 6 | 7 | /** 8 | * Any change made on the resource will be reflected in the response. 9 | * 10 | * @param type of Kubernetes resources 11 | */ 12 | public interface Mutator { 13 | 14 | T mutate(T resource, Operation operation) throws NotAllowedException; 15 | } 16 | -------------------------------------------------------------------------------- /samples/commons/src/main/java/io/javaoperatorsdk/webhook/sample/commons/mapper/MultiVersionHub.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.commons.mapper; 2 | 3 | import io.fabric8.kubernetes.api.model.ObjectMeta; 4 | 5 | public class MultiVersionHub { 6 | 7 | private ObjectMeta metadata = new ObjectMeta(); 8 | 9 | private int value; 10 | 11 | public ObjectMeta getMetadata() { 12 | return metadata; 13 | } 14 | 15 | public void setMetadata(ObjectMeta metadata) { 16 | this.metadata = metadata; 17 | } 18 | 19 | public int getValue() { 20 | return value; 21 | } 22 | 23 | public void setValue(int value) { 24 | this.value = value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/mapper/CustomResourceV3Mapper.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.mapper; 2 | 3 | import io.javaoperatorsdk.webhook.conversion.Mapper; 4 | import io.javaoperatorsdk.webhook.conversion.TargetVersion; 5 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV3; 6 | 7 | @TargetVersion("v3") 8 | public class CustomResourceV3Mapper implements Mapper { 9 | 10 | @Override 11 | public CustomResourceV3 toHub(CustomResourceV3 resource) { 12 | return resource; 13 | } 14 | 15 | @Override 16 | public CustomResourceV3 fromHub(CustomResourceV3 hub) { 17 | return hub; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/crd/CustomResourceV2Spec.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.crd; 2 | 3 | public class CustomResourceV2Spec { 4 | 5 | private String value; 6 | 7 | private String additionalValue; 8 | 9 | public String getValue() { 10 | return value; 11 | } 12 | 13 | public CustomResourceV2Spec setValue(String value) { 14 | this.value = value; 15 | return this; 16 | } 17 | 18 | public String getAdditionalValue() { 19 | return additionalValue; 20 | } 21 | 22 | public CustomResourceV2Spec setAdditionalValue(String additionalValue) { 23 | this.additionalValue = additionalValue; 24 | return this; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/commons/src/main/java/io/javaoperatorsdk/webhook/sample/commons/customresource/MultiVersionCustomResourceV2.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.commons.customresource; 2 | 3 | import io.fabric8.kubernetes.client.CustomResource; 4 | import io.fabric8.kubernetes.model.annotation.Group; 5 | import io.fabric8.kubernetes.model.annotation.Kind; 6 | import io.fabric8.kubernetes.model.annotation.ShortNames; 7 | import io.fabric8.kubernetes.model.annotation.Version; 8 | 9 | @Group("sample.javaoperatorsdk") 10 | @Version(value = "v2") 11 | @Kind("MultiVersionCustomResource") 12 | @ShortNames("tcr") 13 | public class MultiVersionCustomResourceV2 14 | extends CustomResource { 15 | } 16 | -------------------------------------------------------------------------------- /samples/quarkus/src/main/docker/Dockerfile.native-distroless: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a distroless container that runs the Quarkus application in native (no JVM) mode 3 | # 4 | # Before building the container image run: 5 | # 6 | # ./mvnw package -Pnative 7 | # 8 | # Then, build the image with: 9 | # 10 | # docker build -f src/main/docker/Dockerfile.native-distroless -t quarkus/quarkus-sample . 11 | # 12 | # Then run the container using: 13 | # 14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-sample 15 | # 16 | ### 17 | FROM quay.io/quarkus/quarkus-distroless-image:1.0 18 | COPY target/*-runner /application 19 | 20 | EXPOSE 8080 21 | USER nonroot 22 | 23 | CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] 24 | -------------------------------------------------------------------------------- /samples/commons/src/main/java/io/javaoperatorsdk/webhook/sample/commons/customresource/MultiVersionCustomResource.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.commons.customresource; 2 | 3 | import io.fabric8.kubernetes.client.CustomResource; 4 | import io.fabric8.kubernetes.model.annotation.Group; 5 | import io.fabric8.kubernetes.model.annotation.Kind; 6 | import io.fabric8.kubernetes.model.annotation.ShortNames; 7 | import io.fabric8.kubernetes.model.annotation.Version; 8 | 9 | @Group("sample.javaoperatorsdk") 10 | @Version(value = "v1", storage = false) 11 | @Kind("MultiVersionCustomResource") 12 | @ShortNames("tcr") 13 | public class MultiVersionCustomResource 14 | extends CustomResource { 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/conversion/ConversionException.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | public class ConversionException extends RuntimeException { 4 | 5 | public ConversionException() {} 6 | 7 | public ConversionException(String message) { 8 | super(message); 9 | } 10 | 11 | public ConversionException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public ConversionException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public ConversionException(String message, Throwable cause, boolean enableSuppression, 20 | boolean writableStackTrace) { 21 | super(message, cause, enableSuppression, writableStackTrace); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/conversion/TargetVersion.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target({ElementType.TYPE}) 10 | public @interface TargetVersion { 11 | 12 | /** 13 | * The target version of the resource this mapper supports. Example values: "v1","v1beta1". This 14 | * is not the full API Version just the version suffix, for example only the "v1" of api version: 15 | * "apiextensions.k8s.io/v1" 16 | * 17 | * @return version 18 | **/ 19 | String value(); 20 | } 21 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/crd/CustomResourceV1.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.crd; 2 | 3 | import io.fabric8.kubernetes.api.model.Namespaced; 4 | import io.fabric8.kubernetes.client.CustomResource; 5 | import io.fabric8.kubernetes.model.annotation.Group; 6 | import io.fabric8.kubernetes.model.annotation.Kind; 7 | import io.fabric8.kubernetes.model.annotation.ShortNames; 8 | import io.fabric8.kubernetes.model.annotation.Version; 9 | 10 | @Group("sample.javaoperatorsdk") 11 | @Version("v1") 12 | @Kind("MultiVersionTestCustomResource") 13 | @ShortNames("mv1") 14 | public class CustomResourceV1 15 | extends 16 | CustomResource 17 | implements Namespaced { 18 | } 19 | -------------------------------------------------------------------------------- /samples/spring-boot/k8s/mutating-webhook-configuration.yml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: "mutating.spring-boot.example.com" 5 | annotations: 6 | cert-manager.io/inject-ca-from: default/spring-boot-sample 7 | webhooks: 8 | - name: "mutating.spring-boot.example.com" 9 | rules: 10 | - apiGroups: ["networking.k8s.io"] 11 | apiVersions: ["v1"] 12 | operations: ["*"] 13 | resources: ["ingresses"] 14 | scope: "Namespaced" 15 | clientConfig: 16 | service: 17 | namespace: "default" 18 | name: "spring-boot-sample" 19 | path: "/mutate" 20 | admissionReviewVersions: ["v1"] 21 | sideEffects: None 22 | timeoutSeconds: 5 -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/clone/ObjectMapperCloner.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.clone; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | 6 | public class ObjectMapperCloner implements Cloner { 7 | 8 | private final ObjectMapper objectMapper = new ObjectMapper(); 9 | 10 | @Override 11 | @SuppressWarnings("unchecked") 12 | public T clone(T object) { 13 | if (object == null) { 14 | return null; 15 | } 16 | try { 17 | return (T) objectMapper.readValue(objectMapper.writeValueAsString(object), 18 | object.getClass()); 19 | } catch (JsonProcessingException e) { 20 | throw new IllegalStateException(e); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/quarkus/k8s/mutating-webhook-configuration.yml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: "mutating.quarkus.example.com" 5 | annotations: 6 | cert-manager.io/inject-ca-from: default/quarkus-sample 7 | webhooks: 8 | - name: "mutating.quarkus.example.com" 9 | rules: 10 | - apiGroups: ["networking.k8s.io"] 11 | apiVersions: ["v1"] 12 | operations: ["*"] 13 | resources: ["ingresses"] 14 | scope: "Namespaced" 15 | clientConfig: 16 | service: 17 | namespace: "default" 18 | name: "quarkus-sample" 19 | path: "/mutate" 20 | port: 443 21 | admissionReviewVersions: ["v1"] 22 | sideEffects: None 23 | timeoutSeconds: 5 -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/crd/CustomResourceV2.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.crd; 2 | 3 | import io.fabric8.kubernetes.api.model.Namespaced; 4 | import io.fabric8.kubernetes.client.CustomResource; 5 | import io.fabric8.kubernetes.model.annotation.Group; 6 | import io.fabric8.kubernetes.model.annotation.Kind; 7 | import io.fabric8.kubernetes.model.annotation.ShortNames; 8 | import io.fabric8.kubernetes.model.annotation.Version; 9 | 10 | @Group("sample.javaoperatorsdk") 11 | @Version(value = "v2", storage = false) 12 | @Kind("MultiVersionTestCustomResource") 13 | @ShortNames("mv2") 14 | public class CustomResourceV2 15 | extends 16 | CustomResource 17 | implements Namespaced { 18 | } 19 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/crd/CustomResourceV3.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.crd; 2 | 3 | import io.fabric8.kubernetes.api.model.Namespaced; 4 | import io.fabric8.kubernetes.client.CustomResource; 5 | import io.fabric8.kubernetes.model.annotation.Group; 6 | import io.fabric8.kubernetes.model.annotation.Kind; 7 | import io.fabric8.kubernetes.model.annotation.ShortNames; 8 | import io.fabric8.kubernetes.model.annotation.Version; 9 | 10 | @Group("sample.javaoperatorsdk") 11 | @Version(value = "v3", storage = false) 12 | @Kind("MultiVersionTestCustomResource") 13 | @ShortNames("mv3") 14 | public class CustomResourceV3 15 | extends 16 | CustomResource 17 | implements Namespaced { 18 | } 19 | -------------------------------------------------------------------------------- /samples/spring-boot/k8s/validating-webhook-configuration.yml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: ValidatingWebhookConfiguration 3 | metadata: 4 | name: "validating.spring-boot.example.com" 5 | annotations: 6 | cert-manager.io/inject-ca-from: default/spring-boot-sample 7 | webhooks: 8 | - name: "validating.spring-boot.example.com" 9 | rules: 10 | - apiGroups: ["networking.k8s.io"] 11 | apiVersions: ["v1"] 12 | operations: ["*"] 13 | resources: ["ingresses"] 14 | scope: "Namespaced" 15 | clientConfig: 16 | service: 17 | namespace: "default" 18 | name: "spring-boot-sample" 19 | path: "/validate" 20 | admissionReviewVersions: ["v1"] 21 | sideEffects: None 22 | timeoutSeconds: 5 -------------------------------------------------------------------------------- /samples/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.javaoperatorsdk 6 | kubernetes-webhooks-framework 7 | 3.0.2-SNAPSHOT 8 | 9 | kubernetes-webhooks-framework-samples 10 | pom 11 | Kubernetes Webhooks Framework - Samples 12 | 13 | commons 14 | spring-boot 15 | quarkus 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/quarkus/k8s/validating-webhook-configuration.yml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: ValidatingWebhookConfiguration 3 | metadata: 4 | name: "validating.quarkus.example.com" 5 | annotations: 6 | cert-manager.io/inject-ca-from: default/quarkus-sample 7 | webhooks: 8 | - name: "validating.quarkus.example.com" 9 | rules: 10 | - apiGroups: ["networking.k8s.io"] 11 | apiVersions: ["v1"] 12 | operations: ["*"] 13 | resources: ["ingresses"] 14 | scope: "Namespaced" 15 | clientConfig: 16 | service: 17 | namespace: "default" 18 | name: "quarkus-sample" 19 | path: "/validate" 20 | port: 443 21 | admissionReviewVersions: ["v1"] 22 | sideEffects: None 23 | timeoutSeconds: 5 -------------------------------------------------------------------------------- /samples/quarkus/src/main/java/io/javaoperatorsdk/webhook/sample/conversion/ConversionControllerConfig.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.conversion; 2 | 3 | import io.javaoperatorsdk.webhook.conversion.AsyncConversionController; 4 | import io.javaoperatorsdk.webhook.conversion.ConversionController; 5 | import io.javaoperatorsdk.webhook.sample.commons.ConversionControllers; 6 | 7 | import jakarta.inject.Singleton; 8 | 9 | public class ConversionControllerConfig { 10 | 11 | @Singleton 12 | public ConversionController conversionController() { 13 | return ConversionControllers.conversionController(); 14 | } 15 | 16 | @Singleton 17 | public AsyncConversionController asyncConversionController() { 18 | return ConversionControllers.asyncConversionController(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/AdmissionControllerException.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission; 2 | 3 | public class AdmissionControllerException extends RuntimeException { 4 | 5 | public AdmissionControllerException() {} 6 | 7 | public AdmissionControllerException(String message) { 8 | super(message); 9 | } 10 | 11 | public AdmissionControllerException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public AdmissionControllerException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public AdmissionControllerException(String message, Throwable cause, boolean enableSuppression, 20 | boolean writableStackTrace) { 21 | super(message, cause, enableSuppression, writableStackTrace); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | ignore: 13 | # Ignore slf4j-api major version bump because 1.x and 2.x are not compatible 14 | - dependency-name: "slf4j-api" 15 | update-types: ["version-update:semver-major"] 16 | 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "daily" 21 | -------------------------------------------------------------------------------- /samples/commons/src/main/resources/conversion-error-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "apiextensions.k8s.io/v1", 3 | "kind": "ConversionReview", 4 | "request": { 5 | 6 | "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", 7 | 8 | "desiredAPIVersion": "sample.javaoperatorsdk/v2", 9 | 10 | "objects": [ 11 | { 12 | "kind": "MultiVersionCustomResource", 13 | "apiVersion": "sample.javaoperatorsdk/v2", 14 | "metadata": { 15 | "creationTimestamp": "2021-09-04T14:03:02Z", 16 | "name": "resource1", 17 | "namespace": "default", 18 | "resourceVersion": "143", 19 | "uid": "3415a7fc-162b-4300-b5da-fd6083580d66" 20 | }, 21 | "spec": { 22 | "value": "non integer" 23 | }, 24 | "status":{ 25 | "ready": true 26 | } 27 | } 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /samples/quarkus/src/main/docker/Dockerfile.native: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode 3 | # 4 | # Before building the container image run: 5 | # 6 | # ./mvnw package -Pnative 7 | # 8 | # Then, build the image with: 9 | # 10 | # docker build -f src/main/docker/Dockerfile.native -t quarkus/quarkus-sample . 11 | # 12 | # Then run the container using: 13 | # 14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-sample 15 | # 16 | ### 17 | FROM quay.io/quarkus/quarkus-micro-image:1.0 18 | WORKDIR /work/ 19 | RUN chown 1001 /work \ 20 | && chmod "g+rwX" /work \ 21 | && chown 1001:root /work 22 | COPY --chown=1001:root target/*-runner /work/application 23 | 24 | EXPOSE 8080 25 | USER 1001 26 | 27 | CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] 28 | -------------------------------------------------------------------------------- /samples/spring-boot/src/main/java/io/javaoperatorsdk/webhook/sample/springboot/conversion/ConversionConfig.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.springboot.conversion; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | import io.javaoperatorsdk.webhook.conversion.AsyncConversionController; 7 | import io.javaoperatorsdk.webhook.conversion.ConversionController; 8 | import io.javaoperatorsdk.webhook.sample.commons.ConversionControllers; 9 | 10 | @Configuration 11 | public class ConversionConfig { 12 | 13 | @Bean 14 | public ConversionController conversionController() { 15 | return ConversionControllers.conversionController(); 16 | } 17 | 18 | @Bean 19 | public AsyncConversionController asyncConversionController() { 20 | return ConversionControllers.asyncConversionController(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/crd/CustomResourceV3Spec.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.crd; 2 | 3 | public class CustomResourceV3Spec { 4 | 5 | private String value; 6 | private String additionalValue; 7 | private String thirdValue; 8 | 9 | public String getValue() { 10 | return value; 11 | } 12 | 13 | public CustomResourceV3Spec setValue(String value) { 14 | this.value = value; 15 | return this; 16 | } 17 | 18 | public String getAdditionalValue() { 19 | return additionalValue; 20 | } 21 | 22 | public CustomResourceV3Spec setAdditionalValue(String additionalValue) { 23 | this.additionalValue = additionalValue; 24 | return this; 25 | } 26 | 27 | public String getThirdValue() { 28 | return thirdValue; 29 | } 30 | 31 | public CustomResourceV3Spec setThirdValue(String thirdValue) { 32 | this.thirdValue = thirdValue; 33 | return this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /samples/quarkus/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #quarkus.http.insecure-requests=disabled 2 | quarkus.http.port=80 3 | quarkus.http.ssl-port=443 4 | 5 | quarkus.kubernetes.image-pull-policy=IfNotPresent 6 | 7 | ## To generate the Certificate and the Issuer resources 8 | quarkus.certificate.secret-name=tls-secret 9 | quarkus.certificate.dns-names=quarkus-sample.default.svc,localhost 10 | quarkus.certificate.self-signed.enabled=true 11 | quarkus.certificate.subject.organizations=Dekorate,Community 12 | quarkus.certificate.duration=2160h0m0s 13 | quarkus.certificate.renew-before=360h0m0s 14 | quarkus.certificate.private-key.algorithm=RSA 15 | quarkus.certificate.private-key.encoding=PKCS8 16 | quarkus.certificate.private-key.size=2048 17 | quarkus.certificate.keystores.pkcs12.create=true 18 | quarkus.certificate.keystores.pkcs12.password-secret-ref.name=pkcs12-pass 19 | quarkus.certificate.keystores.pkcs12.password-secret-ref.key=password 20 | quarkus.certificate.usages=server auth,client auth 21 | 22 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/mapper/AsyncV3Mapper.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.mapper; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.concurrent.CompletionStage; 5 | 6 | import io.javaoperatorsdk.webhook.conversion.AsyncMapper; 7 | import io.javaoperatorsdk.webhook.conversion.TargetVersion; 8 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV3; 9 | 10 | @TargetVersion("v3") 11 | public class AsyncV3Mapper implements AsyncMapper { 12 | 13 | CustomResourceV3Mapper mapper = new CustomResourceV3Mapper(); 14 | 15 | @Override 16 | public CompletionStage toHub(CustomResourceV3 resource) { 17 | return CompletableFuture.completedStage(mapper.toHub(resource)); 18 | } 19 | 20 | @Override 21 | public CompletionStage fromHub(CustomResourceV3 customResourceV3) { 22 | return CompletableFuture.completedStage(mapper.fromHub(customResourceV3)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/commons/src/main/java/io/javaoperatorsdk/webhook/sample/commons/mapper/AsyncV1Mapper.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.commons.mapper; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.concurrent.CompletionStage; 5 | 6 | import io.javaoperatorsdk.webhook.conversion.AsyncMapper; 7 | import io.javaoperatorsdk.webhook.conversion.TargetVersion; 8 | import io.javaoperatorsdk.webhook.sample.commons.customresource.MultiVersionCustomResource; 9 | 10 | @TargetVersion("v1") 11 | public class AsyncV1Mapper implements AsyncMapper { 12 | 13 | private final V1Mapper mapper = new V1Mapper(); 14 | 15 | @Override 16 | public CompletionStage toHub(MultiVersionCustomResource resource) { 17 | return CompletableFuture.completedStage(mapper.toHub(resource)); 18 | } 19 | 20 | @Override 21 | public CompletionStage fromHub(MultiVersionHub hub) { 22 | return CompletableFuture.completedStage(mapper.fromHub(hub)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/commons/src/main/java/io/javaoperatorsdk/webhook/sample/commons/mapper/AsyncV2Mapper.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.commons.mapper; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.concurrent.CompletionStage; 5 | 6 | import io.javaoperatorsdk.webhook.conversion.AsyncMapper; 7 | import io.javaoperatorsdk.webhook.conversion.TargetVersion; 8 | import io.javaoperatorsdk.webhook.sample.commons.customresource.MultiVersionCustomResourceV2; 9 | 10 | @TargetVersion("v2") 11 | public class AsyncV2Mapper implements AsyncMapper { 12 | 13 | private final V2Mapper mapper = new V2Mapper(); 14 | 15 | @Override 16 | public CompletionStage toHub(MultiVersionCustomResourceV2 resource) { 17 | return CompletableFuture.completedStage(mapper.toHub(resource)); 18 | } 19 | 20 | @Override 21 | public CompletionStage fromHub(MultiVersionHub multiVersionHub) { 22 | return CompletableFuture.completedStage(mapper.fromHub(multiVersionHub)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/mapper/AsyncV1Mapper.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.mapper; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.concurrent.CompletionStage; 5 | 6 | import io.javaoperatorsdk.webhook.conversion.AsyncMapper; 7 | import io.javaoperatorsdk.webhook.conversion.TargetVersion; 8 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV1; 9 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV3; 10 | 11 | @TargetVersion("v1") 12 | public class AsyncV1Mapper implements AsyncMapper { 13 | 14 | CustomResourceV1Mapper mapper = new CustomResourceV1Mapper(); 15 | 16 | @Override 17 | public CompletionStage toHub(CustomResourceV1 resource) { 18 | return CompletableFuture.completedStage(mapper.toHub(resource)); 19 | } 20 | 21 | @Override 22 | public CompletionStage fromHub(CustomResourceV3 customResourceV3) { 23 | return CompletableFuture.completedStage(mapper.fromHub(customResourceV3)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/mapper/AsyncV2Mapper.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.mapper; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.concurrent.CompletionStage; 5 | 6 | import io.javaoperatorsdk.webhook.conversion.AsyncMapper; 7 | import io.javaoperatorsdk.webhook.conversion.TargetVersion; 8 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV2; 9 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV3; 10 | 11 | @TargetVersion("v2") 12 | public class AsyncV2Mapper implements AsyncMapper { 13 | 14 | CustomResourceV2Mapper mapper = new CustomResourceV2Mapper(); 15 | 16 | @Override 17 | public CompletionStage toHub(CustomResourceV2 resource) { 18 | return CompletableFuture.completedStage(mapper.toHub(resource)); 19 | } 20 | 21 | @Override 22 | public CompletionStage fromHub(CustomResourceV3 customResourceV3) { 23 | return CompletableFuture.completedStage(mapper.fromHub(customResourceV3)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/commons/src/main/java/io/javaoperatorsdk/webhook/sample/commons/mapper/V1Mapper.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.commons.mapper; 2 | 3 | import io.javaoperatorsdk.webhook.conversion.Mapper; 4 | import io.javaoperatorsdk.webhook.conversion.TargetVersion; 5 | import io.javaoperatorsdk.webhook.sample.commons.customresource.MultiVersionCustomResource; 6 | import io.javaoperatorsdk.webhook.sample.commons.customresource.MultiVersionCustomResourceSpec; 7 | 8 | @TargetVersion("v1") 9 | public class V1Mapper implements Mapper { 10 | 11 | @Override 12 | public MultiVersionHub toHub(MultiVersionCustomResource resource) { 13 | var hub = new MultiVersionHub(); 14 | hub.setMetadata(resource.getMetadata()); 15 | hub.setValue(resource.getSpec().getValue()); 16 | return hub; 17 | } 18 | 19 | @Override 20 | public MultiVersionCustomResource fromHub(MultiVersionHub hub) { 21 | var res = new MultiVersionCustomResource(); 22 | res.setMetadata(hub.getMetadata()); 23 | 24 | var spec = new MultiVersionCustomResourceSpec(); 25 | spec.setValue(hub.getValue()); 26 | res.setSpec(spec); 27 | return res; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/commons/src/main/java/io/javaoperatorsdk/webhook/sample/commons/mapper/V2Mapper.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.commons.mapper; 2 | 3 | import io.javaoperatorsdk.webhook.conversion.Mapper; 4 | import io.javaoperatorsdk.webhook.conversion.TargetVersion; 5 | import io.javaoperatorsdk.webhook.sample.commons.customresource.MultiVersionCustomResourceSpecV2; 6 | import io.javaoperatorsdk.webhook.sample.commons.customresource.MultiVersionCustomResourceV2; 7 | 8 | @TargetVersion("v2") 9 | public class V2Mapper implements Mapper { 10 | 11 | @Override 12 | public MultiVersionHub toHub(MultiVersionCustomResourceV2 resource) { 13 | var hub = new MultiVersionHub(); 14 | hub.setMetadata(resource.getMetadata()); 15 | hub.setValue(Integer.parseInt(resource.getSpec().getAlteredValue())); 16 | return hub; 17 | } 18 | 19 | @Override 20 | public MultiVersionCustomResourceV2 fromHub(MultiVersionHub hub) { 21 | var res = new MultiVersionCustomResourceV2(); 22 | res.setMetadata(hub.getMetadata()); 23 | res.setSpec(new MultiVersionCustomResourceSpecV2()); 24 | res.getSpec().setAlteredValue(Integer.toString(hub.getValue())); 25 | return res; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/spring-boot/src/main/java/io/javaoperatorsdk/webhook/sample/springboot/admission/AdmissionConfig.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.springboot.admission; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | import io.fabric8.kubernetes.api.model.networking.v1.Ingress; 7 | import io.javaoperatorsdk.webhook.admission.AdmissionController; 8 | import io.javaoperatorsdk.webhook.admission.AsyncAdmissionController; 9 | import io.javaoperatorsdk.webhook.sample.commons.AdmissionControllers; 10 | 11 | @Configuration 12 | public class AdmissionConfig { 13 | 14 | @Bean 15 | public AdmissionController mutatingController() { 16 | return AdmissionControllers.mutatingController(); 17 | } 18 | 19 | @Bean 20 | public AdmissionController validatingController() { 21 | return AdmissionControllers.validatingController(); 22 | } 23 | 24 | @Bean 25 | public AsyncAdmissionController asyncMutatingController() { 26 | return AdmissionControllers.asyncMutatingController(); 27 | } 28 | 29 | @Bean 30 | public AsyncAdmissionController asyncValidatingController() { 31 | return AdmissionControllers.asyncValidatingController(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/commons/src/main/resources/conversion-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "apiextensions.k8s.io/v1", 3 | "kind": "ConversionReview", 4 | "request": { 5 | 6 | "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", 7 | 8 | "desiredAPIVersion": "sample.javaoperatorsdk/v2", 9 | 10 | "objects": [ 11 | { 12 | "kind": "MultiVersionCustomResource", 13 | "apiVersion": "sample.javaoperatorsdk/v1", 14 | "metadata": { 15 | "creationTimestamp": "2021-09-04T14:03:02Z", 16 | "name": "resource1", 17 | "namespace": "default", 18 | "resourceVersion": "143", 19 | "uid": "3415a7fc-162b-4300-b5da-fd6083580d66" 20 | }, 21 | "spec": { 22 | "value": 1 23 | }, 24 | "status":{ 25 | "ready": true 26 | } 27 | }, 28 | { 29 | "kind": "MultiVersionCustomResource", 30 | "apiVersion": "sample.javaoperatorsdk/v1", 31 | "metadata": { 32 | "creationTimestamp": "2021-09-04T14:03:02Z", 33 | "name": "resource2", 34 | "namespace": "default", 35 | "resourceVersion": "14344", 36 | "uid": "1115a7fc-162b-4300-b5da-fd6083580d55" 37 | }, 38 | "spec": { 39 | "value": 2 40 | }, 41 | "status":{ 42 | "ready": false 43 | } 44 | } 45 | ] 46 | } 47 | } -------------------------------------------------------------------------------- /samples/commons/src/main/java/io/javaoperatorsdk/webhook/sample/commons/ConversionControllers.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.commons; 2 | 3 | import io.javaoperatorsdk.webhook.conversion.AsyncConversionController; 4 | import io.javaoperatorsdk.webhook.conversion.ConversionController; 5 | import io.javaoperatorsdk.webhook.sample.commons.mapper.AsyncV1Mapper; 6 | import io.javaoperatorsdk.webhook.sample.commons.mapper.AsyncV2Mapper; 7 | import io.javaoperatorsdk.webhook.sample.commons.mapper.V1Mapper; 8 | import io.javaoperatorsdk.webhook.sample.commons.mapper.V2Mapper; 9 | 10 | public class ConversionControllers { 11 | 12 | public static final String CONVERSION_PATH = "convert"; 13 | public static final String ASYNC_CONVERSION_PATH = "async-convert"; 14 | 15 | public static ConversionController conversionController() { 16 | var controller = new ConversionController(); 17 | controller.registerMapper(new V1Mapper()); 18 | controller.registerMapper(new V2Mapper()); 19 | return controller; 20 | } 21 | 22 | public static AsyncConversionController asyncConversionController() { 23 | var controller = new AsyncConversionController(); 24 | controller.registerMapper(new AsyncV1Mapper()); 25 | controller.registerMapper(new AsyncV2Mapper()); 26 | return controller; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/spring-boot/src/test/java/io/javaoperatorsdk/webhook/sample/springboot/admission/AdditionalAdmissionConfig.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.springboot.admission; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | import io.fabric8.kubernetes.api.model.networking.v1.Ingress; 7 | import io.javaoperatorsdk.webhook.admission.AdmissionController; 8 | import io.javaoperatorsdk.webhook.admission.AsyncAdmissionController; 9 | import io.javaoperatorsdk.webhook.sample.commons.AdmissionControllers; 10 | 11 | @Configuration 12 | public class AdditionalAdmissionConfig { 13 | 14 | @Bean 15 | public AdmissionController errorMutatingController() { 16 | return AdmissionControllers.errorMutatingController(); 17 | } 18 | 19 | @Bean 20 | public AdmissionController errorValidatingController() { 21 | return AdmissionControllers.errorValidatingController(); 22 | } 23 | 24 | @Bean 25 | public AsyncAdmissionController errorAsyncMutatingController() { 26 | return AdmissionControllers.errorAsyncMutatingController(); 27 | } 28 | 29 | @Bean 30 | public AsyncAdmissionController errorAsyncValidatingController() { 31 | return AdmissionControllers.errorAsyncValidatingController(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/quarkus/src/main/docker/Dockerfile.legacy-jar: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode 3 | # 4 | # Before building the container image run: 5 | # 6 | # ./mvnw package -Dquarkus.package.type=legacy-jar 7 | # 8 | # Then, build the image with: 9 | # 10 | # docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/quarkus-sample-legacy-jar . 11 | # 12 | # Then run the container using: 13 | # 14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-sample-legacy-jar 15 | # 16 | # If you want to include the debug port into your docker image 17 | # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 18 | # 19 | # Then run the container using : 20 | # 21 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-sample-legacy-jar 22 | # 23 | ### 24 | FROM registry.access.redhat.com/ubi8/openjdk-11-runtime:1.10 25 | 26 | ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' 27 | 28 | # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. 29 | ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" 30 | 31 | COPY target/lib/* /deployments/lib/ 32 | COPY target/*-runner.jar /deployments/quarkus-run.jar 33 | 34 | EXPOSE 8080 35 | USER 185 36 | 37 | ENTRYPOINT [ "java", "-jar", "/deployments/quarkus-run.jar" ] 38 | -------------------------------------------------------------------------------- /samples/quarkus/src/test/java/io/javaoperatorsdk/webhook/sample/QuarkusWebhooksE2E.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | 6 | import org.junit.jupiter.api.BeforeAll; 7 | 8 | import io.fabric8.kubernetes.client.KubernetesClientBuilder; 9 | 10 | import static io.javaoperatorsdk.webhook.sample.commons.Utils.addConversionHookEndpointToCustomResource; 11 | import static io.javaoperatorsdk.webhook.sample.commons.Utils.applyAndWait; 12 | 13 | class QuarkusWebhooksE2E extends AbstractEndToEndTest { 14 | 15 | @BeforeAll 16 | static void deployService() throws IOException { 17 | try (var client = new KubernetesClientBuilder().build(); 18 | var certManager = new URL( 19 | "https://github.com/cert-manager/cert-manager/releases/download/v1.10.1/cert-manager.yaml") 20 | .openStream()) { 21 | applyAndWait(client, certManager); 22 | applyAndWait(client, "target/kubernetes/minikube.yml"); 23 | applyAndWait(client, "k8s/validating-webhook-configuration.yml"); 24 | applyAndWait(client, "k8s/mutating-webhook-configuration.yml"); 25 | applyAndWait(client, 26 | "../commons/target/classes/META-INF/fabric8/multiversioncustomresources.sample.javaoperatorsdk-v1.yml", 27 | addConversionHookEndpointToCustomResource("quarkus-sample")); 28 | waitForCoreDNS(client); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /samples/spring-boot/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=443 2 | server.ssl.enabled=true 3 | server.ssl.key-store-type=PKCS12 4 | ## Container 5 | dekorate.jib.from=openjdk:11 6 | ## To include the keystore secret 7 | dekorate.options.input-path=kubernetes 8 | dekorate.jib.group=test 9 | ## To generate the Certificate and the Issuer resources 10 | dekorate.certificate.secret-name=tls-secret 11 | dekorate.certificate.dnsNames=spring-boot-sample.default.svc,localhost 12 | dekorate.certificate.self-signed.enabled=true 13 | dekorate.certificate.subject.organizations=Dekorate,Community 14 | dekorate.certificate.duration=2160h0m0s 15 | dekorate.certificate.renewBefore=360h0m0s 16 | dekorate.certificate.privateKey.algorithm=RSA 17 | dekorate.certificate.privateKey.encoding=PKCS8 18 | dekorate.certificate.privateKey.size=2048 19 | dekorate.certificate.keystores.pkcs12.create=true 20 | dekorate.certificate.keystores.pkcs12.passwordSecretRef.name=pkcs12-pass 21 | dekorate.certificate.keystores.pkcs12.passwordSecretRef.key=password 22 | dekorate.certificate.usages=server auth,client auth 23 | 24 | ## To configure the application for using the generated Certificate and Issuer resources 25 | dekorate.kubernetes.env-vars[0].name=SERVER_SSL_KEY_STORE 26 | dekorate.kubernetes.env-vars[0].value=/etc/certs/keystore.p12 27 | dekorate.kubernetes.env-vars[1].name=SERVER_SSL_KEY_STORE_PASSWORD 28 | dekorate.kubernetes.env-vars[1].secret=pkcs12-pass 29 | dekorate.kubernetes.env-vars[1].value=password -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/AdmissionController.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission; 2 | 3 | import io.fabric8.kubernetes.api.model.KubernetesResource; 4 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview; 5 | import io.javaoperatorsdk.webhook.admission.mutation.DefaultAdmissionRequestMutator; 6 | import io.javaoperatorsdk.webhook.admission.mutation.Mutator; 7 | import io.javaoperatorsdk.webhook.admission.validation.DefaultAdmissionRequestValidator; 8 | import io.javaoperatorsdk.webhook.admission.validation.Validator; 9 | 10 | public class AdmissionController { 11 | 12 | private final AdmissionRequestHandler admissionRequestHandler; 13 | 14 | public AdmissionController(Mutator mutator) { 15 | this(new DefaultAdmissionRequestMutator<>(mutator)); 16 | } 17 | 18 | public AdmissionController(Validator validator) { 19 | this(new DefaultAdmissionRequestValidator<>(validator)); 20 | } 21 | 22 | public AdmissionController(AdmissionRequestHandler admissionRequestHandler) { 23 | this.admissionRequestHandler = admissionRequestHandler; 24 | } 25 | 26 | public AdmissionReview handle(AdmissionReview admissionReview) { 27 | var response = admissionRequestHandler.handle(admissionReview.getRequest()); 28 | var responseAdmissionReview = new AdmissionReview(); 29 | responseAdmissionReview.setResponse(response); 30 | response.setUid(admissionReview.getRequest().getUid()); 31 | return responseAdmissionReview; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/quarkus/src/main/docker/Dockerfile.jvm: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode 3 | # 4 | # Before building the container image run: 5 | # 6 | # ./mvnw package 7 | # 8 | # Then, build the image with: 9 | # 10 | # docker build -f src/main/docker/Dockerfile.jvm -t quarkus/quarkus-sample-jvm . 11 | # 12 | # Then run the container using: 13 | # 14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-sample-jvm 15 | # 16 | # If you want to include the debug port into your docker image 17 | # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 18 | # 19 | # Then run the container using : 20 | # 21 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-sample-jvm 22 | # 23 | ### 24 | FROM registry.access.redhat.com/ubi8/openjdk-11-runtime:1.10 25 | 26 | ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' 27 | 28 | # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. 29 | ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" 30 | 31 | # We make four distinct layers so if there are application changes the library layers can be re-used 32 | COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ 33 | COPY --chown=185 target/quarkus-app/*.jar /deployments/ 34 | COPY --chown=185 target/quarkus-app/app/ /deployments/app/ 35 | COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ 36 | 37 | EXPOSE 8080 38 | USER 185 39 | 40 | ENTRYPOINT [ "java", "-jar", "/deployments/quarkus-run.jar" ] 41 | 42 | -------------------------------------------------------------------------------- /samples/spring-boot/src/test/java/io/javaoperatorsdk/webhook/sample/springboot/SpringBootWebhooksE2E.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.springboot; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | 6 | import org.junit.jupiter.api.BeforeAll; 7 | 8 | import io.fabric8.kubernetes.client.ConfigBuilder; 9 | import io.fabric8.kubernetes.client.KubernetesClientBuilder; 10 | import io.javaoperatorsdk.webhook.sample.AbstractEndToEndTest; 11 | 12 | import static io.javaoperatorsdk.webhook.sample.commons.Utils.addConversionHookEndpointToCustomResource; 13 | import static io.javaoperatorsdk.webhook.sample.commons.Utils.applyAndWait; 14 | 15 | class SpringBootWebhooksE2E extends AbstractEndToEndTest { 16 | 17 | @BeforeAll 18 | static void deployService() throws IOException { 19 | try (var client = new KubernetesClientBuilder().withConfig(new ConfigBuilder() 20 | .withNamespace("default") 21 | .build()).build(); 22 | var certManager = new URL( 23 | "https://github.com/cert-manager/cert-manager/releases/download/v1.10.1/cert-manager.yaml") 24 | .openStream()) { 25 | applyAndWait(client, certManager); 26 | applyAndWait(client, "k8s/kubernetes.yml"); 27 | applyAndWait(client, "k8s/validating-webhook-configuration.yml"); 28 | applyAndWait(client, "k8s/mutating-webhook-configuration.yml"); 29 | applyAndWait(client, 30 | "../commons/target/classes/META-INF/fabric8/multiversioncustomresources.sample.javaoperatorsdk-v1.yml", 31 | addConversionHookEndpointToCustomResource("spring-boot-sample")); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/validation/DefaultAdmissionRequestValidator.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission.validation; 2 | 3 | import io.fabric8.kubernetes.api.model.KubernetesResource; 4 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest; 5 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponse; 6 | import io.javaoperatorsdk.webhook.admission.AdmissionRequestHandler; 7 | import io.javaoperatorsdk.webhook.admission.NotAllowedException; 8 | import io.javaoperatorsdk.webhook.admission.Operation; 9 | 10 | import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.allowedAdmissionResponse; 11 | import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.notAllowedExceptionToAdmissionResponse; 12 | 13 | public class DefaultAdmissionRequestValidator 14 | implements AdmissionRequestHandler { 15 | 16 | private final Validator validator; 17 | 18 | public DefaultAdmissionRequestValidator(Validator validator) { 19 | this.validator = validator; 20 | } 21 | 22 | @Override 23 | @SuppressWarnings("unchecked") 24 | public AdmissionResponse handle(AdmissionRequest admissionRequest) { 25 | var operation = Operation.valueOf(admissionRequest.getOperation()); 26 | var originalResource = (T) admissionRequest.getObject(); 27 | var oldResource = (T) admissionRequest.getOldObject(); 28 | try { 29 | validator.validate(originalResource, oldResource, operation); 30 | return allowedAdmissionResponse(); 31 | } catch (NotAllowedException e) { 32 | return notAllowedExceptionToAdmissionResponse(e); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /samples/quarkus/src/main/java/io/javaoperatorsdk/webhook/sample/admission/AdmissionControllerConfig.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.admission; 2 | 3 | import io.fabric8.kubernetes.api.model.networking.v1.Ingress; 4 | import io.javaoperatorsdk.webhook.admission.AdmissionController; 5 | import io.javaoperatorsdk.webhook.admission.AsyncAdmissionController; 6 | import io.javaoperatorsdk.webhook.sample.commons.AdmissionControllers; 7 | 8 | import jakarta.inject.Named; 9 | import jakarta.inject.Singleton; 10 | 11 | public class AdmissionControllerConfig { 12 | 13 | public static final String MUTATING_CONTROLLER = "mutatingController"; 14 | public static final String VALIDATING_CONTROLLER = "validatingController"; 15 | public static final String ASYNC_MUTATING_CONTROLLER = "asyncMutatingController"; 16 | public static final String ASYNC_VALIDATING_CONTROLLER = "asyncValidatingController"; 17 | 18 | @Singleton 19 | @Named(MUTATING_CONTROLLER) 20 | public AdmissionController mutatingController() { 21 | return AdmissionControllers.mutatingController(); 22 | } 23 | 24 | @Singleton 25 | @Named(VALIDATING_CONTROLLER) 26 | public AdmissionController validatingController() { 27 | return AdmissionControllers.validatingController(); 28 | } 29 | 30 | @Singleton 31 | @Named(ASYNC_MUTATING_CONTROLLER) 32 | public AsyncAdmissionController asyncMutatingController() { 33 | return AdmissionControllers.asyncMutatingController(); 34 | } 35 | 36 | @Singleton 37 | @Named(ASYNC_VALIDATING_CONTROLLER) 38 | public AsyncAdmissionController asyncValidatingController() { 39 | return AdmissionControllers.asyncValidatingController(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /samples/commons/src/main/resources/admission-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "AdmissionReview", 3 | "apiVersion": "admission.k8s.io/v1beta1", 4 | "request": { 5 | "uid": "0df28fbd-5f5f-11e8-bc74-36e6bb280816", 6 | "kind": { 7 | "group": "", 8 | "version": "v1", 9 | "kind": "Pod" 10 | }, 11 | "resource": { 12 | "group": "", 13 | "version": "v1", 14 | "resource": "pods" 15 | }, 16 | "namespace": "dummy", 17 | "operation": "CREATE", 18 | "userInfo": { 19 | "username": "system:serviceaccount:kube-system:replicaset-controller", 20 | "uid": "a7e0ab33-5f29-11e8-8a3c-36e6bb280816", 21 | "groups": [ 22 | "system:serviceaccounts", 23 | "system:serviceaccounts:kube-system", 24 | "system:authenticated" 25 | ] 26 | }, 27 | "object": { 28 | "apiVersion": "networking.k8s.io/v1", 29 | "kind": "Ingress", 30 | "metadata": { 31 | "name": "minimal-ingress", 32 | "annotations": { 33 | "nginx.ingress.kubernetes.io/rewrite-target": "/" 34 | } 35 | }, 36 | "spec": { 37 | "ingressClassName": "nginx-example", 38 | "rules": [ 39 | { 40 | "http": { 41 | "paths": [ 42 | { 43 | "path": "/testpath", 44 | "pathType": "Prefix", 45 | "backend": { 46 | "service": { 47 | "name": "test", 48 | "port": { 49 | "number": 80 50 | } 51 | } 52 | } 53 | } 54 | ] 55 | } 56 | } 57 | ] 58 | } 59 | }, 60 | "oldObject": null 61 | } 62 | } -------------------------------------------------------------------------------- /.github/workflows/master-snapshot-release.yml: -------------------------------------------------------------------------------- 1 | name: Test & Release Snapshot to Maven Central 2 | 3 | env: 4 | MAVEN_ARGS: -V -ntp -e 5 | 6 | concurrency: 7 | group: ${{ github.ref }}-${{ github.workflow }} 8 | cancel-in-progress: true 9 | on: 10 | push: 11 | branches: [ main ] 12 | workflow_dispatch: 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | java: [ 17 ] 19 | distribution: [ temurin ] 20 | steps: 21 | - uses: actions/checkout@v5 22 | - name: Set up Java and Maven 23 | uses: actions/setup-java@v5 24 | with: 25 | distribution: ${{ matrix.distribution }} 26 | java-version: ${{ matrix.java }} 27 | cache: 'maven' 28 | - name: Run unit tests 29 | run: ./mvnw ${MAVEN_ARGS} -B test --file pom.xml 30 | - name: Package 31 | run: ./mvnw ${MAVEN_ARGS} -B package --file pom.xml 32 | release-snapshot: 33 | runs-on: ubuntu-latest 34 | needs: test 35 | steps: 36 | - uses: actions/checkout@v5 37 | - name: Set up Java and Maven 38 | uses: actions/setup-java@v5 39 | with: 40 | java-version: 17 41 | distribution: temurin 42 | cache: 'maven' 43 | server-id: central 44 | server-username: MAVEN_USERNAME 45 | server-password: MAVEN_CENTRAL_TOKEN 46 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} 47 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 48 | 49 | - name: Publish to Apache Maven Central 50 | run: mvn package deploy -Prelease 51 | env: 52 | MAVEN_USERNAME: ${{ secrets.NEXUS_USERNAME }} 53 | MAVEN_CENTRAL_TOKEN: ${{ secrets.NEXUS_PASSWORD }} 54 | MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 55 | -------------------------------------------------------------------------------- /samples/quarkus/src/test/java/io/javaoperatorsdk/webhook/sample/admission/AdditionalAdmissionConfig.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.admission; 2 | 3 | import io.fabric8.kubernetes.api.model.networking.v1.Ingress; 4 | import io.javaoperatorsdk.webhook.admission.AdmissionController; 5 | import io.javaoperatorsdk.webhook.admission.AsyncAdmissionController; 6 | import io.javaoperatorsdk.webhook.sample.commons.AdmissionControllers; 7 | 8 | import jakarta.inject.Named; 9 | import jakarta.inject.Singleton; 10 | 11 | public class AdditionalAdmissionConfig { 12 | 13 | public static final String ERROR_MUTATING_CONTROLLER = "errorMutatingController"; 14 | public static final String ERROR_VALIDATING_CONTROLLER = "errorValidatingController"; 15 | public static final String ERROR_ASYNC_MUTATING_CONTROLLER = "errorAsyncMutatingController"; 16 | public static final String ERROR_ASYNC_VALIDATING_CONTROLLER = "errorAsyncValidatingController"; 17 | 18 | @Singleton 19 | @Named(ERROR_MUTATING_CONTROLLER) 20 | public AdmissionController errorMutatingController() { 21 | return AdmissionControllers.errorMutatingController(); 22 | } 23 | 24 | @Singleton 25 | @Named(ERROR_VALIDATING_CONTROLLER) 26 | public AdmissionController errorValidatingController() { 27 | return AdmissionControllers.errorValidatingController(); 28 | } 29 | 30 | @Singleton 31 | @Named(ERROR_ASYNC_MUTATING_CONTROLLER) 32 | public AsyncAdmissionController errorAsyncMutatingController() { 33 | return AdmissionControllers.errorAsyncMutatingController(); 34 | } 35 | 36 | @Singleton 37 | @Named(ERROR_ASYNC_VALIDATING_CONTROLLER) 38 | public AsyncAdmissionController errorAsyncValidatingController() { 39 | return AdmissionControllers.errorAsyncValidatingController(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/sonar.yml: -------------------------------------------------------------------------------- 1 | name: Sonar 2 | 3 | env: 4 | MAVEN_ARGS: -V -ntp -e 5 | 6 | concurrency: 7 | group: ${{ github.ref }}-${{ github.workflow }} 8 | cancel-in-progress: true 9 | on: 10 | push: 11 | branches: [ main ] 12 | pull_request: 13 | types: [ opened, synchronize, reopened ] 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | if: ${{ ( github.event_name == 'push' ) || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login == 'kubernetes-webhooks-framework' ) }} 19 | strategy: 20 | matrix: 21 | java: [ 17 ] 22 | distribution: [ temurin ] 23 | steps: 24 | - uses: actions/checkout@v5 25 | - name: Set up Java and Maven 26 | uses: actions/setup-java@v5 27 | with: 28 | distribution: ${{ matrix.distribution }} 29 | java-version: ${{ matrix.java }} 30 | cache: 'maven' 31 | - name: Cache SonarCloud packages 32 | uses: actions/cache@v4 33 | with: 34 | path: ~/.sonar/cache 35 | key: ${{ runner.os }}-sonar 36 | restore-keys: ${{ runner.os }}-sonar 37 | - name: Cache Maven packages 38 | uses: actions/cache@v4 39 | with: 40 | path: ~/.m2 41 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 42 | restore-keys: ${{ runner.os }}-m2 43 | - name: Build and analyze 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 46 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 47 | run: mvn -B org.jacoco:jacoco-maven-plugin:prepare-agent verify org.jacoco:jacoco-maven-plugin:report org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=java-operator-sdk_admission-controller-framework 48 | 49 | -------------------------------------------------------------------------------- /samples/manual-test.http: -------------------------------------------------------------------------------- 1 | POST https://192.168.49.2:31936/mutate HTTP/1.1 2 | Content-Type: application/json 3 | 4 | { 5 | "kind": "AdmissionReview", 6 | "apiVersion": "admission.k8s.io/v1beta1", 7 | "request": { 8 | "uid": "0df28fbd-5f5f-11e8-bc74-36e6bb280816", 9 | "kind": { 10 | "group": "", 11 | "version": "v1", 12 | "kind": "Ingress" 13 | }, 14 | "resource": { 15 | "group": "", 16 | "version": "v1", 17 | "resource": "ingresses" 18 | }, 19 | "namespace": "dummy", 20 | "operation": "CREATE", 21 | "userInfo": { 22 | "username": "system:serviceaccount:kube-system:replicaset-controller", 23 | "uid": "a7e0ab33-5f29-11e8-8a3c-36e6bb280816", 24 | "groups": [ 25 | "system:serviceaccounts", 26 | "system:serviceaccounts:kube-system", 27 | "system:authenticated" 28 | ] 29 | }, 30 | "object": { 31 | "apiVersion": "networking.k8s.io/v1", 32 | "kind": "Ingress", 33 | "metadata": { 34 | "name": "minimal-ingress", 35 | "annotations": { 36 | "nginx.ingress.kubernetes.io/rewrite-target": "/" 37 | } 38 | }, 39 | "spec": { 40 | "ingressClassName": "nginx-example", 41 | "rules": [ 42 | { 43 | "http": { 44 | "paths": [ 45 | { 46 | "path": "/testpath", 47 | "pathType": "Prefix", 48 | "backend": { 49 | "service": { 50 | "name": "test", 51 | "port": { 52 | "number": 80 53 | } 54 | } 55 | } 56 | } 57 | ] 58 | } 59 | } 60 | ] 61 | } 62 | }, 63 | "oldObject": null 64 | } 65 | } -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/ConversionControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | import java.util.function.Function; 4 | 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionResponse; 9 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionReview; 10 | import io.javaoperatorsdk.webhook.conversion.mapper.CustomResourceV1Mapper; 11 | import io.javaoperatorsdk.webhook.conversion.mapper.CustomResourceV2Mapper; 12 | import io.javaoperatorsdk.webhook.conversion.mapper.CustomResourceV3Mapper; 13 | 14 | public class ConversionControllerTest { 15 | 16 | ConversionTestSupport conversionTestSupport = new ConversionTestSupport(); 17 | ConversionController controller = new ConversionController(); 18 | 19 | @BeforeEach 20 | void setup() { 21 | controller.registerMapper(new CustomResourceV1Mapper()); 22 | controller.registerMapper(new CustomResourceV2Mapper()); 23 | controller.registerMapper(new CustomResourceV3Mapper()); 24 | } 25 | 26 | @Test 27 | void handlesSimpleConversion() { 28 | conversionTestSupport.handlesSimpleConversion(getConversionReviewConversionResponseFunction()); 29 | } 30 | 31 | @Test 32 | void convertsVariousVersionsInSingleRequest() { 33 | conversionTestSupport 34 | .convertsVariousVersionsInSingleRequest(getConversionReviewConversionResponseFunction()); 35 | } 36 | 37 | @Test 38 | void errorResponseOnMissingMapper() { 39 | conversionTestSupport 40 | .errorResponseOnMissingMapper(getConversionReviewConversionResponseFunction()); 41 | } 42 | 43 | private Function getConversionReviewConversionResponseFunction() { 44 | return request -> controller.handle(request).getResponse(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /samples/spring-boot/src/main/java/io/javaoperatorsdk/webhook/sample/springboot/conversion/ConversionEndpoint.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.springboot.conversion; 2 | 3 | import org.springframework.web.bind.annotation.PostMapping; 4 | import org.springframework.web.bind.annotation.RequestBody; 5 | import org.springframework.web.bind.annotation.ResponseBody; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionReview; 9 | import io.javaoperatorsdk.webhook.conversion.AsyncConversionController; 10 | import io.javaoperatorsdk.webhook.conversion.ConversionController; 11 | 12 | import reactor.core.publisher.Mono; 13 | 14 | import static io.javaoperatorsdk.webhook.sample.commons.ConversionControllers.ASYNC_CONVERSION_PATH; 15 | import static io.javaoperatorsdk.webhook.sample.commons.ConversionControllers.CONVERSION_PATH; 16 | 17 | @RestController 18 | public class ConversionEndpoint { 19 | 20 | private final ConversionController conversionController; 21 | private final AsyncConversionController asyncConversionController; 22 | 23 | public ConversionEndpoint(ConversionController conversionController, 24 | AsyncConversionController asyncConversionController) { 25 | this.conversionController = conversionController; 26 | this.asyncConversionController = asyncConversionController; 27 | } 28 | 29 | @PostMapping(CONVERSION_PATH) 30 | @ResponseBody 31 | public ConversionReview convert(@RequestBody ConversionReview conversionReview) { 32 | return conversionController.handle(conversionReview); 33 | } 34 | 35 | @PostMapping(ASYNC_CONVERSION_PATH) 36 | @ResponseBody 37 | public Mono convertAsync(@RequestBody ConversionReview conversionReview) { 38 | return Mono.fromCompletionStage(asyncConversionController.handle(conversionReview)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/AsyncAdmissionController.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission; 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.webhook.admission.mutation.AsyncDefaultAdmissionRequestMutator; 8 | import io.javaoperatorsdk.webhook.admission.mutation.AsyncMutator; 9 | import io.javaoperatorsdk.webhook.admission.mutation.Mutator; 10 | import io.javaoperatorsdk.webhook.admission.validation.AsyncDefaultAdmissionRequestValidator; 11 | import io.javaoperatorsdk.webhook.admission.validation.Validator; 12 | 13 | public class AsyncAdmissionController { 14 | 15 | private final AsyncAdmissionRequestHandler requestHandler; 16 | 17 | public AsyncAdmissionController(Mutator mutator) { 18 | this(new AsyncDefaultAdmissionRequestMutator<>(mutator)); 19 | } 20 | 21 | public AsyncAdmissionController(AsyncMutator asyncMutator) { 22 | this(new AsyncDefaultAdmissionRequestMutator<>(asyncMutator)); 23 | } 24 | 25 | public AsyncAdmissionController(Validator validator) { 26 | this(new AsyncDefaultAdmissionRequestValidator<>(validator)); 27 | } 28 | 29 | public AsyncAdmissionController(AsyncAdmissionRequestHandler requestHandler) { 30 | this.requestHandler = requestHandler; 31 | } 32 | 33 | public CompletionStage handle(AdmissionReview admissionReview) { 34 | return requestHandler.handle(admissionReview.getRequest()) 35 | .thenApply(r -> { 36 | var responseAdmissionReview = new AdmissionReview(); 37 | responseAdmissionReview.setResponse(r); 38 | r.setUid(admissionReview.getRequest().getUid()); 39 | return responseAdmissionReview; 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /samples/quarkus/src/main/java/io/javaoperatorsdk/webhook/sample/conversion/ConversionEndpoint.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.conversion; 2 | 3 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionReview; 4 | import io.javaoperatorsdk.webhook.conversion.AsyncConversionController; 5 | import io.javaoperatorsdk.webhook.conversion.ConversionController; 6 | import io.smallrye.mutiny.Uni; 7 | 8 | import jakarta.ws.rs.Consumes; 9 | import jakarta.ws.rs.POST; 10 | import jakarta.ws.rs.Path; 11 | import jakarta.ws.rs.Produces; 12 | import jakarta.ws.rs.core.MediaType; 13 | 14 | import static io.javaoperatorsdk.webhook.sample.commons.ConversionControllers.ASYNC_CONVERSION_PATH; 15 | import static io.javaoperatorsdk.webhook.sample.commons.ConversionControllers.CONVERSION_PATH; 16 | 17 | @Path("/") 18 | public class ConversionEndpoint { 19 | 20 | private final ConversionController conversionController; 21 | private final AsyncConversionController asyncConversionController; 22 | 23 | public ConversionEndpoint(ConversionController conversionController, 24 | AsyncConversionController asyncConversionController) { 25 | this.conversionController = conversionController; 26 | this.asyncConversionController = asyncConversionController; 27 | } 28 | 29 | @POST 30 | @Path(CONVERSION_PATH) 31 | @Consumes(MediaType.APPLICATION_JSON) 32 | @Produces(MediaType.APPLICATION_JSON) 33 | public ConversionReview convert(ConversionReview conversionReview) { 34 | return conversionController.handle(conversionReview); 35 | } 36 | 37 | @POST 38 | @Path(ASYNC_CONVERSION_PATH) 39 | @Consumes(MediaType.APPLICATION_JSON) 40 | @Produces(MediaType.APPLICATION_JSON) 41 | public Uni convertAsync(ConversionReview conversionReview) { 42 | return Uni.createFrom() 43 | .completionStage(() -> asyncConversionController.handle(conversionReview)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /samples/quarkus/README.md: -------------------------------------------------------------------------------- 1 | # quarkus-sample Project 2 | 3 | This project uses Quarkus, the Supersonic Subatomic Java Framework. 4 | 5 | If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . 6 | 7 | ## Running the application in dev mode 8 | 9 | You can run your application in dev mode that enables live coding using: 10 | ```shell script 11 | ./mvnw compile quarkus:dev 12 | ``` 13 | 14 | > **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. 15 | 16 | ## Packaging and running the application 17 | 18 | The application can be packaged using: 19 | ```shell script 20 | ./mvnw package 21 | ``` 22 | It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. 23 | Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. 24 | 25 | The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. 26 | 27 | If you want to build an _über-jar_, execute the following command: 28 | ```shell script 29 | ./mvnw package -Dquarkus.package.type=uber-jar 30 | ``` 31 | 32 | The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`. 33 | 34 | ## Creating a native executable 35 | 36 | You can create a native executable using: 37 | ```shell script 38 | ./mvnw package -Pnative 39 | ``` 40 | 41 | Or, if you don't have GraalVM installed, you can run the native executable build in a container using: 42 | ```shell script 43 | ./mvnw package -Pnative -Dquarkus.native.container-build=true 44 | ``` 45 | 46 | You can then execute your native executable with: `./target/quarkus-sample-1.0.0-SNAPSHOT-runner` 47 | 48 | If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling. 49 | 50 | ## Provided Code 51 | 52 | ### RESTEasy JAX-RS 53 | 54 | Easily start your RESTful Web Services 55 | 56 | [Related guide section...](https://quarkus.io/guides/getting-started#the-jax-rs-resources) 57 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/conversion/Commons.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | import io.fabric8.kubernetes.api.model.HasMetadata; 7 | import io.fabric8.kubernetes.api.model.KubernetesResource; 8 | import io.fabric8.kubernetes.api.model.Status; 9 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionResponse; 10 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionReview; 11 | 12 | public class Commons { 13 | 14 | public static final String FAILED_STATUS_MESSAGE = "Failed"; 15 | public static final String MAPPER_ALREADY_REGISTERED_FOR_VERSION_MESSAGE = 16 | "Mapper already registered for version: "; 17 | 18 | public static ConversionReview createResponse(List convertedObjects, 19 | ConversionReview conversionReview) { 20 | var result = new ConversionReview(); 21 | var response = new ConversionResponse(); 22 | response.setResult(new Status()); 23 | response.getResult().setStatus("Success"); 24 | response.setUid(conversionReview.getRequest().getUid()); 25 | response.setConvertedObjects(convertedObjects.stream().map(KubernetesResource.class::cast) 26 | .collect(Collectors.toList())); 27 | result.setResponse(response); 28 | return result; 29 | } 30 | 31 | public static ConversionReview createErrorResponse(Exception e, 32 | ConversionReview conversionReview) { 33 | var result = new ConversionReview(); 34 | var response = new ConversionResponse(); 35 | response.setUid(conversionReview.getRequest().getUid()); 36 | response.setResult(new Status()); 37 | response.getResult().setStatus(FAILED_STATUS_MESSAGE); 38 | response.getResult().setMessage(e.getMessage()); 39 | result.setResponse(response); 40 | return result; 41 | } 42 | 43 | public static void throwMissingMapperForVersion(String version) { 44 | throw new MissingConversionMapperException( 45 | "Missing mapper from version: " + version); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/AsyncConversionControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | import java.util.concurrent.ExecutionException; 4 | import java.util.function.Function; 5 | 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionResponse; 10 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionReview; 11 | import io.javaoperatorsdk.webhook.conversion.mapper.AsyncV1Mapper; 12 | import io.javaoperatorsdk.webhook.conversion.mapper.AsyncV2Mapper; 13 | import io.javaoperatorsdk.webhook.conversion.mapper.AsyncV3Mapper; 14 | 15 | class AsyncConversionControllerTest { 16 | 17 | ConversionTestSupport conversionTestSupport = new ConversionTestSupport(); 18 | AsyncConversionController controller = new AsyncConversionController(); 19 | 20 | @BeforeEach 21 | void setup() { 22 | controller.registerMapper(new AsyncV1Mapper()); 23 | controller.registerMapper(new AsyncV2Mapper()); 24 | controller.registerMapper(new AsyncV3Mapper()); 25 | } 26 | 27 | @Test 28 | void handlesSimpleConversion() { 29 | conversionTestSupport.handlesSimpleConversion(getConversionReviewConversionResponseFunction()); 30 | } 31 | 32 | @Test 33 | void convertsVariousVersionsInSingleRequest() { 34 | conversionTestSupport 35 | .convertsVariousVersionsInSingleRequest(getConversionReviewConversionResponseFunction()); 36 | } 37 | 38 | @Test 39 | void errorResponseOnMissingMapper() { 40 | conversionTestSupport 41 | .errorResponseOnMissingMapper(getConversionReviewConversionResponseFunction()); 42 | } 43 | 44 | private Function getConversionReviewConversionResponseFunction() { 45 | return request -> { 46 | try { 47 | return controller.handle(request).toCompletableFuture().get().getResponse(); 48 | } catch (InterruptedException | ExecutionException e) { 49 | throw new RuntimeException(e); 50 | } 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/mutation/DefaultAdmissionRequestMutator.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission.mutation; 2 | 3 | import io.fabric8.kubernetes.api.model.KubernetesResource; 4 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest; 5 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponse; 6 | import io.javaoperatorsdk.webhook.admission.AdmissionRequestHandler; 7 | import io.javaoperatorsdk.webhook.admission.NotAllowedException; 8 | import io.javaoperatorsdk.webhook.admission.Operation; 9 | import io.javaoperatorsdk.webhook.clone.Cloner; 10 | import io.javaoperatorsdk.webhook.clone.ObjectMapperCloner; 11 | 12 | import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.admissionResponseFromMutation; 13 | import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.getTargetResource; 14 | import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.notAllowedExceptionToAdmissionResponse; 15 | 16 | public class DefaultAdmissionRequestMutator 17 | implements AdmissionRequestHandler { 18 | 19 | private final Mutator mutator; 20 | private final Cloner cloner; 21 | 22 | public DefaultAdmissionRequestMutator(Mutator mutator) { 23 | this(mutator, new ObjectMapperCloner<>()); 24 | } 25 | 26 | public DefaultAdmissionRequestMutator(Mutator mutator, Cloner cloner) { 27 | this.mutator = mutator; 28 | this.cloner = cloner; 29 | } 30 | 31 | @Override 32 | @SuppressWarnings("unchecked") 33 | public AdmissionResponse handle(AdmissionRequest admissionRequest) { 34 | var operation = Operation.valueOf(admissionRequest.getOperation()); 35 | var originalResource = (T) getTargetResource(admissionRequest, operation); 36 | var clonedResource = cloner.clone(originalResource); 37 | try { 38 | var mutatedResource = mutator.mutate(clonedResource, operation); 39 | return admissionResponseFromMutation(originalResource, mutatedResource); 40 | } catch (NotAllowedException e) { 41 | return notAllowedExceptionToAdmissionResponse(e); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/NotAllowedException.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission; 2 | 3 | import io.fabric8.kubernetes.api.model.Status; 4 | 5 | public class NotAllowedException extends AdmissionControllerException { 6 | 7 | private final Status status; 8 | 9 | public NotAllowedException() { 10 | status = new Status(); 11 | status.setCode(403); 12 | } 13 | 14 | public NotAllowedException(Status status) { 15 | this.status = status; 16 | } 17 | 18 | public NotAllowedException(Throwable cause, Status status) { 19 | super(cause); 20 | this.status = status; 21 | } 22 | 23 | public NotAllowedException(String message, Throwable cause, boolean enableSuppression, 24 | boolean writableStackTrace, Status status) { 25 | super(message, cause, enableSuppression, writableStackTrace); 26 | this.status = status; 27 | } 28 | 29 | public NotAllowedException(String message) { 30 | super(message); 31 | this.status = new Status(); 32 | this.status.setMessage(message); 33 | this.status.setCode(403); 34 | } 35 | 36 | public NotAllowedException(int code) { 37 | this.status = new Status(); 38 | this.status.setCode(code); 39 | } 40 | 41 | public NotAllowedException(String message, int code) { 42 | super(message); 43 | this.status = new Status(); 44 | this.status.setCode(code); 45 | this.status.setMessage(message); 46 | } 47 | 48 | public NotAllowedException(String message, Throwable cause, int code) { 49 | super(message, cause); 50 | this.status = new Status(); 51 | this.status.setCode(code); 52 | this.status.setMessage(message); 53 | } 54 | 55 | public NotAllowedException(Throwable cause, int code) { 56 | super(cause); 57 | this.status = new Status(); 58 | this.status.setCode(code); 59 | } 60 | 61 | public NotAllowedException(String message, Throwable cause, boolean enableSuppression, 62 | boolean writableStackTrace, int code) { 63 | super(message, cause, enableSuppression, writableStackTrace); 64 | this.status = new Status(); 65 | status.setCode(code); 66 | } 67 | 68 | public Status getStatus() { 69 | return status; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/mapper/CustomResourceV1Mapper.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.mapper; 2 | 3 | import io.javaoperatorsdk.webhook.conversion.Mapper; 4 | import io.javaoperatorsdk.webhook.conversion.TargetVersion; 5 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV1; 6 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV1Spec; 7 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV1Status; 8 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV3; 9 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV3Spec; 10 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV3Status; 11 | 12 | import static io.javaoperatorsdk.webhook.conversion.ConversionTestSupport.DEFAULT_ADDITIONAL_VALUE; 13 | import static io.javaoperatorsdk.webhook.conversion.ConversionTestSupport.DEFAULT_THIRD_VALUE; 14 | 15 | @TargetVersion("v1") 16 | public class CustomResourceV1Mapper implements Mapper { 17 | 18 | @Override 19 | public CustomResourceV3 toHub(CustomResourceV1 resource) { 20 | var hubV3 = new CustomResourceV3(); 21 | hubV3.setMetadata(resource.getMetadata()); 22 | var specV3 = new CustomResourceV3Spec(); 23 | specV3.setValue(Integer.toString(resource.getSpec().getValue())); 24 | specV3.setAdditionalValue(DEFAULT_ADDITIONAL_VALUE); 25 | specV3.setThirdValue(DEFAULT_THIRD_VALUE); 26 | hubV3.setSpec(specV3); 27 | if (resource.getStatus() != null) { 28 | hubV3.setStatus(new CustomResourceV3Status()); 29 | hubV3.getStatus().setValue1(resource.getStatus().getValue1()); 30 | } 31 | return hubV3; 32 | } 33 | 34 | @Override 35 | public CustomResourceV1 fromHub(CustomResourceV3 hub) { 36 | var resourceV1 = new CustomResourceV1(); 37 | resourceV1.setMetadata(hub.getMetadata()); 38 | resourceV1.setSpec(new CustomResourceV1Spec()); 39 | resourceV1.getSpec().setValue(Integer.parseInt(hub.getSpec().getValue())); 40 | if (resourceV1.getStatus() != null) { 41 | resourceV1.setStatus(new CustomResourceV1Status()); 42 | resourceV1.getStatus().setValue1(hub.getStatus().getValue1()); 43 | } 44 | return resourceV1; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/validation/AsyncDefaultAdmissionRequestValidator.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission.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.webhook.admission.AsyncAdmissionRequestHandler; 11 | import io.javaoperatorsdk.webhook.admission.NotAllowedException; 12 | import io.javaoperatorsdk.webhook.admission.Operation; 13 | 14 | import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.allowedAdmissionResponse; 15 | import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.notAllowedExceptionToAdmissionResponse; 16 | 17 | public class AsyncDefaultAdmissionRequestValidator 18 | implements AsyncAdmissionRequestHandler { 19 | 20 | private final Validator validator; 21 | 22 | public AsyncDefaultAdmissionRequestValidator(Validator validator) { 23 | this.validator = validator; 24 | } 25 | 26 | @Override 27 | @SuppressWarnings("unchecked") 28 | public CompletionStage handle(AdmissionRequest admissionRequest) { 29 | var operation = Operation.valueOf(admissionRequest.getOperation()); 30 | var originalResource = (T) admissionRequest.getObject(); 31 | var oldResource = (T) admissionRequest.getOldObject(); 32 | var asyncValidate = 33 | CompletableFuture 34 | .runAsync(() -> validator.validate(originalResource, oldResource, operation)); 35 | return asyncValidate 36 | .thenApply(v -> allowedAdmissionResponse()) 37 | .exceptionally(e -> { 38 | if (e instanceof CompletionException) { 39 | if (e.getCause() instanceof NotAllowedException) { 40 | return notAllowedExceptionToAdmissionResponse((NotAllowedException) e.getCause()); 41 | } 42 | throw new IllegalStateException(e.getCause()); 43 | } 44 | throw new IllegalStateException(e); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/mapper/CustomResourceV2Mapper.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion.mapper; 2 | 3 | import io.javaoperatorsdk.webhook.conversion.Mapper; 4 | import io.javaoperatorsdk.webhook.conversion.TargetVersion; 5 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV2; 6 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV2Spec; 7 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV2Status; 8 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV3; 9 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV3Spec; 10 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV3Status; 11 | 12 | import static io.javaoperatorsdk.webhook.conversion.ConversionTestSupport.DEFAULT_ADDITIONAL_VALUE; 13 | import static io.javaoperatorsdk.webhook.conversion.ConversionTestSupport.DEFAULT_THIRD_VALUE; 14 | 15 | @TargetVersion("v2") 16 | public class CustomResourceV2Mapper implements Mapper { 17 | 18 | @Override 19 | public CustomResourceV3 toHub(CustomResourceV2 resource) { 20 | var hubV3 = new CustomResourceV3(); 21 | hubV3.setMetadata(resource.getMetadata()); 22 | var specV3 = new CustomResourceV3Spec(); 23 | specV3.setValue(resource.getSpec().getValue()); 24 | specV3.setAdditionalValue(DEFAULT_ADDITIONAL_VALUE); 25 | specV3.setThirdValue(DEFAULT_THIRD_VALUE); 26 | hubV3.setSpec(specV3); 27 | if (resource.getStatus() != null) { 28 | hubV3.setStatus(new CustomResourceV3Status()); 29 | hubV3.getStatus().setValue1(resource.getStatus().getValue1()); 30 | } 31 | return hubV3; 32 | } 33 | 34 | @Override 35 | public CustomResourceV2 fromHub(CustomResourceV3 hub) { 36 | var resourceV2 = new CustomResourceV2(); 37 | resourceV2.setMetadata(hub.getMetadata()); 38 | resourceV2.setSpec(new CustomResourceV2Spec()); 39 | resourceV2.getSpec().setValue(hub.getSpec().getValue()); 40 | resourceV2.getSpec().setAdditionalValue(hub.getSpec().getAdditionalValue()); 41 | if (resourceV2.getStatus() != null) { 42 | resourceV2.setStatus(new CustomResourceV2Status()); 43 | resourceV2.getStatus().setValue1(hub.getStatus().getValue1()); 44 | } 45 | return resourceV2; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/AdmissionUtils.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.Base64; 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.fabric8.zjsonpatch.JsonDiff; 10 | 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | 13 | public class AdmissionUtils { 14 | 15 | public static final String JSON_PATCH = "JSONPatch"; 16 | private static final ObjectMapper mapper = new ObjectMapper(); 17 | 18 | private AdmissionUtils() {} 19 | 20 | public static AdmissionResponse allowedAdmissionResponse() { 21 | var admissionResponse = new AdmissionResponse(); 22 | admissionResponse.setAllowed(true); 23 | return admissionResponse; 24 | } 25 | 26 | public static AdmissionResponse notAllowedExceptionToAdmissionResponse( 27 | NotAllowedException notAllowedException) { 28 | var admissionResponse = new AdmissionResponse(); 29 | admissionResponse.setAllowed(false); 30 | admissionResponse.setStatus(notAllowedException.getStatus()); 31 | return admissionResponse; 32 | } 33 | 34 | public static KubernetesResource getTargetResource(AdmissionRequest admissionRequest, 35 | Operation operation) { 36 | return (KubernetesResource) (operation == Operation.DELETE ? admissionRequest.getOldObject() 37 | : admissionRequest.getObject()); 38 | } 39 | 40 | public static AdmissionResponse admissionResponseFromMutation(KubernetesResource originalResource, 41 | KubernetesResource mutatedResource) { 42 | var admissionResponse = new AdmissionResponse(); 43 | admissionResponse.setAllowed(true); 44 | admissionResponse.setPatchType(JSON_PATCH); 45 | var originalResNode = mapper.valueToTree(originalResource); 46 | var mutatedResNode = mapper.valueToTree(mutatedResource); 47 | 48 | var diff = JsonDiff.asJson(originalResNode, mutatedResNode); 49 | var base64Diff = 50 | Base64.getEncoder().encodeToString(diff.toString().getBytes(StandardCharsets.UTF_8)); 51 | admissionResponse.setPatch(base64Diff); 52 | return admissionResponse; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /samples/commons/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.javaoperatorsdk 6 | kubernetes-webhooks-framework-samples 7 | 3.0.2-SNAPSHOT 8 | 9 | 10 | io.javaoperatorsdk.admissioncontroller.sample 11 | sample-commons 12 | Kubernetes Webhooks Framework - Samples - Commons 13 | 14 | 15 | 11 16 | 17 | 18 | 19 | 20 | io.fabric8 21 | kubernetes-client 22 | 23 | 24 | io.javaoperatorsdk 25 | kubernetes-webhooks-framework-core 26 | ${project.version} 27 | 28 | 29 | io.fabric8 30 | crd-generator-apt 31 | 32 | 33 | org.assertj 34 | assertj-core 35 | test 36 | 37 | 38 | org.awaitility 39 | awaitility 40 | test 41 | 42 | 43 | org.junit.jupiter 44 | junit-jupiter-engine 45 | test 46 | 47 | 48 | org.junit.jupiter 49 | junit-jupiter-api 50 | test 51 | 52 | 53 | 54 | 55 | 56 | 57 | org.apache.maven.plugins 58 | maven-jar-plugin 59 | 60 | 61 | 62 | test-jar 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to Maven Central 2 | env: 3 | MAVEN_ARGS: -V -ntp -e 4 | on: 5 | release: 6 | types: [ released ] 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v5 12 | - name: Set up Java and Maven 13 | uses: actions/setup-java@v5 14 | with: 15 | java-version: 17 16 | distribution: temurin 17 | cache: 'maven' 18 | server-id: central 19 | server-username: MAVEN_USERNAME 20 | server-password: MAVEN_CENTRAL_TOKEN 21 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} 22 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 23 | 24 | - name: change version to release version 25 | # Assume that RELEASE_VERSION will have form like: "v1.0.1". So we cut the "v" 26 | run: ./mvnw ${MAVEN_ARGS} versions:set -DnewVersion="${RELEASE_VERSION:1}" versions:commit 27 | env: 28 | RELEASE_VERSION: ${{ github.event.release.tag_name }} 29 | 30 | - name: Publish to Apache Maven Central 31 | run: mvn package deploy -Prelease 32 | env: 33 | MAVEN_USERNAME: ${{ secrets.NEXUS_USERNAME }} 34 | MAVEN_CENTRAL_TOKEN: ${{ secrets.NEXUS_PASSWORD }} 35 | MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 36 | 37 | # This is separate job because there were issues with git after release step, was not able to commit changes. See history. 38 | update-working-version: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v5 42 | - name: Set up Java and Maven 43 | uses: actions/setup-java@v5 44 | with: 45 | java-version: 17 46 | distribution: temurin 47 | cache: 'maven' 48 | - name: change version to release version 49 | run: | 50 | ./mvnw ${MAVEN_ARGS} versions:set -DnewVersion="${RELEASE_VERSION:1}" versions:commit 51 | ./mvnw ${MAVEN_ARGS} -q build-helper:parse-version versions:set -DnewVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.nextIncrementalVersion}-SNAPSHOT versions:commit 52 | git config --local user.email "action@github.com" 53 | git config --local user.name "GitHub Action" 54 | git commit -m "Set new SNAPSHOT version into pom files." -a 55 | env: 56 | RELEASE_VERSION: ${{ github.event.release.tag_name }} 57 | - name: Push changes 58 | uses: ad-m/github-push-action@master 59 | with: 60 | github_token: ${{ secrets.GITHUB_TOKEN }} 61 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/conversion/ConversionController.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.stream.Collectors; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import io.fabric8.kubernetes.api.model.HasMetadata; 12 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionReview; 13 | 14 | import static io.javaoperatorsdk.webhook.conversion.Commons.MAPPER_ALREADY_REGISTERED_FOR_VERSION_MESSAGE; 15 | import static io.javaoperatorsdk.webhook.conversion.Commons.createErrorResponse; 16 | import static io.javaoperatorsdk.webhook.conversion.Commons.createResponse; 17 | import static io.javaoperatorsdk.webhook.conversion.Commons.throwMissingMapperForVersion; 18 | 19 | public class ConversionController implements ConversionRequestHandler { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(ConversionController.class); 22 | 23 | @SuppressWarnings("rawtypes") 24 | private final Map mappers = new HashMap<>(); 25 | 26 | public void registerMapper(Mapper mapper) { 27 | var version = mapper.getClass().getDeclaredAnnotation(TargetVersion.class).value(); 28 | if (mappers.get(version) != null) { 29 | throw new IllegalStateException(MAPPER_ALREADY_REGISTERED_FOR_VERSION_MESSAGE + version); 30 | } 31 | mappers.put(version, mapper); 32 | } 33 | 34 | @Override 35 | public ConversionReview handle(ConversionReview conversionReview) { 36 | try { 37 | var convertedObjects = 38 | convertObjects(conversionReview.getRequest().getObjects().stream() 39 | .map(HasMetadata.class::cast).collect(Collectors.toList()), 40 | Utils.versionOfApiVersion(conversionReview.getRequest().getDesiredAPIVersion())); 41 | return createResponse(convertedObjects, conversionReview); 42 | } catch (MissingConversionMapperException e) { 43 | log.error("Error in conversion hook. UID: {}", conversionReview.getRequest().getUid(), e); 44 | return createErrorResponse(e, conversionReview); 45 | } 46 | } 47 | 48 | private List convertObjects(List objects, String targetVersion) { 49 | return objects.stream().map(r -> mapObject(r, targetVersion)) 50 | .collect(Collectors.toList()); 51 | } 52 | 53 | @SuppressWarnings("unchecked") 54 | private HasMetadata mapObject(HasMetadata resource, String targetVersion) { 55 | var sourceVersion = Utils.versionOfApiVersion(resource.getApiVersion()); 56 | var sourceToHubMapper = mappers.get(sourceVersion); 57 | if (sourceToHubMapper == null) { 58 | throwMissingMapperForVersion(sourceVersion); 59 | } 60 | var hubToTarget = mappers.get(targetVersion); 61 | if (hubToTarget == null) { 62 | throwMissingMapperForVersion(targetVersion); 63 | } 64 | return hubToTarget.fromHub(sourceToHubMapper.toHub(resource)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/admission/mutation/AsyncDefaultAdmissionRequestMutator.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission.mutation; 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.webhook.admission.AsyncAdmissionRequestHandler; 11 | import io.javaoperatorsdk.webhook.admission.NotAllowedException; 12 | import io.javaoperatorsdk.webhook.admission.Operation; 13 | import io.javaoperatorsdk.webhook.clone.Cloner; 14 | import io.javaoperatorsdk.webhook.clone.ObjectMapperCloner; 15 | 16 | import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.admissionResponseFromMutation; 17 | import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.getTargetResource; 18 | import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.notAllowedExceptionToAdmissionResponse; 19 | 20 | public class AsyncDefaultAdmissionRequestMutator 21 | implements AsyncAdmissionRequestHandler { 22 | 23 | private final AsyncMutator asyncMutator; 24 | private final Cloner cloner; 25 | 26 | public AsyncDefaultAdmissionRequestMutator(Mutator mutator) { 27 | this(mutator, new ObjectMapperCloner<>()); 28 | } 29 | 30 | public AsyncDefaultAdmissionRequestMutator(Mutator mutator, Cloner cloner) { 31 | this((AsyncMutator) (resource, operation) -> CompletableFuture.supplyAsync( 32 | () -> mutator.mutate(resource, operation)), cloner); 33 | } 34 | 35 | public AsyncDefaultAdmissionRequestMutator(AsyncMutator asyncMutator) { 36 | this(asyncMutator, new ObjectMapperCloner<>()); 37 | } 38 | 39 | public AsyncDefaultAdmissionRequestMutator(AsyncMutator asyncMutator, Cloner cloner) { 40 | this.asyncMutator = asyncMutator; 41 | this.cloner = cloner; 42 | } 43 | 44 | @Override 45 | @SuppressWarnings("unchecked") 46 | public CompletionStage handle(AdmissionRequest admissionRequest) { 47 | var operation = Operation.valueOf(admissionRequest.getOperation()); 48 | var originalResource = (T) getTargetResource(admissionRequest, operation); 49 | var clonedResource = cloner.clone(originalResource); 50 | return asyncMutator.mutate(clonedResource, operation) 51 | .thenApply(resource -> admissionResponseFromMutation(originalResource, resource)) 52 | .exceptionally(e -> { 53 | if (e instanceof CompletionException) { 54 | if (e.getCause() instanceof NotAllowedException) { 55 | return notAllowedExceptionToAdmissionResponse((NotAllowedException) e.getCause()); 56 | } 57 | throw new IllegalStateException(e.getCause()); 58 | } 59 | throw new IllegalStateException(e); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/admission/AdmissionControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import io.fabric8.kubernetes.api.model.HasMetadata; 6 | import io.javaoperatorsdk.webhook.admission.mutation.Mutator; 7 | import io.javaoperatorsdk.webhook.admission.validation.Validator; 8 | 9 | import static io.javaoperatorsdk.webhook.admission.AdmissionTestSupport.LABEL_TEST_VALUE; 10 | import static io.javaoperatorsdk.webhook.admission.AdmissionTestSupport.MISSING_REQUIRED_LABEL; 11 | 12 | class AdmissionControllerTest { 13 | 14 | AdmissionTestSupport admissionTestSupport = new AdmissionTestSupport(); 15 | 16 | @Test 17 | void validatesResource() { 18 | var admissionController = 19 | new AdmissionController((resource, oldResource, operation) -> { 20 | }); 21 | admissionTestSupport.validatesResource(admissionController::handle); 22 | } 23 | 24 | @Test 25 | void validatesResource_whenNotAllowedException() { 26 | var admissionController = 27 | new AdmissionController<>((Validator) (resource, oldResource, operation) -> { 28 | throw new NotAllowedException(MISSING_REQUIRED_LABEL); 29 | }); 30 | admissionTestSupport.notAllowedException(admissionController::handle); 31 | } 32 | 33 | @Test 34 | void validatesResource_whenOtherException() { 35 | var admissionController = 36 | new AdmissionController<>((Validator) (resource, oldResource, operation) -> { 37 | throw new IllegalArgumentException("Invalid resource"); 38 | }); 39 | 40 | admissionTestSupport.assertThatThrownBy(admissionController::handle) 41 | .isInstanceOf(IllegalArgumentException.class) 42 | .hasMessage("Invalid resource"); 43 | } 44 | 45 | @Test 46 | void mutatesResource() { 47 | var admissionController = 48 | new AdmissionController((resource, operation) -> { 49 | resource.getMetadata().getLabels().putIfAbsent(AdmissionTestSupport.LABEL_KEY, 50 | LABEL_TEST_VALUE); 51 | return resource; 52 | }); 53 | admissionTestSupport.mutatesResource(admissionController::handle); 54 | } 55 | 56 | @Test 57 | void mutatesResource_whenNotAllowedException() { 58 | var admissionController = 59 | new AdmissionController<>((Mutator) (resource, operation) -> { 60 | throw new NotAllowedException(MISSING_REQUIRED_LABEL); 61 | }); 62 | 63 | admissionTestSupport.notAllowedException(admissionController::handle); 64 | } 65 | 66 | @Test 67 | void mutatesResource_whenOtherException() { 68 | var admissionController = 69 | new AdmissionController<>((Mutator) (resource, operation) -> { 70 | throw new IllegalArgumentException("Invalid resource"); 71 | }); 72 | 73 | admissionTestSupport.assertThatThrownBy(admissionController::handle) 74 | .isInstanceOf(IllegalArgumentException.class) 75 | .hasMessage("Invalid resource"); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Verify Pull Request 2 | 3 | env: 4 | MAVEN_ARGS: -V -ntp -e 5 | 6 | concurrency: 7 | group: ${{ github.ref }}-${{ github.workflow }} 8 | cancel-in-progress: true 9 | on: 10 | pull_request: 11 | branches: [ main ] 12 | workflow_dispatch: 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | java: [ 17 ] 19 | distribution: [ temurin ] 20 | steps: 21 | - uses: actions/checkout@v5 22 | - name: Set up Java and Maven 23 | uses: actions/setup-java@v5 24 | with: 25 | distribution: ${{ matrix.distribution }} 26 | java-version: ${{ matrix.java }} 27 | cache: 'maven' 28 | - name: Check code format 29 | run: | 30 | ./mvnw ${MAVEN_ARGS} spotless:check --file pom.xml 31 | - name: Run unit tests 32 | run: ./mvnw ${MAVEN_ARGS} -B test --file pom.xml 33 | spring-boot-e2e-tests: 34 | runs-on: ubuntu-latest 35 | needs: build 36 | strategy: 37 | matrix: 38 | java: [ 17 ] 39 | distribution: [ temurin ] 40 | steps: 41 | - uses: actions/checkout@v5 42 | - name: Set up Java and Maven 43 | uses: actions/setup-java@v5 44 | with: 45 | distribution: ${{ matrix.distribution }} 46 | java-version: ${{ matrix.java }} 47 | cache: 'maven' 48 | - name: Setup Minikube-Kubernetes 49 | uses: manusa/actions-setup-minikube@v2.14.0 50 | with: 51 | minikube version: v1.25.2 52 | kubernetes version: v1.23.6 53 | github token: ${{ secrets.GITHUB_TOKEN }} 54 | driver: docker 55 | - name: Run E2E Test 56 | run: | 57 | set -x 58 | eval $(minikube -p minikube docker-env) 59 | ./mvnw ${MAVEN_ARGS} clean install -DskipTests 60 | cd samples/spring-boot 61 | pwd 62 | ./mvnw ${MAVEN_ARGS} jib:dockerBuild -DskipTests 63 | ./mvnw ${MAVEN_ARGS} test -Pend-to-end-tests 64 | 65 | quarkus-e2e-tests: 66 | runs-on: ubuntu-latest 67 | needs: build 68 | strategy: 69 | matrix: 70 | java: [ 17 ] 71 | distribution: [ temurin ] 72 | steps: 73 | - uses: actions/checkout@v5 74 | - name: Set up Java and Maven 75 | uses: actions/setup-java@v5 76 | with: 77 | distribution: ${{ matrix.distribution }} 78 | java-version: ${{ matrix.java }} 79 | cache: 'maven' 80 | - name: Setup Minikube-Kubernetes 81 | uses: manusa/actions-setup-minikube@v2.14.0 82 | with: 83 | minikube version: v1.25.2 84 | kubernetes version: v1.23.6 85 | github token: ${{ secrets.GITHUB_TOKEN }} 86 | driver: docker 87 | - name: Run E2E Test 88 | run: | 89 | set -x 90 | eval $(minikube -p minikube docker-env) 91 | ./mvnw clean install -DskipTests 92 | cd samples/quarkus 93 | ./mvnw install -Dquarkus.container-image.build=true -DskipTests 94 | ./mvnw ${MAVEN_ARGS} test -Pend-to-end-tests 95 | -------------------------------------------------------------------------------- /samples/quarkus/src/test/java/io/javaoperatorsdk/webhook/sample/conversion/ConversionEndpointTest.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.conversion; 2 | 3 | import java.io.IOException; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | import io.quarkus.test.junit.QuarkusTest; 9 | import io.restassured.http.ContentType; 10 | 11 | import static io.javaoperatorsdk.webhook.sample.commons.ConversionControllers.ASYNC_CONVERSION_PATH; 12 | import static io.javaoperatorsdk.webhook.sample.commons.ConversionControllers.CONVERSION_PATH; 13 | import static io.restassured.RestAssured.given; 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.hamcrest.CoreMatchers.is; 16 | 17 | @QuarkusTest 18 | class ConversionEndpointTest { 19 | 20 | final static String expectedResult = 21 | "{\"apiVersion\":\"apiextensions.k8s.io/v1\",\"kind\":\"ConversionReview\",\"response\":{\"convertedObjects\":[{\"apiVersion\":\"sample.javaoperatorsdk/v2\",\"kind\":\"MultiVersionCustomResource\",\"metadata\":{\"creationTimestamp\":\"2021-09-04T14:03:02Z\",\"name\":\"resource1\",\"namespace\":\"default\",\"resourceVersion\":\"143\",\"uid\":\"3415a7fc-162b-4300-b5da-fd6083580d66\"},\"spec\":{\"alteredValue\":\"1\"}},{\"apiVersion\":\"sample.javaoperatorsdk/v2\",\"kind\":\"MultiVersionCustomResource\",\"metadata\":{\"creationTimestamp\":\"2021-09-04T14:03:02Z\",\"name\":\"resource2\",\"namespace\":\"default\",\"resourceVersion\":\"14344\",\"uid\":\"1115a7fc-162b-4300-b5da-fd6083580d55\"},\"spec\":{\"alteredValue\":\"2\"}}],\"result\":{\"apiVersion\":\"v1\",\"kind\":\"Status\",\"status\":\"Success\"},\"uid\":\"705ab4f5-6393-11e8-b7cc-42010a800002\"}}"; 22 | 23 | @Test 24 | void conversion() { 25 | testConversion(CONVERSION_PATH); 26 | } 27 | 28 | @Test 29 | void asyncConversion() { 30 | testConversion(ASYNC_CONVERSION_PATH); 31 | } 32 | 33 | @Test 34 | void errorConversion() { 35 | testErrorConversion(CONVERSION_PATH); 36 | } 37 | 38 | @Test 39 | void asyncErrorConversion() { 40 | testErrorConversion(ASYNC_CONVERSION_PATH); 41 | } 42 | 43 | private void testErrorConversion(String conversionPath) { 44 | given().contentType(ContentType.JSON) 45 | .body(errorRequest()) 46 | .when().post("/" + conversionPath) 47 | .then() 48 | .statusCode(500); 49 | } 50 | 51 | public void testConversion(String path) { 52 | given().contentType(ContentType.JSON) 53 | .body(request()) 54 | .when().post("/" + path) 55 | .then() 56 | .statusCode(200) 57 | .body(is(expectedResult)); 58 | } 59 | 60 | private String errorRequest() { 61 | return request("/conversion-error-request.json"); 62 | } 63 | 64 | private String request() { 65 | return request("/conversion-request.json"); 66 | } 67 | 68 | private String request(String path) { 69 | try (var is = this.getClass().getResourceAsStream(path)) { 70 | assertThat(is).isNotNull(); 71 | return new String(is.readAllBytes(), StandardCharsets.UTF_8); 72 | } catch (IOException e) { 73 | throw new IllegalStateException(e); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /samples/spring-boot/src/main/java/io/javaoperatorsdk/webhook/sample/springboot/admission/AdmissionEndpoint.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.springboot.admission; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.web.bind.annotation.PostMapping; 6 | import org.springframework.web.bind.annotation.RequestBody; 7 | import org.springframework.web.bind.annotation.ResponseBody; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview; 11 | import io.fabric8.kubernetes.api.model.networking.v1.Ingress; 12 | import io.javaoperatorsdk.webhook.admission.AdmissionController; 13 | import io.javaoperatorsdk.webhook.admission.AsyncAdmissionController; 14 | 15 | import reactor.core.publisher.Mono; 16 | 17 | @RestController 18 | public class AdmissionEndpoint { 19 | 20 | public static final String MUTATE_PATH = "mutate"; 21 | public static final String VALIDATE_PATH = "validate"; 22 | public static final String ASYNC_MUTATE_PATH = "async-mutate"; 23 | public static final String ASYNC_VALIDATE_PATH = "async-validate"; 24 | 25 | private final AdmissionController mutatingController; 26 | private final AdmissionController validatingController; 27 | private final AsyncAdmissionController asyncMutatingController; 28 | private final AsyncAdmissionController asyncValidatingController; 29 | 30 | @Autowired 31 | public AdmissionEndpoint( 32 | @Qualifier("mutatingController") AdmissionController mutationController, 33 | @Qualifier("validatingController") AdmissionController validatingController, 34 | @Qualifier("asyncMutatingController") AsyncAdmissionController asyncMutatingController, 35 | @Qualifier("asyncValidatingController") AsyncAdmissionController asyncValidatingController) { 36 | this.mutatingController = mutationController; 37 | this.validatingController = validatingController; 38 | this.asyncMutatingController = asyncMutatingController; 39 | this.asyncValidatingController = asyncValidatingController; 40 | } 41 | 42 | @PostMapping(MUTATE_PATH) 43 | @ResponseBody 44 | public AdmissionReview mutate(@RequestBody AdmissionReview admissionReview) { 45 | return mutatingController.handle(admissionReview); 46 | } 47 | 48 | @PostMapping(value = VALIDATE_PATH) 49 | @ResponseBody 50 | public AdmissionReview validate(@RequestBody AdmissionReview admissionReview) { 51 | return validatingController.handle(admissionReview); 52 | } 53 | 54 | @PostMapping(ASYNC_MUTATE_PATH) 55 | @ResponseBody 56 | public Mono asyncMutate(@RequestBody AdmissionReview admissionReview) { 57 | return Mono.fromCompletionStage(asyncMutatingController.handle(admissionReview)); 58 | } 59 | 60 | @PostMapping(ASYNC_VALIDATE_PATH) 61 | @ResponseBody 62 | public Mono asyncValidate(@RequestBody AdmissionReview admissionReview) { 63 | return Mono.fromCompletionStage(asyncValidatingController.handle(admissionReview)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /samples/quarkus/src/main/java/io/javaoperatorsdk/webhook/sample/admission/AdmissionEndpoint.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.admission; 2 | 3 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview; 4 | import io.fabric8.kubernetes.api.model.networking.v1.Ingress; 5 | import io.javaoperatorsdk.webhook.admission.AdmissionController; 6 | import io.javaoperatorsdk.webhook.admission.AsyncAdmissionController; 7 | import io.smallrye.mutiny.Uni; 8 | 9 | import jakarta.inject.Inject; 10 | import jakarta.inject.Named; 11 | import jakarta.ws.rs.Consumes; 12 | import jakarta.ws.rs.POST; 13 | import jakarta.ws.rs.Path; 14 | import jakarta.ws.rs.Produces; 15 | import jakarta.ws.rs.core.MediaType; 16 | 17 | @Path("/") 18 | public class AdmissionEndpoint { 19 | 20 | public static final String MUTATE_PATH = "mutate"; 21 | public static final String VALIDATE_PATH = "validate"; 22 | public static final String ASYNC_MUTATE_PATH = "async-mutate"; 23 | public static final String ASYNC_VALIDATE_PATH = "async-validate"; 24 | 25 | private final AdmissionController mutationController; 26 | private final AdmissionController validationController; 27 | private final AsyncAdmissionController asyncMutationController; 28 | private final AsyncAdmissionController asyncValidationController; 29 | 30 | @Inject 31 | public AdmissionEndpoint( 32 | @Named(AdmissionControllerConfig.MUTATING_CONTROLLER) AdmissionController mutationController, 33 | @Named(AdmissionControllerConfig.VALIDATING_CONTROLLER) AdmissionController validationController, 34 | @Named(AdmissionControllerConfig.ASYNC_MUTATING_CONTROLLER) AsyncAdmissionController asyncMutationController, 35 | @Named(AdmissionControllerConfig.ASYNC_VALIDATING_CONTROLLER) AsyncAdmissionController asyncValidationController) { 36 | this.mutationController = mutationController; 37 | this.validationController = validationController; 38 | this.asyncMutationController = asyncMutationController; 39 | this.asyncValidationController = asyncValidationController; 40 | } 41 | 42 | @POST 43 | @Path(MUTATE_PATH) 44 | @Consumes(MediaType.APPLICATION_JSON) 45 | @Produces(MediaType.APPLICATION_JSON) 46 | public AdmissionReview mutate(AdmissionReview admissionReview) { 47 | return mutationController.handle(admissionReview); 48 | } 49 | 50 | @POST 51 | @Path(VALIDATE_PATH) 52 | @Consumes(MediaType.APPLICATION_JSON) 53 | @Produces(MediaType.APPLICATION_JSON) 54 | public AdmissionReview validate(AdmissionReview admissionReview) { 55 | return validationController.handle(admissionReview); 56 | } 57 | 58 | @POST 59 | @Path(ASYNC_MUTATE_PATH) 60 | @Consumes(MediaType.APPLICATION_JSON) 61 | @Produces(MediaType.APPLICATION_JSON) 62 | public Uni asyncMutate(AdmissionReview admissionReview) { 63 | return Uni.createFrom() 64 | .completionStage(() -> this.asyncMutationController.handle(admissionReview)); 65 | } 66 | 67 | @POST 68 | @Path(ASYNC_VALIDATE_PATH) 69 | @Consumes(MediaType.APPLICATION_JSON) 70 | @Produces(MediaType.APPLICATION_JSON) 71 | public Uni asyncValidate(AdmissionReview admissionReview) { 72 | return Uni.createFrom() 73 | .completionStage(() -> this.asyncValidationController.handle(admissionReview)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /samples/spring-boot/src/test/java/io/javaoperatorsdk/webhook/sample/springboot/admission/AdmissionAdditionalTestEndpoint.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.springboot.admission; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.web.bind.annotation.PostMapping; 6 | import org.springframework.web.bind.annotation.RequestBody; 7 | import org.springframework.web.bind.annotation.ResponseBody; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview; 11 | import io.fabric8.kubernetes.api.model.networking.v1.Ingress; 12 | import io.javaoperatorsdk.webhook.admission.AdmissionController; 13 | import io.javaoperatorsdk.webhook.admission.AsyncAdmissionController; 14 | 15 | import reactor.core.publisher.Mono; 16 | 17 | @RestController 18 | public class AdmissionAdditionalTestEndpoint { 19 | 20 | public static final String ERROR_MUTATE_PATH = "error-mutate"; 21 | public static final String ERROR_VALIDATE_PATH = "error-validate"; 22 | public static final String ERROR_ASYNC_MUTATE_PATH = "error-async-mutate"; 23 | public static final String ERROR_ASYNC_VALIDATE_PATH = "error-async-validate"; 24 | 25 | private final AdmissionController errorMutatingController; 26 | private final AdmissionController errorValidatingController; 27 | private final AsyncAdmissionController errorAsyncMutatingController; 28 | private final AsyncAdmissionController errorAsyncValidatingController; 29 | 30 | @Autowired 31 | public AdmissionAdditionalTestEndpoint( 32 | @Qualifier("errorMutatingController") AdmissionController errorMutatingController, 33 | @Qualifier("errorValidatingController") AdmissionController errorValidatingController, 34 | @Qualifier("errorAsyncMutatingController") AsyncAdmissionController errorAsyncMutatingController, 35 | @Qualifier("errorAsyncValidatingController") AsyncAdmissionController errorAsyncValidatingController) { 36 | this.errorMutatingController = errorMutatingController; 37 | this.errorValidatingController = errorValidatingController; 38 | this.errorAsyncMutatingController = errorAsyncMutatingController; 39 | this.errorAsyncValidatingController = errorAsyncValidatingController; 40 | } 41 | 42 | @PostMapping(ERROR_MUTATE_PATH) 43 | @ResponseBody 44 | public AdmissionReview errorMutate(@RequestBody AdmissionReview admissionReview) { 45 | return errorMutatingController.handle(admissionReview); 46 | } 47 | 48 | @PostMapping(value = ERROR_VALIDATE_PATH) 49 | @ResponseBody 50 | public AdmissionReview errorValidate(@RequestBody AdmissionReview admissionReview) { 51 | return errorValidatingController.handle(admissionReview); 52 | } 53 | 54 | @PostMapping(ERROR_ASYNC_MUTATE_PATH) 55 | @ResponseBody 56 | public Mono errorAsyncMutate(@RequestBody AdmissionReview admissionReview) { 57 | return Mono.fromCompletionStage(errorAsyncMutatingController.handle(admissionReview)); 58 | } 59 | 60 | @PostMapping(ERROR_ASYNC_VALIDATE_PATH) 61 | @ResponseBody 62 | public Mono errorAsyncValidate(@RequestBody AdmissionReview admissionReview) { 63 | return Mono.fromCompletionStage(errorAsyncValidatingController.handle(admissionReview)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /samples/quarkus/src/test/java/io/javaoperatorsdk/webhook/sample/admission/AdmissionAdditionalTestEndpoint.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.admission; 2 | 3 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview; 4 | import io.fabric8.kubernetes.api.model.networking.v1.Ingress; 5 | import io.javaoperatorsdk.webhook.admission.AdmissionController; 6 | import io.javaoperatorsdk.webhook.admission.AsyncAdmissionController; 7 | import io.smallrye.mutiny.Uni; 8 | 9 | import jakarta.inject.Inject; 10 | import jakarta.inject.Named; 11 | import jakarta.ws.rs.Consumes; 12 | import jakarta.ws.rs.POST; 13 | import jakarta.ws.rs.Path; 14 | import jakarta.ws.rs.Produces; 15 | import jakarta.ws.rs.core.MediaType; 16 | 17 | @Path("/") 18 | public class AdmissionAdditionalTestEndpoint { 19 | 20 | public static final String ERROR_ASYNC_MUTATE_PATH = "error-async-mutate"; 21 | public static final String ERROR_ASYNC_VALIDATE_PATH = "error-async-validate"; 22 | public static final String ERROR_MUTATE_PATH = "error-mutate"; 23 | public static final String ERROR_VALIDATE_PATH = "error-validate"; 24 | 25 | private final AdmissionController errorMutationController; 26 | private final AdmissionController errorValidationController; 27 | private final AsyncAdmissionController errorAsyncMutationController; 28 | private final AsyncAdmissionController errorAsyncValidationController; 29 | 30 | @Inject 31 | public AdmissionAdditionalTestEndpoint( 32 | @Named(AdditionalAdmissionConfig.ERROR_MUTATING_CONTROLLER) AdmissionController errorMutationController, 33 | @Named(AdditionalAdmissionConfig.ERROR_VALIDATING_CONTROLLER) AdmissionController errorValidationController, 34 | @Named(AdditionalAdmissionConfig.ERROR_ASYNC_MUTATING_CONTROLLER) AsyncAdmissionController errorAsyncMutationController, 35 | @Named(AdditionalAdmissionConfig.ERROR_ASYNC_VALIDATING_CONTROLLER) AsyncAdmissionController errorAsyncValidationController) { 36 | this.errorMutationController = errorMutationController; 37 | this.errorValidationController = errorValidationController; 38 | this.errorAsyncMutationController = errorAsyncMutationController; 39 | this.errorAsyncValidationController = errorAsyncValidationController; 40 | } 41 | 42 | @POST 43 | @Path(ERROR_ASYNC_MUTATE_PATH) 44 | @Consumes(MediaType.APPLICATION_JSON) 45 | @Produces(MediaType.APPLICATION_JSON) 46 | public Uni errorAsyncMutate(AdmissionReview admissionReview) { 47 | return Uni.createFrom() 48 | .completionStage(() -> this.errorAsyncMutationController.handle(admissionReview)); 49 | } 50 | 51 | @POST 52 | @Path(ERROR_ASYNC_VALIDATE_PATH) 53 | @Consumes(MediaType.APPLICATION_JSON) 54 | @Produces(MediaType.APPLICATION_JSON) 55 | public Uni errorAsyncValidate(AdmissionReview admissionReview) { 56 | return Uni.createFrom() 57 | .completionStage(() -> this.errorAsyncValidationController.handle(admissionReview)); 58 | } 59 | 60 | @POST 61 | @Path(ERROR_MUTATE_PATH) 62 | @Consumes(MediaType.APPLICATION_JSON) 63 | @Produces(MediaType.APPLICATION_JSON) 64 | public AdmissionReview errorMutate(AdmissionReview admissionReview) { 65 | return errorMutationController.handle(admissionReview); 66 | } 67 | 68 | @POST 69 | @Path(ERROR_VALIDATE_PATH) 70 | @Consumes(MediaType.APPLICATION_JSON) 71 | @Produces(MediaType.APPLICATION_JSON) 72 | public AdmissionReview errorValidate(AdmissionReview admissionReview) { 73 | return errorValidationController.handle(admissionReview); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /samples/quarkus/src/test/java/io/javaoperatorsdk/webhook/sample/admission/AdmissionEndpointTest.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.admission; 2 | 3 | import java.io.IOException; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | import org.apache.http.HttpStatus; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import io.quarkus.test.junit.QuarkusTest; 10 | import io.restassured.http.ContentType; 11 | 12 | import static io.restassured.RestAssured.given; 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.hamcrest.CoreMatchers.is; 15 | 16 | @QuarkusTest 17 | class AdmissionEndpointTest { 18 | 19 | public static final String MUTATION_RESPONSE = 20 | "{\"apiVersion\":\"admission.k8s.io/v1\",\"kind\":\"AdmissionReview\",\"response\":{\"allowed\":true,\"patch\":\"W3sib3AiOiJhZGQiLCJwYXRoIjoiL21ldGFkYXRhL2xhYmVscyIsInZhbHVlIjp7ImFwcC5rdWJlcm5ldGVzLmlvL2lkIjoibXV0YXRpb24tdGVzdCJ9fV0=\",\"patchType\":\"JSONPatch\",\"uid\":\"0df28fbd-5f5f-11e8-bc74-36e6bb280816\"}}"; 21 | public static final String VALIDATE_RESPONSE = 22 | "{\"apiVersion\":\"admission.k8s.io/v1\",\"kind\":\"AdmissionReview\",\"response\":{\"allowed\":false,\"status\":{\"apiVersion\":\"v1\",\"kind\":\"Status\",\"code\":403,\"message\":\"Missing label: app.kubernetes.io/name\"},\"uid\":\"0df28fbd-5f5f-11e8-bc74-36e6bb280816\"}}"; 23 | 24 | @Test 25 | void mutates() { 26 | testMutate(AdmissionEndpoint.MUTATE_PATH); 27 | } 28 | 29 | @Test 30 | void validates() { 31 | testValidate(AdmissionEndpoint.VALIDATE_PATH); 32 | } 33 | 34 | @Test 35 | void errorMutates() { 36 | testServerErrorOnPath(AdmissionAdditionalTestEndpoint.ERROR_MUTATE_PATH); 37 | } 38 | 39 | @Test 40 | void errorValidates() { 41 | testServerErrorOnPath(AdmissionAdditionalTestEndpoint.ERROR_VALIDATE_PATH); 42 | } 43 | 44 | @Test 45 | void asyncMutates() { 46 | testMutate(AdmissionEndpoint.ASYNC_MUTATE_PATH); 47 | } 48 | 49 | @Test 50 | void asyncValidates() { 51 | testValidate(AdmissionEndpoint.ASYNC_VALIDATE_PATH); 52 | } 53 | 54 | @Test 55 | void errorAsyncValidation() { 56 | testServerErrorOnPath(AdmissionAdditionalTestEndpoint.ERROR_ASYNC_VALIDATE_PATH); 57 | } 58 | 59 | @Test 60 | void errorAsyncMutation() { 61 | testServerErrorOnPath(AdmissionAdditionalTestEndpoint.ERROR_ASYNC_MUTATE_PATH); 62 | } 63 | 64 | private void testServerErrorOnPath(String path) { 65 | given().contentType(ContentType.JSON) 66 | .body(jsonRequest()) 67 | .when().post("/" + path) 68 | .then() 69 | .statusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); 70 | } 71 | 72 | public void testMutate(String path) { 73 | given().contentType(ContentType.JSON) 74 | .body(jsonRequest()) 75 | .when().post("/" + path) 76 | .then() 77 | .statusCode(200) 78 | .body(is(MUTATION_RESPONSE)); 79 | } 80 | 81 | public void testValidate(String path) { 82 | given().contentType(ContentType.JSON) 83 | .body(jsonRequest()) 84 | .when().post("/" + path) 85 | .then() 86 | .statusCode(200) 87 | .body(is( 88 | VALIDATE_RESPONSE)); 89 | } 90 | 91 | private String jsonRequest() { 92 | try (var is = this.getClass().getResourceAsStream("/admission-request.json")) { 93 | assertThat(is).isNotNull(); 94 | return new String(is.readAllBytes(), StandardCharsets.UTF_8); 95 | } catch (IOException e) { 96 | throw new IllegalStateException(e); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /samples/spring-boot/src/test/java/io/javaoperatorsdk/webhook/sample/springboot/conversion/ConversionEndpointTest.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.springboot.conversion; 2 | 3 | import java.io.IOException; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; 8 | import org.springframework.context.annotation.Import; 9 | import org.springframework.core.io.ClassPathResource; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.http.ReactiveHttpOutputMessage; 12 | import org.springframework.test.web.reactive.server.WebTestClient; 13 | import org.springframework.util.FileCopyUtils; 14 | import org.springframework.web.reactive.function.BodyInserter; 15 | import org.springframework.web.reactive.function.BodyInserters; 16 | 17 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionReview; 18 | import io.javaoperatorsdk.webhook.sample.commons.customresource.MultiVersionCustomResourceV2; 19 | 20 | import static io.javaoperatorsdk.webhook.sample.commons.ConversionControllers.ASYNC_CONVERSION_PATH; 21 | import static io.javaoperatorsdk.webhook.sample.commons.ConversionControllers.CONVERSION_PATH; 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | 24 | @Import(ConversionConfig.class) 25 | @WebFluxTest(ConversionEndpoint.class) 26 | class ConversionEndpointTest { 27 | 28 | @Autowired 29 | private WebTestClient webClient; 30 | 31 | @Test 32 | void convert() { 33 | testConversion(CONVERSION_PATH); 34 | } 35 | 36 | @Test 37 | void asyncConvert() { 38 | testConversion(ASYNC_CONVERSION_PATH); 39 | } 40 | 41 | @Test 42 | void errorConversion() { 43 | testErrorConversion(CONVERSION_PATH); 44 | } 45 | 46 | @Test 47 | void asyncErrorConversion() { 48 | testErrorConversion(ASYNC_CONVERSION_PATH); 49 | } 50 | 51 | private void testErrorConversion(String conversionPath) { 52 | webClient.post().uri("/" + conversionPath).contentType(MediaType.APPLICATION_JSON) 53 | .body(errorRequest()) 54 | .exchange() 55 | .expectStatus().is5xxServerError(); 56 | } 57 | 58 | public void testConversion(String path) { 59 | webClient.post().uri("/" + path).contentType(MediaType.APPLICATION_JSON) 60 | .body(request()) 61 | .exchange() 62 | .expectStatus().isOk().expectBody(ConversionReview.class).consumeWith(res -> { 63 | var review = res.getResponseBody(); 64 | assertThat(review).isNotNull(); 65 | var resource1 = 66 | ((MultiVersionCustomResourceV2) review.getResponse().getConvertedObjects().get(0)); 67 | assertThat(review.getResponse().getConvertedObjects()).hasSize(2); 68 | assertThat(resource1.getMetadata().getName()).isEqualTo("resource1"); 69 | }); 70 | } 71 | 72 | private BodyInserter request() { 73 | return requestFromResource("/conversion-request.json"); 74 | } 75 | 76 | private BodyInserter errorRequest() { 77 | return requestFromResource("/conversion-error-request.json"); 78 | } 79 | 80 | private BodyInserter requestFromResource(String resource) { 81 | try { 82 | var classPathResource = new ClassPathResource(resource); 83 | return BodyInserters 84 | .fromValue(new String(FileCopyUtils.copyToByteArray(classPathResource.getInputStream()))); 85 | } catch (IOException e) { 86 | throw new IllegalStateException(e); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/admission/AdmissionTestSupport.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission; 2 | 3 | import java.io.IOException; 4 | import java.util.Base64; 5 | import java.util.UUID; 6 | import java.util.function.Function; 7 | 8 | import org.assertj.core.api.AbstractThrowableAssert; 9 | import org.assertj.core.api.Assertions; 10 | 11 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest; 12 | import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview; 13 | import io.fabric8.kubernetes.api.model.apps.Deployment; 14 | import io.fabric8.kubernetes.client.utils.Serialization; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | 18 | public class AdmissionTestSupport { 19 | 20 | public static final String LABEL_KEY = "app.kubernetes.io/name"; 21 | public static final String MISSING_REQUIRED_LABEL = "Missing required label."; 22 | public static final String LABEL_TEST_VALUE = "mutation-test"; 23 | 24 | public static AdmissionReview createTestAdmissionReview() { 25 | var admissionReview = new AdmissionReview(); 26 | var request = new AdmissionRequest(); 27 | admissionReview.setRequest(request); 28 | request.setOperation(Operation.CREATE.name()); 29 | request.setUid(UUID.randomUUID().toString()); 30 | try (var is = AdmissionTestSupport.class.getResourceAsStream("deployment.yaml")) { 31 | var deployment = Serialization.unmarshal(is, Deployment.class); 32 | request.setObject(deployment); 33 | } catch (IOException e) { 34 | throw new IllegalStateException(e); 35 | } 36 | return admissionReview; 37 | } 38 | 39 | void validatesResource(Function function) { 40 | var inputAdmissionReview = createTestAdmissionReview(); 41 | var admissionReview = function.apply(inputAdmissionReview); 42 | 43 | assertThat(admissionReview.getResponse().getUid()) 44 | .isEqualTo(inputAdmissionReview.getRequest().getUid()); 45 | assertThat(admissionReview.getResponse().getAllowed()).isTrue(); 46 | assertThat(admissionReview.getResponse().getStatus()).isNull(); 47 | } 48 | 49 | void mutatesResource(Function function) { 50 | var inputAdmissionReview = createTestAdmissionReview(); 51 | var admissionReview = function.apply(inputAdmissionReview); 52 | 53 | assertThat(admissionReview.getResponse().getUid()) 54 | .isEqualTo(inputAdmissionReview.getRequest().getUid()); 55 | assertThat(admissionReview.getResponse().getAllowed()).isTrue(); 56 | var patch = new String(Base64.getDecoder().decode(admissionReview.getResponse().getPatch())); 57 | assertThat(patch).isEqualTo( 58 | "[{\"op\":\"add\",\"path\":\"/metadata/labels/app.kubernetes.io~1name\",\"value\":\"mutation-test\"}]"); 59 | } 60 | 61 | void notAllowedException(Function function) { 62 | var inputAdmissionReview = createTestAdmissionReview(); 63 | var admissionReview = function.apply(inputAdmissionReview); 64 | 65 | assertThat(admissionReview.getResponse().getUid()) 66 | .isEqualTo(inputAdmissionReview.getRequest().getUid()); 67 | assertThat(admissionReview.getResponse().getAllowed()).isFalse(); 68 | assertThat(admissionReview.getResponse().getStatus().getCode()).isEqualTo(403); 69 | assertThat(admissionReview.getResponse().getStatus().getMessage()).isEqualTo( 70 | MISSING_REQUIRED_LABEL); 71 | } 72 | 73 | AbstractThrowableAssert assertThatThrownBy( 74 | Function function) { 75 | var inputAdmissionReview = createTestAdmissionReview(); 76 | return Assertions.assertThatThrownBy(() -> function.apply(inputAdmissionReview)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /samples/spring-boot/k8s/kubernetes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | annotations: 6 | app.dekorate.io/vcs-url: <> 7 | labels: 8 | app.kubernetes.io/name: spring-boot-sample 9 | app.kubernetes.io/version: 0.1.0 10 | name: spring-boot-sample 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | app.kubernetes.io/name: spring-boot-sample 16 | app.kubernetes.io/version: 0.1.0 17 | template: 18 | metadata: 19 | annotations: 20 | app.dekorate.io/vcs-url: <> 21 | labels: 22 | app.kubernetes.io/name: spring-boot-sample 23 | app.kubernetes.io/version: 0.1.0 24 | spec: 25 | containers: 26 | - env: 27 | - name: KUBERNETES_NAMESPACE 28 | valueFrom: 29 | fieldRef: 30 | fieldPath: metadata.namespace 31 | - name: SERVER_SSL_KEY_STORE 32 | value: /etc/certs/keystore.p12 33 | - name: SERVER_SSL_KEY_STORE_PASSWORD 34 | valueFrom: 35 | secretKeyRef: 36 | key: password 37 | name: pkcs12-pass 38 | image: test/spring-boot-sample:0.1.0 39 | imagePullPolicy: IfNotPresent 40 | name: spring-boot-sample 41 | ports: 42 | - containerPort: 443 43 | name: http 44 | protocol: TCP 45 | volumeMounts: 46 | - mountPath: /etc/certs 47 | name: volume-certs 48 | readOnly: true 49 | volumes: 50 | - name: volume-certs 51 | secret: 52 | optional: false 53 | secretName: tls-secret 54 | --- 55 | apiVersion: v1 56 | kind: Secret 57 | metadata: 58 | name: pkcs12-pass 59 | data: 60 | password: c3VwZXJzZWNyZXQ= 61 | type: Opaque 62 | --- 63 | apiVersion: cert-manager.io/v1 64 | kind: Issuer 65 | metadata: 66 | annotations: 67 | app.dekorate.io/vcs-url: <> 68 | labels: 69 | app.kubernetes.io/name: spring-boot-sample 70 | app.kubernetes.io/version: 0.1.0 71 | name: spring-boot-sample 72 | spec: 73 | selfSigned: {} 74 | --- 75 | apiVersion: v1 76 | kind: Service 77 | metadata: 78 | annotations: 79 | app.dekorate.io/vcs-url: <> 80 | labels: 81 | app.kubernetes.io/name: spring-boot-sample 82 | app.kubernetes.io/version: 0.1.0 83 | name: spring-boot-sample 84 | spec: 85 | ports: 86 | - name: http 87 | port: 443 88 | protocol: TCP 89 | targetPort: 443 90 | selector: 91 | app.kubernetes.io/name: spring-boot-sample 92 | app.kubernetes.io/version: 0.1.0 93 | type: ClusterIP 94 | --- 95 | apiVersion: cert-manager.io/v1 96 | kind: Certificate 97 | metadata: 98 | annotations: 99 | app.dekorate.io/vcs-url: <> 100 | labels: 101 | app.kubernetes.io/name: spring-boot-sample 102 | app.kubernetes.io/version: 0.1.0 103 | name: spring-boot-sample 104 | spec: 105 | dnsNames: 106 | - spring-boot-sample.default.svc 107 | - localhost 108 | duration: 7776000000000000ns 109 | encodeUsagesInRequest: false 110 | isCA: false 111 | issuerRef: 112 | name: spring-boot-sample 113 | keystores: 114 | pkcs12: 115 | create: true 116 | passwordSecretRef: 117 | key: password 118 | name: pkcs12-pass 119 | privateKey: 120 | algorithm: RSA 121 | encoding: PKCS8 122 | size: 2048 123 | renewBefore: 1296000000000000ns 124 | secretName: tls-secret 125 | subject: 126 | organizations: 127 | - Dekorate 128 | - Community 129 | usages: 130 | - server auth 131 | - client auth 132 | -------------------------------------------------------------------------------- /core/src/main/java/io/javaoperatorsdk/webhook/conversion/AsyncConversionController.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.concurrent.CompletionStage; 9 | import java.util.stream.Collectors; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import io.fabric8.kubernetes.api.model.HasMetadata; 15 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionReview; 16 | 17 | import static io.javaoperatorsdk.webhook.conversion.Commons.MAPPER_ALREADY_REGISTERED_FOR_VERSION_MESSAGE; 18 | import static io.javaoperatorsdk.webhook.conversion.Commons.createErrorResponse; 19 | import static io.javaoperatorsdk.webhook.conversion.Commons.createResponse; 20 | import static io.javaoperatorsdk.webhook.conversion.Commons.throwMissingMapperForVersion; 21 | 22 | public class AsyncConversionController implements AsyncConversionRequestHandler { 23 | 24 | private static final Logger log = LoggerFactory.getLogger(AsyncConversionController.class); 25 | 26 | @SuppressWarnings("rawtypes") 27 | private final Map mappers = new HashMap<>(); 28 | 29 | public void registerMapper(AsyncMapper mapper) { 30 | var version = mapper.getClass().getDeclaredAnnotation(TargetVersion.class).value(); 31 | if (mappers.get(version) != null) { 32 | throw new IllegalStateException(MAPPER_ALREADY_REGISTERED_FOR_VERSION_MESSAGE + version); 33 | } 34 | mappers.put(version, mapper); 35 | } 36 | 37 | @Override 38 | public CompletionStage handle(ConversionReview conversionReview) { 39 | try { 40 | return convertObjects( 41 | conversionReview.getRequest().getObjects().stream() 42 | .map(HasMetadata.class::cast).collect(Collectors.toList()), 43 | Utils.versionOfApiVersion(conversionReview.getRequest().getDesiredAPIVersion())) 44 | .thenApply(convertedObjects -> createResponse(convertedObjects, conversionReview)); 45 | } catch (MissingConversionMapperException e) { 46 | log.error("Error in conversion hook. UID: {}", 47 | conversionReview.getRequest().getUid(), e); 48 | return CompletableFuture.completedStage(createErrorResponse(e, conversionReview)); 49 | } 50 | } 51 | 52 | @SuppressWarnings("unchecked") 53 | private CompletionStage> convertObjects(List objects, 54 | String targetVersion) { 55 | CompletableFuture[] completableFutures = new CompletableFuture[objects.size()]; 56 | for (int i = 0; i < objects.size(); i++) { 57 | completableFutures[i] = mapObject(objects.get(i), targetVersion); 58 | } 59 | return CompletableFuture.allOf(completableFutures).thenApply(r -> { 60 | var result = new ArrayList(completableFutures.length); 61 | for (var cf : completableFutures) { 62 | result.add(cf.join()); 63 | } 64 | return result; 65 | }); 66 | } 67 | 68 | @SuppressWarnings("unchecked") 69 | private CompletableFuture mapObject(HasMetadata resource, String targetVersion) { 70 | var sourceVersion = Utils.versionOfApiVersion(resource.getApiVersion()); 71 | var sourceToHubMapper = mappers.get(sourceVersion); 72 | if (sourceToHubMapper == null) { 73 | throwMissingMapperForVersion(sourceVersion); 74 | } 75 | var hubToTarget = mappers.get(targetVersion); 76 | if (hubToTarget == null) { 77 | throwMissingMapperForVersion(targetVersion); 78 | } 79 | return sourceToHubMapper.toHub(resource) 80 | .thenApply(r -> hubToTarget.fromHub(r).toCompletableFuture().join()) 81 | .toCompletableFuture(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubernetes-webhooks-framework 2 | 3 | Framework and tooling to support 4 | implementing [dynamic admission controllers](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/) 5 | and [conversion hooks](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#webhook-conversion) 6 | for Kubernetes in Java. Supports both **quarkus** and **spring boot**. Both **sync** and **async** programing models. 7 | 8 | ## Documentation 9 | 10 | **For more detailed documentation check the [docs](docs).** 11 | 12 | ## Sample Usage 13 | 14 | Add dependency to you project: 15 | 16 | ```xml 17 | 18 | io.javaoperatorsdk 19 | kubernetes-webhooks-framework-core 20 | ${josdk.webhooks.version} 21 | 22 | ``` 23 | 24 | ### Dynamic Admission Controllers 25 | 26 | Defining a mutation or validation controller is as simple as: 27 | 28 | ```java 29 | 30 | @Singleton 31 | @Named(MUTATING_CONTROLLER) 32 | public AdmissionController mutatingController() { 33 | return new AdmissionController<>((resource, operation) -> { 34 | if (resource.getMetadata().getLabels() == null) { 35 | resource.getMetadata().setLabels(new HashMap<>()); 36 | } 37 | resource.getMetadata().getLabels().putIfAbsent(APP_NAME_LABEL_KEY, "mutation-test"); 38 | return resource; 39 | }); 40 | } 41 | 42 | @Singleton 43 | @Named(VALIDATING_CONTROLLER) 44 | public AdmissionController validatingController() { 45 | return new AdmissionController<>((resource, oldResource, operation) -> { 46 | if (resource.getMetadata().getLabels() == null 47 | || resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null) { 48 | throw new NotAllowedException("Missing label: " + APP_NAME_LABEL_KEY); 49 | } 50 | }); 51 | } 52 | 53 | ``` 54 | 55 | What can be simply used in an endpoint: 56 | 57 | ```java 58 | @POST 59 | @Path(MUTATE_PATH) 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_PATH) 68 | @Consumes(MediaType.APPLICATION_JSON) 69 | @Produces(MediaType.APPLICATION_JSON) 70 | public AdmissionReview validate(AdmissionReview admissionReview) { 71 | return validationController.handle(admissionReview); 72 | } 73 | ``` 74 | 75 | 76 | See samples also for details. 77 | 78 | ### Conversion Hooks 79 | 80 | Conversion hooks follows the same patter described 81 | in [Kuberbuilder](https://book.kubebuilder.io/multiversion-tutorial/conversion-concepts.html), thus first converts the 82 | custom resource from actual version to a hub, and as next step from the hub to the target resource version. 83 | 84 | To create the controller 85 | register [mappers](https://github.com/java-operator-sdk/kubernetes-webhooks-framework/blob/main/core/src/main/java/io/javaoperatorsdk/webhook/conversion/Mapper.java) 86 | : 87 | 88 | ```java 89 | @Singleton 90 | public ConversionController conversionController() { 91 | var controller = new ConversionController(); 92 | controller.registerMapper(new V1Mapper()); 93 | controller.registerMapper(new V2Mapper()); 94 | return controller; 95 | } 96 | ``` 97 | 98 | and use the controllers in the endpoint: 99 | 100 | ```java 101 | @PostMapping(CONVERSION_PATH) 102 | @ResponseBody 103 | public ConversionReview convert(@RequestBody ConversionReview conversionReview) { 104 | return conversionController.handle(conversionReview); 105 | } 106 | ``` 107 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## How to contribute to kubernetes-webhooks-framework 4 | 5 | We'd love to accept your patches! Since we **♥︎♥︎ LOVE ♥︎♥︎** Contributors and Contributions :-) 6 | 7 | You can start contributing to this project by following the below guidelines:- 8 | 9 | (We are assuming you know about git like resolving merge conflicts, squash, setting remote etc.) 10 | 11 | ### Getting Source Code 12 | 13 | * Get the source code by doing a fork and then using the below command 14 | ``` 15 | git clone https://github.com/your_github_username/josdk-webhooks.git 16 | ``` 17 | 18 | * If you want to build/run the project, use command 19 | ``` 20 | mvn clean install 21 | ``` 22 | 23 | ### Starting Development 24 | 25 | Now you can start your contribution work. 26 | 27 | #### * Finding the issue 28 | 29 | There are lots of issues on kubernetes-webhooks-framework's [issue page](https://github.com/operator-framework/josdk-webhooks/issues). Please go through the issues and find a one which you want to fix/develop. If you want to implement something which is not there in the issues, please create a new issue. Please assign that new issue or already existing issue to yourself otherwise it may happen that someone else will fix the same issue. 30 | 31 | #### Creating a new branch 32 | 33 | Please create a new branch to start your development work. You can create the branch by any name but we will suggest you consider the naming convention like iss_issueNumber. Example - iss_989 34 | 35 | ``` 36 | git checkout -b iss_issueNumber 37 | ``` 38 | 39 | #### Create your PATCH 40 | 41 | Do all your development or fixing work here. 42 | 43 | #### Adding Unit and Regression Tests 44 | 45 | After all your development/fixing work is done, do not forget to add `Unit Test` and `Regression Test` around that. It will be nice if you can add an example of the new feature you have added. 46 | 47 | #### Check your work after running all Unit and Regression Tests 48 | 49 | You should run all the unit tests by hitting the following command 50 | 51 | ```shell 52 | mvn clean install 53 | ``` 54 | 55 | #### Commit your work 56 | 57 | After all your work is done, you need to commit the changes. 58 | ``` 59 | git commit -am "Commit-Message" 60 | ``` 61 | Please add a very elaborative [commit message](https://www.conventionalcommits.org/en/v1.0.0/) for the work you have done. It will help the reviewer to understand the things quickly. 62 | 63 | #### Rebase the PR 64 | 65 | It may happen that during the development, someone else submitted another PATCH that is merged before yours. You need to rebase your branch with current upstream master. 66 | 67 | #### Build the project 68 | 69 | Before sending the PR, check whether everything is working fine. To build the project and run test 70 | ```shell 71 | mvn clean install 72 | ``` 73 | 74 | #### Format the files that you touched 75 | 76 | ```shell 77 | mvn spotless:apply 78 | ``` 79 | 80 | #### Push the changes to your fork 81 | 82 | ```shell 83 | git push origin iss_issueNumber 84 | ``` 85 | 86 | #### Create a Pull Request 87 | 88 | Please create a Pull Request from GitHub to kubernetes-webhooks-framework:main. Do not forget to provide very brief Title and elaborative description of PR. Please link the PR to issue by adding `Fix #issueNumber` at the end of the description. 89 | 90 | ### PR Review 91 | 92 | Your PR will get reviewed soon from the maintainers of the project. If they suggest changes, do all the changes, commit the changes, rebase the branch, squash the commits and push the changes. If all will be fine, your PR will be merged. 93 | 94 | That's it! Thank you for your contribution! 95 | 96 | ### Note 97 | 98 | Contribution can be very small, that does not matter. We even love to receive a typo fix PR. Adding feature or fixing a bug is not the only way to contribute. You can send us PR for adding documentation, fixing typos or adding tests. 99 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.javaoperatorsdk 6 | kubernetes-webhooks-framework 7 | 3.0.2-SNAPSHOT 8 | 9 | 10 | kubernetes-webhooks-framework-core 11 | jar 12 | Kubernetes Webhooks Framework - Core 13 | 14 | 15 | 16 | io.fabric8 17 | kubernetes-client 18 | 19 | 20 | org.slf4j 21 | slf4j-api 22 | 23 | 24 | org.apache.logging.log4j 25 | log4j-slf4j-impl 26 | test 27 | 28 | 29 | org.apache.logging.log4j 30 | log4j-core 31 | test 32 | 33 | 34 | org.junit.jupiter 35 | junit-jupiter-api 36 | test 37 | 38 | 39 | org.junit.jupiter 40 | junit-jupiter-engine 41 | test 42 | 43 | 44 | org.mockito 45 | mockito-core 46 | test 47 | 48 | 49 | org.assertj 50 | assertj-core 51 | test 52 | 53 | 54 | 55 | io.fabric8 56 | kubernetes-server-mock 57 | test 58 | 59 | 60 | org.awaitility 61 | awaitility 62 | test 63 | 64 | 65 | io.fabric8 66 | crd-generator-apt 67 | test 68 | 69 | 70 | 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-surefire-plugin 76 | 77 | 78 | 79 | io.github.git-commit-id 80 | git-commit-id-maven-plugin 81 | ${git-commit-id-maven-plugin.version} 82 | 83 | true 84 | ${project.build.outputDirectory}/version.properties 85 | 86 | ^git.build.(time|version)$ 87 | ^git.commit.id.(abbrev|full)$ 88 | git.branch 89 | 90 | full 91 | 92 | 93 | 94 | get-the-git-infos 95 | 96 | revision 97 | 98 | initialize 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /samples/commons/src/main/java/io/javaoperatorsdk/webhook/sample/commons/AdmissionControllers.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.commons; 2 | 3 | import java.util.HashMap; 4 | 5 | import io.fabric8.kubernetes.api.model.networking.v1.Ingress; 6 | import io.javaoperatorsdk.webhook.admission.AdmissionController; 7 | import io.javaoperatorsdk.webhook.admission.AsyncAdmissionController; 8 | import io.javaoperatorsdk.webhook.admission.NotAllowedException; 9 | import io.javaoperatorsdk.webhook.admission.Operation; 10 | import io.javaoperatorsdk.webhook.admission.mutation.AsyncMutator; 11 | import io.javaoperatorsdk.webhook.admission.mutation.Mutator; 12 | import io.javaoperatorsdk.webhook.admission.validation.Validator; 13 | 14 | public class AdmissionControllers { 15 | 16 | public static final String ERROR_MESSAGE = "Some error happened"; 17 | public static final String VALIDATION_TARGET_LABEL = "app.kubernetes.io/name"; 18 | public static final String MUTATION_TARGET_LABEL = "app.kubernetes.io/id"; 19 | 20 | // adds a label to the target resource 21 | public static AdmissionController mutatingController() { 22 | return new AdmissionController<>(new IngressMutator()); 23 | } 24 | 25 | // validates if a resource contains the target label 26 | public static AdmissionController validatingController() { 27 | return new AdmissionController<>(new IngressValidator()); 28 | } 29 | 30 | public static AsyncAdmissionController asyncMutatingController() { 31 | return new AsyncAdmissionController<>(new IngressMutator()); 32 | } 33 | 34 | public static AsyncAdmissionController asyncValidatingController() { 35 | return new AsyncAdmissionController<>(new IngressValidator()); 36 | } 37 | 38 | public static AdmissionController errorMutatingController() { 39 | return new AdmissionController<>((Validator) (resource, oldResource, operation) -> { 40 | throw new IllegalStateException(ERROR_MESSAGE); 41 | }); 42 | } 43 | 44 | public static AdmissionController errorValidatingController() { 45 | return new AdmissionController<>((Mutator) (resource, operation) -> { 46 | throw new IllegalStateException(ERROR_MESSAGE); 47 | }); 48 | } 49 | 50 | public static AsyncAdmissionController errorAsyncMutatingController() { 51 | return new AsyncAdmissionController<>((AsyncMutator) (resource, operation) -> { 52 | throw new IllegalStateException(ERROR_MESSAGE); 53 | }); 54 | } 55 | 56 | public static AsyncAdmissionController errorAsyncValidatingController() { 57 | return new AsyncAdmissionController<>( 58 | (Validator) (resource, oldResource, operation) -> { 59 | throw new IllegalStateException(ERROR_MESSAGE); 60 | }); 61 | } 62 | 63 | private static class IngressMutator implements Mutator { 64 | @Override 65 | public Ingress mutate(Ingress resource, Operation operation) throws NotAllowedException { 66 | if (resource.getMetadata().getLabels() == null) { 67 | resource.getMetadata().setLabels(new HashMap<>()); 68 | } 69 | resource.getMetadata().getLabels().putIfAbsent(MUTATION_TARGET_LABEL, "mutation-test"); 70 | return resource; 71 | } 72 | } 73 | 74 | private static class IngressValidator implements Validator { 75 | @Override 76 | public void validate(Ingress resource, Ingress oldResource, Operation operation) 77 | throws NotAllowedException { 78 | if (operation.equals(Operation.DELETE)) { 79 | return; 80 | } 81 | if (resource.getMetadata().getLabels() == null 82 | || resource.getMetadata().getLabels().get(VALIDATION_TARGET_LABEL) == null) { 83 | throw new NotAllowedException("Missing label: " + VALIDATION_TARGET_LABEL); 84 | } 85 | if (operation.equals(Operation.UPDATE) 86 | && !resource.getSpec().getIngressClassName() 87 | .equals(oldResource.getSpec().getIngressClassName())) { 88 | throw new NotAllowedException("IngressClassName cannot be changed"); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /samples/commons/src/test/java/io/javaoperatorsdk/webhook/sample/AbstractEndToEndTest.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample; 2 | 3 | import java.time.Duration; 4 | import java.util.concurrent.TimeUnit; 5 | import java.util.function.Supplier; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; 10 | import io.fabric8.kubernetes.client.KubernetesClient; 11 | import io.fabric8.kubernetes.client.KubernetesClientBuilder; 12 | import io.fabric8.kubernetes.client.KubernetesClientException; 13 | import io.javaoperatorsdk.webhook.sample.commons.customresource.MultiVersionCustomResource; 14 | import io.javaoperatorsdk.webhook.sample.commons.customresource.MultiVersionCustomResourceSpec; 15 | import io.javaoperatorsdk.webhook.sample.commons.customresource.MultiVersionCustomResourceV2; 16 | 17 | import static io.javaoperatorsdk.webhook.sample.commons.AdmissionControllers.MUTATION_TARGET_LABEL; 18 | import static io.javaoperatorsdk.webhook.sample.commons.Utils.SPIN_UP_GRACE_PERIOD; 19 | import static io.javaoperatorsdk.webhook.sample.commons.Utils.addRequiredLabels; 20 | import static io.javaoperatorsdk.webhook.sample.commons.Utils.testIngress; 21 | import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; 22 | import static org.awaitility.Awaitility.await; 23 | import static org.junit.jupiter.api.Assertions.assertThrows; 24 | 25 | @SuppressWarnings("deprecation") 26 | public abstract class AbstractEndToEndTest { 27 | 28 | protected KubernetesClient client = new KubernetesClientBuilder().build(); 29 | 30 | public static final String TEST_CR_NAME = "test-cr"; 31 | public static final int CR_SPEC_VALUE = 5; 32 | 33 | @Test 34 | void validationHook() { 35 | var ingressWithLabel = testIngress("normal-add-test"); 36 | addRequiredLabels(ingressWithLabel); 37 | await().atMost(Duration.ofSeconds(SPIN_UP_GRACE_PERIOD)).untilAsserted(() -> { 38 | var res = avoidRequestTimeout( 39 | () -> client.network().v1().ingresses().resource(ingressWithLabel).createOrReplace()); 40 | assertThat(res).isNotNull(); 41 | }); 42 | assertThrows(KubernetesClientException.class, 43 | () -> client.network().v1().ingresses().resource(testIngress("validate-test")) 44 | .createOrReplace()); 45 | } 46 | 47 | @Test 48 | void mutationHook() { 49 | var ingressWithLabel = testIngress("mutation-test"); 50 | addRequiredLabels(ingressWithLabel); 51 | await().atMost(Duration.ofSeconds(SPIN_UP_GRACE_PERIOD)).untilAsserted(() -> { 52 | var res = avoidRequestTimeout( 53 | () -> client.network().v1().ingresses().resource(ingressWithLabel).createOrReplace()); 54 | assertThat(res).isNotNull(); 55 | assertThat(res.getMetadata().getLabels()).containsKey(MUTATION_TARGET_LABEL); 56 | }); 57 | } 58 | 59 | @Test 60 | void conversionHook() { 61 | await().atMost(Duration.ofSeconds(SPIN_UP_GRACE_PERIOD)) 62 | .untilAsserted(() -> avoidRequestTimeout(() -> createV1Resource(TEST_CR_NAME))); 63 | MultiVersionCustomResourceV2 v2 = 64 | client.resources(MultiVersionCustomResourceV2.class).withName(TEST_CR_NAME).get(); 65 | assertThat(v2.getSpec().getAlteredValue()).isEqualTo("" + CR_SPEC_VALUE); 66 | } 67 | 68 | @SuppressWarnings("SameParameterValue") 69 | private MultiVersionCustomResource createV1Resource(String name) { 70 | var res = new MultiVersionCustomResource(); 71 | res.setMetadata(new ObjectMetaBuilder() 72 | .withName(name) 73 | .build()); 74 | res.setSpec(new MultiVersionCustomResourceSpec()); 75 | res.getSpec().setValue(CR_SPEC_VALUE); 76 | return client.resource(res).createOrReplace(); 77 | } 78 | 79 | T avoidRequestTimeout(Supplier operator) { 80 | try { 81 | return operator.get(); 82 | } catch (KubernetesClientException e) { 83 | return null; 84 | } 85 | } 86 | 87 | /** On minikube CoreDNS can take some time to start */ 88 | public static void waitForCoreDNS(KubernetesClient client) { 89 | client.apps().deployments().inNamespace("kube-system").withName("coredns").waitUntilReady(2, 90 | TimeUnit.MINUTES); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /samples/spring-boot/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.javaoperatorsdk 6 | kubernetes-webhooks-framework-samples 7 | 3.0.2-SNAPSHOT 8 | 9 | 10 | io.javaoperatorsdk.webhook.sample 11 | spring-boot-sample 12 | Kubernetes Webhooks Framework - Samples - Spring Boot 13 | 14 | 15 | 3.4.6 16 | 4.1.4 17 | 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-dependencies 24 | ${spring-boot-dependencies.version} 25 | pom 26 | import 27 | 28 | 29 | 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter 35 | 36 | 37 | ch.qos.logback 38 | logback-classic 39 | 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-webflux 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-test 49 | test 50 | 51 | 52 | io.projectreactor 53 | reactor-test 54 | test 55 | 56 | 57 | io.javaoperatorsdk 58 | kubernetes-webhooks-framework-core 59 | ${project.version} 60 | 61 | 62 | io.javaoperatorsdk.admissioncontroller.sample 63 | sample-commons 64 | ${project.version} 65 | 66 | 67 | org.assertj 68 | assertj-core 69 | test 70 | 71 | 72 | org.awaitility 73 | awaitility 74 | 75 | 76 | org.slf4j 77 | slf4j-simple 78 | ${slf4j.version} 79 | 80 | 81 | io.javaoperatorsdk.admissioncontroller.sample 82 | sample-commons 83 | ${project.version} 84 | test-jar 85 | test 86 | 87 | 88 | 89 | 90 | 91 | 92 | com.google.cloud.tools 93 | jib-maven-plugin 94 | ${jib-maven-plugin.version} 95 | 96 | 97 | eclipse-temurin:17-jre 98 | 99 | 100 | test/spring-boot-sample:0.1.0 101 | 102 | 103 | 104 | 105 | org.springframework.boot 106 | spring-boot-maven-plugin 107 | ${spring-boot-dependencies.version} 108 | 109 | 110 | 111 | repackage 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /samples/spring-boot/src/test/java/io/javaoperatorsdk/webhook/sample/springboot/admission/AdmissionEndpointTest.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.springboot.admission; 2 | 3 | import java.io.IOException; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; 8 | import org.springframework.context.annotation.Import; 9 | import org.springframework.core.io.ClassPathResource; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.http.ReactiveHttpOutputMessage; 12 | import org.springframework.test.web.reactive.server.WebTestClient; 13 | import org.springframework.util.FileCopyUtils; 14 | import org.springframework.web.reactive.function.BodyInserter; 15 | import org.springframework.web.reactive.function.BodyInserters; 16 | 17 | import static io.javaoperatorsdk.webhook.sample.springboot.admission.AdmissionAdditionalTestEndpoint.ERROR_ASYNC_MUTATE_PATH; 18 | import static io.javaoperatorsdk.webhook.sample.springboot.admission.AdmissionAdditionalTestEndpoint.ERROR_ASYNC_VALIDATE_PATH; 19 | import static io.javaoperatorsdk.webhook.sample.springboot.admission.AdmissionAdditionalTestEndpoint.ERROR_MUTATE_PATH; 20 | import static io.javaoperatorsdk.webhook.sample.springboot.admission.AdmissionAdditionalTestEndpoint.ERROR_VALIDATE_PATH; 21 | import static io.javaoperatorsdk.webhook.sample.springboot.admission.AdmissionEndpoint.ASYNC_MUTATE_PATH; 22 | import static io.javaoperatorsdk.webhook.sample.springboot.admission.AdmissionEndpoint.ASYNC_VALIDATE_PATH; 23 | import static io.javaoperatorsdk.webhook.sample.springboot.admission.AdmissionEndpoint.MUTATE_PATH; 24 | import static io.javaoperatorsdk.webhook.sample.springboot.admission.AdmissionEndpoint.VALIDATE_PATH; 25 | 26 | @Import({AdmissionConfig.class, AdditionalAdmissionConfig.class}) 27 | @WebFluxTest({AdmissionEndpoint.class, AdmissionAdditionalTestEndpoint.class}) 28 | class AdmissionEndpointTest { 29 | 30 | @Autowired 31 | private WebTestClient webClient; 32 | 33 | @Test 34 | void validation() { 35 | testValidate(VALIDATE_PATH); 36 | } 37 | 38 | @Test 39 | void mutation() { 40 | testMutate(MUTATE_PATH); 41 | } 42 | 43 | @Test 44 | void asyncValidation() { 45 | testValidate(ASYNC_VALIDATE_PATH); 46 | } 47 | 48 | @Test 49 | void asyncMutation() { 50 | testMutate(ASYNC_MUTATE_PATH); 51 | } 52 | 53 | @Test 54 | void errorValidation() { 55 | testInternalServerError(ERROR_VALIDATE_PATH); 56 | } 57 | 58 | @Test 59 | void errorMutation() { 60 | testInternalServerError(ERROR_MUTATE_PATH); 61 | } 62 | 63 | @Test 64 | void errorAsyncValidation() { 65 | testInternalServerError(ERROR_ASYNC_VALIDATE_PATH); 66 | } 67 | 68 | @Test 69 | void errorAsyncMutation() { 70 | testInternalServerError(ERROR_ASYNC_MUTATE_PATH); 71 | } 72 | 73 | public void testInternalServerError(String path) { 74 | webClient.post().uri("/" + path).contentType(MediaType.APPLICATION_JSON) 75 | .body(request()) 76 | .exchange() 77 | .expectStatus().is5xxServerError(); 78 | } 79 | 80 | public void testMutate(String path) { 81 | webClient.post().uri("/" + path).contentType(MediaType.APPLICATION_JSON) 82 | .body(request()) 83 | .exchange() 84 | .expectStatus().isOk().expectBody().json( 85 | "{\"apiVersion\":\"admission.k8s.io/v1\",\"kind\":\"AdmissionReview\",\"response\":{\"allowed\":true,\"patch\":\"W3sib3AiOiJhZGQiLCJwYXRoIjoiL21ldGFkYXRhL2xhYmVscyIsInZhbHVlIjp7ImFwcC5rdWJlcm5ldGVzLmlvL2lkIjoibXV0YXRpb24tdGVzdCJ9fV0=\",\"patchType\":\"JSONPatch\",\"uid\":\"0df28fbd-5f5f-11e8-bc74-36e6bb280816\"}}"); 86 | } 87 | 88 | public void testValidate(String path) { 89 | webClient.post().uri("/" + path).contentType(MediaType.APPLICATION_JSON) 90 | .body(request()) 91 | .exchange() 92 | .expectStatus().isOk().expectBody().json("{}"); 93 | } 94 | 95 | private BodyInserter request() { 96 | try { 97 | var classPathResource = new ClassPathResource("admission-request.json"); 98 | return BodyInserters 99 | .fromValue(new String(FileCopyUtils.copyToByteArray(classPathResource.getInputStream()))); 100 | } catch (IOException e) { 101 | throw new IllegalStateException(e); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/conversion/ConversionTestSupport.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.conversion; 2 | 3 | import java.util.Arrays; 4 | import java.util.UUID; 5 | import java.util.function.Function; 6 | import java.util.stream.Collectors; 7 | 8 | import io.fabric8.kubernetes.api.model.HasMetadata; 9 | import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; 10 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionRequest; 11 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionResponse; 12 | import io.fabric8.kubernetes.api.model.apiextensions.v1.ConversionReview; 13 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV1; 14 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV1Spec; 15 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV2; 16 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV2Spec; 17 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV3; 18 | import io.javaoperatorsdk.webhook.conversion.crd.CustomResourceV3Spec; 19 | 20 | import static io.javaoperatorsdk.webhook.conversion.Commons.FAILED_STATUS_MESSAGE; 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | 23 | public class ConversionTestSupport { 24 | 25 | public static final String DEFAULT_ADDITIONAL_VALUE = "defaultAdditionalValue"; 26 | public static final String DEFAULT_THIRD_VALUE = "defaultThirdValue"; 27 | public static final String V1 = "v1"; 28 | public static final String V2 = "v2"; 29 | public static final String V3 = "v3"; 30 | public static final int VALUE = 1; 31 | public static final String V1_NAME = "v1name"; 32 | public static final String V2_NAME = "v2name"; 33 | public static final String V3_NAME = "v3name"; 34 | public static final String API_GROUP = "sample.javaoperatorsdk"; 35 | 36 | void handlesSimpleConversion(Function func) { 37 | var request = createRequest(V2, v1resource()); 38 | var response = func.apply(request); 39 | 40 | assertThat(response.getConvertedObjects()).hasSize(1); 41 | assertThat(response.getUid()).isEqualTo(request.getRequest().getUid()); 42 | var convertedObject = (CustomResourceV2) response.getConvertedObjects().get(0); 43 | assertThat(convertedObject.getMetadata()).isEqualTo(v1resource().getMetadata()); 44 | assertThat(convertedObject.getSpec().getAdditionalValue()).isEqualTo(DEFAULT_ADDITIONAL_VALUE); 45 | assertThat(convertedObject.getSpec().getValue()).isEqualTo(String.valueOf(VALUE)); 46 | } 47 | 48 | void convertsVariousVersionsInSingleRequest(Function func) { 49 | var request = createRequest(V3, v1resource(), v2resource(), v3resource()); 50 | var response = func.apply(request); 51 | 52 | assertThat(response.getConvertedObjects()).hasSize(3); 53 | var namesInOrder = response.getConvertedObjects().stream() 54 | .map(HasMetadata.class::cast) 55 | .map(r -> r.getMetadata().getName()).collect(Collectors.toList()); 56 | assertThat(namesInOrder).containsExactly(V1_NAME, V2_NAME, V3_NAME); 57 | assertThat(response.getConvertedObjects()).allMatch(r -> r instanceof CustomResourceV3); 58 | } 59 | 60 | void errorResponseOnMissingMapper(Function func) { 61 | var request = createRequest("v4", v1resource()); 62 | var response = func.apply(request); 63 | 64 | assertThat(response.getUid()).isEqualTo(request.getRequest().getUid()); 65 | assertThat(response.getResult().getStatus()).isEqualTo(FAILED_STATUS_MESSAGE); 66 | assertThat(response.getResult().getMessage()).contains("Missing", "v4"); 67 | } 68 | 69 | public static ConversionReview createRequest(String targetVersion, HasMetadata... resources) { 70 | var review = new ConversionReview(); 71 | var request = new ConversionRequest(); 72 | request.setDesiredAPIVersion(API_GROUP + "/" + targetVersion); 73 | request.setUid(UUID.randomUUID().toString()); 74 | request.setObjects(Arrays.asList(resources)); 75 | review.setRequest(request); 76 | return review; 77 | } 78 | 79 | public static CustomResourceV1 v1resource() { 80 | var resourceV1 = new CustomResourceV1(); 81 | resourceV1.setMetadata(new ObjectMetaBuilder() 82 | .withName(V1_NAME).withNamespace("default") 83 | .build()); 84 | resourceV1.setSpec(new CustomResourceV1Spec()); 85 | resourceV1.getSpec().setValue(VALUE); 86 | return resourceV1; 87 | } 88 | 89 | public static CustomResourceV2 v2resource() { 90 | var resourceV2 = new CustomResourceV2(); 91 | resourceV2.setMetadata(new ObjectMetaBuilder() 92 | .withName(V2_NAME).withNamespace("default") 93 | .build()); 94 | resourceV2.setSpec(new CustomResourceV2Spec()); 95 | resourceV2.getSpec().setValue("2"); 96 | resourceV2.getSpec().setAdditionalValue("additionalValueV2"); 97 | return resourceV2; 98 | } 99 | 100 | public static CustomResourceV3 v3resource() { 101 | var resourceV3 = new CustomResourceV3(); 102 | resourceV3.setMetadata(new ObjectMetaBuilder() 103 | .withName(V3_NAME).withNamespace("default") 104 | .build()); 105 | resourceV3.setSpec(new CustomResourceV3Spec()); 106 | resourceV3.getSpec().setValue("3"); 107 | resourceV3.getSpec().setAdditionalValue("additionalValueV3"); 108 | resourceV3.getSpec().setThirdValue("thirdValue"); 109 | return resourceV3; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /samples/quarkus/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /samples/spring-boot/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /samples/quarkus/src/main/resources/META-INF/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | quarkus-sample - 1.0.0-SNAPSHOT 6 | 108 | 109 | 110 | 111 | 114 | 115 |
116 |
117 |

Congratulations, you have created a new Quarkus cloud application.

118 | 119 |

What is this page?

120 | 121 |

This page is served by Quarkus. The source is in 122 | src/main/resources/META-INF/resources/index.html.

123 | 124 |

What are your next steps?

125 | 126 |

If not already done, run the application in dev mode using: ./mvnw compile quarkus:dev. 127 |

128 |
    129 |
  • Your static assets are located in src/main/resources/META-INF/resources.
  • 130 |
  • Configure your application in src/main/resources/application.properties.
  • 131 |
  • Quarkus now ships with a Dev UI (available in dev mode only)
  • 132 |
  • Play with the provided code located in src/main/java:
  • 133 |
134 |
135 |

RESTEasy JAX-RS

136 |

Easily start your RESTful Web Services

137 |

@Path: /hello

138 |

Related guide section...

139 |
140 | 141 |
142 |
143 |
144 |

Application

145 |
    146 |
  • GroupId: webhook.sample
  • 147 |
  • ArtifactId: quarkus-sample
  • 148 |
  • Version: 1.0.0-SNAPSHOT
  • 149 |
  • Quarkus Version: 2.5.4.Final
  • 150 |
151 |
152 |
153 |

Do you like Quarkus?

154 |
    155 |
  • Go give it a star on GitHub.
  • 156 |
157 |
158 |
159 |

More reading

160 | 166 |
167 |
168 |
169 | 170 | -------------------------------------------------------------------------------- /samples/commons/src/main/java/io/javaoperatorsdk/webhook/sample/commons/Utils.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.sample.commons; 2 | 3 | import java.io.FileInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.function.UnaryOperator; 10 | import java.util.stream.Collectors; 11 | 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import io.fabric8.kubernetes.api.model.Event; 16 | import io.fabric8.kubernetes.api.model.HasMetadata; 17 | import io.fabric8.kubernetes.api.model.apiextensions.v1.*; 18 | import io.fabric8.kubernetes.api.model.networking.v1.*; 19 | import io.fabric8.kubernetes.client.KubernetesClient; 20 | import io.fabric8.kubernetes.client.KubernetesClientTimeoutException; 21 | import io.fabric8.kubernetes.client.utils.KubernetesSerialization; 22 | 23 | import static io.javaoperatorsdk.webhook.sample.commons.AdmissionControllers.VALIDATION_TARGET_LABEL; 24 | import static io.javaoperatorsdk.webhook.sample.commons.ConversionControllers.CONVERSION_PATH; 25 | 26 | public class Utils { 27 | 28 | private static final Logger log = LoggerFactory.getLogger(Utils.class); 29 | 30 | public static final int SPIN_UP_GRACE_PERIOD = 180; 31 | 32 | public static void applyAndWait(KubernetesClient client, String path) { 33 | applyAndWait(client, path, null); 34 | } 35 | 36 | public static void applyAndWait(KubernetesClient client, String path, 37 | UnaryOperator transform) { 38 | try (FileInputStream fileInputStream = new FileInputStream(path)) { 39 | applyAndWait(client, fileInputStream, transform); 40 | } catch (IOException e) { 41 | throw new IllegalStateException(e); 42 | } 43 | } 44 | 45 | public static void applyAndWait(KubernetesClient client, InputStream is) { 46 | applyAndWait(client, is, null); 47 | } 48 | 49 | public static void applyAndWait(KubernetesClient client, List resources, 50 | UnaryOperator transformer) { 51 | try { 52 | if (transformer != null) { 53 | resources = resources.stream().map(transformer).collect(Collectors.toList()); 54 | } 55 | client.resourceList(resources).createOrReplace(); 56 | client.resourceList(resources).waitUntilReady(5, TimeUnit.MINUTES); 57 | } catch (KubernetesClientTimeoutException e) { 58 | log.info("Timed out resource list: {}", client.resourceList(resources).get()); 59 | var deployment = client.resourceList(resources).get().stream() 60 | .filter(r -> r.getKind().equals("Deployment")).findFirst().orElseThrow(); 61 | log.info("Deployment:\n {} \n", 62 | new KubernetesSerialization().asYaml(deployment)); 63 | 64 | client.v1().events().inNamespace(deployment.getMetadata().getNamespace()).list().getItems() 65 | .stream() 66 | .map(Event::getMessage) 67 | .forEach(ev -> log.info("Event: {}", new KubernetesSerialization().asYaml(ev))); 68 | throw e; 69 | } 70 | } 71 | 72 | public static void applyAndWait(KubernetesClient client, InputStream is, 73 | UnaryOperator transformer) { 74 | var resources = client.load(is).items(); 75 | applyAndWait(client, resources, transformer); 76 | } 77 | 78 | public static void addRequiredLabels(Ingress ingress) { 79 | ingress.getMetadata().setLabels(Map.of(VALIDATION_TARGET_LABEL, "val")); 80 | } 81 | 82 | public static Ingress testIngress(String name) { 83 | return new IngressBuilder() 84 | .withNewMetadata() 85 | .withName(name) 86 | .endMetadata() 87 | .withSpec(new IngressSpecBuilder() 88 | .withIngressClassName("sample") 89 | .withRules(new IngressRuleBuilder() 90 | .withHttp(new HTTPIngressRuleValueBuilder() 91 | .withPaths(new HTTPIngressPathBuilder() 92 | .withPath("/test") 93 | .withPathType("Prefix") 94 | .withBackend(new IngressBackendBuilder() 95 | .withService(new IngressServiceBackendBuilder() 96 | .withName("service") 97 | .withPort(new ServiceBackendPortBuilder() 98 | .withNumber(80) 99 | .build()) 100 | .build()) 101 | .build()) 102 | .build()) 103 | .build()) 104 | .build()) 105 | .build()) 106 | .build(); 107 | } 108 | 109 | public static UnaryOperator addConversionHookEndpointToCustomResource( 110 | String serviceName) { 111 | return r -> { 112 | if (!(r instanceof CustomResourceDefinition)) { 113 | return r; 114 | } 115 | var crd = (CustomResourceDefinition) r; 116 | var crc = new CustomResourceConversion(); 117 | crd.getMetadata() 118 | .setAnnotations(Map.of("cert-manager.io/inject-ca-from", "default/" + serviceName)); 119 | crd.getSpec().setConversion(crc); 120 | crc.setStrategy("Webhook"); 121 | 122 | var whc = new WebhookConversionBuilder() 123 | .withConversionReviewVersions(List.of("v1")) 124 | .withClientConfig(new WebhookClientConfigBuilder() 125 | .withService(new ServiceReferenceBuilder() 126 | .withPath("/" + CONVERSION_PATH) 127 | .withName(serviceName) 128 | .withNamespace("default") 129 | .withPort(443) 130 | .build()) 131 | .build()) 132 | .build(); 133 | crc.setWebhook(whc); 134 | return crd; 135 | }; 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /core/src/test/java/io/javaoperatorsdk/webhook/admission/AsyncAdmissionControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.javaoperatorsdk.webhook.admission; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.concurrent.CompletionException; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | import io.fabric8.kubernetes.api.model.HasMetadata; 9 | import io.javaoperatorsdk.webhook.admission.mutation.AsyncMutator; 10 | import io.javaoperatorsdk.webhook.admission.mutation.Mutator; 11 | import io.javaoperatorsdk.webhook.admission.validation.Validator; 12 | 13 | import static io.javaoperatorsdk.webhook.admission.AdmissionTestSupport.LABEL_KEY; 14 | import static io.javaoperatorsdk.webhook.admission.AdmissionTestSupport.LABEL_TEST_VALUE; 15 | import static io.javaoperatorsdk.webhook.admission.AdmissionTestSupport.MISSING_REQUIRED_LABEL; 16 | 17 | class AsyncAdmissionControllerTest { 18 | 19 | AdmissionTestSupport admissionTestSupport = new AdmissionTestSupport(); 20 | 21 | @Test 22 | void validatesResource() { 23 | var admissionController = 24 | new AsyncAdmissionController((resource, oldResource, operation) -> { 25 | }); 26 | 27 | admissionTestSupport 28 | .validatesResource(res -> admissionController.handle(res).toCompletableFuture().join()); 29 | } 30 | 31 | @Test 32 | void validatesResource_whenNotAllowedException() { 33 | var admissionController = 34 | new AsyncAdmissionController<>( 35 | (Validator) (resource, oldResource, operation) -> { 36 | throw new NotAllowedException(MISSING_REQUIRED_LABEL); 37 | }); 38 | 39 | admissionTestSupport 40 | .notAllowedException(res -> admissionController.handle(res).toCompletableFuture().join()); 41 | } 42 | 43 | @Test 44 | void validatesResource_whenOtherException() { 45 | var admissionController = 46 | new AsyncAdmissionController<>( 47 | (Validator) (resource, oldResource, operation) -> { 48 | throw new IllegalArgumentException("Invalid resource"); 49 | }); 50 | 51 | admissionTestSupport.assertThatThrownBy( 52 | res -> admissionController.handle(res).toCompletableFuture() 53 | .join()) 54 | .isInstanceOf(CompletionException.class) 55 | .hasCauseInstanceOf(IllegalStateException.class) 56 | .hasRootCauseInstanceOf(IllegalArgumentException.class) 57 | .hasRootCauseMessage("Invalid resource"); 58 | } 59 | 60 | @Test 61 | void mutatesResource_withMutator() { 62 | var admissionController = 63 | new AsyncAdmissionController<>((Mutator) (resource, 64 | operation) -> { 65 | resource.getMetadata().getLabels().putIfAbsent(LABEL_KEY, LABEL_TEST_VALUE); 66 | return resource; 67 | }); 68 | 69 | admissionTestSupport 70 | .mutatesResource(res -> admissionController.handle(res).toCompletableFuture().join()); 71 | } 72 | 73 | @Test 74 | void mutatesResource_withAsyncMutator() { 75 | var admissionController = 76 | new AsyncAdmissionController<>((AsyncMutator) (resource, 77 | operation) -> CompletableFuture.supplyAsync(() -> { 78 | resource.getMetadata().getLabels().putIfAbsent(LABEL_KEY, LABEL_TEST_VALUE); 79 | return resource; 80 | })); 81 | 82 | admissionTestSupport 83 | .mutatesResource(res -> admissionController.handle(res).toCompletableFuture().join()); 84 | } 85 | 86 | @Test 87 | void mutatesResource_withMutator_whenNotAllowedException() { 88 | var admissionController = 89 | new AsyncAdmissionController<>((Mutator) (resource, 90 | operation) -> { 91 | throw new NotAllowedException(MISSING_REQUIRED_LABEL); 92 | }); 93 | 94 | admissionTestSupport.notAllowedException( 95 | res -> admissionController.handle(res).toCompletableFuture().join()); 96 | } 97 | 98 | @Test 99 | void mutatesResource_withAsyncMutator_whenNotAllowedException() { 100 | var admissionController = 101 | new AsyncAdmissionController<>((AsyncMutator) (resource, 102 | operation) -> CompletableFuture.supplyAsync(() -> { 103 | throw new NotAllowedException(MISSING_REQUIRED_LABEL); 104 | })); 105 | 106 | admissionTestSupport.notAllowedException( 107 | res -> admissionController.handle(res).toCompletableFuture().join()); 108 | } 109 | 110 | @Test 111 | void mutatesResource_withMutator_whenOtherException() { 112 | var admissionController = 113 | new AsyncAdmissionController<>((Mutator) (resource, 114 | operation) -> { 115 | throw new IllegalArgumentException("Invalid resource"); 116 | }); 117 | 118 | admissionTestSupport.assertThatThrownBy( 119 | res -> admissionController.handle(res).toCompletableFuture() 120 | .join()) 121 | .isInstanceOf(CompletionException.class) 122 | .hasCauseInstanceOf(IllegalStateException.class) 123 | .hasRootCauseInstanceOf(IllegalArgumentException.class) 124 | .hasRootCauseMessage("Invalid resource"); 125 | } 126 | 127 | @Test 128 | void mutatesResource_withAsyncMutator_whenOtherException() { 129 | var admissionController = 130 | new AsyncAdmissionController<>((AsyncMutator) (resource, 131 | operation) -> CompletableFuture.supplyAsync(() -> { 132 | throw new IllegalArgumentException("Invalid resource"); 133 | })); 134 | 135 | admissionTestSupport.assertThatThrownBy( 136 | res -> admissionController.handle(res).toCompletableFuture() 137 | .join()) 138 | .isInstanceOf(CompletionException.class) 139 | .hasCauseInstanceOf(IllegalStateException.class) 140 | .hasRootCauseInstanceOf(IllegalArgumentException.class) 141 | .hasRootCauseMessage("Invalid resource"); 142 | } 143 | } 144 | --------------------------------------------------------------------------------