Skip to content

Commit

Permalink
add support of task's targets set directly in playbook
Browse files Browse the repository at this point in the history
  • Loading branch information
umputun committed May 12, 2023
1 parent 2ff9f01 commit 2ffd159
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 8 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*

Expand Down Expand Up @@ -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:
Expand All @@ -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.*
Expand Down Expand Up @@ -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

Expand Down
38 changes: 36 additions & 2 deletions cmd/spot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down
87 changes: 87 additions & 0 deletions cmd/spot/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 5 additions & 4 deletions pkg/config/playbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 2ffd159

Please sign in to comment.