Skip to content

Commit a8075a5

Browse files
committed
Fix #35551: allow to use read-only root file system in ecs
1 parent cc08141 commit a8075a5

4 files changed

Lines changed: 89 additions & 8 deletions

File tree

src/main/java/eu/openanalytics/containerproxy/backend/ecs/EcsBackend.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,8 @@ private String getTaskDefinition(Authentication user, ContainerSpec spec, EcsSpe
329329
.dockerLabels(dockerLabels)
330330
.logConfiguration(getLogConfiguration(proxy.getSpecId()))
331331
.mountPoints(volumes.getSecond())
332-
.secrets(getSecrets(specExtension));
332+
.secrets(getSecrets(specExtension))
333+
.readonlyRootFilesystem(specExtension.getEcsReadonlyRootFilesystem().getValueOrDefault(false));
333334

334335
String credentials = specExtension.getEcsRepositoryCredentialsParameter().getValueOrDefault(defaultRepositoryCredentialsParameter);
335336
if (credentials != null && !credentials.isBlank()) {
@@ -375,7 +376,7 @@ private LogConfiguration getLogConfiguration(String specId) {
375376

376377
private Pair<List<Volume>, List<MountPoint>> getVolumes(ContainerSpec spec, EcsSpecExtension specExtension) {
377378
List<String> volumeNames = new ArrayList<>();
378-
List<Volume> efsVolumeConfigurations = new ArrayList<>();
379+
List<Volume> volumeConfigurations = new ArrayList<>();
379380
for (EcsEfsVolume volume : specExtension.getEcsEfsVolumes()) {
380381
EFSVolumeConfiguration.Builder efsVolumeConfiguration = EFSVolumeConfiguration.builder();
381382
efsVolumeConfiguration.fileSystemId(volume.getFileSystemId().getValue());
@@ -397,10 +398,18 @@ private Pair<List<Volume>, List<MountPoint>> getVolumes(ContainerSpec spec, EcsS
397398
.name(volume.getName().getValue())
398399
.build();
399400

400-
efsVolumeConfigurations.add(finalVolume);
401+
volumeConfigurations.add(finalVolume);
401402
volumeNames.add(volume.getName().getValue());
402403
}
403404

405+
for (String volume : specExtension.getEcsBindVolumes()) {
406+
volumeConfigurations.add(Volume.builder()
407+
.name(volume)
408+
.build()
409+
);
410+
volumeNames.add(volume);
411+
}
412+
404413
List<MountPoint> mountPoints = new ArrayList<>();
405414
if (spec.getVolumes().isPresent()) {
406415
for (String volume : spec.getVolumes().getValue()) {
@@ -411,7 +420,7 @@ private Pair<List<Volume>, List<MountPoint>> getVolumes(ContainerSpec spec, EcsS
411420
String name = components[0];
412421
String containerPath = components[1];
413422
if (!volumeNames.contains(name)) {
414-
throw new IllegalArgumentException(String.format("Invalid volume configuration: %s, no corresponding EFS volume definition found", volume));
423+
throw new IllegalArgumentException(String.format("Invalid volume configuration: %s, no corresponding (EFS or bind) volume definition found", volume));
415424
}
416425

417426
MountPoint.Builder mountPoint = MountPoint.builder();
@@ -430,7 +439,21 @@ private Pair<List<Volume>, List<MountPoint>> getVolumes(ContainerSpec spec, EcsS
430439
}
431440
}
432441

433-
return Pair.of(efsVolumeConfigurations, mountPoints);
442+
if (specExtension.ecsReadonlyRootFilesystem.getValueOrDefault(false)) {
443+
// if filesystem is read-only, mount read-write volume on /tmp
444+
volumeConfigurations.add(Volume.builder()
445+
.name("tmp")
446+
.build()
447+
);
448+
449+
mountPoints.add(MountPoint.builder()
450+
.sourceVolume("tmp")
451+
.containerPath("/tmp")
452+
.build()
453+
);
454+
}
455+
456+
return Pair.of(volumeConfigurations, mountPoints);
434457
}
435458

436459
private List<Secret> getSecrets(EcsSpecExtension specExtension) {

src/main/java/eu/openanalytics/containerproxy/backend/ecs/EcsSpecExtension.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ public class EcsSpecExtension extends AbstractSpecExtension {
6565
@Builder.Default
6666
List<EcsEfsVolume> ecsEfsVolumes = new ArrayList<>();
6767

68+
@Builder.Default
69+
List<String> ecsBindVolumes = new ArrayList<>();
70+
6871
@Builder.Default
6972
List<EcsManagedSecret> ecsManagedSecrets = new ArrayList<>();
7073

@@ -74,6 +77,9 @@ public class EcsSpecExtension extends AbstractSpecExtension {
7477
@Builder.Default
7578
SpelField.String ecsRepositoryCredentialsParameter = new SpelField.String();
7679

80+
@Builder.Default
81+
SpelField.Boolean ecsReadonlyRootFilesystem = new SpelField.Boolean();
82+
7783
@Override
7884
public ISpecExtension firstResolve(SpecExpressionResolver resolver, SpecExpressionContext context) {
7985
return toBuilder()
@@ -82,10 +88,12 @@ public ISpecExtension firstResolve(SpecExpressionResolver resolver, SpecExpressi
8288
.ecsCpuArchitecture(ecsCpuArchitecture.resolve(resolver, context))
8389
.ecsOperationSystemFamily(ecsOperationSystemFamily.resolve(resolver, context))
8490
.ecsEphemeralStorageSize(ecsEphemeralStorageSize.resolve(resolver, context))
85-
.ecsEfsVolumes(ecsEfsVolumes.stream().map(p -> p.resolve(resolver, context)).collect(Collectors.toList()))
86-
.ecsManagedSecrets(ecsManagedSecrets.stream().map(p -> p.resolve(resolver, context)).collect(Collectors.toList()))
91+
.ecsEfsVolumes(ecsEfsVolumes.stream().map(p -> p.resolve(resolver, context)).toList())
92+
.ecsBindVolumes(ecsBindVolumes.stream().map(v -> resolver.evaluateToString( v, context)).toList())
93+
.ecsManagedSecrets(ecsManagedSecrets.stream().map(p -> p.resolve(resolver, context)).toList())
8794
.ecsEnableExecuteCommand(ecsEnableExecuteCommand.resolve(resolver, context))
8895
.ecsRepositoryCredentialsParameter(ecsRepositoryCredentialsParameter.resolve(resolver, context))
96+
.ecsReadonlyRootFilesystem(ecsReadonlyRootFilesystem.resolve(resolver, context))
8997
.build();
9098
}
9199

src/test/java/eu/openanalytics/containerproxy/test/proxy/TestIntegrationOnEcs.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ private EcsClient getEcsClient() {
6666
return ecsClient;
6767
}
6868

69-
7069
@Test
7170
public void launchProxy() {
7271
Assumptions.assumeTrue(checkAwsCredentials(), "Skipping ECS tests");
@@ -168,6 +167,37 @@ public void launchProxyWithRoles() {
168167
}
169168
}
170169

170+
@Test
171+
public void launchProxyWithReadonlyFilesystem() {
172+
Assumptions.assumeTrue(checkAwsCredentials(), "Skipping ECS tests");
173+
try (ContainerSetup containerSetup = new ContainerSetup("ecs")) {
174+
try (ShinyProxyInstance inst = new ShinyProxyInstance("application-test-ecs.yml", Map.of(), true)) {
175+
inst.enableCleanup();
176+
// launch a proxy on ECS
177+
String id = inst.client.startProxy("01_hello_ro");
178+
Proxy proxy = inst.proxyService.getProxy(id);
179+
inst.client.testProxyReachable(id);
180+
181+
Task task = getTask(proxy);
182+
183+
Assertions.assertEquals(21, task.ephemeralStorage().sizeInGiB());
184+
185+
TaskDefinition taskDefinition = getTaskDefinition(task);
186+
ContainerDefinition containerDefinition = taskDefinition.containerDefinitions().getFirst();
187+
188+
// read only filesystem
189+
Assertions.assertTrue(containerDefinition.readonlyRootFilesystem());
190+
Assertions.assertEquals(5, containerDefinition.mountPoints().size());
191+
Assertions.assertEquals(5, taskDefinition.volumes().size());
192+
193+
inst.client.stopProxy(id);
194+
195+
taskDefinition = getTaskDefinition(task);
196+
Assertions.assertNotNull(taskDefinition.deregisteredAt());
197+
}
198+
}
199+
}
200+
171201
@Test
172202
public void testInvalidConfig1() {
173203
TestHelperException ex = Assertions.assertThrows(TestHelperException.class, () -> new ShinyProxyInstance("application-test-ecs-invalid-1.yml"));

src/test/resources/application-test-ecs.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,23 @@ proxy:
5050
port-mapping:
5151
- name: default
5252
port: 3838
53+
- id: 01_hello_ro
54+
ecs-readonly-root-filesystem: true
55+
ecsBindVolumes:
56+
- tmp2
57+
- tmp3
58+
- tmp4
59+
- tmp5
60+
container-specs:
61+
- image: "openanalytics/shinyproxy-integration-test-app"
62+
cmd: [ "R", "-e", "shinyproxy::run_01_hello()" ]
63+
cpu-request: 1024
64+
memory-request: 2048
65+
volumes:
66+
- "tmp2:/var/lib/nginx/tmp"
67+
- "tmp3:/var/lib/nginx/logs"
68+
- "tmp4:/var/log/nginx/logs"
69+
- "tmp5:/run/nginx/"
70+
port-mapping:
71+
- name: default
72+
port: 3838

0 commit comments

Comments
 (0)