Skip to content

Commit

Permalink
Merge pull request quarkusio#33767 from manusa/deps/kubernetes-client
Browse files Browse the repository at this point in the history
Bump kubernetes-client-bom from 6.6.2 to 6.7.1
  • Loading branch information
geoand committed Jun 12, 2023
2 parents 503d9e5 + fea1aee commit 35cb41a
Show file tree
Hide file tree
Showing 15 changed files with 362 additions and 5 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 java.util.HashSet;
import java.util.ServiceLoader;
import java.util.Set;

import jakarta.inject.Singleton;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.DotName;
import org.jboss.jandex.Type;

import io.fabric8.kubernetes.api.model.KubernetesResource;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.quarkus.kubernetes.client.KubernetesResources;
import io.quarkus.kubernetes.client.runtime.KubernetesSerializationRecorder;
import io.quarkus.kubernetes.client.spi.KubernetesResourcesBuildItem;

public class KubernetesResourceBuildStep {
@BuildStep
void scanKubernetesResourceClasses(
BuildProducer<ServiceProviderBuildItem> serviceProviderProducer,
BuildProducer<KubernetesResourcesBuildItem> kubernetesResourcesBuildItemBuildProducer) {
serviceProviderProducer.produce(ServiceProviderBuildItem.allProvidersFromClassPath(KubernetesResource.class.getName()));
final Set<String> resourceClasses = new HashSet<>();
final var serviceLoader = ServiceLoader.load(KubernetesResource.class);
for (var kr : serviceLoader) {
final var className = kr.getClass().getName();
// Filter build-time only available classes from those that are really available at runtime
// e.g. The Kubernetes extension provides KubernetesResource classes only for deployment purposes (not prod code)
if (QuarkusClassLoader.isClassPresentAtRuntime(className)) {
resourceClasses.add(className);
}
}
kubernetesResourcesBuildItemBuildProducer.produce(
new KubernetesResourcesBuildItem(resourceClasses.toArray(String[]::new)));
}

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
SyntheticBeanBuildItem kubernetesResourceClasses(
KubernetesSerializationRecorder recorder,
KubernetesResourcesBuildItem kubernetesResourcesBuildItem,
BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(KubernetesResources.class));
final var classArray = Type.create(DotName.createSimple(Class[].class.getName()), Type.Kind.ARRAY);
return SyntheticBeanBuildItem
.configure(Object.class).providerType(classArray).addType(classArray)
.scope(Singleton.class)
.qualifiers(AnnotationInstance.builder(KubernetesResources.class).build())
.runtimeValue(recorder.initKubernetesResources(kubernetesResourcesBuildItem.getResourceClasses()))
.done();
}
}
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.runtime;

import java.util.HashSet;
import java.util.Set;

import io.fabric8.kubernetes.api.model.KubernetesResource;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;

@Recorder
public class KubernetesSerializationRecorder {

public RuntimeValue<Class<? extends KubernetesResource>[]> initKubernetesResources(String[] resourceClassNames) {
final Set<Class<?>> resourceClasses = new HashSet<>();
for (var resourceClassName : resourceClassNames) {
try {
resourceClasses.add(
Class.forName(resourceClassName, false, KubernetesSerializationRecorder.class.getClassLoader()));
} catch (ClassNotFoundException e) {
}
}
return new RuntimeValue<>(resourceClasses.toArray(Class[]::new));
}
}
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,18 @@
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;

@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER, TYPE })
public @interface KubernetesResources {
}
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
Loading

0 comments on commit 35cb41a

Please sign in to comment.