Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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-.+");
}

}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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<String[]> 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'");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,23 @@ 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
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
Expand All @@ -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");
}
}

Expand Down
Loading