Skip to content

Commit 0b15bef

Browse files
committed
Merge pull request 'App Recovery (a.k.a. Session Persistence)' (#38) from feature/22820 into develop
2 parents 48da846 + 21b71f1 commit 0b15bef

34 files changed

Lines changed: 1870 additions & 121 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ application.yml
33
logs
44

55
.idea
6-
*.iml
6+
*.iml
7+
*.log

src/main/java/eu/openanalytics/containerproxy/ContainerProxyApplication.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package eu.openanalytics.containerproxy;
2222

2323
import com.fasterxml.jackson.datatype.jsr353.JSR353Module;
24+
import eu.openanalytics.containerproxy.service.AppRecoveryService;
2425
import eu.openanalytics.containerproxy.util.ProxyMappingManager;
2526
import io.undertow.Handlers;
2627
import io.undertow.servlet.api.ServletSessionConfig;
@@ -73,6 +74,9 @@ public class ContainerProxyApplication {
7374

7475
private final Logger log = LogManager.getLogger(getClass());
7576

77+
@Inject
78+
private AppRecoveryService appRecoveryService;
79+
7680
public static void main(String[] args) {
7781
SpringApplication app = new SpringApplication(ContainerProxyApplication.class);
7882

@@ -221,7 +225,7 @@ public static Properties getDefaultProperties() {
221225
// ====================
222226

223227
// enable redisSession check for the readiness probe
224-
properties.put("management.endpoint.health.group.readiness.include", "readinessProbe,redisSession");
228+
properties.put("management.endpoint.health.group.readiness.include", "readinessProbe,redisSession,appRecoveryReadyIndicator");
225229
// disable ldap health endpoint
226230
properties.put("management.health.ldap.enabled", false);
227231
// disable default redis health endpoint since it's managed by redisSession

src/main/java/eu/openanalytics/containerproxy/backend/AbstractContainerBackend.java

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -74,32 +74,31 @@ public abstract class AbstractContainerBackend implements IContainerBackend {
7474
protected static final String PROPERTY_CERT_PATH = "cert-path";
7575
protected static final String PROPERTY_CONTAINER_PROTOCOL = "container-protocol";
7676
protected static final String PROPERTY_PRIVILEGED = "privileged";
77-
77+
7878
protected static final String DEFAULT_TARGET_PROTOCOL = "http";
79-
8079
protected final Logger log = LogManager.getLogger(getClass());
81-
80+
8281
private boolean useInternalNetwork;
8382
private boolean privileged;
84-
83+
8584
@Inject
8685
protected IProxyTargetMappingStrategy mappingStrategy;
8786

8887
@Inject
8988
protected IProxyTestStrategy testStrategy;
90-
89+
9190
@Inject
9291
protected UserService userService;
93-
92+
9493
@Inject
9594
protected Environment environment;
96-
95+
9796
@Inject
9897
protected SpecExpressionResolver expressionResolver;
99-
98+
10099
@Inject
101100
@Lazy
102-
// Note: lazy needed to work around early initialization conflict
101+
// Note: lazy needed to work around early initialization conflict
103102
protected IAuthenticationBackend authBackend;
104103

105104
protected String realmId;
@@ -161,15 +160,16 @@ protected void doStartProxy(Proxy proxy) throws Exception {
161160
expressionResolver,
162161
userService.getCurrentAuth()
163162
);
163+
164164
Container c = startContainer(eSpec, proxy);
165165
c.setSpec(spec);
166166

167167
proxy.getContainers().add(c);
168168
}
169169
}
170-
170+
171171
protected abstract Container startContainer(ContainerSpec spec, Proxy proxy) throws Exception;
172-
172+
173173
@Override
174174
public void stopProxy(Proxy proxy) throws ContainerProxyException {
175175
try {
@@ -182,23 +182,23 @@ public void stopProxy(Proxy proxy) throws ContainerProxyException {
182182
}
183183

184184
protected abstract void doStopProxy(Proxy proxy) throws Exception;
185-
185+
186186
@Override
187187
public BiConsumer<OutputStream, OutputStream> getOutputAttacher(Proxy proxy) {
188188
// Default: do not support output attaching.
189189
return null;
190190
}
191-
191+
192192
protected String getProperty(String key) {
193193
return getProperty(key, null);
194194
}
195-
195+
196196
protected String getProperty(String key, String defaultValue) {
197197
return environment.getProperty(getPropertyPrefix() + key, defaultValue);
198198
}
199-
199+
200200
protected abstract String getPropertyPrefix();
201-
201+
202202
protected Long memoryToBytes(String memory) {
203203
if (memory == null || memory.isEmpty()) return null;
204204
Matcher matcher = Pattern.compile("(\\d+)([bkmg]?)").matcher(memory.toLowerCase());
@@ -225,34 +225,35 @@ protected Map<String, String> buildEnv(ContainerSpec containerSpec, Proxy proxy)
225225

226226
for (RuntimeValue runtimeValue : proxy.getRuntimeValues().values()) {
227227
if (runtimeValue.getKey().getIncludeAsEnvironmentVariable()) {
228-
env.put(runtimeValue.getKey().getKeyAsEnvVar(), runtimeValue.getValue());
228+
env.put(runtimeValue.getKey().getKeyAsEnvVar(), runtimeValue.getValue());
229229
}
230-
}
231230

232-
String envFile = containerSpec.getEnvFile();
233-
if (envFile != null && Files.isRegularFile(Paths.get(envFile))) {
234-
Properties envProps = new Properties();
235-
envProps.load(new FileInputStream(envFile));
236-
for (Object key: envProps.keySet()) {
237-
env.put(key.toString(), envProps.get(key).toString());
231+
String envFile = containerSpec.getEnvFile();
232+
if (envFile != null && Files.isRegularFile(Paths.get(envFile))) {
233+
Properties envProps = new Properties();
234+
envProps.load(new FileInputStream(envFile));
235+
for (Object key : envProps.keySet()) {
236+
env.put(key.toString(), envProps.get(key).toString());
237+
}
238238
}
239-
}
240239

241-
if (containerSpec.getEnv() != null) {
242-
for (Map.Entry<String, String> entry : containerSpec.getEnv().entrySet()) {
243-
env.put(entry.getKey(), entry.getValue());
240+
if (containerSpec.getEnv() != null) {
241+
for (Map.Entry<String, String> entry : containerSpec.getEnv().entrySet()) {
242+
env.put(entry.getKey(), entry.getValue());
243+
}
244244
}
245+
246+
// Allow the authentication backend to add values to the environment, if needed.
247+
if (authBackend != null) authBackend.customizeContainerEnv(env);
245248
}
246-
247-
if (authBackend != null) authBackend.customizeContainerEnv(env);
248-
249+
249250
return env;
250251
}
251-
252+
252253
protected boolean isUseInternalNetwork() {
253254
return useInternalNetwork;
254255
}
255-
256+
256257
protected boolean isPrivileged() {
257258
return privileged;
258259
}

src/main/java/eu/openanalytics/containerproxy/backend/IContainerBackend.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@
2323
import java.io.OutputStream;
2424
import java.util.function.BiConsumer;
2525

26+
import java.util.List;
27+
import java.util.Map;
28+
2629
import eu.openanalytics.containerproxy.ContainerProxyException;
2730
import eu.openanalytics.containerproxy.model.runtime.Proxy;
2831
import eu.openanalytics.containerproxy.model.runtime.ProxyStatus;
2932
import eu.openanalytics.containerproxy.util.SuccessOrFailure;
33+
import eu.openanalytics.containerproxy.model.runtime.ExistingContainerInfo;
34+
import eu.openanalytics.containerproxy.model.runtime.Container;
3035

3136
public interface IContainerBackend {
3237

@@ -68,4 +73,19 @@ public interface IContainerBackend {
6873
* not support output attaching.
6974
*/
7075
public BiConsumer<OutputStream, OutputStream> getOutputAttacher(Proxy proxy);
76+
77+
/**
78+
* Scans for running/existing apps that need to be recovered.
79+
*
80+
* @return a list of existing containers.
81+
*/
82+
public List<ExistingContainerInfo> scanExistingContainers() throws Exception;
83+
84+
/**
85+
* Setups the port mapping for an existing proxy in exact the same way as if the proxy was newly created.
86+
* @param proxy The proxy to create the port mapping for.
87+
* @param container The specific container to create the port mapping for.
88+
* @param portBindings The portbindings of this container, as generated by the scanExistingContainers method.
89+
*/
90+
public void setupPortMappingExistingProxy(Proxy proxy, Container container, Map<Integer, Integer> portBindings) throws Exception;
7191
}

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

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,48 +22,64 @@
2222

2323
import java.io.IOException;
2424
import java.io.OutputStream;
25+
import java.net.URI;
2526
import java.nio.file.Paths;
27+
import java.util.HashMap;
2628
import java.util.ArrayList;
2729
import java.util.List;
2830
import java.util.Map;
2931
import java.util.function.BiConsumer;
3032

33+
import com.google.common.collect.ImmutableMap;
3134
import com.spotify.docker.client.DefaultDockerClient;
3235
import com.spotify.docker.client.DockerCertificates;
3336
import com.spotify.docker.client.DockerClient;
3437
import com.spotify.docker.client.DockerClient.LogsParam;
3538
import com.spotify.docker.client.LogStream;
3639
import com.spotify.docker.client.exceptions.DockerCertificateException;
3740
import com.spotify.docker.client.exceptions.DockerException;
38-
3941
import eu.openanalytics.containerproxy.ContainerProxyException;
4042
import eu.openanalytics.containerproxy.backend.AbstractContainerBackend;
4143
import eu.openanalytics.containerproxy.model.runtime.Container;
4244
import eu.openanalytics.containerproxy.model.runtime.Proxy;
45+
import eu.openanalytics.containerproxy.model.runtime.runtimevalues.InstanceIdKey;
46+
import eu.openanalytics.containerproxy.model.runtime.runtimevalues.RuntimeValue;
47+
import eu.openanalytics.containerproxy.model.runtime.runtimevalues.RuntimeValueKey;
48+
import eu.openanalytics.containerproxy.model.runtime.runtimevalues.RuntimeValueKeyRegistry;
4349
import eu.openanalytics.containerproxy.util.PortAllocator;
4450

51+
import java.io.IOException;
52+
import java.io.OutputStream;
53+
import java.net.URI;
54+
import java.nio.file.Paths;
55+
import java.util.ArrayList;
56+
import java.util.List;
57+
import java.util.Map;
58+
import java.util.Optional;
59+
import java.util.function.BiConsumer;
60+
4561

4662
public abstract class AbstractDockerBackend extends AbstractContainerBackend {
47-
63+
4864
private static final String PROPERTY_PREFIX = "proxy.docker.";
4965

5066
protected static final String PROPERTY_APP_PORT = "port";
5167
protected static final String PROPERTY_PORT_RANGE_START = "port-range-start";
5268
protected static final String PROPERTY_PORT_RANGE_MAX = "port-range-max";
53-
69+
5470
protected static final String DEFAULT_TARGET_URL = DEFAULT_TARGET_PROTOCOL + "://localhost";
55-
71+
5672
protected PortAllocator portAllocator;
5773
protected DockerClient dockerClient;
58-
74+
5975
@Override
6076
public void initialize() throws ContainerProxyException {
6177
super.initialize();
62-
78+
6379
int startPort = Integer.valueOf(getProperty(PROPERTY_PORT_RANGE_START, "20000"));
6480
int maxPort = Integer.valueOf(getProperty(PROPERTY_PORT_RANGE_MAX, "-1"));
6581
portAllocator = new PortAllocator(startPort, maxPort);
66-
82+
6783
DefaultDockerClient.Builder builder = null;
6884
try {
6985
builder = DefaultDockerClient.fromEnv();
@@ -73,7 +89,7 @@ public void initialize() throws ContainerProxyException {
7389

7490
String confCertPath = getProperty(PROPERTY_CERT_PATH);
7591
if (confCertPath != null) {
76-
try {
92+
try {
7793
builder.dockerCertificates(DockerCertificates.builder().dockerCertPath(Paths.get(confCertPath)).build().orNull());
7894
} catch (DockerCertificateException e) {
7995
throw new ContainerProxyException("Failed to initialize docker client using certificates from " + confCertPath, e);
@@ -85,12 +101,12 @@ public void initialize() throws ContainerProxyException {
85101

86102
dockerClient = builder.build();
87103
}
88-
104+
89105
@Override
90106
public BiConsumer<OutputStream, OutputStream> getOutputAttacher(Proxy proxy) {
91107
Container c = getPrimaryContainer(proxy);
92108
if (c == null) return null;
93-
109+
94110
return (stdOut, stdErr) -> {
95111
try {
96112
LogStream logStream = dockerClient.logs(c.getId(), LogsParam.follow(), LogsParam.stdout(), LogsParam.stderr());
@@ -105,11 +121,29 @@ public BiConsumer<OutputStream, OutputStream> getOutputAttacher(Proxy proxy) {
105121
protected String getPropertyPrefix() {
106122
return PROPERTY_PREFIX;
107123
}
108-
124+
109125
protected Container getPrimaryContainer(Proxy proxy) {
110126
return proxy.getContainers().isEmpty() ? null : proxy.getContainers().get(0);
111127
}
112128

129+
abstract protected URI calculateTarget(Container container, int containerPort, int hostPort) throws Exception;
130+
131+
public void setupPortMappingExistingProxy(Proxy proxy, Container container, Map<Integer, Integer> portBindings) throws Exception {
132+
for (String mappingKey : container.getSpec().getPortMapping().keySet()) {
133+
int containerPort = container.getSpec().getPortMapping().get(mappingKey);
134+
135+
int servicePort = -1; // in case of internal networking
136+
if (portBindings.containsKey(containerPort) && portBindings.get(containerPort) != 0) {
137+
// in case of non internal networking
138+
servicePort = portBindings.get(containerPort);
139+
portAllocator.addExistingPort(proxy.getUserId(), servicePort);
140+
}
141+
142+
String mapping = mappingStrategy.createMapping(mappingKey, container, proxy);
143+
URI target = calculateTarget(container, containerPort, servicePort);
144+
proxy.getTargets().put(mapping, target);
145+
}
146+
}
113147

114148
protected List<String> convertEnv(Map<String, String> env) {
115149
List<String> res = new ArrayList<>();
@@ -121,4 +155,33 @@ protected List<String> convertEnv(Map<String, String> env) {
121155
return res;
122156
}
123157

158+
protected Map<RuntimeValueKey, RuntimeValue> parseLabelsAsRuntimeValues(String containerId, ImmutableMap<String, String> labels) {
159+
if (labels == null) {
160+
return null;
161+
}
162+
163+
String containerInstanceId = labels.get(InstanceIdKey.inst.getKeyAsLabel());
164+
if (containerInstanceId == null || !containerInstanceId.equals(instanceId)) {
165+
log.warn("Ignoring container {} because instanceId {} is not correct", containerId, containerInstanceId);
166+
return null;
167+
}
168+
169+
Map<RuntimeValueKey, RuntimeValue> runtimeValues = new HashMap<>();
170+
171+
for (RuntimeValueKey key : RuntimeValueKeyRegistry.getRuntimeValueKeys()) {
172+
if (key.getIncludeAsLabel() || key.getIncludeAsAnnotation()) {
173+
String value = labels.get(key.getKeyAsLabel());
174+
if (value != null) {
175+
runtimeValues.put(key, new RuntimeValue(key, value));
176+
} else if (key.isRequired()) {
177+
// value is null but is required
178+
log.warn("Ignoring container {} because no label named {} is found", containerId, key.getKeyAsLabel());
179+
return null;
180+
}
181+
}
182+
}
183+
184+
return runtimeValues;
185+
}
186+
124187
}

0 commit comments

Comments
 (0)