Skip to content

Commit 6e0259e

Browse files
feat: allow include and exclude by target tag for runbooks
1 parent a1e4760 commit 6e0259e

3 files changed

Lines changed: 184 additions & 120 deletions

File tree

pkg/cmd/runbook/run/run.go

Lines changed: 119 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7-
"github.com/OctopusDeploy/cli/pkg/cmd/runbook/shared"
8-
"github.com/OctopusDeploy/cli/pkg/packages"
9-
"golang.org/x/exp/maps"
107
"io"
118
"math"
129
"sort"
1310
"strings"
1411
"time"
1512

13+
"github.com/OctopusDeploy/cli/pkg/cmd/runbook/shared"
14+
"github.com/OctopusDeploy/cli/pkg/packages"
15+
"golang.org/x/exp/maps"
16+
1617
"github.com/OctopusDeploy/cli/pkg/apiclient"
1718

1819
"github.com/AlecAivazis/survey/v2"
@@ -84,6 +85,9 @@ const (
8485
FlagAliasExcludeTarget = "exclude-target"
8586
FlagAliasExcludeMachines = "excludeMachines" // octo wants a comma separated list. We prefer specifying --exclude-target multiple times, but CSV also works because pflag does it for free
8687

88+
FlagSpecificTargetTag = "specific-target-tag"
89+
FlagExcludedTargetTag = "excluded-target-tag"
90+
8791
FlagVariable = "variable"
8892

8993
FlagGitRef = "git-ref"
@@ -93,48 +97,52 @@ const (
9397
)
9498

9599
type RunFlags struct {
96-
Project *flag.Flag[string]
97-
RunbookName *flag.Flag[string] // the runbook to run
98-
RunbookTags *flag.Flag[[]string]
99-
Environments *flag.Flag[[]string]
100-
Tenants *flag.Flag[[]string]
101-
TenantTags *flag.Flag[[]string]
102-
RunAt *flag.Flag[string]
103-
MaxQueueTime *flag.Flag[string]
104-
Variables *flag.Flag[[]string]
105-
Snapshot *flag.Flag[string]
106-
ExcludedSteps *flag.Flag[[]string]
107-
GuidedFailureMode *flag.Flag[string] // tri-state: true, false, or "use default". Can we model it with an optional bool?
108-
ForcePackageDownload *flag.Flag[bool]
109-
RunTargets *flag.Flag[[]string]
110-
ExcludeTargets *flag.Flag[[]string]
111-
GitRef *flag.Flag[string]
112-
PackageVersion *flag.Flag[string]
113-
PackageVersionSpec *flag.Flag[[]string]
114-
GitResourceRefsSpec *flag.Flag[[]string]
100+
Project *flag.Flag[string]
101+
RunbookName *flag.Flag[string] // the runbook to run
102+
RunbookTags *flag.Flag[[]string]
103+
Environments *flag.Flag[[]string]
104+
Tenants *flag.Flag[[]string]
105+
TenantTags *flag.Flag[[]string]
106+
RunAt *flag.Flag[string]
107+
MaxQueueTime *flag.Flag[string]
108+
Variables *flag.Flag[[]string]
109+
Snapshot *flag.Flag[string]
110+
ExcludedSteps *flag.Flag[[]string]
111+
GuidedFailureMode *flag.Flag[string] // tri-state: true, false, or "use default". Can we model it with an optional bool?
112+
ForcePackageDownload *flag.Flag[bool]
113+
RunTargets *flag.Flag[[]string]
114+
ExcludeTargets *flag.Flag[[]string]
115+
SpecificTargetTagNames *flag.Flag[[]string]
116+
ExcludedTargetTagNames *flag.Flag[[]string]
117+
GitRef *flag.Flag[string]
118+
PackageVersion *flag.Flag[string]
119+
PackageVersionSpec *flag.Flag[[]string]
120+
GitResourceRefsSpec *flag.Flag[[]string]
115121
}
116122

117123
func NewRunFlags() *RunFlags {
118124
return &RunFlags{
119-
Project: flag.New[string](FlagProject, false),
120-
RunbookName: flag.New[string](FlagRunbookName, false),
121-
RunbookTags: flag.New[[]string](FlagRunbookTag, false),
122-
Environments: flag.New[[]string](FlagEnvironment, false),
123-
Tenants: flag.New[[]string](FlagTenant, false),
124-
TenantTags: flag.New[[]string](FlagTenantTag, false),
125-
MaxQueueTime: flag.New[string](FlagRunAtExpiry, false),
126-
RunAt: flag.New[string](FlagRunAt, false),
127-
Variables: flag.New[[]string](FlagVariable, false),
128-
Snapshot: flag.New[string](FlagSnapshot, false),
129-
ExcludedSteps: flag.New[[]string](FlagSkip, false),
130-
GuidedFailureMode: flag.New[string](FlagGuidedFailure, false),
131-
ForcePackageDownload: flag.New[bool](FlagForcePackageDownload, false),
132-
RunTargets: flag.New[[]string](FlagRunTarget, false),
133-
ExcludeTargets: flag.New[[]string](FlagExcludeRunTarget, false),
134-
GitRef: flag.New[string](FlagGitRef, false),
135-
PackageVersion: flag.New[string](FlagPackageVersion, false),
136-
PackageVersionSpec: flag.New[[]string](FlagPackageVersionSpec, false),
137-
GitResourceRefsSpec: flag.New[[]string](FlagGitResourceRefSpec, false),
125+
Project: flag.New[string](FlagProject, false),
126+
RunbookName: flag.New[string](FlagRunbookName, false),
127+
RunbookTags: flag.New[[]string](FlagRunbookTag, false),
128+
Environments: flag.New[[]string](FlagEnvironment, false),
129+
Tenants: flag.New[[]string](FlagTenant, false),
130+
TenantTags: flag.New[[]string](FlagTenantTag, false),
131+
MaxQueueTime: flag.New[string](FlagRunAtExpiry, false),
132+
RunAt: flag.New[string](FlagRunAt, false),
133+
Variables: flag.New[[]string](FlagVariable, false),
134+
Snapshot: flag.New[string](FlagSnapshot, false),
135+
ExcludedSteps: flag.New[[]string](FlagSkip, false),
136+
GuidedFailureMode: flag.New[string](FlagGuidedFailure, false),
137+
ForcePackageDownload: flag.New[bool](FlagForcePackageDownload, false),
138+
RunTargets: flag.New[[]string](FlagRunTarget, false),
139+
ExcludeTargets: flag.New[[]string](FlagExcludeRunTarget, false),
140+
SpecificTargetTagNames: flag.New[[]string](FlagSpecificTargetTag, false),
141+
ExcludedTargetTagNames: flag.New[[]string](FlagExcludedTargetTag, false),
142+
GitRef: flag.New[string](FlagGitRef, false),
143+
PackageVersion: flag.New[string](FlagPackageVersion, false),
144+
PackageVersionSpec: flag.New[[]string](FlagPackageVersionSpec, false),
145+
GitResourceRefsSpec: flag.New[[]string](FlagGitResourceRefSpec, false),
138146
}
139147
}
140148

@@ -173,6 +181,8 @@ func NewCmdRun(f factory.Factory) *cobra.Command {
173181
flags.BoolVarP(&runFlags.ForcePackageDownload.Value, runFlags.ForcePackageDownload.Name, "", false, "Force re-download of packages")
174182
flags.StringArrayVarP(&runFlags.RunTargets.Value, runFlags.RunTargets.Name, "", nil, "Run on this target (can be specified multiple times)")
175183
flags.StringArrayVarP(&runFlags.ExcludeTargets.Value, runFlags.ExcludeTargets.Name, "", nil, "Run on targets except for this (can be specified multiple times)")
184+
flags.StringArrayVarP(&runFlags.SpecificTargetTagNames.Value, runFlags.SpecificTargetTagNames.Name, "", nil, "Run on targets matching this tag (can be specified multiple times)")
185+
flags.StringArrayVarP(&runFlags.ExcludedTargetTagNames.Value, runFlags.ExcludedTargetTagNames.Name, "", nil, "Run on targets except for those matching this tag (can be specified multiple times)")
176186
flags.StringVarP(&runFlags.GitRef.Value, runFlags.GitRef.Name, "", "", "Git Reference e.g. refs/heads/main. Only relevant for config-as-code projects where runbooks are stored in Git.")
177187
flags.StringVarP(&runFlags.PackageVersion.Value, runFlags.PackageVersion.Name, "", "", "Default version to use for all packages. Only relevant for config-as-code projects where runbooks are stored in Git.")
178188
flags.StringArrayVarP(&runFlags.PackageVersionSpec.Value, runFlags.PackageVersionSpec.Name, "", nil, "Version specification for a specific package.\nFormat as {package}:{version}, {step}:{version} or {package-ref-name}:{packageOrStep}:{version}\nYou may specify this multiple times.\nOnly relevant for config-as-code projects where runbooks are stored in Git.")
@@ -279,19 +289,21 @@ func runbookRun(cmd *cobra.Command, f factory.Factory, flags *RunFlags) error {
279289
func runDbRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octopus *octopusApiClient.Client, project *projects.Project, parsedVariables map[string]string, outputFormat string) error {
280290

281291
commonOptions := &executor.TaskOptionsRunbookRunBase{
282-
ProjectName: project.Name,
283-
RunbookName: flags.RunbookName.Value,
284-
Environments: flags.Environments.Value,
285-
Tenants: flags.Tenants.Value,
286-
TenantTags: flags.TenantTags.Value,
287-
ScheduledStartTime: flags.RunAt.Value,
288-
ScheduledExpiryTime: flags.MaxQueueTime.Value,
289-
ExcludedSteps: flags.ExcludedSteps.Value,
290-
GuidedFailureMode: flags.GuidedFailureMode.Value,
291-
ForcePackageDownload: flags.ForcePackageDownload.Value,
292-
RunTargets: flags.RunTargets.Value,
293-
ExcludeTargets: flags.ExcludeTargets.Value,
294-
Variables: parsedVariables,
292+
ProjectName: project.Name,
293+
RunbookName: flags.RunbookName.Value,
294+
Environments: flags.Environments.Value,
295+
Tenants: flags.Tenants.Value,
296+
TenantTags: flags.TenantTags.Value,
297+
ScheduledStartTime: flags.RunAt.Value,
298+
ScheduledExpiryTime: flags.MaxQueueTime.Value,
299+
ExcludedSteps: flags.ExcludedSteps.Value,
300+
GuidedFailureMode: flags.GuidedFailureMode.Value,
301+
ForcePackageDownload: flags.ForcePackageDownload.Value,
302+
RunTargets: flags.RunTargets.Value,
303+
ExcludeTargets: flags.ExcludeTargets.Value,
304+
SpecificTargetTagNames: flags.SpecificTargetTagNames.Value,
305+
ExcludedTargetTagNames: flags.ExcludedTargetTagNames.Value,
306+
Variables: parsedVariables,
295307
}
296308
options := &executor.TaskOptionsRunbookRun{
297309
Snapshot: flags.Snapshot.Value,
@@ -331,6 +343,8 @@ func runDbRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octopu
331343
resolvedFlags.GuidedFailureMode.Value = options.GuidedFailureMode
332344
resolvedFlags.RunTargets.Value = options.RunTargets
333345
resolvedFlags.ExcludeTargets.Value = options.ExcludeTargets
346+
resolvedFlags.SpecificTargetTagNames.Value = options.SpecificTargetTagNames
347+
resolvedFlags.ExcludedTargetTagNames.Value = options.ExcludedTargetTagNames
334348

335349
didMaskSensitiveVariable := false
336350
automationVariables := make(map[string]string, len(options.Variables))
@@ -362,6 +376,8 @@ func runDbRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octopu
362376
resolvedFlags.ForcePackageDownload,
363377
resolvedFlags.RunTargets,
364378
resolvedFlags.ExcludeTargets,
379+
resolvedFlags.SpecificTargetTagNames,
380+
resolvedFlags.ExcludedTargetTagNames,
365381
resolvedFlags.Variables,
366382
)
367383
cmd.Printf("\nAutomation Command: %s\n", autoCmd)
@@ -406,19 +422,21 @@ func runDbRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octopu
406422
func runGitRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octopus *octopusApiClient.Client, project *projects.Project, parsedVariables map[string]string, outputFormat string) error {
407423

408424
commonOptions := &executor.TaskOptionsRunbookRunBase{
409-
ProjectName: project.Name,
410-
RunbookName: flags.RunbookName.Value,
411-
Environments: flags.Environments.Value,
412-
Tenants: flags.Tenants.Value,
413-
TenantTags: flags.TenantTags.Value,
414-
ScheduledStartTime: flags.RunAt.Value,
415-
ScheduledExpiryTime: flags.MaxQueueTime.Value,
416-
ExcludedSteps: flags.ExcludedSteps.Value,
417-
GuidedFailureMode: flags.GuidedFailureMode.Value,
418-
ForcePackageDownload: flags.ForcePackageDownload.Value,
419-
RunTargets: flags.RunTargets.Value,
420-
ExcludeTargets: flags.ExcludeTargets.Value,
421-
Variables: parsedVariables,
425+
ProjectName: project.Name,
426+
RunbookName: flags.RunbookName.Value,
427+
Environments: flags.Environments.Value,
428+
Tenants: flags.Tenants.Value,
429+
TenantTags: flags.TenantTags.Value,
430+
ScheduledStartTime: flags.RunAt.Value,
431+
ScheduledExpiryTime: flags.MaxQueueTime.Value,
432+
ExcludedSteps: flags.ExcludedSteps.Value,
433+
GuidedFailureMode: flags.GuidedFailureMode.Value,
434+
ForcePackageDownload: flags.ForcePackageDownload.Value,
435+
RunTargets: flags.RunTargets.Value,
436+
ExcludeTargets: flags.ExcludeTargets.Value,
437+
SpecificTargetTagNames: flags.SpecificTargetTagNames.Value,
438+
ExcludedTargetTagNames: flags.ExcludedTargetTagNames.Value,
439+
Variables: parsedVariables,
422440
}
423441
options := &executor.TaskOptionsGitRunbookRun{
424442
GitReference: flags.GitRef.Value,
@@ -461,6 +479,8 @@ func runGitRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octop
461479
resolvedFlags.GuidedFailureMode.Value = options.GuidedFailureMode
462480
resolvedFlags.RunTargets.Value = options.RunTargets
463481
resolvedFlags.ExcludeTargets.Value = options.ExcludeTargets
482+
resolvedFlags.SpecificTargetTagNames.Value = options.SpecificTargetTagNames
483+
resolvedFlags.ExcludedTargetTagNames.Value = options.ExcludedTargetTagNames
464484
resolvedFlags.GitRef.Value = options.GitReference
465485
resolvedFlags.PackageVersion.Value = options.DefaultPackageVersion
466486
resolvedFlags.PackageVersionSpec.Value = options.PackageVersionOverrides
@@ -496,6 +516,8 @@ func runGitRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octop
496516
resolvedFlags.ForcePackageDownload,
497517
resolvedFlags.RunTargets,
498518
resolvedFlags.ExcludeTargets,
519+
resolvedFlags.SpecificTargetTagNames,
520+
resolvedFlags.ExcludedTargetTagNames,
499521
resolvedFlags.Variables,
500522
resolvedFlags.PackageVersion,
501523
resolvedFlags.PackageVersionSpec,
@@ -1266,14 +1288,43 @@ func PrintAdvancedSummary(stdout io.Writer, options *executor.TaskOptionsRunbook
12661288
runTargetsStr = sb.String()
12671289
}
12681290

1291+
targetTagsStr := "All included"
1292+
if len(options.SpecificTargetTagNames) != 0 || len(options.ExcludedTargetTagNames) != 0 {
1293+
sb := strings.Builder{}
1294+
if len(options.SpecificTargetTagNames) > 0 {
1295+
sb.WriteString("Include ")
1296+
for idx, name := range options.SpecificTargetTagNames {
1297+
if idx > 0 {
1298+
sb.WriteString(",")
1299+
}
1300+
sb.WriteString(name)
1301+
}
1302+
}
1303+
if len(options.ExcludedTargetTagNames) > 0 {
1304+
if sb.Len() > 0 {
1305+
sb.WriteString("; ")
1306+
}
1307+
1308+
sb.WriteString("Exclude ")
1309+
for idx, name := range options.ExcludedTargetTagNames {
1310+
if idx > 0 {
1311+
sb.WriteString(",")
1312+
}
1313+
sb.WriteString(name)
1314+
}
1315+
}
1316+
targetTagsStr = sb.String()
1317+
}
1318+
12691319
_, _ = fmt.Fprintf(stdout, output.FormatDoc(heredoc.Doc(`
12701320
bold(Additional Options):
12711321
Run At: cyan(%s)
12721322
Skipped Steps: cyan(%s)
12731323
Guided Failure Mode: cyan(%s)
12741324
Package Download: cyan(%s)
12751325
Run Targets: cyan(%s)
1276-
`)), runAtStr, skipStepsStr, gfmStr, pkgDownloadStr, runTargetsStr)
1326+
Target Tags: cyan(%s)
1327+
`)), runAtStr, skipStepsStr, gfmStr, pkgDownloadStr, runTargetsStr, targetTagsStr)
12771328
}
12781329

12791330
func selectRunbook(octopus *octopusApiClient.Client, ask question.Asker, questionText string, space *spaces.Space, project *projects.Project) (*runbooks.Runbook, error) {
@@ -1340,4 +1391,3 @@ func findGitRunbook(octopus *octopusApiClient.Client, spaceID string, projectID
13401391
}
13411392
return result, err
13421393
}
1343-

pkg/cmd/runbook/run/run_test.go

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ func TestRunbookRun_AutomationMode(t *testing.T) {
274274
assert.Equal(t, "", stdErr.String())
275275
}},
276276

277-
{"release deploy specifying all the args", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
277+
{"runbook run specifying all the args", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
278278
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
279279
defer api.Close()
280280
rootCmd.SetArgs([]string{
@@ -290,6 +290,8 @@ func TestRunbookRun_AutomationMode(t *testing.T) {
290290
"--force-package-download",
291291
"--target", "firstMachine", "--target", "secondMachine",
292292
"--exclude-target", "thirdMachine",
293+
"--specific-target-tag", "Role/RunbookServer", "--specific-target-tag", "Environment/Production",
294+
"--excluded-target-tag", "Role/Database", "--excluded-target-tag", "Maintenance/True",
293295
"--variable", "Approver:John", "--variable", "Signoff:Jane",
294296
"--output-format", "basic",
295297
})
@@ -310,15 +312,17 @@ func TestRunbookRun_AutomationMode(t *testing.T) {
310312
EnvironmentNames: []string{"dev", "test"},
311313
Snapshot: "Snapshot FWKMLUX",
312314
CreateExecutionAbstractCommandV1: deployments.CreateExecutionAbstractCommandV1{
313-
SpaceID: "Spaces-1",
314-
ProjectIDOrName: fireProject.Name,
315-
ForcePackageDownload: true,
316-
SpecificMachineNames: []string{"firstMachine", "secondMachine"},
317-
ExcludedMachineNames: []string{"thirdMachine"},
318-
SkipStepNames: []string{"Install", "Cleanup"},
319-
UseGuidedFailure: &trueVar,
320-
RunAt: "2022-09-10 13:32:03 +10:00",
321-
NoRunAfter: "2022-09-10 13:37:03 +10:00",
315+
SpaceID: "Spaces-1",
316+
ProjectIDOrName: fireProject.Name,
317+
ForcePackageDownload: true,
318+
SpecificMachineNames: []string{"firstMachine", "secondMachine"},
319+
ExcludedMachineNames: []string{"thirdMachine"},
320+
SpecificTargetTagNames: []string{"Role/RunbookServer", "Environment/Production"},
321+
ExcludedTargetTagNames: []string{"Role/Database", "Maintenance/True"},
322+
SkipStepNames: []string{"Install", "Cleanup"},
323+
UseGuidedFailure: &trueVar,
324+
RunAt: "2022-09-10 13:32:03 +10:00",
325+
NoRunAfter: "2022-09-10 13:37:03 +10:00",
322326
Variables: map[string]string{
323327
"Approver": "John",
324328
"Signoff": "Jane",
@@ -623,7 +627,7 @@ func TestGitRunbookRun_AutomationMode(t *testing.T) {
623627
assert.Equal(t, "", stdErr.String())
624628
}},
625629

626-
{"runbook run specifying all the args", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
630+
{"git runbook run specifying all the args", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) {
627631
cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) {
628632
defer api.Close()
629633
rootCmd.SetArgs([]string{
@@ -639,6 +643,8 @@ func TestGitRunbookRun_AutomationMode(t *testing.T) {
639643
"--force-package-download",
640644
"--target", "firstMachine", "--target", "secondMachine",
641645
"--exclude-target", "thirdMachine",
646+
"--specific-target-tag", "Role/GitRunner", "--specific-target-tag", "Version/Latest",
647+
"--excluded-target-tag", "Role/Legacy",
642648
"--variable", "Approver:John", "--variable", "Signoff:Jane",
643649
"--package-version", "1.2.0",
644650
"--package", "APackageStep:1.5.0",
@@ -662,15 +668,17 @@ func TestGitRunbookRun_AutomationMode(t *testing.T) {
662668
EnvironmentNames: []string{"dev", "test"},
663669
GitRef: "main",
664670
CreateExecutionAbstractCommandV1: deployments.CreateExecutionAbstractCommandV1{
665-
SpaceID: "Spaces-1",
666-
ProjectIDOrName: fireProject.Name,
667-
ForcePackageDownload: true,
668-
SpecificMachineNames: []string{"firstMachine", "secondMachine"},
669-
ExcludedMachineNames: []string{"thirdMachine"},
670-
SkipStepNames: []string{"Install", "Cleanup"},
671-
UseGuidedFailure: &trueVar,
672-
RunAt: "2022-09-10 13:32:03 +10:00",
673-
NoRunAfter: "2022-09-10 13:37:03 +10:00",
671+
SpaceID: "Spaces-1",
672+
ProjectIDOrName: fireProject.Name,
673+
ForcePackageDownload: true,
674+
SpecificMachineNames: []string{"firstMachine", "secondMachine"},
675+
ExcludedMachineNames: []string{"thirdMachine"},
676+
SpecificTargetTagNames: []string{"Role/GitRunner", "Version/Latest"},
677+
ExcludedTargetTagNames: []string{"Role/Legacy"},
678+
SkipStepNames: []string{"Install", "Cleanup"},
679+
UseGuidedFailure: &trueVar,
680+
RunAt: "2022-09-10 13:32:03 +10:00",
681+
NoRunAfter: "2022-09-10 13:37:03 +10:00",
674682
Variables: map[string]string{
675683
"Approver": "John",
676684
"Signoff": "Jane",

0 commit comments

Comments
 (0)