diff --git a/impl/runner_test.go b/impl/runner_test.go index 5acdb6b..9f06ef9 100644 --- a/impl/runner_test.go +++ b/impl/runner_test.go @@ -73,20 +73,38 @@ func withRunnerCtx(workflowContext ctx.WorkflowContext) taskSupportOpts { // runWorkflowTest is a reusable test function for workflows func runWorkflowTest(t *testing.T, workflowPath string, input, expectedOutput map[string]interface{}) { // Run the workflow - output, err := runWorkflow(t, workflowPath, input, expectedOutput) + output, err := runWorkflow(t, workflowPath, input) assert.NoError(t, err) assertWorkflowRun(t, expectedOutput, output) } func runWorkflowWithErr(t *testing.T, workflowPath string, input, expectedOutput map[string]interface{}, assertErr func(error)) { - output, err := runWorkflow(t, workflowPath, input, expectedOutput) + output, err := runWorkflow(t, workflowPath, input) assert.Error(t, err) assertErr(err) assertWorkflowRun(t, expectedOutput, output) } -func runWorkflow(t *testing.T, workflowPath string, input, expectedOutput map[string]interface{}) (output interface{}, err error) { +func runWorkflow(t *testing.T, workflowPath string, input map[string]interface{}) (output interface{}, err error) { + // Read the workflow YAML from the testdata directory + yamlBytes, err := os.ReadFile(filepath.Clean(workflowPath)) + assert.NoError(t, err, "Failed to read workflow YAML file") + + // Parse the YAML workflow + workflow, err := parser.FromYAMLSource(yamlBytes) + assert.NoError(t, err, "Failed to parse workflow YAML") + + // Initialize the workflow runner + runner, err := NewDefaultRunner(workflow) + assert.NoError(t, err) + + // Run the workflow + output, err = runner.Run(input) + return output, err +} + +func runWorkflowExpectString(t *testing.T, workflowPath string, input interface{}) (output interface{}, err error) { // Read the workflow YAML from the testdata directory yamlBytes, err := os.ReadFile(filepath.Clean(workflowPath)) assert.NoError(t, err, "Failed to read workflow YAML file") diff --git a/impl/task_runner_do.go b/impl/task_runner_do.go index 8b63bfc..48f2e83 100644 --- a/impl/task_runner_do.go +++ b/impl/task_runner_do.go @@ -40,6 +40,8 @@ func NewTaskRunner(taskName string, task model.Task, workflowDef *model.Workflow return NewCallHttpRunner(taskName, t) case *model.ForkTask: return NewForkTaskRunner(taskName, t, workflowDef) + case *model.RunTask: + return NewRunTaskRunner(taskName, t) default: return nil, fmt.Errorf("unsupported task type '%T' for task '%s'", t, taskName) } diff --git a/impl/task_runner_run.go b/impl/task_runner_run.go new file mode 100644 index 0000000..138dc6c --- /dev/null +++ b/impl/task_runner_run.go @@ -0,0 +1,74 @@ +// Copyright 2025 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package impl + +import ( + "fmt" + + "github.com/serverlessworkflow/sdk-go/v3/model" +) + +type RunTaskRunner struct { + Task *model.RunTask + TaskName string +} + +func (d *RunTaskRunner) GetTaskName() string { + return d.TaskName +} + +// RunTaskRunnable defines the interface for running a subtask for RunTask. +type RunTaskRunnable interface { + RunTask(taskConfiguration *model.RunTaskConfiguration, support *TaskSupport, input interface{}) (output interface{}, err error) +} + +func NewRunTaskRunner(taskName string, task *model.RunTask) (*RunTaskRunner, error) { + + if task == nil { + return nil, model.NewErrValidation(fmt.Errorf("no set configuration provided for RunTask %s", taskName), taskName) + } + + return &RunTaskRunner{ + Task: task, + TaskName: taskName, + }, nil +} + +func (d *RunTaskRunner) Run(input interface{}, taskSupport TaskSupport) (output interface{}, err error) { + + if d.Task.Run.Shell != nil { + shellTask := NewRunTaskShell() + return shellTask.RunTask(d, input, taskSupport) + } + + return nil, fmt.Errorf("no set configuration provided for RunTask %s", d.TaskName) + +} + +// ProcessResult Describes the result of a process. +type ProcessResult struct { + Stdout string + Stderr string + Code int +} + +// NewProcessResult creates a new ProcessResult instance. +func NewProcessResult(stdout, stderr string, code int) *ProcessResult { + return &ProcessResult{ + Stdout: stdout, + Stderr: stderr, + Code: code, + } +} diff --git a/impl/task_runner_run_shell.go b/impl/task_runner_run_shell.go new file mode 100644 index 0000000..9c0c4ef --- /dev/null +++ b/impl/task_runner_run_shell.go @@ -0,0 +1,148 @@ +// Copyright 2025 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package impl + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/serverlessworkflow/sdk-go/v3/impl/expr" + "github.com/serverlessworkflow/sdk-go/v3/model" +) + +// RunTaskShell defines the shell configuration for RunTask. +// It implements the RunTask.shell definition. +type RunTaskShell struct { +} + +// NewRunTaskShell creates a new RunTaskShell instance. +func NewRunTaskShell() *RunTaskShell { + return &RunTaskShell{} +} + +func (shellTask *RunTaskShell) RunTask(r *RunTaskRunner, input interface{}, taskSupport TaskSupport) (interface{}, error) { + await := r.Task.Run.Await + shell := r.Task.Run.Shell + var cmdStr string + + if shell == nil { + return nil, model.NewErrValidation(fmt.Errorf("no shell configuration provided for RunTask %s", r.TaskName), r.TaskName) + } + + cmdStr = shell.Command + + if cmdStr == "" { + return nil, model.NewErrValidation(fmt.Errorf("no command provided for RunTask shell: %s ", r.TaskName), r.TaskName) + } + + if shell.Environment != nil { + for key, value := range shell.Environment { + evaluated, evalErr := expr.TraverseAndEvaluate(value, input, taskSupport.GetContext()) + if evalErr != nil { + return nil, model.NewErrRuntime(fmt.Errorf("error evaluating environment variable value for RunTask shell: %s", r.TaskName), r.TaskName) + } + + envVal := fmt.Sprint(evaluated) + if err := os.Setenv(key, envVal); err != nil { + return nil, model.NewErrRuntime(fmt.Errorf("error setting environment variable for RunTask shell: %s", r.TaskName), r.TaskName) + } + } + } + + evaluated, err := expr.TraverseAndEvaluate(cmdStr, input, taskSupport.GetContext()) + if err != nil { + return nil, model.NewErrRuntime(fmt.Errorf("error evaluating command for RunTask shell: %s", r.TaskName), r.TaskName) + } + + cmdEvaluated := fmt.Sprint(evaluated) + + var args []string + + args = append(args, "-c", cmdEvaluated) + + if shell.Arguments != nil { + for key, value := range shell.Arguments { + keyEval, evalErr := expr.TraverseAndEvaluate(key, input, taskSupport.GetContext()) + if evalErr != nil { + return nil, model.NewErrRuntime(fmt.Errorf("error evaluating argument key for RunTask shell: %s", r.TaskName), r.TaskName) + } + + keyStr := fmt.Sprint(keyEval) + + if value != nil { + valueEval, evalErr := expr.TraverseAndEvaluate(value, input, taskSupport.GetContext()) + if evalErr != nil { + return nil, model.NewErrRuntime(fmt.Errorf("error evaluating argument value for RunTask shell: %s", r.TaskName), r.TaskName) + } + valueStr := fmt.Sprint(valueEval) + args = append(args, fmt.Sprintf("%s=%s", keyStr, valueStr)) + } else { + args = append(args, fmt.Sprintf("%s", keyStr)) + } + } + } + + var fullCmd strings.Builder + fullCmd.WriteString(cmdEvaluated) + for i := 2; i < len(args); i++ { + fullCmd.WriteString(" ") + fullCmd.WriteString(args[i]) + } + + if await != nil && !*await { + go func() { + cmd := exec.Command("sh", "-c", fullCmd.String()) + _ = cmd.Start() + _ = cmd.Wait() + }() + return input, nil + } + + cmd := exec.Command("sh", "-c", fullCmd.String()) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err = cmd.Run() + exitCode := 0 + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + exitCode = exitErr.ExitCode() + } + } else if cmd.ProcessState != nil { + exitCode = cmd.ProcessState.ExitCode() + } + + stdoutStr := strings.TrimSpace(stdout.String()) + stderrStr := strings.TrimSpace(stderr.String()) + + switch r.Task.Run.Return { + case "all": + return NewProcessResult(stdoutStr, stderrStr, exitCode), nil + case "stderr": + return stderrStr, nil + case "code": + return exitCode, nil + case "none": + return nil, nil + default: + return stdoutStr, nil + } +} diff --git a/impl/task_runner_run_shell_test.go b/impl/task_runner_run_shell_test.go new file mode 100644 index 0000000..2d5aee8 --- /dev/null +++ b/impl/task_runner_run_shell_test.go @@ -0,0 +1,191 @@ +// Copyright 2025 The Serverless Workflow Specification Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package impl + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRunShellWithTestData(t *testing.T) { + + t.Run("Simple with echo", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo.yaml" + + input := map[string]interface{}{} + output, err := runWorkflow(t, workflowPath, input) + + processResult := output.(*ProcessResult) + + assert.NotNilf(t, output, "output should not be nil") + assert.Equal(t, "Hello, anonymous", processResult.Stdout) + assert.Equal(t, "", processResult.Stderr) + assert.Equal(t, 0, processResult.Code) + assert.NoError(t, err) + }) + + t.Run("Simple echo looking exit code", func(t *testing.T) { + workflowPath := "./testdata/run_shell_exitcode.yaml" + input := map[string]interface{}{} + expectedOutput := 2 + output, err := runWorkflowExpectString(t, workflowPath, input) + assert.NoError(t, err) + assert.Equal(t, expectedOutput, output.(int)) + }) + + t.Run("JQ expression in command with 'all' return", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_jq.yaml" + input := map[string]interface{}{ + "user": map[string]interface{}{ + "name": "Matheus Cruz", + }, + } + output, err := runWorkflowExpectString(t, workflowPath, input) + + processResult := output.(*ProcessResult) + assert.NoError(t, err) + assert.Equal(t, "", processResult.Stderr) + assert.Equal(t, "Hello, Matheus Cruz", processResult.Stdout) + assert.Equal(t, 0, processResult.Code) + }) + + t.Run("Simple echo with 'none' return", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_none.yaml" + input := map[string]interface{}{} + output, err := runWorkflowExpectString(t, workflowPath, input) + + assert.NoError(t, err) + assert.Nil(t, output) + }) + + t.Run("Simple echo with env and await as 'false'", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_env_no_awaiting.yaml" + input := map[string]interface{}{ + "full_name": "John Doe", + } + output, err := runWorkflowExpectString(t, workflowPath, input) + + assert.NoError(t, err) + assert.Equal(t, output, input) + }) + + t.Run("Simple echo not awaiting, function should returns immediately", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_not_awaiting.yaml" + input := map[string]interface{}{ + "full_name": "John Doe", + } + output, err := runWorkflow(t, workflowPath, input) + + assert.NoError(t, err) + assert.Equal(t, output, input) + }) + + t.Run("Simple 'ls' command getting output as stderr", func(t *testing.T) { + workflowPath := "./testdata/run_shell_ls_stderr.yaml" + input := map[string]interface{}{} + + output, err := runWorkflowExpectString(t, workflowPath, input) + + assert.NoError(t, err) + assert.True(t, strings.Contains(output.(string), "ls:")) + }) + + t.Run("Simple echo with args using JQ expression", func(t *testing.T) { + workflowPath := "./testdata/run_shell_with_args_key_value_jq.yaml" + input := map[string]interface{}{ + "user": "Alice", + "passwordKey": "--password", + } + + output, err := runWorkflow(t, workflowPath, input) + + processResult := output.(*ProcessResult) + + assert.NoError(t, err) + assert.True(t, strings.Contains(processResult.Stdout, "--user=Alice --password=serverless")) + assert.Equal(t, 0, processResult.Code) + assert.Equal(t, "", processResult.Stderr) + }) + + t.Run("Simple echo with args", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_with_args.yaml" + input := map[string]interface{}{} + + output, err := runWorkflow(t, workflowPath, input) + + processResult := output.(*ProcessResult) + + // Go does not keep the order of map iteration + // TODO: improve the UnMarshal of args to keep the order + + assert.NoError(t, err) + assert.Equal(t, "--user=john --password=doe", processResult.Stdout) + assert.Equal(t, 0, processResult.Code) + assert.Equal(t, "", processResult.Stderr) + }) + + t.Run("Simple echo with args using only key", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_with_args_only_key.yaml" + input := map[string]interface{}{ + "firstName": "Mary", + "lastName": "Jane", + } + + output, err := runWorkflow(t, workflowPath, input) + + processResult := output.(*ProcessResult) + + assert.NoError(t, err) + + // Go does not keep the order of map iteration + // TODO: improve the UnMarshal of args to keep the order + assert.True(t, strings.Contains(processResult.Stdout, "Mary")) + assert.True(t, strings.Contains(processResult.Stdout, "Jane")) + assert.Equal(t, 0, processResult.Code) + assert.Equal(t, "", processResult.Stderr) + }) + + t.Run("Simple echo with env and JQ", func(t *testing.T) { + workflowPath := "./testdata/run_shell_echo_with_env.yaml" + input := map[string]interface{}{ + "lastName": "Doe", + } + + output, err := runWorkflow(t, workflowPath, input) + + processResult := output.(*ProcessResult) + + assert.NoError(t, err) + assert.True(t, strings.Contains(processResult.Stdout, "Hello John Doe from env!")) + assert.Equal(t, 0, processResult.Code) + assert.Equal(t, "", processResult.Stderr) + }) + + t.Run("Execute touch and cat command", func(t *testing.T) { + workflowPath := "./testdata/run_shell_touch_cat.yaml" + input := map[string]interface{}{} + + output, err := runWorkflow(t, workflowPath, input) + + processResult := output.(*ProcessResult) + + assert.NoError(t, err) + assert.Equal(t, "hello world", strings.TrimSpace(processResult.Stdout)) + assert.Equal(t, 0, processResult.Code) + assert.Equal(t, "", processResult.Stderr) + }) +} diff --git a/impl/testdata/run_shell_echo.yaml b/impl/testdata/run_shell_echo.yaml new file mode 100644 index 0000000..b2c70cb --- /dev/null +++ b/impl/testdata/run_shell_echo.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: 'echo "Hello, anonymous"' + return: all \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_env_no_awaiting.yaml b/impl/testdata/run_shell_echo_env_no_awaiting.yaml new file mode 100644 index 0000000..757d701 --- /dev/null +++ b/impl/testdata/run_shell_echo_env_no_awaiting.yaml @@ -0,0 +1,27 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: echo "hello world not awaiting ($FULL_NAME)" > /tmp/hello-world.txt && cat /tmp/hello-world.txt + environment: + FULL_NAME: ${.full_name} + await: false \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_jq.yaml b/impl/testdata/run_shell_echo_jq.yaml new file mode 100644 index 0000000..706e713 --- /dev/null +++ b/impl/testdata/run_shell_echo_jq.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: ${ "echo Hello, \(.user.name)" } + return: all \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_none.yaml b/impl/testdata/run_shell_echo_none.yaml new file mode 100644 index 0000000..9cf72d3 --- /dev/null +++ b/impl/testdata/run_shell_echo_none.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: 'echo "Serverless Workflow"' + return: none \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_not_awaiting.yaml b/impl/testdata/run_shell_echo_not_awaiting.yaml new file mode 100644 index 0000000..721446f --- /dev/null +++ b/impl/testdata/run_shell_echo_not_awaiting.yaml @@ -0,0 +1,27 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: echo "hello world not awaiting ($FULL_NAME)" > /tmp/hello.txt && cat /tmp/hello.txt + environment: + FULL_NAME: ${.full_name} + await: false \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_with_args.yaml b/impl/testdata/run_shell_echo_with_args.yaml new file mode 100644 index 0000000..d307752 --- /dev/null +++ b/impl/testdata/run_shell_echo_with_args.yaml @@ -0,0 +1,28 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + arguments: + '--user': 'john' + '--password': 'doe' + command: echo + return: all \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_with_args_only_key.yaml b/impl/testdata/run_shell_echo_with_args_only_key.yaml new file mode 100644 index 0000000..45ae332 --- /dev/null +++ b/impl/testdata/run_shell_echo_with_args_only_key.yaml @@ -0,0 +1,31 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + arguments: + 'Hello': + '${.firstName}': + '${.lastName}': + from: + 'args!': + command: echo + return: all \ No newline at end of file diff --git a/impl/testdata/run_shell_echo_with_env.yaml b/impl/testdata/run_shell_echo_with_env.yaml new file mode 100644 index 0000000..5ad6516 --- /dev/null +++ b/impl/testdata/run_shell_echo_with_env.yaml @@ -0,0 +1,28 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: echo "Hello $FIRST_NAME $LAST_NAME from env!" + environment: + FIRST_NAME: John + LAST_NAME: ${.lastName} + return: all \ No newline at end of file diff --git a/impl/testdata/run_shell_exitcode.yaml b/impl/testdata/run_shell_exitcode.yaml new file mode 100644 index 0000000..1c2fd54 --- /dev/null +++ b/impl/testdata/run_shell_exitcode.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: 'ls /nonexistent_directory' + return: code \ No newline at end of file diff --git a/impl/testdata/run_shell_ls_stderr.yaml b/impl/testdata/run_shell_ls_stderr.yaml new file mode 100644 index 0000000..a776ebc --- /dev/null +++ b/impl/testdata/run_shell_ls_stderr.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: 'ls /nonexistent_directory' + return: stderr \ No newline at end of file diff --git a/impl/testdata/run_shell_missing_command.yaml b/impl/testdata/run_shell_missing_command.yaml new file mode 100644 index 0000000..92f0b81 --- /dev/null +++ b/impl/testdata/run_shell_missing_command.yaml @@ -0,0 +1,27 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - missingShellCommand: + run: + shell: + command: '' + environment: + FIRST_NAME: John + LAST_NAME: ${.lastName} \ No newline at end of file diff --git a/impl/testdata/run_shell_touch_cat.yaml b/impl/testdata/run_shell_touch_cat.yaml new file mode 100644 index 0000000..65796f9 --- /dev/null +++ b/impl/testdata/run_shell_touch_cat.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + command: echo "hello world" > /tmp/hello.txt && cat /tmp/hello.txt + return: all \ No newline at end of file diff --git a/impl/testdata/run_shell_with_args_key_value_jq.yaml b/impl/testdata/run_shell_with_args_key_value_jq.yaml new file mode 100644 index 0000000..938bc58 --- /dev/null +++ b/impl/testdata/run_shell_with_args_key_value_jq.yaml @@ -0,0 +1,28 @@ +# Copyright 2025 The Serverless Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +document: + dsl: '1.0.1' + namespace: test + name: run-shell-example + version: '0.1.0' +do: + - runShell: + run: + shell: + arguments: + '--user': '${.user}' + '${.passwordKey}': 'serverless' + command: echo + return: all \ No newline at end of file diff --git a/model/task_run.go b/model/task_run.go index b589cfa..dec6015 100644 --- a/model/task_run.go +++ b/model/task_run.go @@ -35,6 +35,7 @@ type RunTaskConfiguration struct { Script *Script `json:"script,omitempty"` Shell *Shell `json:"shell,omitempty"` Workflow *RunWorkflow `json:"workflow,omitempty"` + Return string `json:"return,omitempty"` } type Container struct { @@ -74,6 +75,7 @@ func (rtc *RunTaskConfiguration) UnmarshalJSON(data []byte) error { Script *Script `json:"script"` Shell *Shell `json:"shell"` Workflow *RunWorkflow `json:"workflow"` + Return string `json:"return"` }{} if err := json.Unmarshal(data, &temp); err != nil { @@ -105,6 +107,7 @@ func (rtc *RunTaskConfiguration) UnmarshalJSON(data []byte) error { } rtc.Await = temp.Await + rtc.Return = temp.Return return nil } @@ -116,12 +119,14 @@ func (rtc *RunTaskConfiguration) MarshalJSON() ([]byte, error) { Script *Script `json:"script,omitempty"` Shell *Shell `json:"shell,omitempty"` Workflow *RunWorkflow `json:"workflow,omitempty"` + Return string `json:"return,omitempty"` }{ Await: rtc.Await, Container: rtc.Container, Script: rtc.Script, Shell: rtc.Shell, Workflow: rtc.Workflow, + Return: rtc.Return, } return json.Marshal(temp) diff --git a/parser/parser.go b/parser/parser.go index 3707132..e9f8770 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -37,6 +37,7 @@ var supportedExt = []string{extYAML, extYML, extJSON} // FromYAMLSource parses the given Serverless Workflow YAML source into the Workflow type. func FromYAMLSource(source []byte) (workflow *model.Workflow, err error) { var jsonBytes []byte + err = yaml.Unmarshal(source, &jsonBytes) if jsonBytes, err = yaml.YAMLToJSON(source); err != nil { return nil, err }