diff --git a/internal/run.go b/internal/run.go index d49bbe7..dd459a9 100644 --- a/internal/run.go +++ b/internal/run.go @@ -31,6 +31,13 @@ func RunSubgraph(ctx context.Context, cancel context.CancelFunc, port int, openB } } + // validate all tasks + for name, task := range wf.Tasks { + if err := task.Validate(); err != nil { + return fmt.Errorf("task %q is invalid: %w", name, err) + } + } + // check skipped tasks are valid for _, name := range tasksToSkip { if _, ok := wf.Tasks[name]; !ok { diff --git a/internal/run_test.go b/internal/run_test.go index 88b51c8..7ef8eb7 100644 --- a/internal/run_test.go +++ b/internal/run_test.go @@ -49,6 +49,18 @@ func TestRunSubgraph(t *testing.T) { assert.EqualError(t, err, "skipped task \"job\" not found in workflow") }) + t.Run("Invalid task", func(t *testing.T) { + ctx, cancel, logger, _ := setup(t) + defer cancel() + wf := &types.Workflow{ + Tasks: map[string]types.Task{ + "job": {Command: []string{"echo"}, Sh: "echo hello"}, + }, + } + err := RunSubgraph(ctx, cancel, 0, false, logger, wf, []string{"job"}, nil) + assert.EqualError(t, err, "task \"job\" is invalid: only one of command or sh is allowed") + }) + t.Run("Single successful job", func(t *testing.T) { ctx, cancel, logger, _ := setup(t) defer cancel() diff --git a/internal/types/task.go b/internal/types/task.go index 6d3b041..b9f5703 100644 --- a/internal/types/task.go +++ b/internal/types/task.go @@ -1,6 +1,7 @@ package types import ( + "fmt" "os" "path/filepath" "time" @@ -12,6 +13,25 @@ func (t *Task) HasMutex() bool { return t != nil && t.Mutex != "" } +// Validate checks that task execution fields are combined in a supported way. +func (t *Task) Validate() error { + hasCommand := len(t.Command) > 0 + hasSh := t.Sh != "" + hasImage := t.Image != "" + hasManifests := len(t.Manifests) > 0 + + // command and sh are alternative ways to specify the task's command; they must not both be set. + if hasCommand && hasSh { + return fmt.Errorf("only one of command or sh is allowed") + } + + // manifests-based tasks are mutually exclusive with image/command/sh-based tasks. + if hasManifests && (hasCommand || hasSh || hasImage) { + return fmt.Errorf("manifests cannot be set together with image, command, or sh") + } + return nil +} + // A task is a container or a command to run. type Task struct { // Type is the type of the task: "service" or "job". If omitted, if there are ports, it's a service, otherwise it's a job. diff --git a/internal/types/task_test.go b/internal/types/task_test.go index 553d603..8393034 100644 --- a/internal/types/task_test.go +++ b/internal/types/task_test.go @@ -36,6 +36,57 @@ func TestTask_AllTargetsExist(t *testing.T) { } } +func TestTask_Validate(t *testing.T) { + t.Run("NoExecField", func(t *testing.T) { + task := &Task{} + assert.NoError(t, task.Validate()) + }) + t.Run("CommandOnly", func(t *testing.T) { + task := &Task{Command: Strings{"echo", "hello"}} + assert.NoError(t, task.Validate()) + }) + t.Run("ShOnly", func(t *testing.T) { + task := &Task{Sh: "echo hello"} + assert.NoError(t, task.Validate()) + }) + t.Run("ImageOnly", func(t *testing.T) { + task := &Task{Image: "nginx"} + assert.NoError(t, task.Validate()) + }) + t.Run("ManifestsOnly", func(t *testing.T) { + task := &Task{Manifests: Strings{"deploy.yaml"}} + assert.NoError(t, task.Validate()) + }) + t.Run("CommandAndSh", func(t *testing.T) { + task := &Task{Command: Strings{"echo"}, Sh: "echo hello"} + assert.EqualError(t, task.Validate(), "only one of command or sh is allowed") + }) + t.Run("ShAndImage", func(t *testing.T) { + task := &Task{Sh: "echo hello", Image: "nginx"} + assert.NoError(t, task.Validate()) + }) + t.Run("CommandAndManifests", func(t *testing.T) { + task := &Task{Command: Strings{"echo"}, Manifests: Strings{"deploy.yaml"}} + assert.EqualError(t, task.Validate(), "manifests cannot be set together with image, command, or sh") + }) + t.Run("CommandAndImage", func(t *testing.T) { + task := &Task{Command: Strings{"echo"}, Image: "nginx"} + assert.NoError(t, task.Validate()) + }) + t.Run("ShAndManifests", func(t *testing.T) { + task := &Task{Sh: "echo hello", Manifests: Strings{"deploy.yaml"}} + assert.EqualError(t, task.Validate(), "manifests cannot be set together with image, command, or sh") + }) + t.Run("ImageAndManifests", func(t *testing.T) { + task := &Task{Image: "nginx", Manifests: Strings{"deploy.yaml"}} + assert.EqualError(t, task.Validate(), "manifests cannot be set together with image, command, or sh") + }) + t.Run("ThreeFields", func(t *testing.T) { + task := &Task{Command: Strings{"echo"}, Sh: "echo hello", Image: "nginx"} + assert.EqualError(t, task.Validate(), "only one of command or sh is allowed") + }) +} + func TestTask_GetType(t *testing.T) { t.Run("Defined", func(t *testing.T) { task := &Task{Type: TaskTypeService}