diff --git a/README.md b/README.md index 24f4d493..a5369473 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,7 @@ Each task consists of a list of commands that will be executed on the remote hos - `on_error`: specifies the command to execute on the local host (the one running the `spot` command) in case of an error. The command can use the `{SPOT_ERROR}` variable to access the last error message. Example: `on_error: "curl -s localhost:8080/error?msg={SPOT_ERROR}"` - `user`: specifies the SSH user to use when connecting to remote hosts. Overrides the user defined in the top section of playbook file for the specified task. +- `targets` - list of target names, group, tags or host addresses to execute the task on. Command line `-t` flag can be used to override this field. *Note: these fields supported in the full playbook type only* @@ -406,7 +407,7 @@ Targets are used to define the remote hosts to execute the tasks on. Targets can All the target types can be combined, i.e. `hosts`, `groups`, `tags`, `hosts` and `names` all can be used together in the same target. To avoid possible duplicates, the final list of hosts is deduplicated by the host+ip+user. -example of targets in the playbook file: +example of targets set in the playbook file: ```yaml targets: @@ -419,6 +420,13 @@ targets: names: ["host1", "host2"] all-servers: groups: ["all"] + +tasks: + - name: task1 + targets: ["dev", "host3.example.com:2222"] + commands: + - name: command1 + script: echo "Hello World" ``` *Note: All the target types available in the full playbook file only. The simplified playbook file only supports a single, anonymous target type combining `hosts` and `names` together.* @@ -449,7 +457,9 @@ The target selection is done in the following order: - if no match found, Spot will try to match on host name in the inventory file. - if no match found, Spot will try to match on host address in the playbook file. - if no match found, Spot will use it as a host address. -- if `--target` is not discovered, Spot will assume the `default` target. +- if `--target` is not set, Spot will try check it `targets` list for the task. If set, it will use it following the same logic as above. +- and finally, Spot will assume the `default` target. + ### Inventory diff --git a/cmd/spot/main.go b/cmd/spot/main.go index 6ddcce4e..71c355e5 100644 --- a/cmd/spot/main.go +++ b/cmd/spot/main.go @@ -186,7 +186,7 @@ func run(opts options) error { } if opts.TaskName != "" { // run single task - for _, targetName := range opts.Targets { + for _, targetName := range targetsForTask(opts, opts.TaskName, conf) { if err := runTaskForTarget(ctx, r, opts.TaskName, targetName); err != nil { return err } @@ -196,7 +196,7 @@ func run(opts options) error { // run all tasks for _, taskName := range conf.Tasks { - for _, targetName := range opts.Targets { + for _, targetName := range targetsForTask(opts, opts.TaskName, conf) { if err := runTaskForTarget(ctx, r, taskName.Name, targetName); err != nil { return err } @@ -217,6 +217,40 @@ func runTaskForTarget(ctx context.Context, r runner.Process, taskName, targetNam return nil } +// get list of targets for task. Usually ths is just list of all targets from command line, +// however if task has targets defined AND cli has default target, then only those targets will be used. +func targetsForTask(opts options, taskName string, conf *config.PlayBook) []string { + if len(opts.Targets) > 1 || (len(opts.Targets) == 1 && opts.Targets[0] != "default") { + // non-default target specified on command line + return opts.Targets + } + + lookupTask := func(name string) (tsk config.Task) { + // get task by name + for _, t := range conf.Tasks { + if t.Name == taskName { + tsk = t + return tsk + } + } + return tsk + } + + tsk := lookupTask(taskName) + if tsk.Name == "" { + // this should never happen, task name is validated on playbook level + return opts.Targets + } + + if len(tsk.Targets) == 0 { + // no targets defined for task + return opts.Targets + } + + log.Printf("[INFO] task %q has %d targets [%s] pre-defined", taskName, len(tsk.Targets), strings.Join(tsk.Targets, ", ")) + return tsk.Targets +} + // makeSecretsProvider creates secrets provider based on options func makeSecretsProvider(sopts SecretsProvider) (config.SecretsProvider, error) { switch sopts.Provider { diff --git a/cmd/spot/main_test.go b/cmd/spot/main_test.go index bf708047..0fda011e 100644 --- a/cmd/spot/main_test.go +++ b/cmd/spot/main_test.go @@ -467,6 +467,93 @@ func Test_formatErrorString(t *testing.T) { } } +func Test_targetsForTask(t *testing.T) { + tests := []struct { + name string + opts options + taskName string + conf *config.PlayBook + expectedResult []string + }{ + { + name: "non-default targets specified on command line", + opts: options{ + Targets: []string{"target1", "target2"}, + }, + taskName: "task1", + conf: &config.PlayBook{}, + expectedResult: []string{"target1", "target2"}, + }, + { + name: "task with targets defined and default in command line", + opts: options{ + Targets: []string{"default"}, + }, + taskName: "task1", + conf: &config.PlayBook{ + Tasks: []config.Task{ + { + Name: "task1", + Targets: []string{"target3", "target4"}, + }, + }, + }, + expectedResult: []string{"target3", "target4"}, + }, + { + name: "task without targets defined", + opts: options{ + Targets: []string{"default"}, + }, + taskName: "task2", + conf: &config.PlayBook{ + Tasks: []config.Task{ + { + Name: "task1", + Targets: []string{"target3", "target4"}, + }, + { + Name: "task2", + }, + }, + }, + expectedResult: []string{"default"}, + }, + { + name: "default target with no task targets", + opts: options{ + Targets: []string{"default"}, + }, + taskName: "task3", + conf: &config.PlayBook{}, + expectedResult: []string{"default"}, + }, + { + name: "non-existing task", + opts: options{ + Targets: []string{"default"}, + }, + taskName: "task3", + conf: &config.PlayBook{ + Tasks: []config.Task{ + { + Name: "task1", + Targets: []string{"target3", "target4"}, + }, + }, + }, + expectedResult: []string{"default"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := targetsForTask(tc.opts, tc.taskName, tc.conf) + assert.Equal(t, tc.expectedResult, result) + }) + } +} + func startTestContainer(t *testing.T) (hostAndPort string, teardown func()) { t.Helper() ctx := context.Background() diff --git a/pkg/config/playbook.go b/pkg/config/playbook.go index 6bf015a3..e13a3e31 100644 --- a/pkg/config/playbook.go +++ b/pkg/config/playbook.go @@ -52,10 +52,11 @@ type SimplePlayBook struct { // Task defines multiple commands runs together type Task struct { - Name string `yaml:"name" toml:"name"` // name of target, set by config caller - User string `yaml:"user" toml:"user"` - Commands []Cmd `yaml:"commands" toml:"commands"` - OnError string `yaml:"on_error" toml:"on_error"` + Name string `yaml:"name" toml:"name"` // name of target, set by config caller + User string `yaml:"user" toml:"user"` + Commands []Cmd `yaml:"commands" toml:"commands"` + OnError string `yaml:"on_error" toml:"on_error"` + Targets []string `yaml:"targets" toml:"targets"` // optional list of targets to run task on, names or groups } // Target defines hosts to run commands on