diff --git a/CHANGELOG.md b/CHANGELOG.md index 760c3671474..9ee8eb29941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### 7.8-SNAPSHOT #### Bugs +* Fix #7964: (kubernetes-client) Pod file-transfer commands now pass `--` before shell-quoted paths to `tar`, `cat`, and `mkdir`, preventing dash-prefixed path values from being interpreted as command options * Fix #7953: (httpclient-jdk) bodyless requests now preserve the requested HTTP method instead of silently defaulting to `GET`. `JdkHttpClientImpl.requestBuilder` only called `HttpRequest.Builder.method(...)` inside the `body != null` branch, so a bodyless `DELETE`/`POST`/`PUT`/`PATCH` (such as `client.raw(uri, "DELETE", null)`) was sent as `GET` on the JDK backend; the method is now set with `BodyPublishers.noBody()` when there is no body, matching the OkHttp, Jetty and Vert.x backends * Fix #7435: (kubernetes-client) A `SharedIndexInformer`'s periodic resync no longer stops permanently and silently when a single resync cycle throws. `DefaultSharedIndexInformer.scheduleResync` runs the resync through `Utils.scheduleAtFixedRate`, whose self-rescheduling chain re-arms the next cycle only when the previous one completes normally; an uncaught exception completed the (unobserved) `resyncFuture` exceptionally and the resync was never scheduled again, with no log, while the independent watch kept `isWatching()` reporting `true` (a restart was required to recover). The resync command now catches and `WARN`-logs the failure so the schedule fires again at the next interval * Fix #7933: (kubernetes-client-api) Deterministic TLS trust failures (untrusted cert, expired cert, hostname mismatch) are now classified as terminal and fail fast instead of being retried by the shared `StandardHttpClient.shouldRetry` backoff loop (~19 s drain). The classifier walks both `getCause()` and `getSuppressed()` trees for `CertificateException`, `CertPathValidatorException`, `CertPathBuilderException`, and `SSLPeerUnverifiedException`. Affects all five HTTP client modules (jdk, jetty, okhttp, vertx-4, vertx-5) on both the HTTP request and WebSocket connect paths 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 46c7efdc3c5..d85a0009845 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 @@ -465,7 +465,7 @@ public InputStream read() { } private String[] readFileCommand(String source) { - return new String[] { "sh", "-c", String.format("cat %s", shellQuote(source)) }; + return new String[] { "sh", "-c", String.format("cat -- %s", shellQuote(source)) }; } private InputStream readFile(String source) { @@ -502,7 +502,7 @@ private void copyFile(String source, File target) { } public InputStream readTar(String source) { - return read("sh", "-c", "tar -cf - " + shellQuote(source)); + return read("sh", "-c", "tar -cf - -- " + shellQuote(source)); } private InputStream read(String... command) { 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 6ec7f9e4bee..90b02cdabc3 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 @@ -165,7 +165,7 @@ private static boolean uploadTar(PodOperationsImpl operation, String directory, } static String extractTarCommand(String directory, String tar) { - return String.format("mkdir -p %1$s; tar -C %1$s -xmf %2$s; e=$?; rm %2$s; exit $e", shellQuote(directory), tar); + return String.format("mkdir -p -- %1$s; tar -C %1$s -xmf %2$s; e=$?; rm %2$s; exit $e", shellQuote(directory), tar); } private static void addFileToTar(String fileName, File file, TarArchiveOutputStream tar) @@ -184,7 +184,7 @@ private static void addFileToTar(String fileName, File file, TarArchiveOutputStr static String createExecCommandForUpload(String file) { return String.format( - "mkdir -p %s && cat - > %s && echo $?", shellQuote(getDirectoryFromFile(file)), shellQuote(file)); + "mkdir -p -- %s && cat - > %s && echo $?", shellQuote(getDirectoryFromFile(file)), shellQuote(file)); } private static String ensureEndsWithSlash(String path) { diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/behavior/UploadTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/behavior/UploadTest.java index e9653f36dd4..748a36b98dd 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/behavior/UploadTest.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/behavior/UploadTest.java @@ -191,7 +191,7 @@ void createsTempDirectoryAndPipesFileInServer() { .extracting(StandardWebSocketBuilder::asHttpRequest) .extracting(StandardHttpRequest::uri) .extracting(URI::getQuery).asString() - .contains("command=mkdir -p '/target/' && cat - > '/target/fabric8-"); + .contains("command=mkdir -p -- '/target/' && cat - > '/target/fabric8-"); } @Test @@ -224,7 +224,7 @@ void extractsTar() { .extracting(StandardHttpRequest::uri) .extracting(URI::getQuery).asString() .matches( - ".+command=mkdir -p '/target-dir/'; tar -C '/target-dir/' -xmf /target-dir/fabric8-.+\\.tar; e=\\$\\?; rm /target-dir/fabric8-.+"); + ".+command=mkdir -p -- '/target-dir/'; tar -C '/target-dir/' -xmf /target-dir/fabric8-.+\\.tar; e=\\$\\?; rm /target-dir/fabric8-.+"); } @Nested @@ -326,7 +326,7 @@ void createsTempDirectoryAndPipesDirInServer() { .extracting(StandardWebSocketBuilder::asHttpRequest) .extracting(StandardHttpRequest::uri) .extracting(URI::getQuery).asString() - .contains("command=mkdir -p '/target/location/' && cat - > '/target/location/fabric8-"); + .contains("command=mkdir -p -- '/target/location/' && cat - > '/target/location/fabric8-"); } @Test @@ -359,7 +359,7 @@ void extractsTar() { .extracting(StandardHttpRequest::uri) .extracting(URI::getQuery).asString() .matches( - ".+command=mkdir -p '/target-dir/location/'; tar -C '/target-dir/location/' -xmf /target-dir/location/fabric8-.+\\.tar; e=\\$\\?; rm /target-dir/location/fabric8-.+"); + ".+command=mkdir -p -- '/target-dir/location/'; tar -C '/target-dir/location/' -xmf /target-dir/location/fabric8-.+\\.tar; e=\\$\\?; rm /target-dir/location/fabric8-.+"); } } @@ -398,7 +398,7 @@ void createsTempDirectoryAndPipesFileInServer() { .extracting(StandardWebSocketBuilder::asHttpRequest) .extracting(StandardHttpRequest::uri) .extracting(URI::getQuery).asString() - .contains("command=mkdir -p '/target/' && cat - > '/target/location"); + .contains("command=mkdir -p -- '/target/' && cat - > '/target/location"); } @Test diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/PodOperationsImplTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/PodOperationsImplTest.java index d052758c7aa..2aa3122747d 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/PodOperationsImplTest.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/PodOperationsImplTest.java @@ -19,13 +19,26 @@ import io.fabric8.kubernetes.api.model.PodBuilder; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.ExecWatch; +import io.fabric8.kubernetes.client.dsl.TtyExecErrorable; import io.fabric8.kubernetes.client.dsl.internal.OperationContext; import io.fabric8.kubernetes.client.dsl.internal.PodOperationContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import java.io.ByteArrayInputStream; +import java.lang.reflect.Method; + +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class PodOperationsImplTest { @@ -135,4 +148,30 @@ void validateOrDefaultContainerIdByNameEmptyContainers() { assertThrows(KubernetesClientException.class, () -> podOperations.validateOrDefaultContainerId("a", pod)); } + @Test + void readTarUsesOptionSeparatorBeforeSource() { + final PodOperationsImpl operation = spy(podOperations); + final TtyExecErrorable execable = mock(TtyExecErrorable.class); + final ExecWatch execWatch = mock(ExecWatch.class); + final ArgumentCaptor commandCaptor = ArgumentCaptor.forClass(String[].class); + doReturn(execable).when(operation).redirectingOutput(); + when(execable.exec(any(String[].class))).thenReturn(execWatch); + when(execWatch.getOutput()).thenReturn(new ByteArrayInputStream(new byte[0])); + + operation.readTar("--checkpoint-action=exec=touch"); + + verify(execable).exec(commandCaptor.capture()); + assertThat(commandCaptor.getValue()) + .containsExactly("sh", "-c", "tar -cf - -- '--checkpoint-action=exec=touch'"); + } + + @Test + void readFileCommandUsesOptionSeparatorBeforeSource() throws Exception { + final Method readFileCommand = PodOperationsImpl.class.getDeclaredMethod("readFileCommand", String.class); + readFileCommand.setAccessible(true); + + assertThat((String[]) readFileCommand.invoke(podOperations, "--help")) + .containsExactly("sh", "-c", "cat -- '--help'"); + } + } diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/uploadable/PodUploadTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/uploadable/PodUploadTest.java index 9509c2e2880..156911ee041 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/uploadable/PodUploadTest.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/uploadable/PodUploadTest.java @@ -49,7 +49,7 @@ void withFileInRootPath_shouldCreateValidExecCommandForUpload() { // When String result = PodUpload.createExecCommandForUpload("/cp.log"); // Then - assertThat(result).isEqualTo("mkdir -p '/' && cat - > '/cp.log' && echo $?"); + assertThat(result).isEqualTo("mkdir -p -- '/' && cat - > '/cp.log' && echo $?"); } @Test @@ -57,16 +57,15 @@ void withNormalFile_shouldCreateValidExecCommandForUpload() { // When String result = PodUpload.createExecCommandForUpload("/tmp/foo/cp.log"); // Then - assertThat(result).isEqualTo("mkdir -p '/tmp/foo/' && cat - > '/tmp/foo/cp.log' && echo $?"); + assertThat(result).isEqualTo("mkdir -p -- '/tmp/foo/' && cat - > '/tmp/foo/cp.log' && echo $?"); } - // @Test void withSingleQuoteInPath() { // When String result = PodUpload.createExecCommandForUpload("/tmp/fo'o/cp.log"); // Then - assertThat(result).isEqualTo("mkdir -p '/tmp/fo\'\\'\'o/' && cat - > '/tmp/fo\'\\'\'o/cp.log' && echo $?"); + assertThat(result).isEqualTo("mkdir -p -- '/tmp/fo\'\\'\'o/' && cat - > '/tmp/fo\'\\'\'o/cp.log' && echo $?"); } @Test @@ -75,7 +74,39 @@ void withMultipleSingleQuotesInPath() { String result = PodUpload.createExecCommandForUpload("/tmp/f'o'o/c'p.log"); // Then assertThat(result) - .isEqualTo("mkdir -p '/tmp/f\'\\'\'o\'\\'\'o/' && cat - > '/tmp/f\'\\'\'o\'\\'\'o/c\'\\'\'p.log' && echo $?"); + .isEqualTo("mkdir -p -- '/tmp/f\'\\'\'o\'\\'\'o/' && cat - > '/tmp/f\'\\'\'o\'\\'\'o/c\'\\'\'p.log' && echo $?"); + } + + @Test + void withArgumentLikeDirectory_shouldCreateExecCommandWithOptionSeparator() { + // When + String result = PodUpload.createExecCommandForUpload("--checkpoint-action=exec=touch/cp.log"); + // Then + assertThat(result).isEqualTo( + "mkdir -p -- '--checkpoint-action=exec=touch/' && cat - > '--checkpoint-action=exec=touch/cp.log' && echo $?"); + } + } + + @Nested + @DisplayName("extractTarCommand") + class ExtractTarCommand { + + @Test + void withNormalDirectory_shouldCreateValidExtractCommand() { + // When + String result = PodUpload.extractTarCommand("/target-dir/", "/target-dir/fabric8-copy.tar"); + // Then + assertThat(result).isEqualTo( + "mkdir -p -- '/target-dir/'; tar -C '/target-dir/' -xmf /target-dir/fabric8-copy.tar; e=$?; rm /target-dir/fabric8-copy.tar; exit $e"); + } + + @Test + void withArgumentLikeDirectory_shouldCreateExtractCommandWithOptionSeparator() { + // When + String result = PodUpload.extractTarCommand("--checkpoint-action=exec=touch/", "/tmp/fabric8-copy.tar"); + // Then + assertThat(result).isEqualTo( + "mkdir -p -- '--checkpoint-action=exec=touch/'; tar -C '--checkpoint-action=exec=touch/' -xmf /tmp/fabric8-copy.tar; e=$?; rm /tmp/fabric8-copy.tar; exit $e"); } }