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 9c6aaaca8a3..0aadd08956e 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 @@ -315,10 +315,11 @@ private URL getURL(String operation, String[] commands) throws MalformedURLExcep */ String validateOrDefaultContainerId(String name) { Pod pod = this.require(); - List containers = pod.getSpec().getContainers(); - if (containers.isEmpty()) { + // spec and container null-checks are not necessary for real k8s clusters, added them to simplify some tests running in the mockserver + if (pod.getSpec() == null || pod.getSpec().getContainers() == null || pod.getSpec().getContainers().isEmpty()) { throw new KubernetesClientException("Pod has no containers!"); } + final List containers = pod.getSpec().getContainers(); if (name == null) { name = pod.getMetadata().getAnnotations().get(DEFAULT_CONTAINER_ANNOTATION_NAME); if (name != null && !hasContainer(containers, name)) { diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/PodOperationsImpl_CVE2021_20218_Test.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/PodOperationsImpl_CVE2021_20218_Test.java index 78887ecad42..b53233c28fd 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/PodOperationsImpl_CVE2021_20218_Test.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/PodOperationsImpl_CVE2021_20218_Test.java @@ -39,17 +39,17 @@ void setUp() { } @Test - void testWithForgedTar(@TempDir Path targetDirParent) throws Exception { + void testWithForgedTar(@TempDir Path targetDirParent) { // Given final Path targetDir = targetDirParent.resolve("target"); final PodOperationsImpl poi = spy(new PodOperationsImpl(baseContext.withDir("/var/source-dir"), new OperationContext())); doReturn(PodOperationsImpl_CVE2021_20218_Test.class.getResourceAsStream("/2021_20218/tar-with-parent-traversal.tar")) - .when(poi).readTar("/var/source-dir"); + .when(poi).readTar("/var/source-dir"); // When final KubernetesClientException exception = assertThrows(KubernetesClientException.class, () -> poi.copy(targetDir)); // Then assertThat(exception).getCause() - .hasMessage("Tar entry '../youve-been-hacked' has an invalid name"); + .hasMessage("Tar entry '../youve-been-hacked' has an invalid name"); assertThat(targetDirParent).isDirectoryNotContaining("glob:**/youve-been-hacked"); } @@ -59,13 +59,13 @@ void testWithValidTar(@TempDir Path targetDirParent) throws Exception { final Path targetDir = targetDirParent.resolve("target"); final PodOperationsImpl poi = spy(new PodOperationsImpl(baseContext.withDir("/var/source-dir"), new OperationContext())); doReturn(PodOperationsImpl_CVE2021_20218_Test.class.getResourceAsStream("/2021_20218/valid.tar")) - .when(poi).readTar("/var/source-dir"); + .when(poi).readTar("/var/source-dir"); // When final boolean result = poi.copy(targetDir); // Then assertThat(result).isTrue(); assertThat(targetDir) - .isDirectoryContaining("glob:**/hello.txt") - .isDirectoryRecursivelyContaining("glob:**/very/nested/dir/answer.txt"); + .isDirectoryContaining("glob:**/hello.txt") + .isDirectoryRecursivelyContaining("glob:**/very/nested/dir/answer.txt"); } } diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PodExecTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PodExecTest.java new file mode 100644 index 00000000000..1da0082b0e4 --- /dev/null +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PodExecTest.java @@ -0,0 +1,158 @@ +/** + * 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.mock; + +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.api.model.Status; +import io.fabric8.kubernetes.api.model.StatusBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.ContainerResource; +import io.fabric8.kubernetes.client.dsl.ExecWatch; +import io.fabric8.kubernetes.client.dsl.PodResource; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +import io.fabric8.kubernetes.client.utils.Serialization; +import io.fabric8.mockwebserver.internal.WebSocketMessage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("unused") +@EnableKubernetesMockClient(crud = true) +class PodExecTest { + + private KubernetesMockServer server; + private KubernetesClient client; + + @BeforeEach + void setUp() { + client.pods().inAnyNamespace().delete(); + } + + @Test + @DisplayName("With no containers, should throw exception") + void withNoContainers() { + client.pods().resource(new PodBuilder().withNewMetadata().withName("no-containers").endMetadata().build()) + .createOrReplace(); + final PodResource pr = client.pods().withName("no-containers"); + assertThatThrownBy(() -> pr.exec("sh", "-c", "echo Greetings Professor Falken")) + .isInstanceOf(KubernetesClientException.class) + .hasMessage("Pod has no containers!"); + } + + @Test + @DisplayName("With single container, should exec in the single container") + void withSingleContainer() throws Exception { + client.pods().resource(new PodBuilder().withNewMetadata().withName("single-container").endMetadata() + .withNewSpec() + .addNewContainer() + .withName("the-single-container") + .endContainer() + .endSpec() + .build()) + .createOrReplace(); + server.expect() + .get() + .withPath("/api/v1/namespaces/test/pods/single-container/exec?command=sleep%201&container=the-single-container") + .andUpgradeToWebSocket() + .open() + .immediately().andEmit(exitZeroEvent()) + .done() + .always(); + final ExecWatch result = client.pods().withName("single-container").exec("sleep 1"); + assertThat(result.exitCode().get(1, TimeUnit.SECONDS)).isZero(); + } + + @Test + @DisplayName("With single container and inContainer with non-existent name, should throw exception") + void withSingleContainerAndInContainer() { + client.pods().resource(new PodBuilder().withNewMetadata().withName("single-container").endMetadata() + .withNewSpec() + .addNewContainer() + .withName("the-single-container") + .endContainer() + .endSpec() + .build()) + .createOrReplace(); + final ContainerResource cr = client.pods().withName("single-container").inContainer("non-existent"); + assertThatThrownBy(() -> cr.exec("exit 0")) + .isInstanceOf(KubernetesClientException.class) + .hasMessage("container non-existent not found in pod single-container"); + } + + @Test + @DisplayName("With multiple containers, should exec in the first container") + void withMultipleContainers() throws Exception { + client.pods().resource(new PodBuilder().withNewMetadata().withName("multiple-containers").endMetadata() + .withNewSpec() + .addNewContainer() + .withName("the-first-container") + .endContainer() + .addNewContainer() + .withName("the-second-container") + .endContainer() + .endSpec() + .build()) + .createOrReplace(); + server.expect() + .get() + .withPath("/api/v1/namespaces/test/pods/multiple-containers/exec?command=sleep%201&container=the-first-container") + .andUpgradeToWebSocket() + .open() + .immediately().andEmit(exitZeroEvent()) + .done() + .always(); + final ExecWatch result = client.pods().withName("multiple-containers").exec("sleep 1"); + assertThat(result.exitCode().get(1, TimeUnit.SECONDS)).isZero(); + } + + @Test + @DisplayName("With multiple containers and inContainer with existent name, should exec in the selected container") + void withMultipleContainersAndInContainer() throws Exception { + client.pods().resource(new PodBuilder().withNewMetadata().withName("multiple-containers").endMetadata() + .withNewSpec() + .addNewContainer() + .withName("the-first-container") + .endContainer() + .addNewContainer() + .withName("the-second-container") + .endContainer() + .endSpec() + .build()) + .createOrReplace(); + server.expect() + .get() + .withPath("/api/v1/namespaces/test/pods/multiple-containers/exec?command=sleep%201&container=the-second-container") + .andUpgradeToWebSocket() + .open() + .immediately().andEmit(exitZeroEvent()) + .done() + .always(); + final ExecWatch result = client.pods().withName("multiple-containers").inContainer("the-second-container").exec("sleep 1"); + assertThat(result.exitCode().get(1, TimeUnit.SECONDS)).isZero(); + } + + private static WebSocketMessage exitZeroEvent() { + final Status success = new StatusBuilder().withStatus("Success").build(); + return new WebSocketMessage(0L, "\u0003" + Serialization.asJson(success), false, true); + } +}