Skip to content

Commit aaa1998

Browse files
authored
fix(ecr): Properly check if host-persistent-path is the name of a Docker volume (#678)
1 parent 91ed829 commit aaa1998

3 files changed

Lines changed: 143 additions & 4 deletions

File tree

src/main/java/io/github/hectorvent/floci/core/common/docker/ContainerLifecycleManager.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.github.dockerjava.api.command.CreateContainerCmd;
66
import com.github.dockerjava.api.command.CreateContainerResponse;
77
import com.github.dockerjava.api.command.InspectContainerResponse;
8+
import com.github.dockerjava.api.exception.DockerException;
89
import com.github.dockerjava.api.exception.NotFoundException;
910
import com.github.dockerjava.api.model.Bind;
1011
import com.github.dockerjava.api.model.Container;
@@ -302,6 +303,42 @@ public DockerClient getDockerClient() {
302303
return dockerClient;
303304
}
304305

306+
/**
307+
* Returns {@code true} if the container runtime (Docker, Moby, or Podman) has a volume
308+
* with the given name. The volume does not need to be attached to the current container.
309+
* <p>
310+
* This method uses the Docker Engine API ({@code /volumes/{name}}) which is supported
311+
* by Docker, Moby, and Podman runtimes on all operating systems.
312+
*
313+
* @param name the volume name to look up
314+
* @return {@code true} if the volume exists, {@code false} otherwise
315+
*/
316+
public boolean volumeExists(String name) {
317+
if (name == null || name.isBlank()) {
318+
return false;
319+
}
320+
// Is a Unix absolute or relative path (e.g. "/var/lib/data", "./data", "../data")
321+
if (name.startsWith("/") || name.startsWith(".")) {
322+
return false;
323+
}
324+
// Is a Windows absolute path (e.g. "C:\Users\data", "D:/sources/data")
325+
if (name.length() >= 3 && Character.isLetter(name.charAt(0))
326+
&& name.charAt(1) == ':' && (name.charAt(2) == '\\' || name.charAt(2) == '/')) {
327+
return false;
328+
}
329+
try {
330+
dockerClient.inspectVolumeCmd(name).exec();
331+
LOG.debugv("Volume ''{0}'' exists in the container runtime", name);
332+
return true;
333+
} catch (NotFoundException e) {
334+
LOG.debugv("Volume ''{0}'' not found in the container runtime", name);
335+
return false;
336+
} catch (DockerException e) {
337+
LOG.warnv("Failed to inspect volume ''{0}'': {1}", name, e.getMessage());
338+
return false;
339+
}
340+
}
341+
305342
private HostConfig buildHostConfig(ContainerSpec spec) {
306343
HostConfig hostConfig = HostConfig.newHostConfig();
307344

src/main/java/io/github/hectorvent/floci/services/ecr/registry/EcrRegistryManager.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,7 @@ public synchronized void ensureStarted() {
180180
private void addPersistenceMounts(ContainerBuilder.Builder specBuilder, List<String> env) {
181181
String hostPersistentPath = config.storage().hostPersistentPath();
182182
boolean inContainer = containerDetector.isRunningInContainer();
183-
boolean isExplicitVolumeName = !hostPersistentPath.startsWith("/")
184-
&& !hostPersistentPath.startsWith(".");
183+
boolean isExplicitVolume = lifecycleManager.volumeExists(hostPersistentPath);
185184
boolean isRelativeDefault = hostPersistentPath.startsWith(".");
186185

187186
if (inContainer && isRelativeDefault) {
@@ -190,10 +189,10 @@ private void addPersistenceMounts(ContainerBuilder.Builder specBuilder, List<Str
190189
LOG.infov("Floci in container with relative host-persistent-path ({0}); "
191190
+ "using named volume {1} for ECR registry data",
192191
hostPersistentPath, NAMED_VOLUME);
193-
} else if (isExplicitVolumeName) {
192+
} else if (isExplicitVolume) {
194193
// User set hostPersistentPath to a Docker named-volume name
195194
String internalMountPath = "/app/data";
196-
specBuilder.withBind(hostPersistentPath, internalMountPath);
195+
specBuilder.withNamedVolume(hostPersistentPath, internalMountPath);
197196
env.add("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=" + internalMountPath + "/ecr/registry");
198197
} else {
199198
// Host path bind-mount.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package io.github.hectorvent.floci.core.common.docker;
2+
3+
import com.github.dockerjava.api.DockerClient;
4+
import com.github.dockerjava.api.command.InspectVolumeCmd;
5+
import com.github.dockerjava.api.command.InspectVolumeResponse;
6+
import com.github.dockerjava.api.exception.DockerException;
7+
import com.github.dockerjava.api.exception.NotFoundException;
8+
import io.github.hectorvent.floci.services.lambda.launcher.ImageCacheService;
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.ExtendWith;
12+
import org.mockito.Mock;
13+
import org.mockito.junit.jupiter.MockitoExtension;
14+
15+
import static org.junit.jupiter.api.Assertions.*;
16+
import static org.mockito.Mockito.*;
17+
18+
@ExtendWith(MockitoExtension.class)
19+
class ContainerLifecycleManagerVolumeTest {
20+
21+
@Mock
22+
private DockerClient dockerClient;
23+
24+
@Mock
25+
private ImageCacheService imageCacheService;
26+
27+
@Mock
28+
private ContainerDetector containerDetector;
29+
30+
@Mock
31+
private PortAllocator portAllocator;
32+
33+
private ContainerLifecycleManager manager;
34+
35+
@BeforeEach
36+
void setUp() {
37+
manager = new ContainerLifecycleManager(dockerClient, imageCacheService, containerDetector, portAllocator);
38+
}
39+
40+
@Test
41+
void volumeExists_returnsTrue_whenVolumeExists() {
42+
InspectVolumeCmd cmd = mock(InspectVolumeCmd.class);
43+
when(dockerClient.inspectVolumeCmd("my-volume")).thenReturn(cmd);
44+
when(cmd.exec()).thenReturn(mock(InspectVolumeResponse.class));
45+
46+
assertTrue(manager.volumeExists("my-volume"));
47+
}
48+
49+
@Test
50+
void volumeExists_returnsFalse_whenVolumeNotFound() {
51+
InspectVolumeCmd cmd = mock(InspectVolumeCmd.class);
52+
when(dockerClient.inspectVolumeCmd("nonexistent")).thenReturn(cmd);
53+
when(cmd.exec()).thenThrow(new NotFoundException("No such volume"));
54+
55+
assertFalse(manager.volumeExists("nonexistent"));
56+
}
57+
58+
@Test
59+
void volumeExists_returnsFalse_forNullName() {
60+
assertFalse(manager.volumeExists(null));
61+
verifyNoInteractions(dockerClient);
62+
}
63+
64+
@Test
65+
void volumeExists_returnsFalse_forBlankName() {
66+
assertFalse(manager.volumeExists(" "));
67+
verifyNoInteractions(dockerClient);
68+
}
69+
70+
@Test
71+
void volumeExists_returnsFalse_forAbsolutePath() {
72+
assertFalse(manager.volumeExists("/var/lib/data"));
73+
verifyNoInteractions(dockerClient);
74+
}
75+
76+
@Test
77+
void volumeExists_returnsFalse_forRelativePath() {
78+
assertFalse(manager.volumeExists("./data"));
79+
verifyNoInteractions(dockerClient);
80+
}
81+
82+
@Test
83+
void volumeExists_returnsFalse_forWindowsAbsolutePathBackslash() {
84+
assertFalse(manager.volumeExists("C:\\Users\\data"));
85+
verifyNoInteractions(dockerClient);
86+
}
87+
88+
@Test
89+
void volumeExists_returnsFalse_forWindowsAbsolutePathForwardSlash() {
90+
assertFalse(manager.volumeExists("D:/sources/data"));
91+
verifyNoInteractions(dockerClient);
92+
}
93+
94+
@Test
95+
void volumeExists_returnsFalse_onDockerException() {
96+
InspectVolumeCmd cmd = mock(InspectVolumeCmd.class);
97+
when(dockerClient.inspectVolumeCmd("some-volume")).thenReturn(cmd);
98+
when(cmd.exec()).thenThrow(new DockerException("Connection refused", 500));
99+
100+
assertFalse(manager.volumeExists("some-volume"));
101+
}
102+
}
103+

0 commit comments

Comments
 (0)