Kubernetes Webhooks Framework makes it simple to implement dynamic admission controllers and conversion hooks for Kubernetes in Java.
Before you start make sure you understand these concepts in Kubernetes, reading the docs mentioned above.
There are samples both for Spring Boot and Quarkus, both of them implement the same logic. Both sync and async APIs are showcased. This documentation describes the Quarkus version, however the Spring Boot version is almost identical.
There are two endpoints, one for admission controllers (a validating and a mutating) and one for the sample conversion hook .
Starting from those endpoints, it should be trivial to understand how the underlying logic works.
The goal of the end-to-end tests is to test the framework in a production-like environment, but also works as an executable documentation to guide developers how to deploy and configure the target service.
The end-to-end tests are using the same test cases and are based on the samples (See Spring Boot version here). To see how those tests are executed during a pull request check the related GitHub Action
The samples are first built, then deployed to a local Kubernetes cluster (in our case minikube is used). For Quarkus most of the deployment artifacts is generated using extensions (works similarly for Spring Boot, using dekorate):
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-kubernetes</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.certmanager</groupId>
<artifactId>quarkus-certmanager</artifactId>
</dependency>
Only additional resources used for admission hooks are present in the k8s directory. These are the configuration files to configure the admission hooks. For example the configuration for validation look like:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: "validating.quarkus.example.com"
annotations:
# Cert Manager annotation to inject CA
cert-manager.io/inject-ca-from: default/quarkus-sample
webhooks:
- name: "validating.quarkus.example.com"
rules:
- apiGroups: [ "networking.k8s.io" ]
apiVersions: [ "v1" ]
operations: [ "*" ]
resources: [ "ingresses" ]
scope: "Namespaced"
clientConfig:
service:
namespace: "default"
name: "quarkus-sample"
path: "/validate"
port: 443
admissionReviewVersions: [ "v1" ]
sideEffects: None
timeoutSeconds: 5
The conversion hook is configured within the CustomResourceDefinition
, see
related Kubernetes docs.
Since this is not yet supported by the fabric8 client CRD
generator, the hook definition is
added before
CRD is applied.
Note that cert manager is used to generate certificates for the application and for configurations.
There are two types of admission controllers: validation and mutation. Both should be extremely simple to use.
To create the related controller simply pass a lambda to the constructor of AdmissionController that validates the resource. (See also the async version of admission controller implementation.)
new AdmissionController<>((resource,operation) -> {
if(resource.getMetadata().getLabels() == null || resource.getMetadata().getLabels().get(APP_NAME_LABEL_KEY) == null){
throw new NotAllowedException("Missing label: "+APP_NAME_LABEL_KEY);
}
});
respectively mutates it:
new AdmissionController<>((resource,operation) -> {
if(resource.getMetadata().getLabels() == null){
resource.getMetadata().setLabels(new HashMap<>());
}
resource.getMetadata().getLabels().putIfAbsent(APP_NAME_LABEL_KEY,"mutation-test");
return resource;
});
All changes made to the resource are reflected in the response created by the admission controller.
ConversionController ( respectively AsyncConversionController) handles conversion between different versions of custom resources using mappers ( respectively async mappers).
The mapper interface is simple:
public interface Mapper<R extends HasMetadata, HUB> {
HUB toHub(R resource);
R fromHub(HUB hub);
}
It handles mapping to and from a Hub. Hub is an intermediate representation in a conversion. Thus, the conversion steps from v1 to v2 happen in the following way: v1 -> HUB -> v2. Using the provided v1 and v2 mappers implementations. Having this approach is useful mainly in case there are more than two versions of resources on the cluster, so there is no need for a mapper for every combination. See also related docs in Kubebuilder.
In order to properly register your own custom types (custom resources) for deserialization it needs to be added to
META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource
file.
See in the samples.
Related release note in fabric8 client:
Fix #4579: the implicit registration of resource and list types that happens when using the resource(class) methods
has been removed. This makes the behavior of the client more predictable as that was an undocumented side-effect.
If you expect to see instances of a custom type from an untyped api call - typically KubernetesClient.load,
KubernetesClient.resourceList, KubernetesClient.resource(InputStream|String), then you must either create a
META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource file (see above #3923), or make calls to
KubernetesDeserializer.registerCustomKind - however since KubernetesDeserializer is an internal class that mechanism
is not preferred.