
io.fabric8.kubernetes.client.server.mock.KubernetesMockServer Maven / Gradle / Ivy
/*
* 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.api.model.APIResource;
import io.fabric8.kubernetes.api.model.APIResourceBuilder;
import io.fabric8.kubernetes.api.model.APIResourceList;
import io.fabric8.kubernetes.api.model.APIResourceListBuilder;
import io.fabric8.kubernetes.api.model.NamedContext;
import io.fabric8.kubernetes.api.model.NamedContextBuilder;
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;
import io.fabric8.mockwebserver.Context;
import io.fabric8.mockwebserver.DefaultMockServer;
import io.fabric8.mockwebserver.MockWebServer;
import io.fabric8.mockwebserver.ServerRequest;
import io.fabric8.mockwebserver.ServerResponse;
import io.fabric8.mockwebserver.http.Dispatcher;
import io.fabric8.mockwebserver.internal.MockDispatcher;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.function.Consumer;
import java.util.regex.Pattern;
public class KubernetesMockServer extends DefaultMockServer implements Resetable, CustomResourceAware {
private final Map> responses;
private final Dispatcher dispatcher;
private VersionInfo versionInfo;
private List unsupportedPatterns;
public KubernetesMockServer() {
this(true);
}
public KubernetesMockServer(boolean useHttps) {
this(new MockWebServer(), new HashMap<>(), useHttps);
}
public KubernetesMockServer(MockWebServer server, Map> responses,
boolean useHttps) {
this(new Context(Serialization.jsonMapper()), server, responses, useHttps);
}
public KubernetesMockServer(Context context, MockWebServer server,
Map> responses,
boolean useHttps) {
this(context, server, responses, new MockDispatcher(responses), useHttps);
}
public KubernetesMockServer(Context context, MockWebServer server,
Map> responses,
Dispatcher dispatcher, boolean useHttps) {
this(context, server, responses, dispatcher, useHttps,
new VersionInfo.Builder().withMajor("0").withMinor("0").build());
}
public KubernetesMockServer(Context context, MockWebServer server,
Map> responses,
Dispatcher dispatcher, boolean useHttps, VersionInfo versionInfo) {
super(context, server, responses, dispatcher, useHttps);
this.dispatcher = dispatcher;
this.responses = responses;
this.versionInfo = versionInfo;
unsupportedPatterns = Collections.emptyList();
}
@Override
public void onStart() {
expect().get().withPath("/").andReturn(200, new RootPathsBuilder().addToPaths(getRootPaths()).build()).always();
expect().get().withPath("/version").andReply(200, request -> versionInfo).always();
}
public void init() {
start();
}
public void init(InetAddress address, int port) {
start(address, port);
}
public void destroy() {
shutdown();
}
public String[] getRootPaths() {
return new String[] { "/api", "/apis/extensions" };
}
public NamespacedKubernetesClient createClient() {
return createClient(new KubernetesClientBuilderCustomizer());
}
public NamespacedKubernetesClient createClient(HttpClient.Factory factory) {
return createClient(b -> b.withHttpClientFactory(factory));
}
/**
* Creates a client using the cusotmized {@link KubernetesClientBuilder} provided in the {@link Consumer} parameter.
*
* The function is invoked using an initial {@link Config} instance that is initialized with the mock server's
* URL and the {@link TlsVersion} to use.
*
* The following snippet shows how you can use this method in your tests:
*
* @BeforeEach
* void setUp() {
* server = new KubernetesMockServer();
* server.start();
* client = server.createClient(b -> {/* customize builder */}));
* }
* }
*
* @param kubernetesClientBuilderCustomizer Consumer function to enable further customization of the provided
* KubernetesClientBuilder.
* @return a NamespacedKubernetesClient instance from the provided configuration.
*/
public NamespacedKubernetesClient createClient(Consumer kubernetesClientBuilderCustomizer) {
final KubernetesClientBuilder kubernetesClientBuilder = new KubernetesClientBuilder().withConfig(initConfig());
kubernetesClientBuilderCustomizer.accept(kubernetesClientBuilder);
final BaseClient client = kubernetesClientBuilder.build().adapt(BaseClient.class);
client.setMatchingGroupPredicate(s -> unsupportedPatterns.stream().noneMatch(p -> p.matcher(s).find()));
return client.adapt(NamespacedKubernetesClient.class);
}
/**
* Replace the current {@link VersionInfo} instance.
*
* @param versionInfo the new VersionInfo.
*/
public final void setVersionInfo(VersionInfo versionInfo) {
this.versionInfo = Objects.requireNonNull(versionInfo);
}
/**
* Used to exclude support for the given apiGroups.
*
* Each is a simple expression of the form: group[/version]
*
* where * is a wildcard.
*
* For example to exclude all openshift support, you would specify
* openshift.io
*
* To exclude a specific apiVersion, you would fully specify
* route.openshift.io/v1
*
* NOTE this affects calls to {@link Client#hasApiGroup(String, boolean)}
* and {@link Client#supports(Class)}. Other calls to get the full root path or other
* api group metadata will not return valid results in mock scenarios.
*
* @param unsupported apiGroup patterns
*/
public void setUnsupported(String... unsupported) {
this.unsupportedPatterns = new ArrayList<>(unsupported.length);
for (int i = 0; i < unsupported.length; i++) {
String asRegex = unsupported[i].replace(".", "\\.").replace("*", ".*");
if (!asRegex.contains("/")) {
asRegex += "(/.*)?";
}
asRegex += "$";
this.unsupportedPatterns.add(Pattern.compile(asRegex));
}
}
/**
* Removes all recorded expectations.
*/
public void clearExpectations() {
responses.clear();
}
protected Config initConfig() {
final NamedContext mockServerContext = new NamedContextBuilder()
.withName("fabric8-mock-server-context")
.withNewContext()
.withNamespace("test")
.withCluster(String.format("localhost:%d", getPort()))
.withUser("fabric8-mock-server-user")
.endContext()
.build();
return new ConfigBuilder(Config.empty())
.withMasterUrl(url("/"))
.withTrustCerts(true)
.withTlsVersions(TlsVersion.TLS_1_2)
.withNamespace("test")
.withHttp2Disable(true)
.addToContexts(mockServerContext)
.withCurrentContext(mockServerContext)
.withUsername("fabric8-mock-server-user")
.withOauthToken("secret")
.build();
}
@Override
public void reset() {
clearExpectations();
onStart();
unsupportedPatterns.clear();
if (this.dispatcher instanceof Resetable) {
((Resetable) this.dispatcher).reset();
}
}
/**
* Ensure that the server will supply an {@link APIResourceList} containing an {@link APIResource}
* representing the {@link CustomResourceDefinitionContext} from the apis/group/version endpoint.
*
* This is useful when testing calls through the {@link KubernetesClient#genericKubernetesResources(String, String)}
* entry point.
*
* If this is a crud server, the custom resource will be added to the set of previously added resources
* and the resources inferred from custom resource definitions that have been added.
*
* If this server is not crud, this call will add a single expectation for the given resource. Direct handling of
* multiple resources for a given api group/version has not yet been added.
*
* @param rdc the resource definition context
*/
@Override
public void expectCustomResource(CustomResourceDefinitionContext rdc) {
if (this.dispatcher instanceof CustomResourceAware) {
((CustomResourceAware) this.dispatcher).expectCustomResource(rdc);
} else {
expect()
.get()
.withPath(String.format("/apis/%s/%s", rdc.getGroup(), rdc.getVersion()))
.andReturn(HttpURLConnection.HTTP_OK,
new APIResourceListBuilder()
.withResources(
new APIResourceBuilder().withKind(rdc.getKind())
.withNamespaced(rdc.isNamespaceScoped())
.withName(rdc.getPlural())
.build())
.withGroupVersion(ApiVersionUtil.joinApiGroupAndVersion(rdc.getGroup(), rdc.getVersion()))
.build())
.once();
}
}
}