From 60ac32ccaf11659045b4a582599021d3fc01cbe0 Mon Sep 17 00:00:00 2001 From: Simon Emms Date: Tue, 2 Dec 2025 17:12:53 +0000 Subject: [PATCH 1/3] Update run.container for new spec updates Signed-off-by: Simon Emms --- model/task_run.go | 9 +++++++++ model/task_run_test.go | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/model/task_run.go b/model/task_run.go index b589cfa..1f65ea2 100644 --- a/model/task_run.go +++ b/model/task_run.go @@ -39,10 +39,19 @@ 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 { diff --git a/model/task_run_test.go b/model/task_run_test.go index 026b9c8..1b8f506 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,6 +128,11 @@ 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) { From 7ae420a2c3f0e03071bc2fe0e045420473d3e4d4 Mon Sep 17 00:00:00 2001 From: Simon Emms Date: Tue, 2 Dec 2025 17:26:14 +0000 Subject: [PATCH 2/3] Update run.script for new spec updates Signed-off-by: Simon Emms --- model/task_run.go | 3 ++- model/task_run_test.go | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/model/task_run.go b/model/task_run.go index 1f65ea2..0dbf085 100644 --- a/model/task_run.go +++ b/model/task_run.go @@ -55,11 +55,12 @@ type ContainerLifetime struct { } type Script struct { - Language string `json:"language" validate:"required"` + Language string `json:"language" validate:"required,oneof=javascript js python"` // "js" exists for legacy reasons, use "javascript" instead Arguments map[string]interface{} `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 { diff --git a/model/task_run_test.go b/model/task_run_test.go index 1b8f506..56f0f70 100644 --- a/model/task_run_test.go +++ b/model/task_run_test.go @@ -135,7 +135,7 @@ func TestRunTask_UnmarshalJSON(t *testing.T) { 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}"}, @@ -158,6 +158,7 @@ func TestRunTaskScript_MarshalJSON(t *testing.T) { "ENV_VAR": "value", }, InlineCode: stringPtr("print('Hello, World!')"), + Input: "example-input", }, }, } @@ -177,13 +178,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"} }, @@ -197,7 +199,8 @@ 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" } } }` @@ -216,6 +219,7 @@ func TestRunTaskScript_UnmarshalJSON(t *testing.T) { assert.Equal(t, map[string]interface{}{"arg1": "value1"}, runTask.Run.Script.Arguments) 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 { From 265e87d5b4b708dbd7d00b16918e9233e3db4314 Mon Sep 17 00:00:00 2001 From: Simon Emms Date: Tue, 2 Dec 2025 20:54:44 +0000 Subject: [PATCH 3/3] Add RunArguments type to handle object and array inputs Signed-off-by: Simon Emms --- model/task_run.go | 61 ++++++++++++++++++++++---- model/task_run_test.go | 99 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 147 insertions(+), 13 deletions(-) diff --git a/model/task_run.go b/model/task_run.go index 0dbf085..b7a3f76 100644 --- a/model/task_run.go +++ b/model/task_run.go @@ -55,18 +55,18 @@ type ContainerLifetime struct { } type Script struct { - Language string `json:"language" validate:"required,oneof=javascript js python"` // "js" exists for legacy reasons, use "javascript" instead - Arguments map[string]interface{} `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"` + 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 { @@ -136,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 56f0f70..554de6b 100644 --- a/model/task_run_test.go +++ b/model/task_run_test.go @@ -151,8 +151,10 @@ func TestRunTaskScriptArgsMap_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", @@ -216,10 +218,99 @@ func TestRunTaskScriptArgsMap_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, 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) + 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" + } + } + }` + + 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, []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 {