Skip to content

Commit 4f63228

Browse files
committed
[FLINK-26570][statefun] Remote module configuration interpolation
1 parent 38f5518 commit 4f63228

File tree

13 files changed

+339
-31
lines changed

13 files changed

+339
-31
lines changed

docs/content/docs/modules/overview.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,36 @@ spec:
6161
6262
A module YAML file can contain multiple YAML documents, separated by `---`, each representing a component to be included in the application.
6363
Each component is defined by a kind typename string and a spec object containing the component's properties.
64+
65+
# Configuration string interpolation
66+
You can use `${placeholders}` inside `spec` elements. These will be replaced by entries from a configuration map, consisting of:
67+
1. System properties
68+
2. Environment variables
69+
3. flink-conf.yaml entries with prefix 'statefun.module.global-config.'
70+
4. Command line args
71+
72+
where (4) override (3) which override (2) which override (1).
73+
74+
Example:
75+
```yaml
76+
kind: io.statefun.endpoints.v2/http
77+
spec:
78+
functions: com.example/*
79+
urlPathTemplate: ${FUNC_PROTOCOL}://${FUNC_DNS}/{function.name}
80+
---
81+
kind: io.statefun.kafka.v1/ingress
82+
spec:
83+
id: com.example/my-ingress
84+
address: ${KAFKA_ADDRESS}:${KAFKA_PORT}
85+
consumerGroupId: my-consumer-group
86+
topics:
87+
- topic: ${KAFKA_INGRESS_TOPIC}
88+
(...)
89+
properties:
90+
- ssl.truststore.location: ${SSL_TRUSTSTORE_LOCATION}
91+
- ssl.truststore.password: ${SSL_TRUSTSTORE_PASSWORD}
92+
(...)
93+
```
94+
{{< hint info >}}
95+
Please note that `{function.name}` is not a placeholder to be replaced by entries from the merged configuration. See [url template]({{< ref "docs/modules/http-endpoint" >}})
96+
{{< /hint >}}

statefun-e2e-tests/statefun-smoke-e2e-golang/src/test/java/org/apache/flink/statefun/e2e/smoke/golang/SmokeVerificationGolangE2E.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public void runWith() throws Throwable {
4848
StatefulFunctionsAppContainers.builder("flink-statefun-cluster", NUM_WORKERS)
4949
.withBuildContextFileFromClasspath("remote-module", "/remote-module/")
5050
.withBuildContextFileFromClasspath("ssl/", "ssl/")
51+
.withModuleGlobalConfiguration("MAX_NUM_BATCH_REQUESTS", "10000")
5152
.dependsOn(remoteFunction);
5253

5354
SmokeRunner.run(parameters, builder);

statefun-e2e-tests/statefun-smoke-e2e-golang/src/test/resources/remote-module/module.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ kind: io.statefun.endpoints.v2/http
1717
spec:
1818
functions: statefun.smoke.e2e/command-interpreter-fn
1919
urlPathTemplate: https://remote-function-host
20-
maxNumBatchRequests: 10000
20+
maxNumBatchRequests: ${MAX_NUM_BATCH_REQUESTS}

statefun-e2e-tests/statefun-smoke-e2e-java/src/test/java/org/apache/flink/statefun/e2e/smoke/java/SmokeVerificationJavaE2E.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ public void runWith() throws Throwable {
4848
StatefulFunctionsAppContainers.builder("flink-statefun-cluster", NUM_WORKERS)
4949
.withBuildContextFileFromClasspath("remote-module", "/remote-module/")
5050
.withBuildContextFileFromClasspath("certs", "/certs/")
51+
.withModuleGlobalConfiguration("TEST_COMMAND_INTERPRETER_FN", "command-interpreter-fn")
52+
.withModuleGlobalConfiguration("TEST_SERVER_PROTOCOL", "https://")
53+
.withModuleGlobalConfiguration("TEST_NUM_BATCH_REQUESTS", "10000")
54+
.withModuleGlobalConfiguration("TEST_REMOTE_FUNCTION_PORT", "8000")
55+
// TEST_REMOTE_FUNCTION_HOST placeholder value is taken from docker env variables
5156
.dependsOn(remoteFunction);
5257

5358
SmokeRunner.run(parameters, builder);

statefun-e2e-tests/statefun-smoke-e2e-java/src/test/resources/Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ COPY statefun-smoke-e2e-driver.jar /opt/statefun/modules/statefun-smoke-e2e/
2020
COPY remote-module/ /opt/statefun/modules/statefun-smoke-e2e/
2121
COPY certs/ /opt/statefun/modules/statefun-smoke-e2e/certs/
2222
COPY flink-conf.yaml $FLINK_HOME/conf/flink-conf.yaml
23+
24+
ENV TEST_REMOTE_FUNCTION_HOST=remote-function-host
25+
ENV TEST_SERVER_PROTOCOL=WILL-BE-REPLACED-BY-GLOBAL-VARIABLE-FROM-CLI

statefun-e2e-tests/statefun-smoke-e2e-java/src/test/resources/remote-module/module.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515

1616
kind: io.statefun.endpoints.v2/http
1717
spec:
18-
functions: statefun.smoke.e2e/command-interpreter-fn
19-
urlPathTemplate: https://remote-function-host:8000
20-
maxNumBatchRequests: 10000
18+
functions: statefun.smoke.e2e/${TEST_COMMAND_INTERPRETER_FN}
19+
urlPathTemplate: ${TEST_SERVER_PROTOCOL}${TEST_REMOTE_FUNCTION_HOST}:${TEST_REMOTE_FUNCTION_PORT}
20+
maxNumBatchRequests: ${TEST_NUM_BATCH_REQUESTS}
2121
transport:
2222
type: io.statefun.transports.v1/async
2323
trust_cacerts: file:/opt/statefun/modules/statefun-smoke-e2e/certs/a_ca.pem

statefun-e2e-tests/statefun-smoke-e2e-js/src/test/java/org/apache/flink/statefun/e2e/smoke/js/SmokeVerificationJsE2E.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public void runWith() throws Throwable {
4747
StatefulFunctionsAppContainers.Builder builder =
4848
StatefulFunctionsAppContainers.builder("flink-statefun-cluster", NUM_WORKERS)
4949
.withBuildContextFileFromClasspath("remote-module", "/remote-module/")
50+
.withModuleGlobalConfiguration("REMOTE_FUNCTION_HOST", "remote-function-host")
5051
.dependsOn(remoteFunction);
5152

5253
SmokeRunner.run(parameters, builder);

statefun-e2e-tests/statefun-smoke-e2e-js/src/test/resources/remote-module/module.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@
1616
kind: io.statefun.endpoints.v2/http
1717
spec:
1818
functions: statefun.smoke.e2e/command-interpreter-fn
19-
urlPathTemplate: http://remote-function-host:8000
19+
urlPathTemplate: http://${REMOTE_FUNCTION_HOST}:8000
2020
maxNumBatchRequests: 10000

statefun-flink/statefun-flink-core/src/main/java/org/apache/flink/statefun/flink/core/jsonmodule/RemoteModule.java

Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,27 @@
2020

2121
import static org.apache.flink.statefun.flink.core.spi.ExtensionResolverAccessor.getExtensionResolver;
2222

23-
import java.util.List;
24-
import java.util.Map;
25-
import java.util.Objects;
23+
import java.util.*;
24+
import java.util.function.Function;
25+
import java.util.regex.Matcher;
26+
import java.util.regex.Pattern;
2627
import java.util.stream.Collectors;
28+
import java.util.stream.Stream;
2729
import java.util.stream.StreamSupport;
30+
import org.apache.commons.lang3.NotImplementedException;
31+
import org.apache.flink.api.java.utils.ParameterTool;
2832
import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.JsonNode;
33+
import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.node.*;
2934
import org.apache.flink.statefun.extensions.ComponentBinder;
3035
import org.apache.flink.statefun.extensions.ComponentJsonObject;
3136
import org.apache.flink.statefun.flink.core.spi.ExtensionResolver;
3237
import org.apache.flink.statefun.sdk.spi.StatefulFunctionModule;
38+
import org.slf4j.Logger;
39+
import org.slf4j.LoggerFactory;
3340

3441
public final class RemoteModule implements StatefulFunctionModule {
35-
42+
private static final Logger LOG = LoggerFactory.getLogger(RemoteModule.class);
43+
private static final Pattern PLACEHOLDER_REGEX = Pattern.compile("\\$\\{(.*?)\\}");
3644
private final List<JsonNode> componentNodes;
3745

3846
RemoteModule(List<JsonNode> componentNodes) {
@@ -41,8 +49,16 @@ public final class RemoteModule implements StatefulFunctionModule {
4149

4250
@Override
4351
public void configure(Map<String, String> globalConfiguration, Binder moduleBinder) {
52+
Map<String, String> systemPropsThenEnvVarsThenGlobalConfig =
53+
ParameterTool.fromSystemProperties()
54+
.mergeWith(
55+
ParameterTool.fromMap(System.getenv())
56+
.mergeWith(ParameterTool.fromMap(globalConfiguration)))
57+
.toMap();
4458
parseComponentNodes(componentNodes)
45-
.forEach(component -> bindComponent(component, moduleBinder));
59+
.forEach(
60+
component ->
61+
bindComponent(component, moduleBinder, systemPropsThenEnvVarsThenGlobalConfig));
4662
}
4763

4864
private static List<ComponentJsonObject> parseComponentNodes(
@@ -53,10 +69,102 @@ private static List<ComponentJsonObject> parseComponentNodes(
5369
.collect(Collectors.toList());
5470
}
5571

56-
private static void bindComponent(ComponentJsonObject component, Binder moduleBinder) {
72+
private static void bindComponent(
73+
ComponentJsonObject component, Binder moduleBinder, Map<String, String> configuration) {
74+
75+
JsonNode resolvedSpec = valueResolutionFunction(configuration).apply(component.specJsonNode());
76+
ComponentJsonObject resolvedComponent = new ComponentJsonObject(component.get(), resolvedSpec);
77+
5778
final ExtensionResolver extensionResolver = getExtensionResolver(moduleBinder);
5879
final ComponentBinder componentBinder =
59-
extensionResolver.resolveExtension(component.binderTypename(), ComponentBinder.class);
60-
componentBinder.bind(component, moduleBinder);
80+
extensionResolver.resolveExtension(
81+
resolvedComponent.binderTypename(), ComponentBinder.class);
82+
componentBinder.bind(resolvedComponent, moduleBinder);
83+
}
84+
85+
private static Function<JsonNode, JsonNode> valueResolutionFunction(Map<String, String> config) {
86+
return value -> {
87+
if (value.isObject()) {
88+
return resolveObject((ObjectNode) value, config);
89+
} else if (value.isArray()) {
90+
return resolveArray((ArrayNode) value, config);
91+
} else if (value.isValueNode()) {
92+
return resolveValueNode((ValueNode) value, config);
93+
}
94+
95+
LOG.warn(
96+
"Unrecognised type (not in: object, array, value). Skipping ${placeholder} resolution for that node.");
97+
return value;
98+
};
99+
}
100+
101+
private static Function<Map.Entry<String, JsonNode>, AbstractMap.SimpleEntry<String, JsonNode>>
102+
keyValueResolutionFunction(Map<String, String> config) {
103+
return fieldNameValuePair ->
104+
new AbstractMap.SimpleEntry<>(
105+
fieldNameValuePair.getKey(),
106+
valueResolutionFunction(config).apply(fieldNameValuePair.getValue()));
107+
}
108+
109+
private static ValueNode resolveValueNode(ValueNode node, Map<String, String> config) {
110+
StringBuffer stringBuffer = new StringBuffer();
111+
Matcher placeholderMatcher = PLACEHOLDER_REGEX.matcher(node.asText());
112+
boolean placeholderReplaced = false;
113+
114+
while (placeholderMatcher.find()) {
115+
if (config.containsKey(placeholderMatcher.group(1))) {
116+
placeholderMatcher.appendReplacement(stringBuffer, config.get(placeholderMatcher.group(1)));
117+
placeholderReplaced = true;
118+
} else {
119+
throw new IllegalArgumentException(
120+
String.format(
121+
"Could not resolve placeholder '%s'. An entry for this key was not found in the configuration.",
122+
node.asText()));
123+
}
124+
}
125+
126+
if (placeholderReplaced) {
127+
placeholderMatcher.appendTail(stringBuffer);
128+
return new TextNode(stringBuffer.toString());
129+
}
130+
131+
return node;
132+
}
133+
134+
private static ObjectNode resolveObject(ObjectNode node, Map<String, String> config) {
135+
return getFieldStream(node)
136+
.map(keyValueResolutionFunction(config))
137+
.reduce(
138+
new ObjectNode(JsonNodeFactory.instance),
139+
(accumulatedObjectNode, resolvedFieldNameValueTuple) -> {
140+
accumulatedObjectNode.put(
141+
resolvedFieldNameValueTuple.getKey(), resolvedFieldNameValueTuple.getValue());
142+
return accumulatedObjectNode;
143+
},
144+
(objectNode1, objectNode2) -> {
145+
throw new NotImplementedException("This reduce is not used with parallel streams");
146+
});
147+
}
148+
149+
private static ArrayNode resolveArray(ArrayNode node, Map<String, String> config) {
150+
return getElementStream(node)
151+
.map(valueResolutionFunction(config))
152+
.reduce(
153+
new ArrayNode(JsonNodeFactory.instance),
154+
(accumulatedArrayNode, resolvedValue) -> {
155+
accumulatedArrayNode.add(resolvedValue);
156+
return accumulatedArrayNode;
157+
},
158+
(arrayNode1, arrayNode2) -> {
159+
throw new NotImplementedException("This reduce is not used with parallel streams");
160+
});
161+
}
162+
163+
private static Stream<Map.Entry<String, JsonNode>> getFieldStream(ObjectNode node) {
164+
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(node.fields(), 0), false);
165+
}
166+
167+
private static Stream<JsonNode> getElementStream(ArrayNode node) {
168+
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(node.elements(), 0), false);
61169
}
62170
}

0 commit comments

Comments
 (0)