Skip to content

Commit

Permalink
Add --wait, -w flag to run-task (#2359) (#2611)
Browse files Browse the repository at this point in the history
When the wait flag is supplied to run-task, the cli will
not exit until the task enters a terminal state; SUCCEEDED or FAILED.

The cli exits 0 when the task succeeds and exits 1 when the task fails.

This improves the scriptability of tasks. They can be run
serially in a script.

Cherry-pick of 8671a5b (#1861) to v8
Addresses Issue #1104, #2238

Authored-by: Zach Robinson <zrobinson@pivotal.io>

Co-authored-by: Stephan Merker <merker.stephan@gmail.com>
  • Loading branch information
ccjaimes and stephanme authored Oct 13, 2023
1 parent 3ceccf9 commit f92a174
Show file tree
Hide file tree
Showing 12 changed files with 459 additions and 1 deletion.
7 changes: 7 additions & 0 deletions actor/actionerror/task_failed_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package actionerror

type TaskFailedError struct{}

func (TaskFailedError) Error() string {
return "Task failed to complete successfully"
}
1 change: 1 addition & 0 deletions actor/v7action/cloud_controller_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ type CloudControllerClient interface {
GetAppFeature(appGUID string, featureName string) (resources.ApplicationFeature, ccv3.Warnings, error)
GetStacks(query ...ccv3.Query) ([]resources.Stack, ccv3.Warnings, error)
GetStagingSecurityGroups(spaceGUID string, queries ...ccv3.Query) ([]resources.SecurityGroup, ccv3.Warnings, error)
GetTask(guid string) (resources.Task, ccv3.Warnings, error)
GetUser(userGUID string) (resources.User, ccv3.Warnings, error)
GetUsers(query ...ccv3.Query) ([]resources.User, ccv3.Warnings, error)
MakeRequestSendReceiveRaw(Method string, URL string, headers http.Header, requestBody []byte) ([]byte, *http.Response, error)
Expand Down
32 changes: 32 additions & 0 deletions actor/v7action/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package v7action

import (
"strconv"
"time"

"sort"

"code.cloudfoundry.org/cli/actor/actionerror"
"code.cloudfoundry.org/cli/api/cloudcontroller/ccerror"
"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3"
"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant"
"code.cloudfoundry.org/cli/resources"
log "github.com/sirupsen/logrus"
)

// Run resources.Task runs the provided command in the application environment associated
Expand Down Expand Up @@ -67,3 +70,32 @@ func (actor Actor) TerminateTask(taskGUID string) (resources.Task, Warnings, err
task, warnings, err := actor.CloudControllerClient.UpdateTaskCancel(taskGUID)
return resources.Task(task), Warnings(warnings), err
}

func (actor Actor) PollTask(task resources.Task) (resources.Task, Warnings, error) {
var allWarnings Warnings

for task.State != constant.TaskSucceeded && task.State != constant.TaskFailed {

time.Sleep(actor.Config.PollingInterval())

ccTask, warnings, err := actor.CloudControllerClient.GetTask(task.GUID)
log.WithFields(log.Fields{
"task_guid": task.GUID,
"state": task.State,
}).Debug("polling task state")

allWarnings = append(allWarnings, warnings...)

if err != nil {
return resources.Task{}, allWarnings, err
}

task = resources.Task(ccTask)
}

if task.State == constant.TaskFailed {
return task, allWarnings, actionerror.TaskFailedError{}
}

return task, allWarnings, nil
}
63 changes: 62 additions & 1 deletion actor/v7action/task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ var _ = Describe("Task Actions", func() {
var (
actor *Actor
fakeCloudControllerClient *v7actionfakes.FakeCloudControllerClient
fakeConfig *v7actionfakes.FakeConfig
)

BeforeEach(func() {
fakeCloudControllerClient = new(v7actionfakes.FakeCloudControllerClient)
actor = NewActor(fakeCloudControllerClient, nil, nil, nil, nil, nil)
fakeConfig = new(v7actionfakes.FakeConfig)
actor = NewActor(fakeCloudControllerClient, fakeConfig, nil, nil, nil, nil)
})

Describe("RunTask", func() {
Expand Down Expand Up @@ -305,4 +307,63 @@ var _ = Describe("Task Actions", func() {
})
})
})

Describe("PollTask", func() {

It("polls for SUCCEDED state", func() {
firstTaskResponse := resources.Task{State: constant.TaskRunning}
secondTaskResponse := resources.Task{State: constant.TaskSucceeded}

fakeCloudControllerClient.GetTaskReturnsOnCall(0, firstTaskResponse, nil, nil)
fakeCloudControllerClient.GetTaskReturnsOnCall(1, secondTaskResponse, nil, nil)

task, _, _ := actor.PollTask(resources.Task{})

Expect(task.State).To(Equal(constant.TaskSucceeded))
})

It("polls for FAILED state", func() {
firstTaskResponse := resources.Task{State: constant.TaskRunning}
secondTaskResponse := resources.Task{State: constant.TaskFailed}

fakeCloudControllerClient.GetTaskReturnsOnCall(0, firstTaskResponse, nil, nil)
fakeCloudControllerClient.GetTaskReturnsOnCall(1, secondTaskResponse, nil, nil)

task, _, _ := actor.PollTask(resources.Task{})

Expect(task.State).To(Equal(constant.TaskFailed))
})

It("aggregates warnings from all requests made while polling", func() {
firstTaskResponse := resources.Task{State: constant.TaskRunning}
secondTaskResponse := resources.Task{State: constant.TaskSucceeded}

fakeCloudControllerClient.GetTaskReturnsOnCall(0, firstTaskResponse, ccv3.Warnings{"warning-1"}, nil)
fakeCloudControllerClient.GetTaskReturnsOnCall(1, secondTaskResponse, ccv3.Warnings{"warning-2"}, nil)

_, warnings, _ := actor.PollTask(resources.Task{})

Expect(warnings).To(ConsistOf("warning-1", "warning-2"))
})

It("handles errors from requests to cc", func() {
firstTaskResponse := resources.Task{State: constant.TaskSucceeded}

fakeCloudControllerClient.GetTaskReturnsOnCall(0, firstTaskResponse, nil, errors.New("request-error"))

_, _, err := actor.PollTask(resources.Task{})

Expect(err).To(MatchError("request-error"))
})

It("returns an error if the task failed", func() {
firstTaskResponse := resources.Task{State: constant.TaskFailed}

fakeCloudControllerClient.GetTaskReturnsOnCall(0, firstTaskResponse, nil, nil)

_, _, err := actor.PollTask(resources.Task{})

Expect(err).To(MatchError("Task failed to complete successfully"))
})
})
})
83 changes: 83 additions & 0 deletions actor/v7action/v7actionfakes/fake_cloud_controller_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions api/cloudcontroller/ccv3/internal/api_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const (
GetSpaceStagingSecurityGroupsRequest = "GetSpaceStagingSecurityGroups"
GetSSHEnabled = "GetSSHEnabled"
GetStacksRequest = "GetStacks"
GetTaskRequest = "GetTask"
GetUserRequest = "GetUser"
GetUsersRequest = "GetUsers"
MapRouteRequest = "MapRoute"
Expand Down Expand Up @@ -343,6 +344,7 @@ var APIRoutes = map[string]Route{
DeleteSpaceQuotaFromSpaceRequest: {Path: "/v3/space_quotas/:quota_guid/relationships/spaces/:space_guid", Method: http.MethodDelete},
GetStacksRequest: {Path: "/v3/stacks", Method: http.MethodGet},
PatchStackRequest: {Path: "/v3/stacks/:stack_guid", Method: http.MethodPatch},
GetTaskRequest: {Path: "/v3/tasks/:task_guid", Method: http.MethodGet},
PutTaskCancelRequest: {Path: "/v3/tasks/:task_guid/cancel", Method: http.MethodPut},
GetUsersRequest: {Path: "/v3/users", Method: http.MethodGet},
GetUserRequest: {Path: "/v3/users/:user_guid", Method: http.MethodGet},
Expand Down
14 changes: 14 additions & 0 deletions api/cloudcontroller/ccv3/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,17 @@ func (client *Client) UpdateTaskCancel(taskGUID string) (resources.Task, Warning

return responseBody, warnings, err
}

func (client *Client) GetTask(guid string) (resources.Task, Warnings, error) {
var responseBody resources.Task

_, warnings, err := client.MakeRequest(RequestParams{
RequestName: internal.GetTaskRequest,
URIParams: internal.Params{
"task_guid": guid,
},
ResponseBody: &responseBody,
})

return responseBody, warnings, err
}
85 changes: 85 additions & 0 deletions api/cloudcontroller/ccv3/task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,4 +443,89 @@ var _ = Describe("Task", func() {
})
})
})

Describe("GetTask", func() {
When("the task exists", func() {
BeforeEach(func() {
response := `{
"guid": "the-task-guid",
"sequence_id": 1,
"name": "task-1",
"command": "some-command",
"state": "SUCCEEDED",
"created_at": "2016-11-07T05:59:01Z"
}`

server.AppendHandlers(
CombineHandlers(
VerifyRequest(http.MethodGet, "/v3/tasks/the-task-guid"),
RespondWith(http.StatusOK, response, http.Header{"X-Cf-Warnings": {"warning"}}),
),
)
})

It("returns the task and all warnings", func() {
task, warnings, err := client.GetTask("the-task-guid")
Expect(err).ToNot(HaveOccurred())

expectedTask := resources.Task{
GUID: "the-task-guid",
SequenceID: 1,
Name: "task-1",
State: constant.TaskSucceeded,
CreatedAt: "2016-11-07T05:59:01Z",
Command: "some-command",
}

Expect(task).To(Equal(expectedTask))
Expect(warnings).To(ConsistOf("warning"))
})
})

When("the cloud controller returns errors and warnings", func() {
BeforeEach(func() {
response := `{
"errors": [
{
"code": 10008,
"detail": "The request is semantically invalid: command presence",
"title": "CF-UnprocessableEntity"
},
{
"code": 10010,
"detail": "Task not found",
"title": "CF-ResourceNotFound"
}
]
}`
server.AppendHandlers(
CombineHandlers(
VerifyRequest(http.MethodGet, "/v3/tasks/the-task-guid"),
RespondWith(http.StatusTeapot, response, http.Header{"X-Cf-Warnings": {"warning"}}),
),
)
})

It("returns the errors and all warnings", func() {
_, warnings, err := client.GetTask("the-task-guid")

Expect(err).To(MatchError(ccerror.MultiError{
ResponseCode: http.StatusTeapot,
Errors: []ccerror.V3Error{
{
Code: 10008,
Detail: "The request is semantically invalid: command presence",
Title: "CF-UnprocessableEntity",
},
{
Code: 10010,
Detail: "Task not found",
Title: "CF-ResourceNotFound",
},
},
}))
Expect(warnings).To(ConsistOf("warning"))
})
})
})
})
1 change: 1 addition & 0 deletions command/v7/actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ type Actor interface {
PollPackage(pkg resources.Package) (resources.Package, v7action.Warnings, error)
PollStart(app resources.Application, noWait bool, handleProcessStats func(string)) (v7action.Warnings, error)
PollStartForRolling(app resources.Application, deploymentGUID string, noWait bool, handleProcessStats func(string)) (v7action.Warnings, error)
PollTask(task resources.Task) (resources.Task, v7action.Warnings, error)
PollUploadBuildpackJob(jobURL ccv3.JobURL) (v7action.Warnings, error)
PrepareBuildpackBits(inputPath string, tmpDirPath string, downloader v7action.Downloader) (string, error)
PurgeServiceInstance(serviceInstanceName, spaceGUID string) (v7action.Warnings, error)
Expand Down
Loading

0 comments on commit f92a174

Please sign in to comment.