Skip to content

Commit 67aaa98

Browse files
committed
feat: add task fields only_groups and skip_groups
1 parent d7bf3a0 commit 67aaa98

7 files changed

Lines changed: 97 additions & 40 deletions

File tree

docs/README.md

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -496,8 +496,8 @@ playbook:
496496
sudo: false
497497
tasks:
498498
- name: OS-specific Task
499-
command: echo "Running on {{.puppet_facts.os.name}}"
500-
when: "{{.puppet_facts.os.family}} == RedHat"
499+
command: echo "Running on {% raw %}{{.puppet_facts.os.name}}{% endraw %}"
500+
when: "{% raw %}{{.puppet_facts.os.family}} == RedHat{% endraw %}"
501501
```
502502

503503
#### Collector Configuration
@@ -517,26 +517,26 @@ Facts are available as variables in your tasks, using the collector name as the
517517
##### Basic Usage
518518
```yaml
519519
- name: Show OS Information
520-
command: echo "Running on {{.puppet_facts.os.name}} {{.puppet_facts.os.release.full}}"
520+
command: echo "Running on {% raw %}{{.puppet_facts.os.name}} {{.puppet_facts.os.release.full}}{% endraw %}"
521521
```
522522

523523
##### Conditional Execution
524524
```yaml
525525
- name: Debian-specific Task
526526
command: apt-get update
527-
when: "{{.puppet_facts.os.family}} == Debian"
527+
when: "{% raw %}{{.puppet_facts.os.family}} == Debian{% endraw %}"
528528
529529
- name: RedHat-specific Task
530530
command: yum update
531-
when: "{{.puppet_facts.os.family}} == RedHat"
531+
when: "{% raw %}{{.puppet_facts.os.family}} == RedHat{% endraw %}"
532532
```
533533

534534
##### Nested Facts
535535

536536
Facts can have nested structures, which can be accessed using dot notation:
537537
```yaml
538538
- name: Show Memory Information
539-
command: echo "Total memory: {{.puppet_facts.memory.system.total}}"
539+
command: echo "Total memory: {% raw %}{{.puppet_facts.memory.system.total}}{% endraw %}"
540540
```
541541

542542
#### Using Puppet Facter
@@ -609,11 +609,11 @@ facts:
609609
Access the facts in your tasks:
610610
```yaml
611611
- name: Show Application Status
612-
command: echo "App version {{.app_status.version}} is {{.app_status.status}}"
612+
command: echo "App version {% raw %}{{.app_status.version}} is {{.app_status.status}}{% endraw %}"
613613
614614
- name: Restart if Connections Too High
615615
command: systemctl restart myapp
616-
when: "{{.app_status.connections}} > 100"
616+
when: "{% raw %}{{.app_status.connections}}{% endraw %} > 100"
617617
```
618618

619619
#### Troubleshooting
@@ -640,7 +640,7 @@ If jq reports errors, your JSON is not valid.
640640

641641
If you have complex nested facts, you can use the dot notation to access nested values:
642642
```
643-
{{.puppet_facts.networking.interfaces.eth0.ip}}
643+
{% raw %}{{.puppet_facts.networking.interfaces.eth0.ip}}{% endraw %}
644644
```
645645

646646
#### Examples
@@ -664,7 +664,7 @@ playbook:
664664
sudo: true
665665
tasks:
666666
- name: Show System Information
667-
command: echo "Host: {{.system.networking.hostname}}, OS: {{.system.os.name}} {{.system.os.release.full}}, CPU: {{.system.processors.models.0}}, RAM: {{.system.memory.system.total}}"
667+
command: echo "Host: {% raw %}{{.system.networking.hostname}}, OS: {{.system.os.name}} {{.system.os.release.full}}, CPU: {{.system.processors.models.0}}, RAM: {{.system.memory.system.total}}{% endraw %}"
668668
```
669669

670670
##### OS-Specific Deployment
@@ -690,12 +690,12 @@ playbook:
690690
- name: Install Dependencies (Debian)
691691
command: apt-get install -y nginx nodejs
692692
sudo: true
693-
when: "{{.os_info.os.family}} == Debian"
693+
when: "{% raw %}{{.os_info.os.family}}{% endraw %} == Debian"
694694
695695
- name: Install Dependencies (RedHat)
696696
command: yum install -y nginx nodejs
697697
sudo: true
698-
when: "{{.os_info.os.family}} == RedHat"
698+
when: "{% raw %}{{.os_info.os.family}}{% endraw %} == RedHat"
699699
700700
- name: Deploy Application
701701
command: /usr/local/bin/deploy.sh
@@ -757,6 +757,31 @@ groups:
757757
hosts: [...]
758758
```
759759

760+
### Task Group Restrictions
761+
762+
Restrict tasks to specific groups:
763+
```yaml
764+
tasks:
765+
- name: Database Backup
766+
command: /usr/local/bin/backup-db.sh
767+
sudo: true
768+
only_groups: [database]
769+
770+
- name: Web Server Config
771+
command: /etc/nginx/sites-available/default
772+
sudo: true
773+
only_groups: [webserver]
774+
775+
- name: Update All Servers
776+
command: apt-get update
777+
sudo: true
778+
# No only_groups, runs on all hosts
779+
780+
- name: Test Environment Only
781+
command: /usr/local/bin/test-feature.sh
782+
skip_groups: [production]
783+
```
784+
760785
### Retries and Error Handling
761786
```yaml
762787
tasks:

executor.go

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,41 @@ func (e *Executor) ExecuteTask(task Task) error {
126126
writer = os.Stdout
127127
}
128128

129+
if execOptions.Verbose {
130+
e.mu.Lock()
131+
log.SetOutput(writer)
132+
log.Printf("[VERBOSE] [%s] Executing task: %s", e.host.Name, task.Name)
133+
log.SetOutput(os.Stderr)
134+
e.mu.Unlock()
135+
}
136+
137+
// Check if task is restricted to specific groups
138+
if len(task.OnlyGroups) > 0 {
139+
groupAllowed := false
140+
for _, group := range task.OnlyGroups {
141+
if group == e.groupName {
142+
groupAllowed = true
143+
break
144+
}
145+
}
146+
if !groupAllowed {
147+
// Skip task, not in allowed groups
148+
fmt.Fprintf(writer, " ⊘ Skipped (not in allowed groups: %v)\n", task.OnlyGroups)
149+
return nil
150+
}
151+
}
152+
153+
// Check if task should skip specific groups
154+
if len(task.SkipGroups) > 0 {
155+
for _, group := range task.SkipGroups {
156+
if group == e.groupName {
157+
// Skip task, in excluded group
158+
fmt.Fprintf(writer, " ⊘ Skipped (in excluded group: %s)\n", group)
159+
return nil
160+
}
161+
}
162+
}
163+
129164
// Check for delegation - if this task is delegated to a different host,
130165
// skip it unless we're the delegated host
131166
if task.DelegateTo != "" && task.DelegateTo != e.host.Name && task.DelegateTo != "localhost" {
@@ -165,14 +200,6 @@ func (e *Executor) ExecuteTask(task Task) error {
165200
runOnceTasks.Unlock()
166201
}
167202

168-
if execOptions.Verbose {
169-
e.mu.Lock()
170-
log.SetOutput(writer)
171-
log.Printf("[VERBOSE] [%s] Executing task: %s", e.host.Name, task.Name)
172-
log.SetOutput(os.Stderr)
173-
e.mu.Unlock()
174-
}
175-
176203
if len(task.DependsOn) > 0 {
177204
for _, dep := range task.DependsOn {
178205
if !e.completedTasks[dep] {

playbook.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"time"
1111
)
1212

13-
func executeOnHost(host Host, tasks []Task, captureOutput bool) HostResult {
13+
func executeOnHost(host Host, tasks []Task, captureOutput bool, groupName string) HostResult {
1414
var output bytes.Buffer
1515
var writer io.Writer = os.Stdout
1616

@@ -28,7 +28,7 @@ func executeOnHost(host Host, tasks []Task, captureOutput bool) HostResult {
2828
fmt.Fprintf(writer, "%s┌─ Host: %s%s%s (%s)\n", color(ColorCyan), color(ColorBold), host.Name, color(ColorReset), displayTarget)
2929
fmt.Fprintf(writer, "%s│%s\n", color(ColorCyan), color(ColorReset))
3030

31-
executor, err := NewExecutor(host)
31+
executor, err := NewExecutor(host, groupName)
3232
if err != nil {
3333
fmt.Fprintf(writer, "%s│%s %s✗ Connection failed:%s %v\n", color(ColorCyan), color(ColorReset), color(ColorRed), color(ColorReset), err)
3434
fmt.Fprintf(writer, "%s└─ ✗ Connection Failed%s\n\n", color(ColorRed), color(ColorReset))
@@ -78,15 +78,15 @@ func executeOnHost(host Host, tasks []Task, captureOutput bool) HostResult {
7878
return HostResult{Host: host, Success: true, Error: nil, Output: output.String()}
7979
}
8080

81-
func executeHostsParallel(hosts []Host, tasks []Task) []HostResult {
81+
func executeHostsParallel(hosts []Host, tasks []Task, groupName string) []HostResult {
8282
var wg sync.WaitGroup
8383
resultsChan := make(chan HostResult, len(hosts))
8484

8585
for _, host := range hosts {
8686
wg.Add(1)
8787
go func(h Host) {
8888
defer wg.Done()
89-
result := executeOnHost(h, tasks, true)
89+
result := executeOnHost(h, tasks, true, groupName)
9090
resultsChan <- result
9191
}(host)
9292
}
@@ -111,11 +111,11 @@ func executeHostsParallel(hosts []Host, tasks []Task) []HostResult {
111111
return results
112112
}
113113

114-
func executeHostsSequential(hosts []Host, tasks []Task) []HostResult {
114+
func executeHostsSequential(hosts []Host, tasks []Task, groupName string) []HostResult {
115115
var results []HostResult
116116

117117
for _, host := range hosts {
118-
result := executeOnHost(host, tasks, false)
118+
result := executeOnHost(host, tasks, false, groupName)
119119
results = append(results, result)
120120

121121
if !result.Success {
@@ -172,9 +172,9 @@ func executeWithGroups(config Config) ([]HostResult, error) {
172172
var groupResults []HostResult
173173

174174
if group.Parallel {
175-
groupResults = executeHostsParallel(group.Hosts, config.Playbook.Tasks)
175+
groupResults = executeHostsParallel(group.Hosts, config.Playbook.Tasks, group.Name)
176176
} else {
177-
groupResults = executeHostsSequential(group.Hosts, config.Playbook.Tasks)
177+
groupResults = executeHostsSequential(group.Hosts, config.Playbook.Tasks, group.Name)
178178
}
179179

180180
allResults = append(allResults, groupResults...)
@@ -286,9 +286,9 @@ func RunPlaybook(playbookPath string) error {
286286
}
287287
} else if len(config.Inventory.Hosts) > 0 {
288288
if parallel {
289-
results = executeHostsParallel(config.Inventory.Hosts, config.Playbook.Tasks)
289+
results = executeHostsParallel(config.Inventory.Hosts, config.Playbook.Tasks, "")
290290
} else {
291-
results = executeHostsSequential(config.Inventory.Hosts, config.Playbook.Tasks)
291+
results = executeHostsSequential(config.Inventory.Hosts, config.Playbook.Tasks, "")
292292
}
293293
} else {
294294
return fmt.Errorf("no hosts or groups defined in inventory")

playbook_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ func TestExecuteHostsSequential(t *testing.T) {
198198
{Name: "Task1", Command: "echo test"},
199199
}
200200

201-
results := executeHostsSequential(hosts, tasks)
201+
results := executeHostsSequential(hosts, tasks, "")
202202

203203
if len(results) != 2 {
204204
t.Errorf("Expected 2 results, got %d", len(results))
@@ -229,7 +229,7 @@ func TestExecuteHostsParallel(t *testing.T) {
229229
{Name: "Task2", Command: "echo test2"},
230230
}
231231

232-
results := executeHostsParallel(hosts, tasks)
232+
results := executeHostsParallel(hosts, tasks, "")
233233

234234
if len(results) != 3 {
235235
t.Errorf("Expected 3 results, got %d", len(results))
@@ -261,7 +261,7 @@ func TestExecuteOnHost_DryRun(t *testing.T) {
261261
{Name: "Task2", Command: "echo world"},
262262
}
263263

264-
result := executeOnHost(host, tasks, false)
264+
result := executeOnHost(host, tasks, false, "")
265265

266266
if !result.Success {
267267
t.Errorf("executeOnHost should succeed in dry-run, got error: %v", result.Error)

ssh.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"golang.org/x/crypto/ssh/knownhosts"
1515
)
1616

17-
func NewExecutor(host Host) (*Executor, error) {
17+
func NewExecutor(host Host, groupName string) (*Executor, error) {
1818
if execOptions.Verbose {
1919
log.Printf("[VERBOSE] Connecting to host: %s", host.Name)
2020
}
@@ -141,6 +141,7 @@ func NewExecutor(host Host) (*Executor, error) {
141141
variables: vars,
142142
registers: make(map[string]string),
143143
completedTasks: make(map[string]bool),
144+
groupName: groupName,
144145
outputWriter: os.Stdout,
145146
startTime: time.Now(),
146147
}, nil
@@ -161,6 +162,7 @@ func NewExecutor(host Host) (*Executor, error) {
161162
variables: host.Vars,
162163
registers: make(map[string]string),
163164
completedTasks: make(map[string]bool),
165+
groupName: groupName,
164166
outputWriter: os.Stdout,
165167
startTime: time.Now(),
166168
}, nil

ssh_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func TestNewExecutor_DryRun(t *testing.T) {
6969
Port: 22,
7070
}
7171

72-
executor, err := NewExecutor(host)
72+
executor, err := NewExecutor(host, "")
7373
if err != nil {
7474
t.Fatalf("NewExecutor failed in dry-run: %v", err)
7575
}
@@ -116,7 +116,7 @@ func TestNewExecutor_NoAddress(t *testing.T) {
116116
// No address or hostname
117117
}
118118

119-
_, err := NewExecutor(host)
119+
_, err := NewExecutor(host, "")
120120
if err == nil {
121121
t.Error("NewExecutor should fail with no address")
122122
}
@@ -144,7 +144,7 @@ func TestNewExecutor_NoAuthMethod(t *testing.T) {
144144
// No password, key file, or agent
145145
}
146146

147-
_, err := NewExecutor(host)
147+
_, err := NewExecutor(host, "")
148148
if err == nil {
149149
t.Error("NewExecutor should fail with no auth method")
150150
}
@@ -164,7 +164,7 @@ func TestNewExecutor_WithPassword(t *testing.T) {
164164
Password: "testpass",
165165
}
166166

167-
executor, err := NewExecutor(host)
167+
executor, err := NewExecutor(host, "")
168168
if err != nil {
169169
t.Fatalf("NewExecutor with password failed: %v", err)
170170
}
@@ -198,7 +198,7 @@ func TestNewExecutor_UseAgentEnabled(t *testing.T) {
198198
}
199199

200200
// This should fail because use_agent is true but no agent is available
201-
_, err := NewExecutor(host)
201+
_, err := NewExecutor(host, "")
202202
if err == nil {
203203
t.Error("NewExecutor should fail when use_agent is true but no agent available")
204204
}
@@ -220,7 +220,7 @@ func TestNewExecutor_HostnameInsteadOfAddress(t *testing.T) {
220220
Password: "testpass",
221221
}
222222

223-
executor, err := NewExecutor(host)
223+
executor, err := NewExecutor(host, "")
224224
if err != nil {
225225
t.Fatalf("NewExecutor with hostname failed: %v", err)
226226
}

types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ type Task struct {
108108
Sudo bool `yaml:"sudo,omitempty"`
109109
When string `yaml:"when,omitempty"`
110110
Register string `yaml:"register,omitempty"`
111+
OnlyGroups []string `yaml:"only_groups,omitempty"`
112+
SkipGroups []string `yaml:"skip_groups,omitempty"`
111113
LocalAction string `yaml:"local_action,omitempty"`
112114
DelegateTo string `yaml:"delegate_to,omitempty"`
113115
RunOnce bool `yaml:"run_once,omitempty"`
@@ -134,6 +136,7 @@ type Executor struct {
134136
variables map[string]interface{}
135137
registers map[string]string
136138
completedTasks map[string]bool
139+
groupName string
137140
mu sync.Mutex
138141
outputWriter io.Writer
139142
startTime time.Time

0 commit comments

Comments
 (0)