Skip to content

Commit 84b1bac

Browse files
authored
feat: add option to cancel task on wait timeout
Add cancel on timeout option to the task wait command
2 parents 8c7590c + d4f2f5e commit 84b1bac

4 files changed

Lines changed: 138 additions & 35 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/AlecAivazis/survey/v2 v2.3.7
77
github.com/MakeNowJust/heredoc/v2 v2.0.1
88
github.com/OctopusDeploy/go-octodiff v1.0.0
9-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.70.1
9+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.72.0
1010
github.com/bmatcuk/doublestar/v4 v4.4.0
1111
github.com/briandowns/spinner v1.19.0
1212
github.com/google/uuid v1.3.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n
4646
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
4747
github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4VDhOQ0Ven0=
4848
github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU=
49-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.70.1 h1:aW1K0utvdDPSRElTnvp40Xs9oPmJR6IyPAT6PBvPSJs=
50-
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.70.1/go.mod h1:ZCOnCz9ae/uuOk7AIQ9NzjnzFbuN8Q7H3oj2Eq4QSgQ=
49+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.72.0 h1:q7bAzC/gdTvgeVxypHyTSlBYoH0ejbjE3VIyDfJ2lzw=
50+
github.com/OctopusDeploy/go-octopusdeploy/v2 v2.72.0/go.mod h1:ZCOnCz9ae/uuOk7AIQ9NzjnzFbuN8Q7H3oj2Eq4QSgQ=
5151
github.com/bmatcuk/doublestar/v4 v4.4.0 h1:LmAwNwhjEbYtyVLzjcP/XeVw4nhuScHGkF/XWXnvIic=
5252
github.com/bmatcuk/doublestar/v4 v4.4.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
5353
github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E=

pkg/cmd/task/wait/wait.go

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,49 @@ import (
1616
)
1717

1818
const (
19-
FlagTimeout = "timeout"
20-
FlagProgress = "progress"
21-
DefaultTimeout = 600
19+
FlagTimeout = "timeout"
20+
FlagPollInterval = "poll-interval"
21+
FlagCancelOnTimeout = "cancel-on-timeout"
22+
FlagProgress = "progress"
23+
DefaultTimeout = 600
24+
DefaultPollInterval = 10
2225
)
2326

2427
type WaitOptions struct {
2528
*cmd.Dependencies
26-
TaskIDs []string
27-
GetServerTasksCallback ServerTasksCallback
28-
GetTaskDetailsCallback TaskDetailsCallback
29-
Timeout int
30-
ShowProgress bool
29+
TaskIDs []string
30+
GetServerTasksCallback ServerTasksCallback
31+
GetTaskDetailsCallback TaskDetailsCallback
32+
CancelServerTasksCallback ServerTasksCallback
33+
Timeout int
34+
PollInterval int
35+
CancelOnTimeout bool
36+
ShowProgress bool
3137
}
3238

3339
type ServerTasksCallback func([]string) ([]*tasks.Task, error)
3440
type TaskDetailsCallback func(string) (*tasks.TaskDetailsResource, error)
3541

36-
func NewWaitOps(dependencies *cmd.Dependencies, taskIDs []string) *WaitOptions {
42+
func NewWaitOps(dependencies *cmd.Dependencies, taskIDs []string, timeout int, pollInterval int, cancelOnTimeout bool, showProgress bool) *WaitOptions {
3743
return &WaitOptions{
38-
Dependencies: dependencies,
39-
TaskIDs: taskIDs,
40-
GetServerTasksCallback: GetServerTasksCallback(dependencies.Client),
41-
GetTaskDetailsCallback: GetTaskDetailsCallback(dependencies.Client),
42-
Timeout: DefaultTimeout,
43-
ShowProgress: false,
44+
Dependencies: dependencies,
45+
TaskIDs: taskIDs,
46+
GetServerTasksCallback: getServerTasksCallback(dependencies.Client),
47+
GetTaskDetailsCallback: getTaskDetailsCallback(dependencies.Client),
48+
CancelServerTasksCallback: cancelServerTasksCallback(dependencies.Client),
49+
Timeout: timeout,
50+
PollInterval: pollInterval,
51+
CancelOnTimeout: cancelOnTimeout,
52+
ShowProgress: showProgress,
4453
}
4554
}
4655

4756
func NewCmdWait(f factory.Factory) *cobra.Command {
4857
var timeout int
58+
var pollInterval int
59+
var cancelOnTimeout bool
4960
var showProgress bool
61+
5062
cmd := &cobra.Command{
5163
Use: "wait [TaskIDs]",
5264
Short: "Wait for task(s) to finish",
@@ -57,18 +69,17 @@ func NewCmdWait(f factory.Factory) *cobra.Command {
5769
copy(taskIDs, args)
5870

5971
taskIDs = append(taskIDs, util.ReadValuesFromPipe()...)
60-
6172
dependencies := cmd.NewDependencies(f, c)
62-
opts := NewWaitOps(dependencies, taskIDs)
63-
opts.Timeout = timeout
64-
opts.ShowProgress = showProgress
73+
opts := NewWaitOps(dependencies, taskIDs, timeout, pollInterval, cancelOnTimeout, showProgress)
6574

6675
return WaitRun(opts)
6776
},
6877
}
6978

7079
flags := cmd.Flags()
71-
flags.IntVar(&timeout, FlagTimeout, DefaultTimeout, "Duration to wait (in seconds) before stopping execution")
80+
flags.IntVar(&timeout, FlagTimeout, DefaultTimeout, "Time in seconds to wait for the tasks to complete")
81+
flags.IntVar(&pollInterval, FlagPollInterval, DefaultPollInterval, "Polling interval in seconds to check task status during wait")
82+
flags.BoolVar(&cancelOnTimeout, FlagCancelOnTimeout, false, "Cancel the tasks if the wait timeout is reached")
7283
flags.BoolVar(&showProgress, FlagProgress, false, "Show detailed progress of the tasks")
7384

7485
return cmd
@@ -83,20 +94,20 @@ func WaitRun(opts *WaitOptions) error {
8394
return fmt.Errorf("--progress flag is only supported when waiting for a single task")
8495
}
8596

86-
tasks, err := opts.GetServerTasksCallback(opts.TaskIDs)
97+
serverTasks, err := opts.GetServerTasksCallback(opts.TaskIDs)
8798
if err != nil {
8899
return err
89100
}
90101

91-
if len(tasks) == 0 {
102+
if len(serverTasks) == 0 {
92103
return fmt.Errorf("no server tasks found")
93104
}
94105

95106
pendingTaskIDs := make([]string, 0)
96107
failedTaskIDs := make([]string, 0)
97108
formatter := NewTaskOutputFormatter(opts.Out)
98109

99-
for _, t := range tasks {
110+
for _, t := range serverTasks {
100111
if t.IsCompleted == nil || !*t.IsCompleted {
101112
pendingTaskIDs = append(pendingTaskIDs, t.ID)
102113
}
@@ -109,7 +120,7 @@ func WaitRun(opts *WaitOptions) error {
109120

110121
if len(pendingTaskIDs) == 0 {
111122
if len(failedTaskIDs) != 0 {
112-
return fmt.Errorf("One or more deployment tasks failed: %s", strings.Join(failedTaskIDs, ", "))
123+
return fmt.Errorf("one or more deployment tasks failed: %s", strings.Join(failedTaskIDs, ", "))
113124
}
114125
return nil
115126
}
@@ -120,13 +131,13 @@ func WaitRun(opts *WaitOptions) error {
120131

121132
go func() {
122133
for len(pendingTaskIDs) != 0 {
123-
time.Sleep(5 * time.Second)
124-
tasks, err = opts.GetServerTasksCallback(pendingTaskIDs)
134+
time.Sleep(time.Duration(opts.PollInterval) * time.Second)
135+
serverTasks, err = opts.GetServerTasksCallback(pendingTaskIDs)
125136
if err != nil {
126137
gotError <- err
127138
return
128139
}
129-
for _, t := range tasks {
140+
for _, t := range serverTasks {
130141
if opts.ShowProgress {
131142
details, err := opts.GetTaskDetailsCallback(t.ID)
132143
if err != nil {
@@ -151,7 +162,7 @@ func WaitRun(opts *WaitOptions) error {
151162
}
152163
}
153164
if len(failedTaskIDs) != 0 {
154-
gotError <- fmt.Errorf("One or more deployment tasks failed: %s", strings.Join(failedTaskIDs, ", "))
165+
gotError <- fmt.Errorf("one or more deployment tasks failed: %s", strings.Join(failedTaskIDs, ", "))
155166
return
156167
}
157168
done <- true
@@ -163,11 +174,18 @@ func WaitRun(opts *WaitOptions) error {
163174
case err := <-gotError:
164175
return err
165176
case <-time.After(time.Duration(opts.Timeout) * time.Second):
177+
if opts.CancelOnTimeout {
178+
fmt.Fprintf(opts.Dependencies.Out, "Cancelling remaining tasks: %s\n", strings.Join(pendingTaskIDs, ", "))
179+
_, err := opts.CancelServerTasksCallback(pendingTaskIDs)
180+
if err != nil {
181+
return err
182+
}
183+
}
166184
return fmt.Errorf("timeout while waiting for pending tasks")
167185
}
168186
}
169187

170-
func GetServerTasksCallback(octopus *client.Client) ServerTasksCallback {
188+
func getServerTasksCallback(octopus *client.Client) ServerTasksCallback {
171189
return func(taskIDs []string) ([]*tasks.Task, error) {
172190
query := tasks.TasksQuery{
173191
IDs: taskIDs,
@@ -187,12 +205,26 @@ func GetServerTasksCallback(octopus *client.Client) ServerTasksCallback {
187205
}
188206
}
189207

190-
func GetTaskDetailsCallback(octopus *client.Client) TaskDetailsCallback {
208+
func getTaskDetailsCallback(octopus *client.Client) TaskDetailsCallback {
191209
return func(taskID string) (*tasks.TaskDetailsResource, error) {
192210
return tasks.GetDetails(octopus, octopus.GetSpaceID(), taskID)
193211
}
194212
}
195213

214+
func cancelServerTasksCallback(octopus *client.Client) ServerTasksCallback {
215+
return func(taskIDs []string) ([]*tasks.Task, error) {
216+
serverTasks := make([]*tasks.Task, len(taskIDs))
217+
for _, taskID := range taskIDs {
218+
serverTask, err := tasks.Cancel(octopus, octopus.GetSpaceID(), taskID)
219+
if err != nil {
220+
return nil, err
221+
}
222+
serverTasks = append(serverTasks, serverTask)
223+
}
224+
return serverTasks, nil
225+
}
226+
}
227+
196228
func removeTaskID(taskIDs []string, taskID string) []string {
197229
for i, p := range taskIDs {
198230
if p == taskID {

pkg/cmd/task/wait/wait_test.go

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net/url"
77
"testing"
8+
"time"
89

910
"github.com/MakeNowJust/heredoc/v2"
1011
"github.com/OctopusDeploy/cli/pkg/cmd"
@@ -76,6 +77,7 @@ func TestWait(t *testing.T) {
7677
GetServerTasksCallback: getServerTaskCallback,
7778
GetTaskDetailsCallback: nil,
7879
Timeout: taskWaitCreate.DefaultTimeout,
80+
PollInterval: 1,
7981
ShowProgress: false,
8082
}
8183

@@ -122,11 +124,12 @@ func TestWait_FailedTask(t *testing.T) {
122124
GetServerTasksCallback: getServerTaskCallback,
123125
GetTaskDetailsCallback: nil,
124126
Timeout: taskWaitCreate.DefaultTimeout,
127+
PollInterval: 1,
125128
ShowProgress: false,
126129
}
127130

128131
err := taskWaitCreate.WaitRun(opts)
129-
assert.EqualError(t, err, "One or more deployment tasks failed: TaskID1")
132+
assert.EqualError(t, err, "one or more deployment tasks failed: TaskID1")
130133
expectedOutput := heredoc.Doc(`
131134
TaskID1: Deploy Bar 1 release 0.0.2 to Foo: Failed
132135
`)
@@ -177,15 +180,83 @@ func TestWait_FailedPendingTask(t *testing.T) {
177180
GetServerTasksCallback: getServerTaskCallback,
178181
GetTaskDetailsCallback: nil,
179182
Timeout: taskWaitCreate.DefaultTimeout,
183+
PollInterval: 1,
180184
ShowProgress: false,
181185
}
182186

183187
err := taskWaitCreate.WaitRun(opts)
184-
assert.EqualError(t, err, "One or more deployment tasks failed: TaskID1")
188+
assert.EqualError(t, err, "one or more deployment tasks failed: TaskID1")
185189
assert.Equal(t, 2, timesCalled)
186190
expectedOutput := heredoc.Doc(`
187191
TaskID1: Deploy Bar 1 release 0.0.2 to Foo: Executing
188192
TaskID1: Deploy Bar 1 release 0.0.2 to Foo: Failed
189193
`)
190194
assert.Equal(t, expectedOutput, out.String())
191195
}
196+
197+
func TestWait__CancelOnTimeout(t *testing.T) {
198+
out := bytes.Buffer{}
199+
defaultTaskIDs := []string{
200+
"TaskID1",
201+
"TaskID2",
202+
}
203+
204+
taskList := []*tasks.Task{
205+
tasks.NewTask(),
206+
tasks.NewTask(),
207+
}
208+
209+
// bool vars as bool constants can't be used as pointers for IsCompleted
210+
boolFalse := false
211+
boolTrue := true
212+
213+
taskList[0].ID = defaultTaskIDs[0]
214+
taskList[0].IsCompleted = &boolFalse
215+
taskList[0].FinishedSuccessfully = &boolFalse
216+
taskList[0].Description = "Deploy Bar 1 release 0.0.2 to Foo"
217+
taskList[0].State = "Executing"
218+
219+
taskList[1].ID = defaultTaskIDs[1]
220+
taskList[1].IsCompleted = &boolTrue
221+
taskList[1].FinishedSuccessfully = &boolTrue
222+
taskList[1].Description = "Deploy Bar 2 release 0.0.2 to Foo"
223+
taskList[1].State = "Success"
224+
225+
getServerTaskCallback := func(taskIDs []string) ([]*tasks.Task, error) {
226+
time.Sleep(1 * time.Second)
227+
assert.Len(t, taskIDs, 2)
228+
assert.Equal(t, defaultTaskIDs[0], taskIDs[0])
229+
assert.Equal(t, defaultTaskIDs[1], taskIDs[1])
230+
return taskList, nil
231+
}
232+
233+
cancelCalled := false
234+
cancelServerTaskCallback := func(taskIDs []string) ([]*tasks.Task, error) {
235+
cancelCalled = true
236+
return []*tasks.Task{}, nil
237+
}
238+
239+
opts := &taskWaitCreate.WaitOptions{
240+
Dependencies: &cmd.Dependencies{
241+
Out: &out,
242+
},
243+
TaskIDs: defaultTaskIDs,
244+
GetServerTasksCallback: getServerTaskCallback,
245+
GetTaskDetailsCallback: nil,
246+
CancelServerTasksCallback: cancelServerTaskCallback,
247+
Timeout: 1,
248+
PollInterval: 1,
249+
CancelOnTimeout: true,
250+
ShowProgress: false,
251+
}
252+
253+
err := taskWaitCreate.WaitRun(opts)
254+
assert.True(t, cancelCalled)
255+
assert.EqualError(t, err, "timeout while waiting for pending tasks")
256+
expectedOutput := heredoc.Doc(`
257+
TaskID1: Deploy Bar 1 release 0.0.2 to Foo: Executing
258+
TaskID2: Deploy Bar 2 release 0.0.2 to Foo: Success
259+
Cancelling remaining tasks: TaskID1
260+
`)
261+
assert.Equal(t, expectedOutput, out.String())
262+
}

0 commit comments

Comments
 (0)