From 5251f4a881bde4c5dcbcdc1342ba3b23e685af08 Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Fri, 21 Jul 2023 11:56:13 +0200 Subject: [PATCH] feat: KubernetesMockServer + JUnit5 extension support clint builder customization Signed-off-by: Marc Nuri --- CHANGELOG.md | 2 +- .../mock/EnableKubernetesMockClient.java | 10 +++ ...etesMockClientKubernetesClientBuilder.java | 41 ++++++++++ .../server/mock/KubernetesMockServer.java | 41 ++++------ .../mock/KubernetesMockServerExtension.java | 21 +++-- ...rExtensionKubernetesClientBuilderTest.java | 82 +++++++++++++++++++ .../server/mock/OpenShiftMockServer.java | 7 +- 7 files changed, 166 insertions(+), 38 deletions(-) create mode 100644 junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockClientKubernetesClientBuilder.java create mode 100644 junit/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockServerExtensionKubernetesClientBuilderTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a44c43570bf..5fa962d2f33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,6 @@ * Fix #5186: Support for Pod uploads with big numbers * Fix #5221: Empty kube config file causes NPE * Fix #5281: Ensure the KubernetesCrudDispatcher's backing map is accessed w/lock -* Fix #5293: Ensured the mock server uses only generic or JsonNode parsing * Fix #5298: Prevent requests needing authentication from causing a 403 response * Fix #5327: Ensured that the informer reconnect task terminates after client close * Fix #5113: Clashing package names in trigger model dependencies @@ -17,6 +16,7 @@ * Fix #5233: Generalized SchemaSwap to allow for cycle expansion * Fix #5262: all built-in collections will omit empty in their serialized form. * Fix #5287: Add an option to filter the files processed by the java-generator, based on a suffix allowlist +* Fix #5293: Mock server supports KubernetesClientBuilder customization * Fix #5315: Introduced `kubernetes-junit-jupiter-autodetect` to use with [automatic extension registration](https://junit.org/junit5/docs/current/user-guide/#extensions-registration-automatic) * Fix #5339: `@PrinterColumn` annotation has configuration field for priority diff --git a/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/EnableKubernetesMockClient.java b/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/EnableKubernetesMockClient.java index 1a98ed2f49f..700d13ef71f 100644 --- a/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/EnableKubernetesMockClient.java +++ b/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/EnableKubernetesMockClient.java @@ -16,10 +16,12 @@ package io.fabric8.kubernetes.client.server.mock; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.junit.jupiter.api.extension.ExtendWith; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import java.util.function.Function; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.METHOD; @@ -40,4 +42,12 @@ boolean crud() default false; + /** + * No-arg constructor class implementing {@link Function} interface that returns {@link KubernetesClientBuilder} instance. + *

+ * Enables the customization of the automatically bootstrapped and injected + * {@link io.fabric8.kubernetes.client.KubernetesClient} instance. + */ + Class> kubernetesClientBuilder() default KubernetesMockClientKubernetesClientBuilder.class; + } diff --git a/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockClientKubernetesClientBuilder.java b/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockClientKubernetesClientBuilder.java new file mode 100644 index 00000000000..0aa16919b19 --- /dev/null +++ b/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockClientKubernetesClientBuilder.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.fabric8.kubernetes.client.server.mock; + +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.http.TlsVersion; + +import java.util.function.Function; + +public class KubernetesMockClientKubernetesClientBuilder implements Function { + + @Override + public KubernetesClientBuilder apply(String url) { + return new KubernetesClientBuilder().withConfig(initConfig(url)); + } + + protected Config initConfig(String url) { + return new ConfigBuilder(Config.empty()) + .withMasterUrl(url) + .withTrustCerts(true) + .withTlsVersions(TlsVersion.TLS_1_2) + .withNamespace("test") + .withHttp2Disable(true) + .build(); + } +} diff --git a/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockServer.java b/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockServer.java index 33cebdf1a79..0731e62132f 100644 --- a/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockServer.java +++ b/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockServer.java @@ -21,15 +21,12 @@ import io.fabric8.kubernetes.api.model.APIResourceListBuilder; import io.fabric8.kubernetes.api.model.RootPathsBuilder; import io.fabric8.kubernetes.client.Client; -import io.fabric8.kubernetes.client.Config; -import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.NamespacedKubernetesClient; import io.fabric8.kubernetes.client.VersionInfo; import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; import io.fabric8.kubernetes.client.http.HttpClient; -import io.fabric8.kubernetes.client.http.TlsVersion; import io.fabric8.kubernetes.client.impl.BaseClient; import io.fabric8.kubernetes.client.utils.ApiVersionUtil; import io.fabric8.kubernetes.client.utils.Serialization; @@ -52,7 +49,7 @@ import java.util.Map; import java.util.Objects; import java.util.Queue; -import java.util.function.Consumer; +import java.util.function.Function; import java.util.regex.Pattern; public class KubernetesMockServer extends DefaultMockServer implements Resetable, CustomResourceAware { @@ -121,21 +118,17 @@ public String[] getRootPaths() { } public NamespacedKubernetesClient createClient() { - return createClient(ignored -> { - }); + return createClient(new KubernetesMockClientKubernetesClientBuilder()); } - public NamespacedKubernetesClient createClient(Consumer customizer) { - KubernetesClientBuilder builder = new KubernetesClientBuilder().withConfig(getMockConfiguration()); - customizer.accept(builder); - KubernetesClient client = builder.build(); - client.adapt(BaseClient.class) - .setMatchingGroupPredicate(s -> unsupportedPatterns.stream().noneMatch(p -> p.matcher(s).find())); - return client.adapt(NamespacedKubernetesClient.class); + public NamespacedKubernetesClient createClient(HttpClient.Factory factory) { + return createClient(url -> new KubernetesMockClientKubernetesClientBuilder().apply(url).withHttpClientFactory(factory)); } - public NamespacedKubernetesClient createClient(HttpClient.Factory factory) { - return createClient(builder -> builder.withHttpClientFactory(factory)); + public NamespacedKubernetesClient createClient(Function kubernetesClientBuilder) { + final BaseClient client = kubernetesClientBuilder.apply(url("/")).build().adapt(BaseClient.class); + client.setMatchingGroupPredicate(s -> unsupportedPatterns.stream().noneMatch(p -> p.matcher(s).find())); + return client.adapt(NamespacedKubernetesClient.class); } /** @@ -185,19 +178,13 @@ public void clearExpectations() { responses.clear(); } - protected Config getMockConfiguration() { - return new ConfigBuilder(Config.empty()) - .withMasterUrl(url("/")) - .withTrustCerts(true) - .withTlsVersions(TlsVersion.TLS_1_2) - .withNamespace("test") - .withHttp2Disable(true) - .build(); - } - + /** + * @deprecated Use {@code client.adapt(NamespacedServiceCatalogClient.class)} instead. + * @return A {@link NamespacedServiceCatalogClient} instance. + */ + @Deprecated public NamespacedServiceCatalogClient createServiceCatalog() { - Config config = this.getMockConfiguration(); - return new DefaultServiceCatalogClient(config); + return new DefaultServiceCatalogClient(createClient()); } @Override diff --git a/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockServerExtension.java b/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockServerExtension.java index e3bd1e9f343..a78d51f2378 100644 --- a/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockServerExtension.java +++ b/junit/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockServerExtension.java @@ -23,6 +23,8 @@ import io.fabric8.mockwebserver.Context; import io.fabric8.mockwebserver.ServerRequest; import io.fabric8.mockwebserver.ServerResponse; +import io.fabric8.mockwebserver.internal.MockDispatcher; +import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; @@ -98,13 +100,20 @@ protected void setFieldIfEqualsToProvidedType(ExtensionContext context, boolean protected void initializeKubernetesClientAndMockServer(Class testClass) { EnableKubernetesMockClient a = testClass.getAnnotation(EnableKubernetesMockClient.class); final Map> responses = new HashMap<>(); - mock = a.crud() - ? new KubernetesMockServer(new Context(Serialization.jsonMapper()), new MockWebServer(), responses, - new KubernetesMixedDispatcher(responses), - a.https()) - : new KubernetesMockServer(a.https()); + final Dispatcher dispatcher; + if (a.crud()) { + dispatcher = new KubernetesMixedDispatcher(responses); + } else { + dispatcher = new MockDispatcher(responses); + } + mock = new KubernetesMockServer(new Context(Serialization.jsonMapper()), new MockWebServer(), responses, dispatcher, + a.https()); mock.init(); - client = mock.createClient(); + try { + client = mock.createClient(a.kubernetesClientBuilder().getConstructor().newInstance()); + } catch (Exception e) { + throw new IllegalArgumentException("The provided kubernetesClientBuilder is invalid", e); + } } protected void destroy() { diff --git a/junit/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockServerExtensionKubernetesClientBuilderTest.java b/junit/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockServerExtensionKubernetesClientBuilderTest.java new file mode 100644 index 00000000000..b1cb8d1fa78 --- /dev/null +++ b/junit/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/KubernetesMockServerExtensionKubernetesClientBuilderTest.java @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.fabric8.kubernetes.client.server.mock; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.utils.KubernetesSerialization; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +@EnableKubernetesMockClient(crud = true, kubernetesClientBuilder = KubernetesMockServerExtensionKubernetesClientBuilderTest.CustomSerialization.class) +class KubernetesMockServerExtensionKubernetesClientBuilderTest { + + KubernetesClient client; + + @Test + void usesCustomMapper() { + // Given + final Pod pod = new PodBuilder().withNewMetadata().withName("name").endMetadata().build(); + // When + client.pods().resource(pod).create(); + // Then + assertThat(client.pods()) + .returns(null, pr -> pr.withName("name").get()) + .extracting(pr -> pr.withName("name-extended").get()) + .isNotNull(); + } + + public static final class CustomSerialization extends KubernetesMockClientKubernetesClientBuilder { + @Override + public KubernetesClientBuilder apply(String url) { + final KubernetesClientBuilder kubernetesClientBuilder = super.apply(url); + final ObjectMapper customMapper = new ObjectMapper(); + customMapper.addMixIn(ObjectMeta.class, ObjectMetaMixin.class); + kubernetesClientBuilder.withKubernetesSerialization(new KubernetesSerialization(customMapper, true)); + return kubernetesClientBuilder; + } + + private static final class ObjectMetaMixin { + @JsonSerialize(using = StringAppenderSerializer.class) + @JsonProperty("name") + String name; + } + + private static final class StringAppenderSerializer extends StdSerializer { + + private StringAppenderSerializer() { + super(String.class); + } + + @Override + public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeString(s + "-extended"); + } + } + } +} diff --git a/junit/openshift-server-mock/src/main/java/io/fabric8/openshift/client/server/mock/OpenShiftMockServer.java b/junit/openshift-server-mock/src/main/java/io/fabric8/openshift/client/server/mock/OpenShiftMockServer.java index 5b630a24926..8bde6bdc0a2 100644 --- a/junit/openshift-server-mock/src/main/java/io/fabric8/openshift/client/server/mock/OpenShiftMockServer.java +++ b/junit/openshift-server-mock/src/main/java/io/fabric8/openshift/client/server/mock/OpenShiftMockServer.java @@ -20,7 +20,6 @@ import io.fabric8.mockwebserver.Context; import io.fabric8.mockwebserver.ServerRequest; import io.fabric8.mockwebserver.ServerResponse; -import io.fabric8.openshift.client.DefaultOpenShiftClient; import io.fabric8.openshift.client.NamespacedOpenShiftClient; import io.fabric8.openshift.client.OpenShiftConfig; import okhttp3.mockwebserver.Dispatcher; @@ -55,9 +54,9 @@ public String[] getRootPaths() { } public NamespacedOpenShiftClient createOpenShiftClient() { - OpenShiftConfig config = OpenShiftConfig.wrap(getMockConfiguration()); - config.setDisableApiGroupCheck(disableApiGroupCheck); - return new DefaultOpenShiftClient(config); + final NamespacedOpenShiftClient client = createClient().adapt(NamespacedOpenShiftClient.class); + ((OpenShiftConfig) client.getConfiguration()).setDisableApiGroupCheck(disableApiGroupCheck); + return client; } public boolean isDisableApiGroupCheck() {