Skip to content

Commit

Permalink
feat: customizable KubernetesClient-specific ObjectMapper and Kuberne…
Browse files Browse the repository at this point in the history
…tesSerialization

Signed-off-by: Marc Nuri <[email protected]>
  • Loading branch information
manusa committed Jun 9, 2023
1 parent 51a2d56 commit 2f85dad
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 4 deletions.
43 changes: 43 additions & 0 deletions docs/src/main/asciidoc/kubernetes-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ In dev mode and when running tests, xref:kubernetes-dev-services.adoc[Dev Servic

Quarkus provides multiple integration points for influencing the Kubernetes Client provided as a CDI bean.

==== Kubernetes Client Config customization

The first integration point is the use of the `io.quarkus.kubernetes.client.KubernetesConfigCustomizer` interface. When such a bean exists,
it allows for arbitrary customizations of the `io.fabric8.kubernetes.client.Config` created by Quarkus (which takes into account the `quarkus.kubernetes-client.*` properties).

Expand All @@ -82,6 +84,47 @@ public class KubernetesClientProducer {
}
----

==== Kubernetes Client ObjectMapper customization

The Fabric8 Kubernetes Client uses its own `ObjectMapper` instance for serialization and deserialization of Kubernetes resources.
This mapper is provided to the client through a `KubernetesSerialization` instance that's injected into
the `KubernetesClient` bean.

If for some reason you must customize the default `ObjectMapper` bean provided by this extension and used by the Kubernetes Client, you can do so by declaring a bean that implements the `KubernetesClientObjectMapperCustomizer` interface.

The following code snippet contains an example of a `KubernetesClientObjectMapperCustomizer` to set the `ObjectMapper` locale:

[source,java]
----
@Singleton
public static class Customizer implements KubernetesClientObjectMapperCustomizer {
@Override
public void customize(ObjectMapper objectMapper) {
objectMapper.setLocale(Locale.ROOT);
}
}
----

Furthermore, if you must replace the default `ObjectMapper` bean used by the Kubernetes Client that the extension creates automatically, you can do so by declaring a bean of type `@KubernetesClientObjectMapper`.
The following code snippet shows how you can declare this bean:

[source,java]
----
@Singleton
public class KubernetesObjectMapperProducer {
@KubernetesClientObjectMapper
@Singleton
@Produces
public ObjectMapper kubernetesClientObjectMapper() {
return new ObjectMapper();
}
}
----


WARNING: The static `io.fabric8.kubernetes.client.utils.Serialization` utils class is deprecated and should not be used.
Access to `Serialization.jsonMapper()` should be replaced by the usage of @KubernetesClientObjectMapperCustomizer` declared beans.

== Testing

To make testing against a mock Kubernetes API extremely simple, Quarkus provides the `WithKubernetesTestServer` annotation which automatically launches
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@
import io.quarkus.deployment.util.JandexUtil;
import io.quarkus.jackson.deployment.IgnoreJsonDeserializeClassBuildItem;
import io.quarkus.kubernetes.client.runtime.KubernetesClientBuildConfig;
import io.quarkus.kubernetes.client.runtime.KubernetesClientObjectMapperProducer;
import io.quarkus.kubernetes.client.runtime.KubernetesClientProducer;
import io.quarkus.kubernetes.client.runtime.KubernetesConfigProducer;
import io.quarkus.kubernetes.client.runtime.KubernetesSerializationProducer;
import io.quarkus.kubernetes.client.spi.KubernetesClientCapabilityBuildItem;
import io.quarkus.maven.dependency.ArtifactKey;

Expand All @@ -77,6 +79,9 @@ public class KubernetesClientProcessor {
@BuildStep
public void registerBeanProducers(BuildProducer<AdditionalBeanBuildItem> additionalBeanBuildItemBuildItem,
Capabilities capabilities) {
additionalBeanBuildItemBuildItem
.produce(AdditionalBeanBuildItem.unremovableOf(KubernetesClientObjectMapperProducer.class));
additionalBeanBuildItemBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(KubernetesSerializationProducer.class));
// wire up the Config bean support
additionalBeanBuildItemBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(KubernetesConfigProducer.class));
// do not register our client producer if the openshift client is present, because it provides it too
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.quarkus.kubernetes.client.deployment;

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.quarkus.kubernetes.client.KubernetesClientObjectMapper;
import io.quarkus.kubernetes.client.KubernetesClientObjectMapperCustomizer;
import io.quarkus.test.QuarkusUnitTest;

public class KubernetesClientObjectMapperCDITest {

@Inject
KubernetesClient kubernetesClient;

@Inject
@KubernetesClientObjectMapper
ObjectMapper objectMapper;

@Test
public void kubernetesClientObjectMapperCustomizer() throws JsonProcessingException {
final var result = objectMapper.readValue("{\"quarkusName\":\"the-name\"}", ObjectMeta.class);
assertEquals("the-name", result.getName());
}

@Test
public void kubernetesClientUsesCustomizedObjectMapper() {
final var result = kubernetesClient.getKubernetesSerialization()
.unmarshal("{\"quarkusName\":\"the-name\"}", ObjectMeta.class);
assertEquals("the-name", result.getName());
}

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(KubernetesClientCDITest.Customizer.class))
.overrideConfigKey("quarkus.kubernetes-client.devservices.enabled", "false");

@Singleton
public static class Customizer implements KubernetesClientObjectMapperCustomizer {
@Override
public void customize(ObjectMapper objectMapper) {
objectMapper.addMixIn(ObjectMeta.class, ObjectMetaMixin.class);
}

private static final class ObjectMetaMixin {
@JsonProperty("quarkusName")
String name;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public class KubernetesClientUtils {

private static final String PREFIX = "quarkus.kubernetes-client.";

private KubernetesClientUtils() {
}

public static Config createConfig(KubernetesClientBuildConfig buildConfig, TlsConfig tlsConfig) {
Config base = Config.autoConfigure(null);
boolean trustAll = buildConfig.trustCerts.isPresent() ? buildConfig.trustCerts.get() : tlsConfig.trustAll;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.kubernetes.client;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.inject.Qualifier;

/**
* {@link Qualifier} to inject the Fabric8 Kubernetes Client specific {@link com.fasterxml.jackson.databind.ObjectMapper}.
* <p>
* Allows users to modify the behavior of the mapper for very specific use cases (such as adding Kotlin-specific modules).
* Otherwise, it's not recommended to modify the mapper since it might break the Kubernetes Client.
*/
@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER, TYPE })
public @interface KubernetesClientObjectMapper {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.quarkus.kubernetes.client;

import java.util.List;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
import io.quarkus.kubernetes.client.runtime.KubernetesClientObjectMapperProducer;
import io.quarkus.kubernetes.client.runtime.KubernetesClientProducer;
import io.quarkus.kubernetes.client.runtime.KubernetesSerializationProducer;

/**
* Allow the provision of beans that customize the default {@link ObjectMapper} used by the KubernetesClient to
* perform kubernetes-specific serialization and deserialization operations.
* <p>
* The resulting {@link ObjectMapper} is used to produce the default {@link KubernetesSerialization} bean, which is in turn
* used to produce the default {@link io.fabric8.kubernetes.client.KubernetesClient} bean.
* <p>
* The following code snippet shows how to provide a KubernetesClientObjectMapperCustomizer:
*
* <pre>{@code
*
* &#64;Singleton
* public static class Customizer implements KubernetesClientObjectMapperCustomizer {
* @Override
* public void customize(ObjectMapper objectMapper) {
* objectMapper.setLocale(Locale.ROOT);
* }
* }
*
* }</pre>
*
* @see KubernetesClientObjectMapperProducer#kubernetesClientObjectMapper(List)
* @see KubernetesSerializationProducer#kubernetesSerialization(ObjectMapper, Class[])
* @see KubernetesClientProducer#kubernetesClient(KubernetesSerialization, Config)
*/
public interface KubernetesClientObjectMapperCustomizer {
void customize(ObjectMapper objectMapper);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
import io.quarkus.kubernetes.client.runtime.KubernetesClientBuildConfig;
import io.quarkus.kubernetes.client.runtime.KubernetesClientProducer;
import io.quarkus.kubernetes.client.runtime.KubernetesConfigProducer;
Expand All @@ -16,8 +17,8 @@
* The {@link Config} is in turn used to produce the default {@link KubernetesClient}
* <p>
*
* @see KubernetesConfigProducer#config(KubernetesClientBuildConfig, TlsConfig, List) }
* @see KubernetesClientProducer#kubernetesClient(Config) }
* @see KubernetesConfigProducer#config(KubernetesClientBuildConfig, TlsConfig, List)
* @see KubernetesClientProducer#kubernetesClient(KubernetesSerialization, Config)
*/
public interface KubernetesConfigCustomizer {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.kubernetes.client.runtime;

import java.util.List;

import jakarta.annotation.Priority;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.arc.All;
import io.quarkus.arc.DefaultBean;
import io.quarkus.kubernetes.client.KubernetesClientObjectMapper;
import io.quarkus.kubernetes.client.KubernetesClientObjectMapperCustomizer;

@Singleton
public class KubernetesClientObjectMapperProducer {

@KubernetesClientObjectMapper
@DefaultBean
@Priority(Integer.MIN_VALUE)
@Singleton
@Produces
public ObjectMapper kubernetesClientObjectMapper(@All List<KubernetesClientObjectMapperCustomizer> customizers) {
final var result = new ObjectMapper();
for (var customizer : customizers) {
customizer.customize(result);
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
import io.quarkus.arc.DefaultBean;

@Singleton
Expand All @@ -17,8 +18,9 @@ public class KubernetesClientProducer {
@DefaultBean
@Singleton
@Produces
public KubernetesClient kubernetesClient(Config config) {
client = new KubernetesClientBuilder().withConfig(config).build();
public KubernetesClient kubernetesClient(KubernetesSerialization kubernetesSerialization, Config config) {
client = new KubernetesClientBuilder()
.withKubernetesSerialization(kubernetesSerialization).withConfig(config).build();
return client;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkus.kubernetes.client.runtime;

import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
import io.quarkus.arc.DefaultBean;
import io.quarkus.kubernetes.client.KubernetesClientObjectMapper;

@Singleton
public class KubernetesSerializationProducer {

@DefaultBean
@Singleton
@Produces
public KubernetesSerialization kubernetesSerialization(@KubernetesClientObjectMapper ObjectMapper objectMapper) {
final var kubernetesSerialization = new KubernetesSerialization(objectMapper, false);
KubernetesClientUtils.scanKubernetesResources().forEach(kubernetesSerialization::registerKubernetesResource);
return kubernetesSerialization;
}
}

0 comments on commit 2f85dad

Please sign in to comment.