From 6bd6bad385b4dc5e7856701dae5a0af281c72ca2 Mon Sep 17 00:00:00 2001 From: Steve Hawkins Date: Tue, 18 Oct 2022 07:36:32 -0400 Subject: [PATCH] fix #4355: adding logic to set/validate the container name --- CHANGELOG.md | 1 + .../internal/core/v1/PodOperationsImpl.java | 58 +++- .../dsl/internal/uploadable/PodUpload.java | 4 - .../kubernetes/client/mock/PodTest.java | 315 ++++++++++++++---- 4 files changed, 293 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f719b8a399..973824041af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * Fix #4348: Introduce specific annotations for the generators * Refactor #4441: refactoring `TokenRefreshInterceptor` * Fix #4365: The Watch retry logic will handle more cases, as well as perform an exceptional close for events that are not properly handled. Informers can directly provide those exceptional outcomes via the SharedIndexInformer.stopped CompletableFuture. +* Fix #4355: for exec, attach, upload, and copy operations the container id/name will be validated or chosen prior to the remote call. You may also use the kubectl.kubernetes.io/default-container annotation to specify the default container. * Fix #4396: Provide more error context when @Group/@Version annotations are missing * Fix #4384: The Java generator now supports the generation of specific annotations (min, max, pattern, etc.), as defined by #4348 * Fix #3864: Change ManagedOpenShiftClient OSGi ConfigurationPolicy to REQUIRE diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/PodOperationsImpl.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/PodOperationsImpl.java index dc0bcbe9106..f5426040f86 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/PodOperationsImpl.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/PodOperationsImpl.java @@ -15,6 +15,7 @@ */ package io.fabric8.kubernetes.client.dsl.internal.core.v1; +import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.DeleteOptions; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Pod; @@ -59,6 +60,8 @@ import io.fabric8.kubernetes.client.utils.Utils; import io.fabric8.kubernetes.client.utils.internal.Base64; import io.fabric8.kubernetes.client.utils.internal.PodOperationUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.BufferedOutputStream; import java.io.File; @@ -78,6 +81,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -90,6 +94,9 @@ public class PodOperationsImpl extends HasMetadataOperation= 1 ? command : EMPTY_COMMAND; try { - URL url = getExecURLWithCommandParams(actualCommands); + URL url = getURL("exec", actualCommands); return setupConnectionToPod(url.toURI()); } catch (Throwable t) { throw KubernetesClientException.launderThrowable(forOperationType("exec"), t); @@ -282,28 +289,55 @@ public ExecWatch exec(String... command) { @Override public ExecWatch attach() { try { - URL url = getAttachURL(); + URL url = getURL("attach", null); return setupConnectionToPod(url.toURI()); } catch (Throwable t) { throw KubernetesClientException.launderThrowable(forOperationType("attach"), t); } } - private URL getExecURLWithCommandParams(String[] commands) throws MalformedURLException { - String url = URLUtils.join(getResourceUrl().toString(), "exec"); + private URL getURL(String operation, String[] commands) throws MalformedURLException { + String url = URLUtils.join(getResourceUrl().toString(), operation); URLBuilder httpUrlBuilder = new URLBuilder(url); - for (String cmd : commands) { - httpUrlBuilder.addQueryParameter("command", cmd); + if (commands != null) { + for (String cmd : commands) { + httpUrlBuilder.addQueryParameter("command", cmd); + } } - getContext().addQueryParameters(httpUrlBuilder); + PodOperationContext contextToUse = getContext(); + contextToUse = contextToUse.withContainerId(validateOrDefaultContainerId(contextToUse.getContainerId())); + contextToUse.addQueryParameters(httpUrlBuilder); return httpUrlBuilder.build(); } - private URL getAttachURL() throws MalformedURLException { - String url = URLUtils.join(getResourceUrl().toString(), "attach"); - URLBuilder httpUrlBuilder = new URLBuilder(url); - getContext().addQueryParameters(httpUrlBuilder); - return httpUrlBuilder.build(); + /** + * If not specified, choose an appropriate default container id + */ + String validateOrDefaultContainerId(String name) { + Pod pod = this.require(); + List containers = pod.getSpec().getContainers(); + if (containers.isEmpty()) { + throw new KubernetesClientException("Pod has no containers!"); + } + if (name == null) { + name = pod.getMetadata().getAnnotations().get(DEFAULT_CONTAINER_ANNOTATION_NAME); + if (name != null && !hasContainer(containers, name)) { + LOG.warn("Default container {} from annotation not found in pod {}", name, pod.getMetadata().getName()); + name = null; + } + if (name == null) { + name = containers.get(0).getName(); + LOG.debug("using first container {} in pod {}", name, pod.getMetadata().getName()); + } + } else if (!hasContainer(containers, name)) { + throw new KubernetesClientException( + String.format("container %s not found in pod %s", name, pod.getMetadata().getName())); + } + return name; + } + + private boolean hasContainer(List containers, String toFind) { + return containers.stream().map(Container::getName).anyMatch(s -> s.equals(toFind)); } private ExecWebSocketListener setupConnectionToPod(URI uri) { diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/uploadable/PodUpload.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/uploadable/PodUpload.java index 2b7dd58dc78..5ad38ee4f92 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/uploadable/PodUpload.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/uploadable/PodUpload.java @@ -63,10 +63,6 @@ private static interface UploadProcessor { private static boolean upload(PodOperationsImpl operation, String command, UploadProcessor processor) throws IOException { operation = operation.redirectingInput().terminateOnError(); - String containerId = operation.getContext().getContainerId(); - if (Utils.isNotNullOrEmpty(containerId)) { - operation = operation.inContainer(containerId); - } CompletableFuture exitFuture; try (ExecWatch execWatch = operation.exec("sh", "-c", command)) { OutputStream out = execWatch.getInput(); diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PodTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PodTest.java index d83a9277483..6f9168da497 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PodTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PodTest.java @@ -95,15 +95,27 @@ void setUp() { @Test void testList() { server.expect().withPath("/api/v1/namespaces/test/pods").andReturn(200, new PodListBuilder().build()).once(); - server.expect().withPath("/api/v1/namespaces/ns1/pods").andReturn(200, new PodListBuilder() - .addNewItem().and() - .addNewItem().and().build()).once(); + server.expect() + .withPath("/api/v1/namespaces/ns1/pods") + .andReturn(200, new PodListBuilder() + .addNewItem() + .and() + .addNewItem() + .and() + .build()) + .once(); - server.expect().withPath("/api/v1/pods").andReturn(200, new PodListBuilder() - .addNewItem().and() - .addNewItem().and() - .addNewItem() - .and().build()).once(); + server.expect() + .withPath("/api/v1/pods") + .andReturn(200, new PodListBuilder() + .addNewItem() + .and() + .addNewItem() + .and() + .addNewItem() + .and() + .build()) + .once(); PodList podList = client.pods().list(); assertNotNull(podList); @@ -121,13 +133,19 @@ void testList() { @Test void testListWithLabels() { server.expect() - .withPath("/api/v1/namespaces/test/pods?labelSelector=" + Utils.toUrlEncoded("key1=value1,key2=value2,key3=value3")) - .andReturn(200, new PodListBuilder().build()).always(); - server.expect().withPath("/api/v1/namespaces/test/pods?labelSelector=" + Utils.toUrlEncoded("key1=value1,key2=value2")) + .withPath( + "/api/v1/namespaces/test/pods?labelSelector=" + Utils.toUrlEncoded("key1=value1,key2=value2,key3=value3")) + .andReturn(200, new PodListBuilder().build()) + .always(); + server.expect() + .withPath("/api/v1/namespaces/test/pods?labelSelector=" + Utils.toUrlEncoded("key1=value1,key2=value2")) .andReturn(200, new PodListBuilder() - .addNewItem().and() - .addNewItem().and() - .addNewItem().and() + .addNewItem() + .and() + .addNewItem() + .and() + .addNewItem() + .and() .build()) .once(); @@ -151,11 +169,15 @@ void testListWithLabels() { @Test void testListWithFields() { - server.expect().withPath( - "/api/v1/namespaces/test/pods?fieldSelector=" + Utils.toUrlEncoded("key1=value1,key2=value2,key3!=value3,key3!=value4")) + server.expect() + .withPath( + "/api/v1/namespaces/test/pods?fieldSelector=" + + Utils.toUrlEncoded("key1=value1,key2=value2,key3!=value3,key3!=value4")) .andReturn(200, new PodListBuilder() - .addNewItem().and() - .addNewItem().and() + .addNewItem() + .and() + .addNewItem() + .and() .build()) .once(); @@ -173,7 +195,10 @@ void testListWithFields() { @Test void testEditMissing() { // Given - server.expect().withPath("/api/v1/namespaces/test/pods/pod1").andReturn(404, "error message from kubernetes").always(); + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod1") + .andReturn(404, "error message from kubernetes") + .always(); // When PodResource podOp = client.pods().withName("pod1"); @@ -228,19 +253,37 @@ void testDeleteWithPropagationPolicy() { Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); server.expect().withPath("/api/v1/namespaces/test/pods/pod1").andReturn(200, pod1).once(); - Boolean deleted = client.pods().inNamespace("test").withName("pod1").withPropagationPolicy(DeletionPropagation.FOREGROUND) - .delete().size() == 1; + Boolean deleted = client.pods() + .inNamespace("test") + .withName("pod1") + .withPropagationPolicy(DeletionPropagation.FOREGROUND) + .delete() + .size() == 1; assertTrue(deleted); } @Test void testEvict() { - server.expect().withPath("/api/v1/namespaces/test/pods/pod1/eviction").andReturn(200, new PodBuilder().build()).once(); - server.expect().withPath("/api/v1/namespaces/ns1/pods/pod2/eviction").andReturn(200, new PodBuilder().build()).once(); - server.expect().withPath("/api/v1/namespaces/ns1/pods/pod3/eviction") - .andReturn(PodOperationsImpl.HTTP_TOO_MANY_REQUESTS, new PodBuilder().build()).once(); - server.expect().withPath("/api/v1/namespaces/ns1/pods/pod3/eviction").andReturn(200, new PodBuilder().build()).once(); - server.expect().withPath("/api/v1/namespaces/ns1/pods/pod4/eviction").andReturn(500, new PodBuilder().build()).once(); + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod1/eviction") + .andReturn(200, new PodBuilder().build()) + .once(); + server.expect() + .withPath("/api/v1/namespaces/ns1/pods/pod2/eviction") + .andReturn(200, new PodBuilder().build()) + .once(); + server.expect() + .withPath("/api/v1/namespaces/ns1/pods/pod3/eviction") + .andReturn(PodOperationsImpl.HTTP_TOO_MANY_REQUESTS, new PodBuilder().build()) + .once(); + server.expect() + .withPath("/api/v1/namespaces/ns1/pods/pod3/eviction") + .andReturn(200, new PodBuilder().build()) + .once(); + server.expect() + .withPath("/api/v1/namespaces/ns1/pods/pod4/eviction") + .andReturn(500, new PodBuilder().build()) + .once(); Boolean deleted = client.pods().withName("pod1").evict(); assertTrue(deleted); @@ -267,19 +310,23 @@ void testEvict() { @Test void testEvictWithPolicyV1Eviction() { // Given - server.expect().post() + server.expect() + .post() .withPath("/api/v1/namespaces/ns1/pods/foo/eviction") .andReturn(HttpURLConnection.HTTP_OK, new PodBuilder().build()) .once(); // When - boolean evicted = client.pods().inNamespace("ns1").withName("foo").evict(new EvictionBuilder() - .withNewMetadata() + boolean evicted = client.pods() + .inNamespace("ns1") .withName("foo") - .withNamespace("ns1") - .endMetadata() - .withDeleteOptions(new DeleteOptionsBuilder().build()) - .build()); + .evict(new EvictionBuilder() + .withNewMetadata() + .withName("foo") + .withNamespace("ns1") + .endMetadata() + .withDeleteOptions(new DeleteOptionsBuilder().build()) + .build()); // Then assertTrue(evicted); @@ -302,14 +349,22 @@ void testGetLog() { server.expect().withPath("/api/v1/namespaces/test/pods/pod1/log?pretty=true").andReturn(200, pod1Log).once(); server.expect().withPath("/api/v1/namespaces/test/pods/pod2/log?pretty=false").andReturn(200, pod2Log).once(); - server.expect().withPath("/api/v1/namespaces/test/pods/pod3/log?pretty=false&container=cnt3").andReturn(200, pod3Log) + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod3/log?pretty=false&container=cnt3") + .andReturn(200, pod3Log) .once(); - server.expect().withPath("/api/v1/namespaces/test4/pods/pod4/log?pretty=true&container=cnt4").andReturn(200, pod4Log) + server.expect() + .withPath("/api/v1/namespaces/test4/pods/pod4/log?pretty=true&container=cnt4") + .andReturn(200, pod4Log) .once(); - server.expect().withPath("/api/v1/namespaces/test4/pods/pod1/log?pretty=false&limitBytes=100").andReturn(200, pod1Log) + server.expect() + .withPath("/api/v1/namespaces/test4/pods/pod1/log?pretty=false&limitBytes=100") + .andReturn(200, pod1Log) + .once(); + server.expect() + .withPath("/api/v1/namespaces/test5/pods/pod1/log?pretty=false&tailLines=1×tamps=true") + .andReturn(200, pod1Log) .once(); - server.expect().withPath("/api/v1/namespaces/test5/pods/pod1/log?pretty=false&tailLines=1×tamps=true") - .andReturn(200, pod1Log).once(); String log = client.pods().withName("pod1").getLog(true); assertEquals(pod1Log, log); @@ -333,15 +388,29 @@ void testGetLog() { @Test void testExec() throws InterruptedException { String expectedOutput = "file1 file2"; - server.expect().withPath("/api/v1/namespaces/test/pods/pod1/exec?command=ls&stdout=true") + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod1/exec?command=ls&container=default&stdout=true") .andUpgradeToWebSocket() .open(new OutputStreamMessage(expectedOutput)) .done() .always(); + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod1") + .andReturn(200, + new PodBuilder().withNewMetadata() + .endMetadata() + .withNewSpec() + .addNewContainer() + .withName("default") + .endContainer() + .endSpec() + .build()) + .once(); final CountDownLatch execLatch = new CountDownLatch(1); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ExecWatch watch = client.pods().withName("pod1") + ExecWatch watch = client.pods() + .withName("pod1") .writingOutput(baos) .usingListener(createCountDownLatchListener(execLatch)) .exec("ls"); @@ -363,7 +432,8 @@ void testAttachWithWritingOutput() throws InterruptedException, IOException { String shutdownInput = "shutdown"; - server.expect().withPath("/api/v1/namespaces/test/pods/pod1/attach?stdin=true&stdout=true&stderr=true") + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod1/attach?container=default&stdin=true&stdout=true&stderr=true") .andUpgradeToWebSocket() .open() .expect("\u0000" + validInput) // \u0000 is the file descriptor for stdin @@ -378,13 +448,31 @@ void testAttachWithWritingOutput() throws InterruptedException, IOException { .done() .always(); + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod1") + .andReturn(200, + new PodBuilder().withNewMetadata() + .addToAnnotations(PodOperationsImpl.DEFAULT_CONTAINER_ANNOTATION_NAME, "default") + .endMetadata() + .withNewSpec() + .addNewContainer() + .withName("first") + .endContainer() + .addNewContainer() + .withName("default") + .endContainer() + .endSpec() + .build()) + .once(); + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); ByteArrayOutputStream stderr = new ByteArrayOutputStream(); CountDownLatch latch = new CountDownLatch(1); // When - ExecWatch watch = client.pods().withName("pod1") + ExecWatch watch = client.pods() + .withName("pod1") .redirectingInput() .writingOutput(stdout) @@ -408,6 +496,38 @@ void testAttachWithWritingOutput() throws InterruptedException, IOException { watch.close(); } + @Test + void testExecExplicitDefaultContainerMissing() throws InterruptedException, IOException { + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod1/exec?command=ls&container=first&stderr=true") + .andUpgradeToWebSocket() + .open() + .done() + .always(); + + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod1") + .andReturn(200, + new PodBuilder().withNewMetadata() + .addToAnnotations(PodOperationsImpl.DEFAULT_CONTAINER_ANNOTATION_NAME, "default") + .endMetadata() + .withNewSpec() + .addNewContainer() + .withName("first") + .endContainer() + .endSpec() + .build()) + .once(); + + // When + ExecWatch watch = client.pods() + .withName("pod1") + .terminateOnError() + .exec("ls"); + + watch.close(); + } + @Test void testAttachWithRedirectOutput() throws InterruptedException, IOException { // Given @@ -417,7 +537,8 @@ void testAttachWithRedirectOutput() throws InterruptedException, IOException { String invalidInput = "invalid"; String expectedError = "error"; - server.expect().withPath("/api/v1/namespaces/test/pods/pod1/attach?stdin=true&stdout=true&stderr=true") + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod1/attach?container=first&stdin=true&stdout=true&stderr=true") .andUpgradeToWebSocket() .open() .expect("\u0000" + validInput) // \u0000 is the file descriptor for stdin @@ -429,13 +550,30 @@ void testAttachWithRedirectOutput() throws InterruptedException, IOException { .done() .always(); + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod1") + .andReturn(200, + new PodBuilder().withNewMetadata() + .endMetadata() + .withNewSpec() + .addNewContainer() + .withName("first") + .endContainer() + .addNewContainer() + .withName("default") + .endContainer() + .endSpec() + .build()) + .once(); + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); ByteArrayOutputStream stderr = new ByteArrayOutputStream(); CountDownLatch latch = new CountDownLatch(1); // When - ExecWatch watch = client.pods().withName("pod1") + ExecWatch watch = client.pods() + .withName("pod1") .redirectingInput() .redirectingOutput() .redirectingError() @@ -451,7 +589,8 @@ void testAttachWithRedirectOutput() throws InterruptedException, IOException { InputStreamPumper.pump(watch.getError(), stderr::write, Executors.newSingleThreadExecutor()); // Then - Awaitility.await().atMost(30, TimeUnit.SECONDS) + Awaitility.await() + .atMost(30, TimeUnit.SECONDS) .until(() -> stdout.toString().equals(expectedOutput) && stderr.toString().equals(expectedError)); watch.close(); @@ -482,17 +621,21 @@ void testWatch() throws InterruptedException { .withResourceVersion("1") .endMetadata() .build(); - server.expect().withPath("/api/v1/namespaces/test/pods").andReturn(200, new PodListBuilder() - .withNewMetadata() - .withResourceVersion("1") - .endMetadata() - .addToItems(pod1) - .build()).once(); + server.expect() + .withPath("/api/v1/namespaces/test/pods") + .andReturn(200, new PodListBuilder() + .withNewMetadata() + .withResourceVersion("1") + .endMetadata() + .addToItems(pod1) + .build()) + .once(); server.expect() .withPath("/api/v1/namespaces/test/pods?fieldSelector=metadata.name%3Dpod1&allowWatchBookmarks=true&watch=true") .andUpgradeToWebSocket() .open() - .waitFor(50).andEmit(new WatchEvent(pod1, "DELETED")) + .waitFor(50) + .andEmit(new WatchEvent(pod1, "DELETED")) .done() .always(); final CountDownLatch deleteLatch = new CountDownLatch(1); @@ -559,14 +702,20 @@ void testWait() throws InterruptedException { .endStatus() .build(); - server.expect().get().withPath("/api/v1/namespaces/test/pods?fieldSelector=metadata.name%3Dpod1").andReturn(200, notReady) + server.expect() + .get() + .withPath("/api/v1/namespaces/test/pods?fieldSelector=metadata.name%3Dpod1") + .andReturn(200, notReady) .once(); - server.expect().get().withPath( - "/api/v1/namespaces/test/pods?fieldSelector=metadata.name%3Dpod1&resourceVersion=1&allowWatchBookmarks=true&watch=true") + server.expect() + .get() + .withPath( + "/api/v1/namespaces/test/pods?fieldSelector=metadata.name%3Dpod1&resourceVersion=1&allowWatchBookmarks=true&watch=true") .andUpgradeToWebSocket() .open() - .waitFor(50).andEmit(new WatchEvent(ready, "MODIFIED")) + .waitFor(50) + .andEmit(new WatchEvent(ready, "MODIFIED")) .done() .always(); @@ -576,13 +725,18 @@ void testWait() throws InterruptedException { @Test void testPortForward() throws IOException { - server.expect().withPath("/api/v1/namespaces/test/pods/pod1/portforward?ports=123") + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod1/portforward?ports=123") .andUpgradeToWebSocket() .open() - .waitFor(10).andEmit(portForwardEncode(true, "12")) // data channel info - .waitFor(10).andEmit(portForwardEncode(false, "12")) // error channel info - .waitFor(10).andEmit(portForwardEncode(true, "Hell")) - .waitFor(10).andEmit(portForwardEncode(true, "o World")) + .waitFor(10) + .andEmit(portForwardEncode(true, "12")) // data channel info + .waitFor(10) + .andEmit(portForwardEncode(false, "12")) // error channel info + .waitFor(10) + .andEmit(portForwardEncode(true, "Hell")) + .waitFor(10) + .andEmit(portForwardEncode(true, "o World")) .done() .once(); @@ -619,13 +773,18 @@ void testPortForward() throws IOException { @Test void testPortForwardWithChannel() throws InterruptedException, IOException { - server.expect().withPath("/api/v1/namespaces/test/pods/pod1/portforward?ports=123") + server.expect() + .withPath("/api/v1/namespaces/test/pods/pod1/portforward?ports=123") .andUpgradeToWebSocket() .open() - .waitFor(10).andEmit(portForwardEncode(true, "12")) // data channel info - .waitFor(10).andEmit(portForwardEncode(false, "12")) // error channel info - .waitFor(10).andEmit(portForwardEncode(true, "Hell")) - .waitFor(10).andEmit(portForwardEncode(true, "o World!")) + .waitFor(10) + .andEmit(portForwardEncode(true, "12")) // data channel info + .waitFor(10) + .andEmit(portForwardEncode(false, "12")) // error channel info + .waitFor(10) + .andEmit(portForwardEncode(true, "Hell")) + .waitFor(10) + .andEmit(portForwardEncode(true, "o World!")) .done() .once(); @@ -654,6 +813,22 @@ void testOptionalUpload() { @Test void testOptionalCopy() { + server.expect() + .withPath("/api/v1/namespaces/ns1/pods/pod2") + .andReturn(200, + new PodBuilder().withNewMetadata() + .endMetadata() + .withNewSpec() + .addNewContainer() + .withName("first") + .endContainer() + .addNewContainer() + .withName("default") + .endContainer() + .endSpec() + .build()) + .once(); + Assertions.assertThrows(KubernetesClientException.class, () -> { client.pods().inNamespace("ns1").withName("pod2").file("/etc/hosts").copy(tempDir.toAbsolutePath()); }); @@ -715,9 +890,11 @@ void testListFromServer() { .endStatus() .build(); - server.expect().get() + server.expect() + .get() .withPath("/api/v1/namespaces/test/pods/pod1") - .andReturn(200, serverPod).once(); + .andReturn(200, serverPod) + .once(); List resources = client.resourceList(clientPod).fromServer().get();