Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/nais_runtime_connector #3800

Merged
merged 6 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
ALTINN_URL: ${sm\://altinn-url}
MASKINPORTEN_CLIENT_ID: ef2960de-7fa6-4396-80a5-2eca00e4af28
MASKINPORTEN_SCOPES: altinn:resourceregistry/accesslist.read altinn:resourceregistry/accesslist.write altinn:accessmanagement/authorizedparties.resourceowner

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTINN_URL: dummy
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
CRYPTOGRAPHY_SECRET: dummy
TOKEN_X_CLIENT_ID: dev-gcp:dolly:testnav-bruker-service-dev

spring:
Expand Down
2 changes: 2 additions & 0 deletions apps/bruker-service/src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
CRYPTOGRAPHY_SECRET: dummy

spring:
flyway:
url: jdbc:h2:mem:testdb
Expand Down
2 changes: 2 additions & 0 deletions apps/dolly-frontend/src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
IDPORTEN_CLIENT_ID: dummy
IDPORTEN_CLIENT_JWK: dummy
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
SLACK_CHANNEL: dummy
SLACK_TOKEN: dummy

consumers:
profil-api:
url: https://testnorge-profil-api.intern.dev.nav.no
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
SLACK_TOKEN: dummy
SLACK_CHANNEL: dummy
SLACK_CHANNEL: dummy
SLACK_TOKEN: dummy
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.lang.NonNull;

import java.util.Map;
import java.util.stream.Stream;

@Slf4j
Expand All @@ -21,45 +21,52 @@ public void initialize(@NonNull ConfigurableApplicationContext context) {
.of(environment.getActiveProfiles())
.forEach(profile -> {
switch (profile) {
case "local", "localdb" -> configureForLocalProfile(environment.getSystemProperties());
case "test" -> configureForTestProfile(environment.getSystemProperties());
default -> configureForOtherProfiles(environment.getSystemProperties());
case "local", "localdb" -> configureForLocalProfile(environment);
case "test" -> configureForTestProfile(environment);
default -> configureForOtherProfiles(environment);
}
});

}

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

log.info("Configuring environment for local profile using Secret Manager");
var properties = environment.getSystemProperties();

// Dynamically load any configured environment variables from NAIS pod.
try {
new NaisRuntimeEnvironmentConnector(environment)
.getEnvironmentVariables()
.forEach(properties::putIfAbsent);
} catch (NaisEnvironmentException e) {
log.warn("Failed to dynamically load environment variables using {}", NaisRuntimeEnvironmentConnector.class.getSimpleName(), e);
}

// Emulating NAIS provided environment variables.
properties.putIfAbsent("ALTINN_URL", "${sm\\://altinn-url}"); // Used by altinn3-tilgang-service only.
properties.putIfAbsent("AZURE_APP_CLIENT_ID", "${sm\\://azure-app-client-id}");
properties.putIfAbsent("AZURE_APP_CLIENT_SECRET", "${sm\\://azure-app-client-secret}");
properties.putIfAbsent("AZURE_NAV_APP_CLIENT_ID", DUMMY); // Value found in pod, if needed.
properties.putIfAbsent("AZURE_NAV_APP_CLIENT_SECRET", DUMMY); // Value found in pod, if needed.
properties.putIfAbsent("AZURE_NAV_OPENID_CONFIG_TOKEN_ENDPOINT", "${sm\\://azure-nav-openid-config-token-endpoint}");
properties.putIfAbsent("AZURE_OPENID_CONFIG_ISSUER", "${sm\\://azure-openid-config-issuer}");
properties.putIfAbsent("AZURE_OPENID_CONFIG_TOKEN_ENDPOINT", "${sm\\://azure-openid-config-token-endpoint}");
properties.putIfAbsent("AZURE_TRYGDEETATEN_APP_CLIENT_ID", DUMMY);
properties.putIfAbsent("AZURE_TRYGDEETATEN_APP_CLIENT_SECRET", DUMMY);
properties.putIfAbsent("AZURE_TRYGDEETATEN_OPENID_CONFIG_TOKEN_ENDPOINT", DUMMY);
properties.putIfAbsent("CRYPTOGRAPHY_SECRET", DUMMY); // Used by bruker-service only.
properties.putIfAbsent("JWT_SECRET", DUMMY); // Used by bruker-service only.
properties.putIfAbsent("AZURE_TRYGDEETATEN_APP_CLIENT_ID", DUMMY); // Value found in pod, if needed.
properties.putIfAbsent("AZURE_TRYGDEETATEN_APP_CLIENT_SECRET", DUMMY); // Value found in pod, if needed.
properties.putIfAbsent("AZURE_TRYGDEETATEN_OPENID_CONFIG_TOKEN_ENDPOINT", "${sm\\://azure-trygdeetaten-openid-config-token-endpoint}");
properties.putIfAbsent("JWT_SECRET", DUMMY);
properties.putIfAbsent("MASKINPORTEN_CLIENT_ID", DUMMY); // Used by tenor-search-service and altinn3-tilgang-service only.
properties.putIfAbsent("MASKINPORTEN_CLIENT_JWK", DUMMY); // Used by tenor-search-service and altinn3-tilgang-service only.
properties.putIfAbsent("MASKINPORTEN_SCOPES", DUMMY); // Used by tenor-search-service and altinn3-tilgang-service only.
properties.putIfAbsent("MASKINPORTEN_WELL_KNOWN_URL", "${sm\\://maskinporten-well-known-url}"); // Used by tenor-search-service and altinn3-tilgang-service only.
properties.putIfAbsent("SLACK_CHANNEL", DUMMY); // Used by tilbakemelding-api only.
properties.putIfAbsent("SLACK_TOKEN", DUMMY); // Used by tilbakemelding-api only.
properties.putIfAbsent("TOKEN_X_ISSUER", "${sm\\://token-x-issuer}");

}

private static void configureForTestProfile(Map<String, Object> properties) {
private static void configureForTestProfile(ConfigurableEnvironment environment) {

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

// Disabling Secret Manager (not available when running builds on GitHub).
properties.putIfAbsent("spring.cloud.gcp.secretmanager.enabled", "false");
Expand All @@ -70,13 +77,9 @@ private static void configureForTestProfile(Map<String, Object> properties) {
"spring.cloud.vault.token", // For apps using no.nav.testnav.libs:vault.

"ALTINN_API_KEY",
"ALTINN_URL",
"AZURE_OPENID_CONFIG_ISSUER",
"AZURE_OPENID_CONFIG_TOKEN_ENDPOINT",
"CRYPTOGRAPHY_SECRET", // Used by bruker-service only.
"IDPORTEN_CLIENT_ID", // Used by dolly-frontend only.
"IDPORTEN_CLIENT_JWK", // Used by dolly-frontend only.
"JWT_SECRET", // Used by bruker-service only.
"JWT_SECRET",
"MASKINPORTEN_CLIENT_ID",
"MASKINPORTEN_CLIENT_JWK",
"MASKINPORTEN_SCOPES",
Expand All @@ -88,9 +91,10 @@ private static void configureForTestProfile(Map<String, Object> properties) {

}

private static void configureForOtherProfiles(Map<String, Object> properties) {
private static void configureForOtherProfiles(ConfigurableEnvironment environment) {

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

properties.putIfAbsent("spring.main.banner-mode", "off");
properties.putIfAbsent("spring.cloud.gcp.secretmanager.enabled", "false"); // Unless we actually start using Secret Manager in deployment.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package no.nav.dolly.libs.nais;

class NaisEnvironmentException extends Exception {

NaisEnvironmentException(String message) {
super(message);
}

NaisEnvironmentException(String message, Throwable cause) {
super(message, cause);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package no.nav.dolly.libs.nais;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.ConfigurableEnvironment;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Slf4j
class NaisRuntimeEnvironmentConnector {

private final ConfigurableEnvironment environment;

Map<String, String> getEnvironmentVariables()
throws NaisEnvironmentException {

if (!environment.matchesProfiles("local")) {
throw new NaisEnvironmentException("Attempted to load environment variables from pod in non-local profile");
}
try {
var applicationName = resolveApplicationName();
var requestedKeys = resolveRequestedKeys();
if (applicationName == null || requestedKeys.isEmpty()) {
log.info("Application is not configured for dynamic environment variables from NAIS");
return Map.of();
}
var cluster = resolveCluster();
var pod = resolvePod(cluster, applicationName);
return getVariables(cluster, pod, requestedKeys);
} catch (NaisEnvironmentException e) {
throw e;
} catch (Exception e) {
throw new NaisEnvironmentException("Unexpected failure", e);
}

}

private List<String> resolveRequestedKeys() {

var keys = new ArrayList<String>();
for (int i = 0; ; i++) {
var key = "dolly.nais.variables[%d]".formatted(i);
if (!environment.containsProperty(key)) {
break;
}
keys.add(environment.getProperty(key));
}
return keys;

}

private String resolveApplicationName() {

return Optional
.ofNullable(environment.getProperty("dolly.nais.name"))
.map(Object::toString)
.orElse(null);

}

private String resolveCluster() {

return Optional
.ofNullable(environment.getProperty("dolly.nais.cluster"))
.map(Object::toString)
.orElseGet(() -> {
log.info("Cannot determine cluster from dolly.nais.cluster, guessing on dev-gcp");
return "dev-gcp";
});

}

private String resolvePod(String cluster, String applicationName)
throws NaisEnvironmentException {

var pods = getAllPodsInClusterWithName(cluster, applicationName);
if (pods.isEmpty()) {
throw new NaisEnvironmentException("No running pods found for application %s in %s".formatted(applicationName, cluster));
}
if (pods.size() > 1) {
log.warn("Multiple pods found for {} in {}, picking {}", applicationName, cluster, pods.getFirst());
}
return pods.getFirst();

}

private Map<String, String> getVariables(String cluster, String pod, List<String> requestedKeys)
throws NaisEnvironmentException {

var command = "kubectl exec --cluster=%s --namespace=dolly %s -- env"
.formatted(cluster, pod);
var output = execute(command);
var variables = output
.stream()
.map(line -> line.split("="))
.filter(elements -> requestedKeys.contains(elements[0]))
.collect(Collectors.toMap(
elements -> elements[0],
elements -> elements[1],
(a, b) -> a));
log.info("Retrieved {}/{} keys from pod {}:{}", variables.size(), requestedKeys.size(), cluster, pod);
return variables;

}

private List<String> getAllPodsInClusterWithName(String cluster, String name)
throws NaisEnvironmentException {

var command = "kubectl get pods --cluster=%s --namespace=dolly -l app=%s -o name"
.formatted(cluster, name);
return execute(command)
.stream()
.map(pod -> pod.substring(pod.lastIndexOf("/") + 1).trim())
.sorted()
.toList();

}

private List<String> execute(String command)
throws NaisEnvironmentException {

var processBuilder = new ProcessBuilder(command.split(" "));
try {
var process = processBuilder.start();
var reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
var output = new ArrayList<String>();
String line;
while ((line = reader.readLine()) != null) {
output.add(line);
}
var status = process.waitFor();
if (status != 0) {
log.warn("Command terminated with status {}: {}", status, command);
}
return output;
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for command: {}", command, e);
Thread.currentThread().interrupt();
return List.of();
}
catch (Exception e) {
throw new NaisEnvironmentException("Failed to execute command: " + command, e);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"properties": [
{
"name": "dolly.nais.cluster",
"type": "java.lang.String",
"defaultValue": "dev-gcp",
"description": "The cluster where the application is running.. Defaults to dev-gcp if not set."
},
{
"name": "dolly.nais.name",
"type": "java.lang.String",
"description": "Application name, as defined in the NAIS manifest (which is not necessarily the same as in Spring config)."
},
{
"name": "dolly.nais.variables",
"type": "java.util.List",
"description": "List of environment variables to be read from the NAIS environment, and set as system properties when running in local profile."
}
]
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
dolly:
nais:
cluster: dev-fss
name: testnav-aareg-proxy
variables:
- AZURE_TRYGDEETATEN_APP_CLIENT_ID
- AZURE_TRYGDEETATEN_APP_CLIENT_SECRET

spring:
config:
import: "sm://"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
dolly:
nais:
name: testnav-arbeidssoekerregisteret-proxy
variables:
- AZURE_TRYGDEETATEN_APP_CLIENT_ID
- AZURE_TRYGDEETATEN_APP_CLIENT_SECRET

spring:
config:
import: "sm://"
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
dolly:
nais:
cluster: dev-fss
name: testnav-brregstub-reverse-proxy
variables:
- AZURE_NAV_APP_CLIENT_ID
- AZURE_NAV_APP_CLIENT_SECRET

spring:
config:
import: "sm://"
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
dolly:
nais:
cluster: dev-fss
name: testnav-dokarkiv-proxy
variables:
- AZURE_TRYGDEETATEN_APP_CLIENT_ID
- AZURE_TRYGDEETATEN_APP_CLIENT_SECRET

spring:
config:
import: "sm://"
Expand Down
8 changes: 8 additions & 0 deletions proxies/inst-proxy/src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
dolly:
nais:
cluster: dev-fss
name: testnav-inst-proxy
variables:
- AZURE_TRYGDEETATEN_APP_CLIENT_ID
- AZURE_TRYGDEETATEN_APP_CLIENT_SECRET

spring:
config:
import: "sm://"
Loading