Skip to content

Commit a2a028d

Browse files
committed
feat: Make EnvVarResolver extendable, and its pattern detection more strict
1 parent 7d09a27 commit a2a028d

4 files changed

Lines changed: 87 additions & 114 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ helm install sql-example -f <your-helm-values>.yaml <your-helm-chart>
143143

144144
### Environment Variable Substitution
145145

146-
Flink SQL Runner automatically substitutes environment variables in your configuration files, SQL scripts, and compiled plans for secrets and environment specific configuration. Environment variables must be of the form `${ENV_VARIABLE}` and inside of strings.
146+
Flink SQL Runner automatically substitutes environment variables in your configuration files, SQL scripts, and compiled plans for secrets and environment specific configuration.
147+
Environment variables must be of the form `${ENV_VARIABLE}` and inside of strings.
148+
They also support bash-style defaults using `${ENV_VARIABLE:-default}` or `${ENV_VARIABLE:=default}`.
147149

148150
For example, `${DATA_PATH}` is an environment variable inside the connector configuration of a table that is substituted at runtime:
149151
```sql

env-utils/src/main/java/com/datasqrl/flinkrunner/utils/EnvVarResolver.java

Lines changed: 8 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
import com.fasterxml.jackson.databind.ObjectMapper;
2222
import com.fasterxml.jackson.databind.module.SimpleModule;
2323
import java.io.IOException;
24-
import java.util.HashMap;
2524
import java.util.HashSet;
2625
import java.util.Map;
2726
import java.util.regex.Matcher;
2827
import java.util.regex.Pattern;
28+
import lombok.Builder;
29+
import lombok.experimental.SuperBuilder;
2930
import lombok.extern.slf4j.Slf4j;
3031

3132
/**
@@ -36,106 +37,14 @@
3637
* non-strict mode.
3738
*/
3839
@Slf4j
40+
@SuperBuilder
3941
public class EnvVarResolver {
4042

41-
private static final Pattern ENVIRONMENT_VARIABLE_PATTERN = Pattern.compile("\\$\\{(.*?)\\}");
43+
private static final Pattern ENVIRONMENT_VARIABLE_PATTERN =
44+
Pattern.compile("\\$\\{(?!\\{)(.*?)\\}");
4245

43-
private final Map<String, String> envVars;
44-
private final ObjectMapper objectMapper;
45-
private final boolean strict;
46-
47-
private EnvVarResolver(Map<String, String> envVars, boolean strict) {
48-
this.envVars = envVars;
49-
this.strict = strict;
50-
objectMapper = initObjectMapper();
51-
}
52-
53-
/**
54-
* Creates a strict resolver backed by the current process environment.
55-
*
56-
* @return a resolver backed by {@link System#getenv()}
57-
*/
58-
public static EnvVarResolver of() {
59-
return of(true);
60-
}
61-
62-
/**
63-
* Creates a resolver backed by the current process environment.
64-
*
65-
* @param strict whether missing variables should fail resolution
66-
* @return a resolver backed by {@link System#getenv()}
67-
*/
68-
public static EnvVarResolver of(boolean strict) {
69-
return of(System.getenv(), strict);
70-
}
71-
72-
/**
73-
* Creates a strict resolver backed by the supplied environment variables.
74-
*
75-
* @param envVars environment variables used for placeholder resolution
76-
* @return a resolver backed by the supplied environment variables
77-
*/
78-
public static EnvVarResolver of(Map<String, String> envVars) {
79-
return of(envVars, true);
80-
}
81-
82-
/**
83-
* Creates a resolver backed by the supplied environment variables.
84-
*
85-
* @param envVars environment variables used for placeholder resolution
86-
* @param strict whether missing variables should fail resolution
87-
* @return a resolver backed by the supplied environment variables
88-
*/
89-
public static EnvVarResolver of(Map<String, String> envVars, boolean strict) {
90-
return new EnvVarResolver(envVars, strict);
91-
}
92-
93-
/**
94-
* Creates a strict resolver backed by the current process environment plus deployment defaults.
95-
*
96-
* @return a resolver with deployment defaults applied
97-
*/
98-
public static EnvVarResolver withDeploymentDefaults() {
99-
return withDeploymentDefaults(true);
100-
}
101-
102-
/**
103-
* Creates a resolver backed by the current process environment plus deployment defaults.
104-
*
105-
* @param strict whether missing variables should fail resolution
106-
* @return a resolver with deployment defaults applied
107-
*/
108-
public static EnvVarResolver withDeploymentDefaults(boolean strict) {
109-
return new EnvVarResolver(EnvUtils.getEnvWithDeploymentDefaults(), strict);
110-
}
111-
112-
/**
113-
* Creates a strict resolver backed by the supplied environment variables plus deployment
114-
* defaults.
115-
*
116-
* @param envVars environment variables used for placeholder resolution
117-
* @return a resolver with deployment defaults applied
118-
*/
119-
public static EnvVarResolver withDeploymentDefaults(Map<String, String> envVars) {
120-
return withDeploymentDefaults(envVars, true);
121-
}
122-
123-
/**
124-
* Creates a resolver backed by the supplied environment variables plus deployment defaults.
125-
*
126-
* <p>Deployment defaults are only added when the supplied map does not already contain those
127-
* keys.
128-
*
129-
* @param envVars environment variables used for placeholder resolution
130-
* @param strict whether missing variables should fail resolution
131-
* @return a resolver with deployment defaults applied
132-
*/
133-
public static EnvVarResolver withDeploymentDefaults(Map<String, String> envVars, boolean strict) {
134-
var modifiedEnvVars = new HashMap<>(envVars);
135-
EnvUtils.getDeploymentDefaults().forEach(modifiedEnvVars::putIfAbsent);
136-
137-
return new EnvVarResolver(Map.copyOf(modifiedEnvVars), strict);
138-
}
46+
@Builder.Default private final Map<String, String> envVars = System.getenv();
47+
@Builder.Default private final boolean strict = true;
13948

14049
/**
14150
* Resolves environment variables referenced in a given source string. Searches for environment
@@ -205,6 +114,7 @@ public String resolve(String src) {
205114
* @throws IOException if the JSON processing fails in any way
206115
*/
207116
public String resolveInJson(String jsonSrc) throws IOException {
117+
var objectMapper = initObjectMapper();
208118
var res = objectMapper.readValue(jsonSrc, Map.class);
209119

210120
return objectMapper.writeValueAsString(res);

env-utils/src/test/java/com/datasqrl/flinkrunner/utils/EnvVarResolverTest.java

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020

2121
import com.fasterxml.jackson.databind.ObjectMapper;
2222
import java.io.IOException;
23+
import java.util.HashMap;
2324
import java.util.Map;
25+
import lombok.experimental.SuperBuilder;
2426
import org.junit.jupiter.api.Test;
2527
import org.junit.jupiter.params.ParameterizedTest;
2628
import org.junit.jupiter.params.provider.CsvSource;
@@ -43,7 +45,7 @@ void givenEnvVariables_whenReplaceWithEnv_thenReplaceCorrectlyVars(
4345
Map.of(
4446
"USER", "John",
4547
"PATH", "/usr/bin");
46-
resolver = EnvVarResolver.of(envVariables);
48+
resolver = EnvVarResolver.builder().envVars(envVariables).build();
4749
var result = resolver.resolve(command);
4850
assertThat(result).isEqualTo(expected);
4951
}
@@ -57,7 +59,7 @@ void givenEnvVariables_whenReplaceWithEnv_thenReplaceCorrectlyVars(
5759
})
5860
void givenMissingEnvVariables_whenResolveEnv_Vars_thenThrowException(String command) {
5961
Map<String, String> envVariables = Map.of();
60-
resolver = EnvVarResolver.of(envVariables); // Empty map to simulate missing variables
62+
resolver = EnvVarResolver.builder().envVars(envVariables).build();
6163
assertThatThrownBy(() -> resolver.resolve(command))
6264
.isInstanceOf(IllegalStateException.class)
6365
.hasMessageStartingWith(
@@ -76,7 +78,7 @@ void givenSimilarEnvVariableNames_whenResolveEnv_Vars_thenPartialMatchDoesNotOcc
7678
Map.of(
7779
"USER", "John",
7880
"NAME", "exists");
79-
resolver = EnvVarResolver.of(envVariables);
81+
resolver = EnvVarResolver.builder().envVars(envVariables).build();
8082
assertThatThrownBy(() -> resolver.resolve(command))
8183
.isInstanceOf(IllegalStateException.class)
8284
.hasMessage(
@@ -105,7 +107,7 @@ void givenDefaultEnvValues_whenResolve_thenFallbackOrUseEnvValue(
105107
Map.of(
106108
"USER", "John",
107109
"PATH", "/usr/bin");
108-
resolver = EnvVarResolver.of(envVariables);
110+
resolver = EnvVarResolver.builder().envVars(envVariables).build();
109111
var result = resolver.resolve(command);
110112
assertThat(result).isEqualTo(expected);
111113
}
@@ -120,7 +122,7 @@ void givenDefaultEnvValues_whenResolve_thenFallbackOrUseEnvValue(
120122
})
121123
void givenMissingEnvWithoutDefault_whenResolve_thenThrowException(String command) {
122124
Map<String, String> envVariables = Map.of("A", "something");
123-
resolver = EnvVarResolver.of(envVariables);
125+
resolver = EnvVarResolver.builder().envVars(envVariables).build();
124126
assertThatThrownBy(() -> resolver.resolve(command))
125127
.isInstanceOf(IllegalStateException.class)
126128
.hasMessageContaining("referenced, but not found");
@@ -136,22 +138,36 @@ void givenMissingEnvWithoutDefault_whenResolve_thenThrowException(String command
136138
})
137139
void givenFallbackWithColons_whenResolve_thenParseCorrectly(String command, String expected) {
138140
Map<String, String> envVariables = Map.of(); // No TOKEN set
139-
resolver = EnvVarResolver.of(envVariables);
141+
resolver = EnvVarResolver.builder().envVars(envVariables).build();
140142
var result = resolver.resolve(command);
141143
assertThat(result).isEqualTo(expected);
142144
}
143145

146+
@ParameterizedTest
147+
@CsvSource({
148+
"'${{SECRET}}', '${{SECRET}}'",
149+
"'Token=${{SECRET}}', 'Token=${{SECRET}}'",
150+
"'Host=${HOST}, password=${{DB_PASSWORD}}', 'Host=db.example.com, password=${{DB_PASSWORD}}'"
151+
})
152+
void givenSecretEnvTemplate_whenResolve_thenLeaveUnchanged(String command, String expected) {
153+
resolver = EnvVarResolver.builder().envVars(Map.of("HOST", "db.example.com")).build();
154+
155+
var result = resolver.resolve(command);
156+
157+
assertThat(result).isEqualTo(expected);
158+
}
159+
144160
@Test
145161
void givenNullOrBlankSource_whenResolve_thenReturnSource() {
146-
resolver = EnvVarResolver.of(Map.of());
162+
resolver = EnvVarResolver.builder().envVars(Map.of()).build();
147163

148164
assertThat(resolver.resolve(null)).isNull();
149165
assertThat(resolver.resolve(" ")).isEqualTo(" ");
150166
}
151167

152168
@Test
153169
void givenNonStrictResolver_whenMissingEnvVariables_thenLeavesPlaceholdersUnresolved() {
154-
resolver = EnvVarResolver.of(Map.of("USER", "John"), false);
170+
resolver = EnvVarResolver.builder().envVars(Map.of("USER", "John")).strict(false).build();
155171

156172
var result = resolver.resolve("Hello ${USER}, ${MISSING}!");
157173

@@ -161,10 +177,13 @@ void givenNonStrictResolver_whenMissingEnvVariables_thenLeavesPlaceholdersUnreso
161177
@Test
162178
void givenDeploymentDefaults_whenResolve_thenUseDefaultsAndSuppliedValues() {
163179
resolver =
164-
EnvVarResolver.withDeploymentDefaults(
165-
Map.of(
166-
"DEPLOYMENT_ID", "deployment-1",
167-
"USER", "John"));
180+
EnvVarResolver.builder()
181+
.envVars(
182+
withDeploymentDefaults(
183+
Map.of(
184+
"DEPLOYMENT_ID", "deployment-1",
185+
"USER", "John")))
186+
.build();
168187

169188
var result = resolver.resolve("${DEPLOYMENT_ID}|${DEPLOYMENT_TIMESTAMP}|${USER}");
170189
var parts = result.split("\\|");
@@ -178,7 +197,10 @@ void givenDeploymentDefaults_whenResolve_thenUseDefaultsAndSuppliedValues() {
178197
@Test
179198
void givenNonStrictDeploymentDefaults_whenMissingNonDefaultEnvVariable_thenLeavesPlaceholder() {
180199
resolver =
181-
EnvVarResolver.withDeploymentDefaults(Map.of("DEPLOYMENT_ID", "deployment-1"), false);
200+
EnvVarResolver.builder()
201+
.envVars(withDeploymentDefaults(Map.of("DEPLOYMENT_ID", "deployment-1")))
202+
.strict(false)
203+
.build();
182204

183205
var result = resolver.resolve("${DEPLOYMENT_ID}|${MISSING}");
184206

@@ -187,7 +209,7 @@ void givenNonStrictDeploymentDefaults_whenMissingNonDefaultEnvVariable_thenLeave
187209

188210
@Test
189211
void givenJsonSource_whenResolveInJson_thenResolveStringLeafNodes() throws IOException {
190-
resolver = EnvVarResolver.of(Map.of("USER", "John"));
212+
resolver = EnvVarResolver.builder().envVars(Map.of("USER", "John")).build();
191213

192214
var result =
193215
resolver.resolveInJson(
@@ -198,4 +220,36 @@ void givenJsonSource_whenResolveInJson_thenResolveStringLeafNodes() throws IOExc
198220
assertThat(json.get("count").asInt()).isEqualTo(1);
199221
assertThat(json.get("nested").get("path").asText()).isEqualTo("/tmp");
200222
}
223+
224+
@Test
225+
void givenSubclass_whenBuild_thenInheritedBuilderFieldsAreApplied() {
226+
resolver =
227+
TestEnvVarResolver.builder()
228+
.envVars(Map.of("USER", "John"))
229+
.strict(false)
230+
.prefix("resolved=")
231+
.build();
232+
233+
var result = resolver.resolve("${USER}:${MISSING}");
234+
235+
assertThat(result).isEqualTo("resolved=John:${MISSING}");
236+
}
237+
238+
private Map<String, String> withDeploymentDefaults(Map<String, String> envVars) {
239+
var modifiedEnvVars = new HashMap<>(envVars);
240+
EnvUtils.getDeploymentDefaults().forEach(modifiedEnvVars::putIfAbsent);
241+
242+
return Map.copyOf(modifiedEnvVars);
243+
}
244+
245+
@SuperBuilder
246+
private static class TestEnvVarResolver extends EnvVarResolver {
247+
248+
private final String prefix;
249+
250+
@Override
251+
public String resolve(String src) {
252+
return prefix + super.resolve(src);
253+
}
254+
}
201255
}

flink-sql-runner/src/main/java/com/datasqrl/flinkrunner/CliRunner.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.datasqrl.flinkrunner;
1717

18+
import com.datasqrl.flinkrunner.utils.EnvUtils;
1819
import com.datasqrl.flinkrunner.utils.EnvVarResolver;
1920
import java.util.Arrays;
2021
import java.util.concurrent.Callable;
@@ -76,7 +77,13 @@ public CliRunner(
7677
@Nullable String planFile,
7778
@Nullable String configDir,
7879
@Nullable String udfPath) {
79-
this(mode, EnvVarResolver.withDeploymentDefaults(), sqlFile, planFile, configDir, udfPath);
80+
this(
81+
mode,
82+
EnvVarResolver.builder().envVars(EnvUtils.getEnvWithDeploymentDefaults()).build(),
83+
sqlFile,
84+
planFile,
85+
configDir,
86+
udfPath);
8087
}
8188

8289
@VisibleForTesting

0 commit comments

Comments
 (0)