Skip to content

Commit d4bcf07

Browse files
committed
Ref #29447: add config change test
1 parent 57f71d3 commit d4bcf07

5 files changed

Lines changed: 238 additions & 4 deletions

File tree

src/main/java/eu/openanalytics/containerproxy/backend/docker/DockerEngineBackend.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.mandas.docker.client.exceptions.ConflictException;
3939
import org.mandas.docker.client.exceptions.ContainerNotFoundException;
4040
import org.mandas.docker.client.exceptions.DockerException;
41+
import org.mandas.docker.client.exceptions.DockerRequestException;
4142
import org.mandas.docker.client.exceptions.NotFoundException;
4243
import org.mandas.docker.client.messages.AttachedNetwork;
4344
import org.mandas.docker.client.messages.ContainerConfig;
@@ -207,7 +208,11 @@ protected void doStopProxy(Proxy proxy) throws Exception {
207208
if (containerInfo != null && containerInfo.networkSettings() != null
208209
&& containerInfo.networkSettings().networks() != null) {
209210
for (AttachedNetwork network : containerInfo.networkSettings().networks().values()) {
210-
dockerClient.disconnectFromNetwork(container.getId(), network.networkId());
211+
try {
212+
dockerClient.disconnectFromNetwork(container.getId(), network.networkId());
213+
} catch (DockerRequestException ex) {
214+
// ignore, network is already disconnected
215+
}
211216
}
212217
}
213218
dockerClient.removeContainer(container.getId(), DockerClient.RemoveContainerParam.forceKill());
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2024 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.test.helpers;
22+
23+
24+
import org.mandas.docker.client.DockerClient;
25+
import org.mandas.docker.client.builder.jersey.JerseyDockerClientBuilder;
26+
import org.mandas.docker.client.exceptions.DockerCertificateException;
27+
import org.mandas.docker.client.exceptions.DockerException;
28+
import org.mandas.docker.client.messages.ContainerConfig;
29+
import org.mandas.docker.client.messages.HostConfig;
30+
import org.mandas.docker.client.messages.PortBinding;
31+
32+
import java.util.Collections;
33+
import java.util.Map;
34+
35+
public class RedisServer implements AutoCloseable {
36+
37+
private final String containerId;
38+
39+
public RedisServer() {
40+
try (DockerClient dockerClient = new JerseyDockerClientBuilder().fromEnv().build()) {
41+
dockerClient.pull("redis:7");
42+
containerId = dockerClient.createContainer(ContainerConfig.builder()
43+
.image("redis:7")
44+
.hostConfig(HostConfig.builder()
45+
.portBindings(Map.of("6379", Collections.singletonList(PortBinding.of("127.0.0.1", "3379"))))
46+
.build())
47+
.exposedPorts("6379")
48+
.build(), "redis-itest").id();
49+
try {
50+
dockerClient.startContainer(containerId);
51+
} catch (Throwable t) {
52+
dockerClient.removeContainer(containerId, DockerClient.RemoveContainerParam.forceKill());
53+
throw new TestHelperException("Error while setting up Redis", t);
54+
}
55+
} catch (Throwable t) {
56+
throw new TestHelperException("Error while setting up Redis", t);
57+
}
58+
}
59+
60+
@Override
61+
public void close() {
62+
try (DockerClient dockerClient = new JerseyDockerClientBuilder().fromEnv().build()) {
63+
dockerClient.removeContainer(containerId, DockerClient.RemoveContainerParam.forceKill());
64+
} catch (DockerCertificateException | DockerException | InterruptedException e) {
65+
throw new RuntimeException(e);
66+
}
67+
}
68+
69+
}

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

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import eu.openanalytics.containerproxy.model.runtime.runtimevalues.PublicPathKey;
3434
import eu.openanalytics.containerproxy.model.runtime.runtimevalues.TargetIdKey;
3535
import eu.openanalytics.containerproxy.test.helpers.ContainerSetup;
36+
import eu.openanalytics.containerproxy.test.helpers.RedisServer;
3637
import eu.openanalytics.containerproxy.test.helpers.ShinyProxyInstance;
3738
import eu.openanalytics.containerproxy.test.helpers.TestHelperException;
3839
import eu.openanalytics.containerproxy.test.helpers.TestProxySharingScaler;
@@ -55,6 +56,7 @@
5556
import java.util.ArrayList;
5657
import java.util.List;
5758
import java.util.Map;
59+
import java.util.Optional;
5860
import java.util.stream.Stream;
5961

6062
public class TestPreInitialization {
@@ -245,7 +247,7 @@ public void testDelegateProxyCrashed() throws DockerCertificateException, Docker
245247
// DelegateProxy should have no seats and marked for removal
246248
delegateProxy = delegateProxyStore.getDelegateProxy(proxy.getTargetId());
247249
Assertions.assertEquals(DelegateProxyStatus.ToRemove, delegateProxy.getDelegateProxyStatus());
248-
Assertions.assertTrue( delegateProxy.getSeatIds().isEmpty());
250+
Assertions.assertTrue(delegateProxy.getSeatIds().isEmpty());
249251

250252
// enable cleanup
251253
proxySharingScaler.enableCleanup();
@@ -261,9 +263,85 @@ public void testDelegateProxyCrashed() throws DockerCertificateException, Docker
261263
}
262264
}
263265

266+
@ParameterizedTest
267+
@MethodSource("backends")
268+
public void testConfigChange(String backend, Map<String, String> properties) {
269+
try (ContainerSetup containerSetup = new ContainerSetup(backend)) {
270+
try (RedisServer redisServer = new RedisServer()) {
271+
// launch an instance and app
272+
String oldAppId;
273+
try (ShinyProxyInstance inst = new ShinyProxyInstance("application-test-pre-initialization-redis-1.yml", properties, true)) {
274+
oldAppId = inst.client.startProxy("myApp");
275+
Proxy proxy = inst.proxyService.getProxy(oldAppId);
276+
inst.client.testProxyReachable(proxy.getTargetId());
277+
278+
// target id should be different from proxy id
279+
Assertions.assertNotEquals(proxy.getTargetId(), proxy.getId());
280+
Assertions.assertEquals("/api/route/" + proxy.getTargetId() + "/", proxy.getRuntimeValue(PublicPathKey.inst));
281+
Assertions.assertEquals(proxy.getTargetId(), proxy.getRuntimeValue(TargetIdKey.inst));
282+
Assertions.assertNotNull(proxy.getRuntimeValue(SeatIdKey.inst));
283+
284+
}
285+
// re-start instance with updated app config
286+
try (ShinyProxyInstance inst = new ShinyProxyInstance("application-test-pre-initialization-redis-2.yml", properties, true)) {
287+
TestProxySharingScaler proxySharingScaler = inst.getBean("proxySharingScaler_myApp", TestProxySharingScaler.class);
288+
proxySharingScaler.disableCleanup();
289+
IDelegateProxyStore delegateProxyStore = proxySharingScaler.getDelegateProxyStore();
290+
inst.enableCleanup();
291+
292+
Proxy proxy = inst.proxyService.getProxy(oldAppId);
293+
// old app should still exist
294+
Assertions.assertNotNull(proxy);
295+
// old app should still be reachable
296+
inst.client.testProxyReachable(proxy.getTargetId());
297+
298+
DelegateProxy delegateProxy = delegateProxyStore.getDelegateProxy(proxy.getTargetId());
299+
Assertions.assertEquals(DelegateProxyStatus.ToRemove, delegateProxy.getDelegateProxyStatus());
300+
Assertions.assertEquals("641fead76e0c432d0ae258a9745bcf69eac7b3c3", delegateProxy.getProxySpecHash());
301+
302+
// should create new instance with new hash
303+
waitUntilNumberOfDelegateProxies(proxySharingScaler, 3);
304+
305+
// running DelegateProxy with old config should exist in DelegateProxyStore
306+
Optional<DelegateProxy> delegateProxyInUse = delegateProxyStore.getAllDelegateProxies().stream()
307+
.filter(it -> it.getProxy().getId().equals(proxy.getTargetId()))
308+
.findFirst();
309+
Assertions.assertTrue(delegateProxyInUse.isPresent());
310+
311+
// a second DelegateProxy with old config should exist in DelegateProxyStore
312+
Optional<DelegateProxy> secondDelegateProxy = delegateProxyStore.getAllDelegateProxies().stream()
313+
.filter(it -> it.getProxySpecHash().equals("641fead76e0c432d0ae258a9745bcf69eac7b3c3") && !it.getProxy().getId().equals(proxy.getTargetId()))
314+
.findFirst();
315+
Assertions.assertTrue(secondDelegateProxy.isPresent());
316+
Assertions.assertEquals(DelegateProxyStatus.ToRemove, secondDelegateProxy.get().getDelegateProxyStatus());
317+
318+
// a DelegateProxy with new config should exist in DelegateProxyStore
319+
Optional<DelegateProxy> newDelegateProxy = delegateProxyStore.getAllDelegateProxies().stream()
320+
.filter(it -> !it.getProxySpecHash().equals("641fead76e0c432d0ae258a9745bcf69eac7b3c3"))
321+
.findFirst();
322+
Assertions.assertTrue(newDelegateProxy.isPresent());
323+
waitUntilDelegateProxyIsAvailable(proxySharingScaler, newDelegateProxy.get().getProxy().getId());
324+
Assertions.assertEquals(DelegateProxyStatus.Available, newDelegateProxy.get().getDelegateProxyStatus());
325+
Assertions.assertEquals("b21e966c35c7689f465f06722b13ec28cead0e33", newDelegateProxy.get().getProxySpecHash());
326+
327+
// stop running app
328+
inst.client.stopProxy(oldAppId);
329+
330+
proxySharingScaler.enableCleanup();
331+
332+
// proxies with old version should get cleaned up
333+
waitUntilNumberOfDelegateProxies(proxySharingScaler, 1);
334+
DelegateProxy newDelegateProxy2 = delegateProxyStore.getAllDelegateProxies().stream().findFirst().get();
335+
Assertions.assertEquals(newDelegateProxy.get().getProxy().getId(), newDelegateProxy2.getProxy().getId());
336+
Assertions.assertEquals("b21e966c35c7689f465f06722b13ec28cead0e33", newDelegateProxy2.getProxySpecHash());
337+
}
338+
}
339+
}
340+
}
341+
264342
// TODO test scaleDownDelay
265-
// TODO test minimumSeatsAvailable
266-
// TODO test config change
343+
// TODO test multiple seats
344+
// TODO test DelegateProxy structure
267345

268346
private void waitUntilNoPendingSeats(ProxySharingScaler proxySharingScaler) {
269347
boolean noPendingSeats = Retrying.retry((c, m) -> {
@@ -286,4 +364,11 @@ private void waitUntilNumberOfDelegateProxies(TestProxySharingScaler proxySharin
286364
Assertions.assertTrue(noPendingSeats);
287365
}
288366

367+
private void waitUntilDelegateProxyIsAvailable(TestProxySharingScaler proxySharingScaler, String delegateProxyId) {
368+
boolean noPendingSeats = Retrying.retry((c, m) -> {
369+
return proxySharingScaler.getDelegateProxyStore().getDelegateProxy(delegateProxyId).getDelegateProxyStatus() == DelegateProxyStatus.Available;
370+
}, 60_000, "assert number delegated proxies", 1, true);
371+
Assertions.assertTrue(noPendingSeats);
372+
}
373+
289374
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
spring:
2+
session:
3+
store-type: redis
4+
data:
5+
redis:
6+
host: localhost
7+
port: 3379
8+
repositories:
9+
enabled: false
10+
proxy:
11+
authentication: simple
12+
heartbeat-timeout: -1
13+
default-stop-proxy-on-logout: false
14+
stop-proxies-on-shutdown: false
15+
store-mode: Redis
16+
17+
users:
18+
- name: demo
19+
password: demo
20+
groups:
21+
- group1
22+
- group2
23+
- name: demo2
24+
password: demo2
25+
26+
docker:
27+
url: http://localhost:2375
28+
29+
specs:
30+
- id: myApp
31+
minimum-seats-available: 1
32+
container-specs:
33+
- image: "openanalytics/shinyproxy-integration-test-app"
34+
cmd: [ "R", "-e", "shinyproxy::run_01_hello()" ]
35+
port-mapping:
36+
- name: default
37+
port: 3838
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
spring:
2+
session:
3+
store-type: redis
4+
data:
5+
redis:
6+
host: localhost
7+
port: 3379
8+
repositories:
9+
enabled: false
10+
proxy:
11+
authentication: simple
12+
heartbeat-timeout: -1
13+
default-stop-proxy-on-logout: false
14+
stop-proxies-on-shutdown: false
15+
store-mode: Redis
16+
17+
users:
18+
- name: demo
19+
password: demo
20+
groups:
21+
- group1
22+
- group2
23+
- name: demo2
24+
password: demo2
25+
26+
docker:
27+
url: http://localhost:2375
28+
29+
specs:
30+
- id: myApp
31+
display-name: myUpdatedDisplayName
32+
minimum-seats-available: 1
33+
container-specs:
34+
- image: "openanalytics/shinyproxy-integration-test-app"
35+
cmd: [ "R", "-e", "shinyproxy::run_01_hello()" ]
36+
port-mapping:
37+
- name: default
38+
port: 3838

0 commit comments

Comments
 (0)