From 948faa9ea83684794ef690e6f464a88c045ecf34 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 68453a3c73..933cae428f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Fix #4547: preventing timing issues with leader election cancel #### Improvements +* 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. #### Dependency Upgrade 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 fa873acc0f..9c6aaaca8a 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 (Exception e) { throw KubernetesClientException.launderThrowable(forOperationType("exec"), e); @@ -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 (Exception e) { throw KubernetesClientException.launderThrowable(forOperationType("attach"), e); } } - 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 2b7dd58dc7..5ad38ee4f9 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 e924838a1b..10267e9ca7 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();