diff --git a/CHANGELOG.md b/CHANGELOG.md index d8f023cc53b..5d8f309a12b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ * Fix #5006: Use BouncyCastle JDK 1.8 compatible jars #### New Features +* Fix #5041: exposed Client.raw methods for arbitrary calls #### _**Note**_: Breaking changes * Fix #4875: Removed unused options from the java-generator diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Client.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Client.java index 28ca35614f4..6f244d72a06 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Client.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Client.java @@ -189,4 +189,23 @@ default > MixedOperat Config getConfiguration(); + /** + * GET the response from the given uri as a String + * + * @param uri must start with / if relative + * @return the response, or null if a 404 code + */ + default String raw(String uri) { + return raw(uri, "GET", null); + } + + /** + * The response from the given uri as a String + * + * @param uri must start with / if relative + * @param method an http method verb such as GET, DELETE, PUT, POST + * @param payload a non-String value will be converted to json + * @return the response + */ + String raw(String uri, String method, Object payload); } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/NamespacedKubernetesClientAdapter.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/NamespacedKubernetesClientAdapter.java index b402a39153b..b7319c25a2e 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/NamespacedKubernetesClientAdapter.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/NamespacedKubernetesClientAdapter.java @@ -420,4 +420,9 @@ public void visitResources(ApiVisitor visitor) { getClient().visitResources(visitor); } + @Override + public String raw(String uri, String method, Object payload) { + return getClient().raw(uri, method, payload); + } + } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ClientAdapter.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ClientAdapter.java index 40f991a200e..eb6a3d6ae30 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ClientAdapter.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ClientAdapter.java @@ -153,4 +153,9 @@ public Client newClient(RequestConfig requestConfig) { public abstract C newInstance(); + @Override + public String raw(String uri, String method, Object payload) { + return client.raw(uri, method, payload); + } + } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpRequest.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpRequest.java index ddf7537c855..d92c85cd481 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpRequest.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpRequest.java @@ -216,7 +216,9 @@ public HttpRequest.Builder method(String method, String contentType, String body this.method = method; this.contentType = contentType; this.bodyAsString = body; - this.body = new StringBodyContent(body); + if (body != null) { + this.body = new StringBodyContent(body); + } return this; } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpResponse.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpResponse.java index fc3db136579..85fd744666d 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpResponse.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpResponse.java @@ -15,6 +15,8 @@ */ package io.fabric8.kubernetes.client.http; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Optional; @@ -82,7 +84,8 @@ public HttpResponse withPreviousResponse(HttpResponse previousResponse) { return this; } - public static TestHttpResponse from(int code, String body) { - return new TestHttpResponse().withCode(code).withBody(body.getBytes(StandardCharsets.UTF_8)); + public static TestHttpResponse from(int code, String body) { + return new TestHttpResponse().withCode(code) + .withBody(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); } } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java index 251940af8dc..8d939af0e2b 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java @@ -44,7 +44,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; @@ -504,11 +503,7 @@ protected T handleApproveOrDeny(T csr, Class type) th * NOTE: Currently does not utilize the retry logic */ protected T handleRawGet(URL resourceUrl, Class type) throws IOException { - HttpRequest.Builder requestBuilder = httpClient.newHttpRequestBuilder().url(resourceUrl); - HttpRequest request = requestBuilder.build(); - HttpResponse response = waitForResult(httpClient.sendAsync(request, type)); - assertResponseCode(request, response); - return response.body(); + return handleRaw(type, resourceUrl.toString(), "GET", null); } /** @@ -585,11 +580,11 @@ protected CompletableFuture handleResponse(HttpClient client, HttpRequest VersionUsageUtils.log(this.resourceT, this.apiGroupVersion); HttpRequest request = requestBuilder.build(); - return client.sendAsync(request, byte[].class).thenApply(response -> { + return client.sendAsync(request, InputStream.class).thenApply(response -> { try { assertResponseCode(request, response); if (type != null && type.getType() != null) { - return Serialization.unmarshal(new ByteArrayInputStream(response.body()), type); + return Serialization.unmarshal(response.body(), type); } else { return null; } @@ -776,4 +771,28 @@ public R1 restCall(Class result, String... path) { } } + /** + * Send a raw request - where the type should be one of String, Reader, InputStream + */ + public R1 handleRaw(Class result, String uri, String method, Object payload) { + try { + if (uri.startsWith("/")) { + // master ends with slash + uri = config.getMasterUrl() + uri.substring(1, uri.length()); + } + String body = null; + if (payload instanceof String) { + body = (String) payload; + } else if (payload != null) { + body = Serialization.asJson(payload); + } + HttpRequest request = httpClient.newHttpRequestBuilder().uri(uri).method(method, JSON, body).build(); + HttpResponse response = waitForResult(httpClient.sendAsync(request, result)); + assertResponseCode(request, response); + return response.body(); + } catch (IOException e) { + throw KubernetesClientException.launderThrowable(e); + } + } + } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/impl/BaseClient.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/impl/BaseClient.java index b867e847297..db97a37ff4f 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/impl/BaseClient.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/impl/BaseClient.java @@ -41,6 +41,7 @@ import io.fabric8.kubernetes.client.utils.ApiVersionUtil; import io.fabric8.kubernetes.client.utils.Utils; +import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.List; @@ -354,4 +355,21 @@ public Executor getExecutor() { return executor; } + @Override + public String raw(String uri) { + try { + return raw(uri, "GET", null); + } catch (KubernetesClientException e) { + if (e.getCode() != HttpURLConnection.HTTP_NOT_FOUND) { + throw e; + } + return null; + } + } + + @Override + public String raw(String uri, String method, Object payload) { + return this.getOperationSupport().handleRaw(String.class, uri, method, payload); + } + } diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/BaseOperationTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/BaseOperationTest.java index 0643cee23cf..043a4485c61 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/BaseOperationTest.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/BaseOperationTest.java @@ -250,7 +250,7 @@ private HttpClient newHttpClientWithSomeFailures(final AtomicInteger httpExecuti HttpRequest.Builder mockRequestBuilder = mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); when(mockClient.newHttpRequestBuilder()).thenReturn(mockRequestBuilder); when(mockRequestBuilder.build()).thenReturn(new StandardHttpRequest.Builder().uri("https://k8s.example.com").build()); - when(mockClient.sendAsync(Mockito.any(), Mockito.eq(byte[].class))).thenAnswer( + when(mockClient.sendAsync(Mockito.any(), Mockito.eq(InputStream.class))).thenAnswer( invocation -> { int count = httpExecutionCounter.getAndIncrement(); if (count < numFailures) { diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/DryRunTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/DryRunTest.java index ae7504bb1eb..8759561748a 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/DryRunTest.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/DryRunTest.java @@ -35,6 +35,7 @@ import org.mockito.Mockito; import java.io.IOException; +import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.List; @@ -58,7 +59,7 @@ public void setUp() throws IOException { builders = new ArrayList<>(); this.mockClient = Mockito.mock(HttpClient.class, Mockito.RETURNS_DEEP_STUBS); Config config = new ConfigBuilder().withMasterUrl("https://localhost:8443/").build(); - when(mockClient.sendAsync(any(), Mockito.eq(byte[].class))) + when(mockClient.sendAsync(any(), Mockito.eq(InputStream.class))) .thenReturn(CompletableFuture.completedFuture(TestHttpResponse.from(200, "{\"kind\":\"Pod\", \"apiVersion\":\"v1\"}"))); kubernetesClient = new KubernetesClientImpl(mockClient, config); diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/PatchTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/PatchTest.java index dbbb7fb7268..a74440a0cfb 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/PatchTest.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/PatchTest.java @@ -35,6 +35,7 @@ import org.mockito.Mockito; import java.io.IOException; +import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; @@ -59,7 +60,7 @@ public void setUp() throws IOException { // TODO: fully mocking makes this logic more difficult and basically copied in other tests, we may want to rely on an actual implementation instead builders = new ArrayList<>(); this.mockClient = Mockito.mock(HttpClient.class, Mockito.RETURNS_DEEP_STUBS); - when(mockClient.sendAsync(any(), Mockito.eq(byte[].class))) + when(mockClient.sendAsync(any(), Mockito.eq(InputStream.class))) .thenReturn(CompletableFuture.completedFuture(TestHttpResponse.from(200, "{}"))); Config config = new ConfigBuilder().withMasterUrl("https://localhost:8443/").build(); kubernetesClient = new KubernetesClientImpl(mockClient, config); @@ -118,8 +119,8 @@ void testYamlPatchConvertedToJson() { @Test void testPatchThrowExceptionWhenResourceNotFound() { // Given - when(mockClient.sendAsync(any(), Mockito.eq(byte[].class))) - .thenReturn(CompletableFuture.completedFuture(new TestHttpResponse().withCode(404))); + when(mockClient.sendAsync(any(), Mockito.eq(InputStream.class))) + .thenReturn(CompletableFuture.completedFuture(new TestHttpResponse().withCode(404))); // When PodResource podResource = kubernetesClient.pods() diff --git a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ClientIT.java b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ClientIT.java new file mode 100644 index 00000000000..6cb1ee986a8 --- /dev/null +++ b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ClientIT.java @@ -0,0 +1,47 @@ +/** + * 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; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.utils.Serialization; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ClientIT { + + KubernetesClient client; + + @Test + void getRaw() { + String node = client.nodes().list().getItems().stream().map(n -> n.getMetadata().getName()).findFirst().orElse("minikube"); + String result = client.raw(String.format("/api/v1/nodes/%s/proxy/stats/summary", node)); + assertTrue(Serialization.unmarshal(result, Map.class).containsKey("node")); + } + + @Test + void postRaw() { + String result = client.raw(String.format("/api/v1/namespaces/%s/configmaps", client.getNamespace()), "POST", new ConfigMapBuilder().withNewMetadata().withName("test").endMetadata().build()); + assertEquals("test", Serialization.unmarshal(result, ConfigMap.class).getMetadata().getName()); + } + +} diff --git a/openshift-client/src/test/java/io/fabric8/openshift/client/dsl/internal/build/BuildConfigOperationsImplTest.java b/openshift-client/src/test/java/io/fabric8/openshift/client/dsl/internal/build/BuildConfigOperationsImplTest.java index 6a38bafb298..e1f4d47bee6 100644 --- a/openshift-client/src/test/java/io/fabric8/openshift/client/dsl/internal/build/BuildConfigOperationsImplTest.java +++ b/openshift-client/src/test/java/io/fabric8/openshift/client/dsl/internal/build/BuildConfigOperationsImplTest.java @@ -28,6 +28,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -106,9 +107,9 @@ protected String getRecentEvents() { // When ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[0]); - CompletableFuture> future = new CompletableFuture<>(); + CompletableFuture> future = new CompletableFuture<>(); future.completeExceptionally(new IOException()); - when(httpClient.sendAsync(any(), eq(byte[].class))).thenReturn(future); + when(httpClient.sendAsync(any(), eq(InputStream.class))).thenReturn(future); KubernetesClientException exception = assertThrows(KubernetesClientException.class, () -> impl.submitToApiServer(inputStream, 0)); @@ -127,11 +128,11 @@ protected String getRecentEvents() { }; }; - HttpResponse response = mock(HttpResponse.class, Mockito.CALLS_REAL_METHODS); + HttpResponse response = mock(HttpResponse.class, Mockito.CALLS_REAL_METHODS); when(response.code()).thenReturn(200); - when(response.body()).thenReturn(new byte[0]); + when(response.body()).thenReturn(new ByteArrayInputStream(new byte[0])); - when(httpClient.sendAsync(any(), eq(byte[].class))).thenReturn(CompletableFuture.completedFuture(response)); + when(httpClient.sendAsync(any(), eq(InputStream.class))).thenReturn(CompletableFuture.completedFuture(response)); impl.submitToApiServer(new ByteArrayInputStream(new byte[0]), 0); Mockito.verify(response, Mockito.times(1)).body();