Skip to content

Commit eb711a6

Browse files
committed
Adds a new NaisRuntimeEnvironmentConnector supporting dynamically set environment variables from pod when running locally.
1 parent bb1f211 commit eb711a6

File tree

4 files changed

+206
-7
lines changed

4 files changed

+206
-7
lines changed

libs/testing/src/main/java/no/nav/dolly/libs/nais/NaisEnvironmentApplicationContextInitializer.java

+19-7
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import lombok.extern.slf4j.Slf4j;
44
import org.springframework.context.ApplicationContextInitializer;
55
import org.springframework.context.ConfigurableApplicationContext;
6+
import org.springframework.core.env.ConfigurableEnvironment;
67
import org.springframework.lang.NonNull;
78

8-
import java.util.Map;
99
import java.util.stream.Stream;
1010

1111
@Slf4j
@@ -21,17 +21,27 @@ public void initialize(@NonNull ConfigurableApplicationContext context) {
2121
.of(environment.getActiveProfiles())
2222
.forEach(profile -> {
2323
switch (profile) {
24-
case "local", "localdb" -> configureForLocalProfile(environment.getSystemProperties());
25-
case "test" -> configureForTestProfile(environment.getSystemProperties());
26-
default -> configureForOtherProfiles(environment.getSystemProperties());
24+
case "local", "localdb" -> configureForLocalProfile(environment);
25+
case "test" -> configureForTestProfile(environment);
26+
default -> configureForOtherProfiles(environment);
2727
}
2828
});
2929

3030
}
3131

32-
private static void configureForLocalProfile(Map<String, Object> properties) {
32+
private static void configureForLocalProfile(ConfigurableEnvironment environment) {
3333

3434
log.info("Configuring environment for local profile using Secret Manager");
35+
var properties = environment.getSystemProperties();
36+
37+
// Dynamically load any configured environment variables from NAIS pod.
38+
try {
39+
new NaisRuntimeEnvironmentConnector(environment)
40+
.getEnvironmentVariables()
41+
.forEach(properties::putIfAbsent);
42+
} catch (NaisEnvironmentException e) {
43+
log.warn("Failed to dynamically load environment variables using {}", NaisRuntimeEnvironmentConnector.class.getSimpleName(), e);
44+
}
3545

3646
// Emulating NAIS provided environment variables.
3747
properties.putIfAbsent("AZURE_APP_CLIENT_ID", "${sm\\://azure-app-client-id}");
@@ -53,9 +63,10 @@ private static void configureForLocalProfile(Map<String, Object> properties) {
5363

5464
}
5565

56-
private static void configureForTestProfile(Map<String, Object> properties) {
66+
private static void configureForTestProfile(ConfigurableEnvironment environment) {
5767

5868
log.info("Configuring environment for test profile using dummy values");
69+
var properties = environment.getSystemProperties();
5970

6071
// Disabling Secret Manager (not available when running builds on GitHub).
6172
properties.putIfAbsent("spring.cloud.gcp.secretmanager.enabled", "false");
@@ -80,9 +91,10 @@ private static void configureForTestProfile(Map<String, Object> properties) {
8091

8192
}
8293

83-
private static void configureForOtherProfiles(Map<String, Object> properties) {
94+
private static void configureForOtherProfiles(ConfigurableEnvironment environment) {
8495

8596
log.info("Configuring environment for non-test, non-local profiles");
97+
var properties = environment.getSystemProperties();
8698

8799
properties.putIfAbsent("spring.main.banner-mode", "off");
88100
properties.putIfAbsent("spring.cloud.gcp.secretmanager.enabled", "false"); // Unless we actually start using Secret Manager in deployment.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package no.nav.dolly.libs.nais;
2+
3+
class NaisEnvironmentException extends Exception {
4+
5+
NaisEnvironmentException(String message) {
6+
super(message);
7+
}
8+
9+
NaisEnvironmentException(String message, Throwable cause) {
10+
super(message, cause);
11+
}
12+
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package no.nav.dolly.libs.nais;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.core.env.ConfigurableEnvironment;
6+
7+
import java.io.BufferedReader;
8+
import java.io.InputStreamReader;
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.Optional;
13+
import java.util.stream.Collectors;
14+
15+
@RequiredArgsConstructor
16+
@Slf4j
17+
class NaisRuntimeEnvironmentConnector {
18+
19+
private final ConfigurableEnvironment environment;
20+
21+
Map<String, String> getEnvironmentVariables()
22+
throws NaisEnvironmentException {
23+
24+
if (!environment.matchesProfiles("local")) {
25+
throw new NaisEnvironmentException("Attempted to load environment variables from pod in non-local profile");
26+
}
27+
try {
28+
var applicationName = resolveApplicationName();
29+
var requestedKeys = resolveRequestedKeys();
30+
if (applicationName == null || requestedKeys.isEmpty()) {
31+
log.info("Application is not configured for dynamic environment variables from NAIS");
32+
return Map.of();
33+
}
34+
var cluster = resolveCluster();
35+
var pod = resolvePod(cluster, applicationName);
36+
return getVariables(cluster, pod, requestedKeys);
37+
} catch (NaisEnvironmentException e) {
38+
throw e;
39+
} catch (Exception e) {
40+
throw new NaisEnvironmentException("Unexpected failure", e);
41+
}
42+
43+
}
44+
45+
private List<String> resolveRequestedKeys() {
46+
47+
var keys = new ArrayList<String>();
48+
for (int i = 0; ; i++) {
49+
var key = "dolly.nais.variables[%d]".formatted(i);
50+
if (!environment.containsProperty(key)) {
51+
break;
52+
}
53+
keys.add(environment.getProperty(key));
54+
}
55+
return keys;
56+
57+
}
58+
59+
private String resolveApplicationName() {
60+
61+
return Optional
62+
.ofNullable(environment.getProperty("dolly.nais.name"))
63+
.map(Object::toString)
64+
.orElse(null);
65+
66+
}
67+
68+
private String resolveCluster() {
69+
70+
return Optional
71+
.ofNullable(environment.getProperty("dolly.nais.cluster"))
72+
.map(Object::toString)
73+
.orElseGet(() -> {
74+
log.info("Cannot determine cluster from dolly.nais.cluster, guessing on dev-gcp");
75+
return "dev-gcp";
76+
});
77+
78+
}
79+
80+
private String resolvePod(String cluster, String applicationName)
81+
throws NaisEnvironmentException {
82+
83+
var pods = getAllPodsInClusterWithName(cluster, applicationName);
84+
if (pods.isEmpty()) {
85+
throw new NaisEnvironmentException("No pods found for %s in %s".formatted(applicationName, cluster));
86+
}
87+
if (pods.size() > 1) {
88+
log.warn("Multiple pods found for {} in {}, picking {}", applicationName, cluster, pods.getFirst());
89+
}
90+
return pods.getFirst();
91+
92+
}
93+
94+
private Map<String, String> getVariables(String cluster, String pod, List<String> requestedKeys)
95+
throws NaisEnvironmentException {
96+
97+
var command = "kubectl exec --cluster=%s --namespace=dolly %s -- env"
98+
.formatted(cluster, pod);
99+
var output = execute(command);
100+
var variables = output
101+
.stream()
102+
.map(line -> line.split("="))
103+
.filter(elements -> requestedKeys.contains(elements[0]))
104+
.collect(Collectors.toMap(
105+
elements -> elements[0],
106+
elements -> elements[1],
107+
(a, b) -> a));
108+
log.info("Retrieved {}/{} keys from pod {}:{}", variables.size(), requestedKeys.size(), cluster, pod);
109+
return variables;
110+
111+
}
112+
113+
private List<String> getAllPodsInClusterWithName(String cluster, String name)
114+
throws NaisEnvironmentException {
115+
116+
var command = "kubectl get pods --cluster=%s --namespace=dolly -l app=%s -o name"
117+
.formatted(cluster, name);
118+
return execute(command)
119+
.stream()
120+
.map(pod -> pod.substring(pod.lastIndexOf("/") + 1).trim())
121+
.sorted()
122+
.toList();
123+
124+
}
125+
126+
private List<String> execute(String command)
127+
throws NaisEnvironmentException {
128+
129+
var processBuilder = new ProcessBuilder(command.split(" "));
130+
try {
131+
var process = processBuilder.start();
132+
var reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
133+
var output = new ArrayList<String>();
134+
String line;
135+
while ((line = reader.readLine()) != null) {
136+
output.add(line);
137+
}
138+
var status = process.waitFor();
139+
if (status != 0) {
140+
log.warn("Command terminated with status {}: {}", status, command);
141+
}
142+
return output;
143+
} catch (InterruptedException e) {
144+
log.warn("Interrupted while waiting for command: {}", command, e);
145+
Thread.currentThread().interrupt();
146+
return List.of();
147+
}
148+
catch (Exception e) {
149+
throw new NaisEnvironmentException("Failed to execute command: " + command, e);
150+
}
151+
152+
}
153+
154+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"properties": [
3+
{
4+
"name": "dolly.nais.cluster",
5+
"type": "java.lang.String",
6+
"defaultValue": "dev-gcp",
7+
"description": "The cluster where the application is running.. Defaults to dev-gcp if not set."
8+
},
9+
{
10+
"name": "dolly.nais.name",
11+
"type": "java.lang.String",
12+
"description": "Application name, as defined in the NAIS manifest (which is not necessarily the same as in Spring config)."
13+
},
14+
{
15+
"name": "dolly.nais.variables",
16+
"type": "java.util.List",
17+
"description": "List of environment variables to be read from the NAIS environment, and set as system properties when running in local profile."
18+
}
19+
]
20+
}

0 commit comments

Comments
 (0)