Skip to content

Commit 94563b3

Browse files
authored
runtime-v2: out parameters for scripts (walmartlabs#540)
1 parent 2ac9da2 commit 94563b3

File tree

27 files changed

+314
-17
lines changed

27 files changed

+314
-17
lines changed

examples/runtime-v2/demo-flow/concord.yml

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ flows:
8686
# get and set script variables
8787
scriptFlow:
8888
- script: scripts/test-script.groovy
89+
out: newVar
8990
meta:
9091
segmentName: "Processing ${myVar} Script Variable..."
9192
error:
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// get a variable
2-
def v = execution.variables().get('myVar')
3-
println('Hello ' + myVar)
2+
def v = myVar
3+
println('Hello ' + v)
44

55
// set a variable
6-
execution.variables().set('newVar', 'Hello, world!')
6+
result.set('newVar', 'Hello, world!')
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
configuration:
2+
runtime: "concord-v2"
3+
dependencies:
4+
- "mvn://org.codehaus.groovy:groovy-all:pom:2.5.8"
5+
6+
flows:
7+
default:
8+
- script: groovy
9+
body: |
10+
result.set("myVar", "myValue");
11+
out: scriptResult
12+
13+
- log: "result: ${scriptResult}" # result: {myVar=myValue}
14+
15+
- script: groovy
16+
body: |
17+
result.set("myVar", "myValue");
18+
out:
19+
myVar: ${result.myVar}
20+
21+
- log: "myVar: ${myVar}" # myVar: myValue
22+

examples/runtime-v2/out_groovy/run.sh

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
3+
SERVER_ADDR="$1"
4+
5+
rm -rf target && mkdir target
6+
cp -R concord.yml target/
7+
8+
cd target && zip -r payload.zip ./* > /dev/null && cd ..
9+
10+
read -p "Username: " CURL_USER
11+
curl -v \
12+
-u ${CURL_USER} \
13+
-F archive=@target/payload.zip \
14+
-F out=myVar \
15+
http://${SERVER_ADDR}/api/v1/process
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
configuration:
2+
runtime: "concord-v2"
3+
4+
flows:
5+
default:
6+
- script: js
7+
body: |
8+
result.set("myVar", "myValue");
9+
out: scriptResult
10+
11+
- log: "result: ${scriptResult}" # result: {myVar=myValue}
12+
13+
- script: js
14+
body: |
15+
result.set("myVar", "myValue");
16+
out:
17+
myVar: ${result.myVar}
18+
19+
- log: "myVar: ${myVar}" # myVar: myValue

examples/runtime-v2/out_js/run.sh

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
3+
SERVER_ADDR="$1"
4+
5+
rm -rf target && mkdir target
6+
cp -R concord.yml target/
7+
8+
cd target && zip -r payload.zip ./* > /dev/null && cd ..
9+
10+
read -p "Username: " CURL_USER
11+
curl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
configuration:
2+
runtime: "concord-v2"
3+
dependencies:
4+
- "mvn://org.python:jython-standalone:2.7.0"
5+
6+
flows:
7+
default:
8+
- script: python
9+
body:
10+
result.set("myVar", "myValue");
11+
out: scriptResult
12+
13+
- log: "result: ${scriptResult}" # result: {myVar=myValue}
14+
15+
- script: python
16+
body:
17+
result.set("myVar", "myValue");
18+
out:
19+
myVar: ${result.myVar}
20+
21+
- log: "myVar: ${myVar}" # myVar: myValue

examples/runtime-v2/out_python/run.sh

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
3+
SERVER_ADDR="$1"
4+
5+
rm -rf target && mkdir target
6+
cp -R concord.yml target/
7+
8+
cd target && zip -r payload.zip ./* > /dev/null && cd ..
9+
10+
read -p "Username: " CURL_USER
11+
curl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
configuration:
2+
runtime: "concord-v2"
3+
dependencies:
4+
- "mvn://org.jruby:jruby:9.1.13.0"
5+
6+
flows:
7+
default:
8+
- script: ruby
9+
body: |
10+
$result.set("myVar", "myValue");
11+
out: scriptResult
12+
13+
- log: "result: ${scriptResult}" # result: {myVar=myValue}
14+
15+
- script: ruby
16+
body: |
17+
$result.set("myVar", "myValue");
18+
out:
19+
myVar: ${result.myVar}
20+
21+
- log: "myVar: ${myVar}" # myVar: myValue

examples/runtime-v2/out_ruby/run.sh

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
3+
SERVER_ADDR="$1"
4+
5+
rm -rf target && mkdir target
6+
cp -R concord.yml target/
7+
8+
cd target && zip -r payload.zip ./* > /dev/null && cd ..
9+
10+
read -p "Username: " CURL_USER
11+
curl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process

runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/ScriptCallOptions.java

+9
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ default Map<String, Serializable> input() {
4545
return Collections.emptyMap();
4646
}
4747

48+
@Nullable
49+
String out();
50+
51+
@Value.Default
52+
@AllowNulls
53+
default Map<String, Serializable> outExpr() {
54+
return Collections.emptyMap();
55+
}
56+
4857
@Nullable
4958
String inputExpression();
5059

runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ScriptGrammar.java

+5
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,16 @@ private static Parser<Atom, ImmutableScriptCallOptions.Builder> scriptCallInOpti
3838
return orError(or(maybeMap.map(o::input), maybeExpression.map(o::inputExpression)), YamlValueType.SCRIPT_CALL_IN);
3939
}
4040

41+
private static Parser<Atom, ImmutableScriptCallOptions.Builder> scriptCallOutOption(ImmutableScriptCallOptions.Builder o) {
42+
return orError(or(maybeMap.map(o::outExpr), maybeString.map(o::out)), YamlValueType.SCRIPT_CALL_OUT);
43+
}
44+
4145
private static Parser<Atom, ScriptCallOptions> scriptOptions(String stepName) {
4246
return with(() -> optionsBuilder(stepName),
4347
o -> options(
4448
optional("body", stringVal.map(o::body)),
4549
optional("in", scriptCallInOption(o)),
50+
optional("out", scriptCallOutOption(o)),
4651
optional("meta", mapVal.map(o::meta)),
4752
optional("name", stringVal.map(v -> o.putMeta(Constants.SEGMENT_NAME, v))),
4853
optional("withItems", nonNullVal.map(v -> o.withItems(WithItems.of(v, WithItems.Mode.SERIAL)))),

runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/YamlValueType.java

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public final class YamlValueType<T> {
5454
public static final YamlValueType<SuspendStep> SUSPEND = type("SUSPEND");
5555
public static final YamlValueType<ScriptCall> SCRIPT = type("SCRIPT");
5656
public static final YamlValueType<ImmutableScriptCallOptions.Builder> SCRIPT_CALL_IN = type("OBJECT or EXPRESSION");
57+
public static final YamlValueType<ImmutableScriptCallOptions.Builder> SCRIPT_CALL_OUT = type("STRING or OBJECT");
5758
public static final YamlValueType<Expression> EXPRESSION = type("EXPRESSION");
5859
public static final YamlValueType<String> EXPRESSION_VAL = type("EXPRESSION");
5960
public static final YamlValueType<Retry> RETRY = type("RETRY");

runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/ScriptCallMixIn.java

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ public interface ScriptCallMixIn extends StepMixIn {
4343
@JsonSchemaInject(json = "{\"oneOf\": [ {\"type\": \"string\"}, {\"type\": \"object\"} ]}", merge = false)
4444
Object input();
4545

46+
@JsonProperty("out")
47+
@JsonSchemaInject(json = "{\"oneOf\": [ {\"type\": \"string\"}, {\"type\": \"object\"} ]}", merge = false)
48+
Object out();
49+
4650
@JsonProperty("withItems")
4751
WithItemsMixIn withItems();
4852

runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/ScriptCallStepSerializer.java

+3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ private static void serializeOptions(ScriptCallOptions options, JsonGenerator ge
6767
writeNotEmptyObjectField("in", options.input(), gen);
6868
writeNotEmptyObjectField("in", options.inputExpression(), gen);
6969

70+
writeNotEmptyObjectField("out", options.out(), gen);
71+
writeNotEmptyObjectField("out", options.outExpr(), gen);
72+
7073
if (options.withItems() != null) {
7174
WithItems items = Objects.requireNonNull(options.withItems());
7275
writeWithItems(items, gen);

runtime/v2/model/src/test/java/com/walmartlabs/concord/project/runtime/v2/parser/YamlErrorParserTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -2246,7 +2246,7 @@ public void test1701() throws Exception {
22462246

22472247
@Test
22482248
public void test1702() throws Exception {
2249-
String msg = "(002.yml): Error @ line: 4, col: 14. Unknown options: ['body1' [STRING] @ line: 4, col: 14], expected: [body, error, in, meta, name, parallelWithItems, retry, withItems]. Remove invalid options and/or fix indentation\n" +
2249+
String msg = "(002.yml): Error @ line: 4, col: 14. Unknown options: ['body1' [STRING] @ line: 4, col: 14], expected: [body, error, in, meta, name, out, parallelWithItems, retry, withItems]. Remove invalid options and/or fix indentation\n" +
22502250
"\twhile processing steps:\n" +
22512251
"\t'script' @ line: 3, col: 7\n" +
22522252
"\t\t'main' @ line: 2, col: 3\n" +

runtime/v2/model/src/test/java/com/walmartlabs/concord/project/runtime/v2/parser/YamlOkParserTest.java

+3
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,9 @@ public void test014() throws Exception {
437437
input.put("k", "v1");
438438
assertEquals(input, t.getOptions().input());
439439

440+
// out
441+
assertEquals("result", t.getOptions().out());
442+
440443
// retry
441444
assertNotNull(t.getOptions().retry());
442445
assertEquals(1, t.getOptions().retry().times());

runtime/v2/model/src/test/resources/014.yml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ flows:
55
throw new RuntimeException("kaboom!")
66
in:
77
k: "v1"
8+
out: result
89
error:
910
- log: "Caught an error"
1011
withItems: 1

runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/script/DefaultScriptEvaluator.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -59,27 +59,30 @@ public DefaultScriptEvaluator(TaskProviders taskProviders) {
5959
}
6060

6161
@Override
62-
public void eval(Context context, String language, Reader input, Map<String, Object> variables) {
62+
public ScriptResult eval(Context context, String language, Reader input, Map<String, Object> variables) {
6363
ScriptEngine engine = getEngine(language);
6464

6565
if (engine == null) {
6666
throw new RuntimeException("Script engine not found: " + language);
6767
}
6868

69-
ScriptContext ctx = new ScriptContext(context);
7069
Bindings b = engine.createBindings();
7170
b.put("polyglot.js.allowAllAccess", true);
7271

72+
ScriptResult scriptResult = new ScriptResult();
73+
ScriptContext ctx = new ScriptContext(context);
7374
for (String ctxVar: CONTEXT_VARIABLE_NAMES) {
7475
b.put(ctxVar, ctx);
7576
}
7677
b.put("tasks", new TaskAccessor(taskProviders, ctx));
7778
b.put("log", log);
7879
b.putAll(context.variables().toMap());
7980
b.putAll(variables);
81+
b.put("result", scriptResult);
8082

8183
try {
8284
engine.eval(input, b);
85+
return scriptResult;
8386
} catch (ScriptException e) {
8487
if (e.getCause() instanceof PolyglotException) {
8588
throw new RuntimeException(e.getCause().getMessage());
@@ -114,7 +117,6 @@ public String getLanguage(String languageOrExtension) {
114117

115118
@SuppressWarnings({"rawtypes", "unchecked"})
116119
private ScriptEngine getEngine(String language) {
117-
// Javascript array is converted in Java to an empty map #214 (https://github.com/oracle/graaljs/issues/214)
118120
if (new GraalJSEngineFactory().getNames().contains(language)) {
119121
// Javascript array is converted in Java to an empty map #214 (https://github.com/oracle/graaljs/issues/214)
120122
HostAccess access = HostAccess.newBuilder(HostAccess.ALL)

runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/script/ScriptEvaluator.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
public interface ScriptEvaluator {
2929

30-
void eval(Context context, String language, Reader input, Map<String, Object> variables);
30+
ScriptResult eval(Context context, String language, Reader input, Map<String, Object> variables);
3131

3232
String getLanguage(String languageOrExtension);
3333
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.walmartlabs.concord.runtime.v2.runner.script;
2+
3+
/*-
4+
* *****
5+
* Concord
6+
* -----
7+
* Copyright (C) 2017 - 2022 Walmart Inc.
8+
* -----
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =====
21+
*/
22+
23+
import java.util.Map;
24+
import java.util.concurrent.ConcurrentHashMap;
25+
26+
public class ScriptResult {
27+
28+
private final Map<String, Object> items = new ConcurrentHashMap<>();
29+
30+
public ScriptResult set(String key, Object value) {
31+
Object sanitized = VariablesSanitizer.sanitize(value);
32+
items.put(key, sanitized);
33+
return this;
34+
}
35+
36+
public Map<String, Object> items() {
37+
return items;
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.walmartlabs.concord.runtime.v2.runner.vm;
2+
3+
/*-
4+
* *****
5+
* Concord
6+
* -----
7+
* Copyright (C) 2017 - 2022 Walmart Inc.
8+
* -----
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =====
21+
*/
22+
23+
import com.walmartlabs.concord.runtime.v2.runner.el.EvalContextFactory;
24+
import com.walmartlabs.concord.runtime.v2.runner.el.ExpressionEvaluator;
25+
import com.walmartlabs.concord.runtime.v2.sdk.Context;
26+
import com.walmartlabs.concord.svm.Runtime;
27+
28+
import java.io.Serializable;
29+
import java.util.Collections;
30+
import java.util.Map;
31+
32+
public final class OutputUtils {
33+
34+
public static void process(Runtime runtime, Context ctx, Map<String, Object> result, String out, Map<String, Serializable> outExpr) {
35+
if (out != null) {
36+
ctx.variables().set(out, result);
37+
} else if (!outExpr.isEmpty()) {
38+
ExpressionEvaluator expressionEvaluator = runtime.getService(ExpressionEvaluator.class);
39+
Map<String, Object> vars = Collections.singletonMap("result", result);
40+
Map<String, Serializable> evalOut = expressionEvaluator.evalAsMap(EvalContextFactory.global(ctx, vars), outExpr);
41+
evalOut.forEach((k, v) -> ctx.variables().set(k, v));
42+
}
43+
}
44+
45+
private OutputUtils() {
46+
}
47+
}

0 commit comments

Comments
 (0)