Skip to content

Commit 43e6397

Browse files
committed
Add RunTask.shell with environment and expression
Signed-off-by: Matheus Cruz <[email protected]>
1 parent fd6c7b9 commit 43e6397

File tree

10 files changed

+321
-0
lines changed

10 files changed

+321
-0
lines changed

impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ public TaskExecutorBuilder<? extends TaskBase> getTaskExecutor(
7676
return new ListenExecutorBuilder(position, task.getListenTask(), definition);
7777
} else if (task.getEmitTask() != null) {
7878
return new EmitExecutorBuilder(position, task.getEmitTask(), definition);
79+
} else if (task.getRunTask() != null) {
80+
return new RunTaskExecutor.RunTaskExecutorBuilder(position, task.getRunTask(), definition);
7981
}
8082
throw new UnsupportedOperationException(task.get().getClass().getName() + " not supported yet");
8183
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl.executors;
17+
18+
public record ProcessResult(int code, String stdout, String stderr) {}

impl/core/src/main/java/io/serverlessworkflow/impl/executors/RegularTaskExecutor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ protected RegularTaskExecutor(RegularTaskExecutorBuilder<T> builder) {
3535
public abstract static class RegularTaskExecutorBuilder<T extends TaskBase>
3636
extends AbstractTaskExecutorBuilder<T, RegularTaskExecutor<T>> {
3737

38+
TaskExecutor<?> taskExecutor;
3839
private TransitionInfoBuilder transition;
3940

4041
protected RegularTaskExecutorBuilder(
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl.executors;
17+
18+
import io.serverlessworkflow.api.types.RunShell;
19+
import io.serverlessworkflow.api.types.RunTask;
20+
import io.serverlessworkflow.api.types.TaskBase;
21+
import io.serverlessworkflow.impl.TaskContext;
22+
import io.serverlessworkflow.impl.WorkflowApplication;
23+
import io.serverlessworkflow.impl.WorkflowContext;
24+
import io.serverlessworkflow.impl.WorkflowDefinition;
25+
import io.serverlessworkflow.impl.WorkflowModel;
26+
import io.serverlessworkflow.impl.WorkflowMutablePosition;
27+
import io.serverlessworkflow.impl.WorkflowUtils;
28+
import io.serverlessworkflow.impl.expressions.ExpressionUtils;
29+
import java.io.IOException;
30+
import java.io.OutputStream;
31+
import java.util.Map;
32+
import java.util.Objects;
33+
import java.util.concurrent.CompletableFuture;
34+
35+
public abstract class RunTaskExecutor<T extends TaskBase> extends RegularTaskExecutor<T> {
36+
37+
protected RunTaskExecutor(RegularTaskExecutorBuilder<T> builder) {
38+
super(builder);
39+
}
40+
41+
public static class RunTaskExecutorBuilder
42+
extends RegularTaskExecutor.RegularTaskExecutorBuilder<RunTask> {
43+
44+
protected RunTaskExecutorBuilder(
45+
WorkflowMutablePosition position, RunTask task, WorkflowDefinition definition) {
46+
super(position, task, definition);
47+
}
48+
49+
@Override
50+
protected RegularTaskExecutor<RunTask> buildInstance() {
51+
if (task.getRun().getRunShell() != null) {
52+
return new RunShellTaskExecutor(this);
53+
} else {
54+
throw new RuntimeException("Unsupported run task type");
55+
}
56+
}
57+
}
58+
59+
public static class RunShellTaskExecutor extends RunTaskExecutor<RunTask> {
60+
61+
private final WorkflowApplication application;
62+
63+
protected RunShellTaskExecutor(RegularTaskExecutorBuilder<RunTask> builder) {
64+
super(builder);
65+
this.application = builder.application;
66+
}
67+
68+
@Override
69+
protected CompletableFuture<WorkflowModel> internalExecute(
70+
WorkflowContext workflow, TaskContext taskContext) {
71+
72+
if (taskContext.task() != null
73+
&& taskContext.task() instanceof RunTask runTask
74+
&& runTask.getRun().getRunShell() != null) {
75+
RunShell runShell = runTask.getRun().getRunShell();
76+
String shellCommand = runShell.getShell().getCommand();
77+
78+
Objects.requireNonNull(
79+
shellCommand, "Shell command must be provided in RunShell taskContext");
80+
81+
try {
82+
String command =
83+
ExpressionUtils.isExpr(shellCommand)
84+
? WorkflowUtils.buildStringResolver(
85+
application, shellCommand, taskContext.input().asJavaObject())
86+
.apply(workflow, taskContext, taskContext.input())
87+
: shellCommand;
88+
89+
ProcessBuilder processBuilder = new ProcessBuilder("sh", "-c", command);
90+
91+
if (runShell.getShell().getEnvironment() != null
92+
&& !runShell.getShell().getEnvironment().getAdditionalProperties().isEmpty()) {
93+
for (Map.Entry<String, Object> entry :
94+
runShell.getShell().getEnvironment().getAdditionalProperties().entrySet()) {
95+
String value =
96+
ExpressionUtils.isExpr(entry.getValue())
97+
? WorkflowUtils.buildStringResolver(
98+
application,
99+
entry.getValue().toString(),
100+
taskContext.input().asJavaObject())
101+
.apply(workflow, taskContext, taskContext.input())
102+
: entry.getValue().toString();
103+
processBuilder.environment().put(entry.getKey(), value);
104+
}
105+
}
106+
107+
ProcessResult result = startProcessUsing(processBuilder);
108+
109+
return CompletableFuture.supplyAsync(() -> application.modelFactory().fromAny(result));
110+
111+
} catch (IOException | InterruptedException e) {
112+
throw new RuntimeException(e);
113+
}
114+
}
115+
116+
throw new RuntimeException("Task must be of type RunTask");
117+
}
118+
119+
private ProcessResult startProcessUsing(ProcessBuilder processBuilder)
120+
throws IOException, InterruptedException {
121+
Process process = processBuilder.start();
122+
123+
StringBuilder output = new StringBuilder();
124+
StringBuilder error = new StringBuilder();
125+
126+
process.getInputStream().transferTo(getOutputStream(output));
127+
process.getErrorStream().transferTo(getOutputStream(error));
128+
129+
int exitCode = process.waitFor();
130+
131+
return new ProcessResult(exitCode, output.toString().trim(), error.toString().trim());
132+
}
133+
134+
private static OutputStream getOutputStream(StringBuilder output) {
135+
return new OutputStream() {
136+
@Override
137+
public void write(int b) {
138+
output.append((char) b);
139+
}
140+
};
141+
}
142+
}
143+
}

impl/test/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
<groupId>io.serverlessworkflow</groupId>
4242
<artifactId>serverlessworkflow-impl-openapi</artifactId>
4343
</dependency>
44+
<dependency>
45+
<groupId>io.serverlessworkflow</groupId>
46+
<artifactId>serverlessworkflow-impl-shell-process</artifactId>
47+
</dependency>
4448
<dependency>
4549
<groupId>org.glassfish.jersey.core</groupId>
4650
<artifactId>jersey-client</artifactId>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl.test.shell;
17+
18+
import io.serverlessworkflow.api.WorkflowReader;
19+
import io.serverlessworkflow.api.types.Workflow;
20+
import io.serverlessworkflow.impl.WorkflowApplication;
21+
import io.serverlessworkflow.impl.WorkflowModel;
22+
import io.serverlessworkflow.impl.executors.ProcessResult;
23+
import java.io.IOException;
24+
import java.util.Map;
25+
import org.assertj.core.api.SoftAssertions;
26+
import org.junit.jupiter.api.Test;
27+
28+
public class RunTaskExecutorTest {
29+
30+
@Test
31+
void testEcho() throws IOException {
32+
Workflow workflow =
33+
WorkflowReader.readWorkflowFromClasspath("workflows-samples/shell-process/echo.yaml");
34+
try (WorkflowApplication appl = WorkflowApplication.builder().build()) {
35+
WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join();
36+
SoftAssertions.assertSoftly(
37+
softly -> {
38+
ProcessResult result = model.as(ProcessResult.class).orElseThrow();
39+
softly.assertThat(result.code()).isEqualTo(0);
40+
softly.assertThat(result.stderr()).isEmpty();
41+
softly.assertThat(result.stdout()).contains("Hello, anonymous");
42+
});
43+
}
44+
}
45+
46+
@Test
47+
void testEchoWithJqExpression() throws IOException {
48+
Workflow workflow =
49+
WorkflowReader.readWorkflowFromClasspath("workflows-samples/shell-process/echo-jq.yaml");
50+
try (WorkflowApplication appl = WorkflowApplication.builder().build()) {
51+
WorkflowModel model =
52+
appl.workflowDefinition(workflow)
53+
.instance(new Input(new User("John Doe")))
54+
.start()
55+
.join();
56+
SoftAssertions.assertSoftly(
57+
softly -> {
58+
ProcessResult result = model.as(ProcessResult.class).orElseThrow();
59+
softly.assertThat(result.code()).isEqualTo(0);
60+
softly.assertThat(result.stderr()).isEmpty();
61+
softly.assertThat(result.stdout()).contains("Hello, John Doe");
62+
});
63+
}
64+
}
65+
66+
@Test
67+
void testEchoWithEnvironment() throws IOException {
68+
Workflow workflow =
69+
WorkflowReader.readWorkflowFromClasspath(
70+
"workflows-samples/shell-process/echo-with-env.yaml");
71+
try (WorkflowApplication appl = WorkflowApplication.builder().build()) {
72+
WorkflowModel model =
73+
appl.workflowDefinition(workflow).instance(Map.of("lastName", "Doe")).start().join();
74+
SoftAssertions.assertSoftly(
75+
softly -> {
76+
ProcessResult result = model.as(ProcessResult.class).orElseThrow();
77+
softly.assertThat(result.code()).isEqualTo(0);
78+
softly.assertThat(result.stderr()).isEmpty();
79+
softly.assertThat(result.stdout()).contains("Hello John Doe from env!");
80+
});
81+
}
82+
}
83+
84+
@Test
85+
void testTouchAndCat() throws IOException {
86+
Workflow workflow =
87+
WorkflowReader.readWorkflowFromClasspath("workflows-samples/shell-process/touch-cat.yaml");
88+
try (WorkflowApplication appl = WorkflowApplication.builder().build()) {
89+
WorkflowModel model =
90+
appl.workflowDefinition(workflow).instance(Map.of("lastName", "Doe")).start().join();
91+
SoftAssertions.assertSoftly(
92+
softly -> {
93+
ProcessResult result = model.as(ProcessResult.class).orElseThrow();
94+
softly.assertThat(result.code()).isEqualTo(0);
95+
softly.assertThat(result.stderr()).isEmpty();
96+
softly.assertThat(result.stdout()).contains("hello world");
97+
});
98+
}
99+
}
100+
101+
record Input(User user) {}
102+
103+
record User(String name) {}
104+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
document:
2+
dsl: '1.0.1'
3+
namespace: test
4+
name: run-shell-example
5+
version: '0.1.0'
6+
do:
7+
- runShell:
8+
run:
9+
shell:
10+
command: ${ "echo Hello, \(.user.name)" }
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
document:
2+
dsl: '1.0.1'
3+
namespace: test
4+
name: run-shell-example
5+
version: '0.1.0'
6+
do:
7+
- runShell:
8+
run:
9+
shell:
10+
command: echo "Hello $FIRST_NAME $LAST_NAME from env!"
11+
environment:
12+
FIRST_NAME: John
13+
LAST_NAME: ${.lastName}
14+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
document:
2+
dsl: '1.0.1'
3+
namespace: test
4+
name: run-shell-example
5+
version: '0.1.0'
6+
do:
7+
- runShell:
8+
run:
9+
shell:
10+
command: 'echo "Hello, anonymous"'
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
document:
2+
dsl: '1.0.1'
3+
namespace: test
4+
name: run-shell-example
5+
version: '0.1.0'
6+
do:
7+
- runShell:
8+
run:
9+
shell:
10+
# write hello world to a file and then cat it
11+
command: echo "hello world" > /tmp/hello.txt && cat /tmp/hello.txt
12+
environment:
13+
FIRST_NAME: John
14+
LAST_NAME: ${.lastName}
15+

0 commit comments

Comments
 (0)