diff --git a/model/task_run.go b/model/task_run.go index b589cfa..b7a3f76 100644 --- a/model/task_run.go +++ b/model/task_run.go @@ -39,24 +39,34 @@ type RunTaskConfiguration struct { type Container struct { Image string `json:"image" validate:"required"` + Name string `json:"name,omitempty"` Command string `json:"command,omitempty"` Ports map[string]interface{} `json:"ports,omitempty"` Volumes map[string]interface{} `json:"volumes,omitempty"` Environment map[string]string `json:"environment,omitempty"` + Input string `json:"stdin,omitempty"` + Arguments []string `json:"arguments,omitempty"` + Lifetime *ContainerLifetime `json:"lifetime,omitempty"` +} + +type ContainerLifetime struct { + Cleanup string `json:"cleanup" validate:"required,oneof=always never eventually"` + After *Duration `json:"after" validate:"required_if=Cleanup eventually"` } type Script struct { - Language string `json:"language" validate:"required"` - Arguments map[string]interface{} `json:"arguments,omitempty"` - Environment map[string]string `json:"environment,omitempty"` - InlineCode *string `json:"code,omitempty"` - External *ExternalResource `json:"source,omitempty"` + Language string `json:"language" validate:"required,oneof=javascript js python"` // "js" exists for legacy reasons, use "javascript" instead + Arguments *RunArguments `json:"arguments,omitempty"` + Environment map[string]string `json:"environment,omitempty"` + InlineCode *string `json:"code,omitempty"` + External *ExternalResource `json:"source,omitempty"` + Input string `json:"stdin,omitempty"` } type Shell struct { - Command string `json:"command" validate:"required"` - Arguments map[string]interface{} `json:"arguments,omitempty"` - Environment map[string]string `json:"environment,omitempty"` + Command string `json:"command" validate:"required"` + Arguments *RunArguments `json:"arguments,omitempty"` + Environment map[string]string `json:"environment,omitempty"` } type RunWorkflow struct { @@ -126,3 +136,46 @@ func (rtc *RunTaskConfiguration) MarshalJSON() ([]byte, error) { return json.Marshal(temp) } + +type RunArguments struct { + Value any `json:"-"` +} + +func (a *RunArguments) MarshalJSON() ([]byte, error) { + switch v := a.Value.(type) { + case map[string]interface{}, []string: + return json.Marshal(v) + default: + return nil, errors.New("unknown RunArguments type") + } +} + +func (a *RunArguments) UnmarshalJSON(data []byte) error { + var m map[string]interface{} + if err := json.Unmarshal(data, &m); err == nil { + a.Value = m + return nil + } + + var s []string + if err := json.Unmarshal(data, &s); err == nil { + a.Value = s + return nil + } + + return errors.New("data must be a valid array of strings or object map") +} + +func (a *RunArguments) AsMap() map[string]interface{} { + if v, ok := a.Value.(map[string]interface{}); ok { + return v + } + return nil +} + +func (a *RunArguments) AsSlice() []string { + if v, ok := a.Value.([]string); ok { + return v + } + return nil +} diff --git a/model/task_run_test.go b/model/task_run_test.go index 026b9c8..554de6b 100644 --- a/model/task_run_test.go +++ b/model/task_run_test.go @@ -37,6 +37,7 @@ func TestRunTask_MarshalJSON(t *testing.T) { Await: boolPtr(true), Container: &Container{ Image: "example-image", + Name: "example-name", Command: "example-command", Ports: map[string]interface{}{ "8080": "80", @@ -44,6 +45,15 @@ func TestRunTask_MarshalJSON(t *testing.T) { Environment: map[string]string{ "ENV_VAR": "value", }, + Input: "example-input", + Arguments: []string{ + "arg1", + "arg2", + }, + Lifetime: &ContainerLifetime{ + Cleanup: "eventually", + After: NewDurationExpr("20s"), + }, }, }, } @@ -61,9 +71,16 @@ func TestRunTask_MarshalJSON(t *testing.T) { "await": true, "container": { "image": "example-image", + "name": "example-name", "command": "example-command", "ports": {"8080": "80"}, - "environment": {"ENV_VAR": "value"} + "environment": {"ENV_VAR": "value"}, + "stdin": "example-input", + "arguments": ["arg1","arg2"], + "lifetime": { + "cleanup": "eventually", + "after": "20s" + } } } }`, string(data)) @@ -81,9 +98,18 @@ func TestRunTask_UnmarshalJSON(t *testing.T) { "await": true, "container": { "image": "example-image", + "name": "example-name", "command": "example-command", "ports": {"8080": "80"}, - "environment": {"ENV_VAR": "value"} + "environment": {"ENV_VAR": "value"}, + "stdin": "example-input", + "arguments": ["arg1","arg2"], + "lifetime": { + "cleanup": "eventually", + "after": { + "seconds": 20 + } + } } } }` @@ -102,9 +128,14 @@ func TestRunTask_UnmarshalJSON(t *testing.T) { assert.Equal(t, "example-command", runTask.Run.Container.Command) assert.Equal(t, map[string]interface{}{"8080": "80"}, runTask.Run.Container.Ports) assert.Equal(t, map[string]string{"ENV_VAR": "value"}, runTask.Run.Container.Environment) + assert.Equal(t, "example-name", runTask.Run.Container.Name) + assert.Equal(t, "example-input", runTask.Run.Container.Input) + assert.Equal(t, []string{"arg1", "arg2"}, runTask.Run.Container.Arguments) + assert.Equal(t, "eventually", runTask.Run.Container.Lifetime.Cleanup) + assert.Equal(t, &DurationInline{Seconds: 20}, runTask.Run.Container.Lifetime.After.AsInline()) } -func TestRunTaskScript_MarshalJSON(t *testing.T) { +func TestRunTaskScriptArgsMap_MarshalJSON(t *testing.T) { runTask := RunTask{ TaskBase: TaskBase{ If: &RuntimeExpression{Value: "${condition}"}, @@ -120,13 +151,16 @@ func TestRunTaskScript_MarshalJSON(t *testing.T) { Await: boolPtr(true), Script: &Script{ Language: "python", - Arguments: map[string]interface{}{ - "arg1": "value1", + Arguments: &RunArguments{ + Value: map[string]interface{}{ + "arg1": "value1", + }, }, Environment: map[string]string{ "ENV_VAR": "value", }, InlineCode: stringPtr("print('Hello, World!')"), + Input: "example-input", }, }, } @@ -146,13 +180,14 @@ func TestRunTaskScript_MarshalJSON(t *testing.T) { "language": "python", "arguments": {"arg1": "value1"}, "environment": {"ENV_VAR": "value"}, - "code": "print('Hello, World!')" + "code": "print('Hello, World!')", + "stdin": "example-input" } } }`, string(data)) } -func TestRunTaskScript_UnmarshalJSON(t *testing.T) { +func TestRunTaskScriptArgsMap_UnmarshalJSON(t *testing.T) { jsonData := `{ "if": "${condition}", "input": { "from": {"key": "value"} }, @@ -166,7 +201,97 @@ func TestRunTaskScript_UnmarshalJSON(t *testing.T) { "language": "python", "arguments": {"arg1": "value1"}, "environment": {"ENV_VAR": "value"}, - "code": "print('Hello, World!')" + "code": "print('Hello, World!')", + "stdin": "example-input" + } + } + }` + + var runTask RunTask + err := json.Unmarshal([]byte(jsonData), &runTask) + assert.NoError(t, err) + assert.Equal(t, &RuntimeExpression{Value: "${condition}"}, runTask.If) + assert.Equal(t, &Input{From: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"key": "value"}}}, runTask.Input) + assert.Equal(t, &Output{As: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"result": "output"}}}, runTask.Output) + assert.Equal(t, &TimeoutOrReference{Timeout: &Timeout{After: NewDurationExpr("10s")}}, runTask.Timeout) + assert.Equal(t, &FlowDirective{Value: "continue"}, runTask.Then) + assert.Equal(t, map[string]interface{}{"meta": "data"}, runTask.Metadata) + assert.Equal(t, true, *runTask.Run.Await) + assert.Equal(t, "python", runTask.Run.Script.Language) + assert.Equal(t, map[string]interface{}{"arg1": "value1"}, runTask.Run.Script.Arguments.AsMap()) + assert.Equal(t, map[string]string{"ENV_VAR": "value"}, runTask.Run.Script.Environment) + assert.Equal(t, "print('Hello, World!')", *runTask.Run.Script.InlineCode) + assert.Equal(t, "example-input", runTask.Run.Script.Input) +} + +func TestRunTaskScriptArgArray_MarshalJSON(t *testing.T) { + runTask := RunTask{ + TaskBase: TaskBase{ + If: &RuntimeExpression{Value: "${condition}"}, + Input: &Input{From: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"key": "value"}}}, + Output: &Output{As: &ObjectOrRuntimeExpr{Value: map[string]interface{}{"result": "output"}}}, + Timeout: &TimeoutOrReference{Timeout: &Timeout{After: NewDurationExpr("10s")}}, + Then: &FlowDirective{Value: "continue"}, + Metadata: map[string]interface{}{ + "meta": "data", + }, + }, + Run: RunTaskConfiguration{ + Await: boolPtr(true), + Script: &Script{ + Language: "python", + Arguments: &RunArguments{ + Value: []string{ + "arg1=value1", + }, + }, + Environment: map[string]string{ + "ENV_VAR": "value", + }, + InlineCode: stringPtr("print('Hello, World!')"), + Input: "example-input", + }, + }, + } + + data, err := json.Marshal(runTask) + assert.NoError(t, err) + assert.JSONEq(t, `{ + "if": "${condition}", + "input": { "from": {"key": "value"} }, + "output": { "as": {"result": "output"} }, + "timeout": { "after": "10s" }, + "then": "continue", + "metadata": {"meta": "data"}, + "run": { + "await": true, + "script": { + "language": "python", + "arguments": ["arg1=value1"], + "environment": {"ENV_VAR": "value"}, + "code": "print('Hello, World!')", + "stdin": "example-input" + } + } + }`, string(data)) +} + +func TestRunTaskScriptArgsArray_UnmarshalJSON(t *testing.T) { + jsonData := `{ + "if": "${condition}", + "input": { "from": {"key": "value"} }, + "output": { "as": {"result": "output"} }, + "timeout": { "after": "10s" }, + "then": "continue", + "metadata": {"meta": "data"}, + "run": { + "await": true, + "script": { + "language": "python", + "arguments": ["arg1=value1"], + "environment": {"ENV_VAR": "value"}, + "code": "print('Hello, World!')", + "stdin": "example-input" } } }` @@ -182,9 +307,10 @@ func TestRunTaskScript_UnmarshalJSON(t *testing.T) { assert.Equal(t, map[string]interface{}{"meta": "data"}, runTask.Metadata) assert.Equal(t, true, *runTask.Run.Await) assert.Equal(t, "python", runTask.Run.Script.Language) - assert.Equal(t, map[string]interface{}{"arg1": "value1"}, runTask.Run.Script.Arguments) + assert.Equal(t, []string{"arg1=value1"}, runTask.Run.Script.Arguments.AsSlice()) assert.Equal(t, map[string]string{"ENV_VAR": "value"}, runTask.Run.Script.Environment) assert.Equal(t, "print('Hello, World!')", *runTask.Run.Script.InlineCode) + assert.Equal(t, "example-input", runTask.Run.Script.Input) } func boolPtr(b bool) *bool {