Skip to content

Commit

Permalink
feat: KubernetesMockServer + JUnit5 extension support clint builder c…
Browse files Browse the repository at this point in the history
…ustomization

Signed-off-by: Marc Nuri <marc@marcnuri.com>
  • Loading branch information
manusa committed Jul 21, 2023
1 parent ffe1b2a commit 5251f4a
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 38 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,4 +42,12 @@

boolean crud() default false;

/**
* No-arg constructor class implementing {@link Function} interface that returns {@link KubernetesClientBuilder} instance.
* <p>
* Enables the customization of the automatically bootstrapped and injected
* {@link io.fabric8.kubernetes.client.KubernetesClient} instance.
*/
Class<? extends Function<String, KubernetesClientBuilder>> kubernetesClientBuilder() default KubernetesMockClientKubernetesClientBuilder.class;

}
Original file line number Diff line number Diff line change
@@ -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<String, KubernetesClientBuilder> {

@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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -121,21 +118,17 @@ public String[] getRootPaths() {
}

public NamespacedKubernetesClient createClient() {
return createClient(ignored -> {
});
return createClient(new KubernetesMockClientKubernetesClientBuilder());
}

public NamespacedKubernetesClient createClient(Consumer<KubernetesClientBuilder> 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<String, KubernetesClientBuilder> 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);
}

/**
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -98,13 +100,20 @@ protected void setFieldIfEqualsToProvidedType(ExtensionContext context, boolean
protected void initializeKubernetesClientAndMockServer(Class<?> testClass) {
EnableKubernetesMockClient a = testClass.getAnnotation(EnableKubernetesMockClient.class);
final Map<ServerRequest, Queue<ServerResponse>> 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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> {

private StringAppenderSerializer() {
super(String.class);
}

@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(s + "-extended");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 5251f4a

Please sign in to comment.