Skip to content

Commit

Permalink
Add --wait, -w flag to run-task
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.

Addresses Issue #1104

Authored-by: Zach Robinson <zrobinson@pivotal.io>
  • Loading branch information
zrob authored and a-b committed May 4, 2021
1 parent b76c5c1 commit 8671a5b
Show file tree
Hide file tree
Showing 13 changed files with 461 additions and 3 deletions.
7 changes: 7 additions & 0 deletions actor/actionerror/task_failed_error.go
@@ -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
Expand Up @@ -123,6 +123,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)
MapRoute(routeGUID string, appGUID string) (ccv3.Warnings, error)
Expand Down
32 changes: 32 additions & 0 deletions actor/v7action/task.go
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
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
Expand Up @@ -92,6 +92,7 @@ const (
GetSpaceStagingSecurityGroupsRequest = "GetSpaceStagingSecurityGroups"
GetSSHEnabled = "GetSSHEnabled"
GetStacksRequest = "GetStacks"
GetTaskRequest = "GetTask"
GetUserRequest = "GetUser"
GetUsersRequest = "GetUsers"
MapRouteRequest = "MapRoute"
Expand Down Expand Up @@ -303,6 +304,7 @@ var APIRoutes = []Route{
{Resource: StacksResource, Path: "/", Method: http.MethodGet, Name: GetStacksRequest},
{Resource: StacksResource, Path: "/:stack_guid", Method: http.MethodPatch, Name: PatchStackRequest},
{Resource: TasksResource, Path: "/:task_guid/cancel", Method: http.MethodPut, Name: PutTaskCancelRequest},
{Resource: TasksResource, Path: "/:task_guid", Method: http.MethodGet, Name: GetTaskRequest},
{Resource: UsersResource, Path: "/", Method: http.MethodGet, Name: GetUsersRequest},
{Resource: UsersResource, Path: "/:user_guid", Method: http.MethodGet, Name: GetUserRequest},
{Resource: UsersResource, Path: "/", Method: http.MethodPost, Name: PostUserRequest},
Expand Down
14 changes: 14 additions & 0 deletions api/cloudcontroller/ccv3/task.go
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
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
Expand Up @@ -164,6 +164,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)
PurgeServiceOfferingByNameAndBroker(serviceOfferingName, serviceBrokerName string) (v7action.Warnings, error)
Expand Down

0 comments on commit 8671a5b

Please sign in to comment.