Skip to content

Commit 3d28b10

Browse files
authored
Add cf task <app> <id> command (#3315)
1 parent 8bcd84d commit 3d28b10

File tree

12 files changed

+499
-6
lines changed

12 files changed

+499
-6
lines changed

command/common/command_list_v7.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ type commandList struct {
172172
Start v7.StartCommand `command:"start" alias:"st" description:"Start an app"`
173173
Stop v7.StopCommand `command:"stop" alias:"sp" description:"Stop an app"`
174174
Target v7.TargetCommand `command:"target" alias:"t" description:"Set or view the targeted org or space"`
175+
Task v7.TaskCommand `command:"task" description:"Display a task of an app"`
175176
Tasks v7.TasksCommand `command:"tasks" description:"List tasks of an app"`
176177
TerminateTask v7.TerminateTaskCommand `command:"terminate-task" description:"Terminate a running task of an app"`
177178
MoveRoute v7.MoveRouteCommand `command:"move-route" description:"Assign a route to a different space"`

command/common/internal/help_all_display.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ var HelpCategoryList = []HelpCategory{
1515
{"push", "scale", "delete", "rename"},
1616
{"cancel-deployment", "continue-deployment"},
1717
{"start", "stop", "restart", "stage-package", "restage", "restart-app-instance"},
18-
{"run-task", "tasks", "terminate-task"},
18+
{"run-task", "task", "tasks", "terminate-task"},
1919
{"packages", "create-package"},
2020
{"revisions", "rollback"},
2121
{"droplets", "set-droplet", "download-droplet"},

command/flag/arguments.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,3 +408,8 @@ type RemoveNetworkPolicyArgsV7 struct {
408408
SourceApp string `positional-arg-name:"SOURCE_APP" required:"true" description:"The source app"`
409409
DestApp string `positional-arg-name:"DESTINATION_APP" required:"true" description:"The destination app"`
410410
}
411+
412+
type TaskArgs struct {
413+
AppName string `positional-arg-name:"APP_NAME" required:"true" description:"The application name"`
414+
TaskID int `positional-arg-name:"TASK_ID" required:"true" description:"The Task ID for the application"`
415+
}

command/v7/run_task_command.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type RunTaskCommand struct {
1919
Process string `long:"process" description:"Process type to use as a template for command, memory, and disk for the created task."`
2020
Wait bool `long:"wait" short:"w" description:"Wait for the task to complete before exiting"`
2121
usage interface{} `usage:"CF_NAME run-task APP_NAME [--command COMMAND] [-k DISK] [-m MEMORY] [-l LOG_RATE_LIMIT] [--name TASK_NAME] [--process PROCESS_TYPE]\n\nTIP:\n Use 'cf logs' to display the logs of the app and all its tasks. If your task name is unique, grep this command's output for the task name to view task-specific logs.\n\nEXAMPLES:\n CF_NAME run-task my-app --command \"bundle exec rake db:migrate\" --name migrate\n\n CF_NAME run-task my-app --process batch_job\n\n CF_NAME run-task my-app"`
22-
relatedCommands interface{} `related_commands:"logs, tasks, terminate-task"`
22+
relatedCommands interface{} `related_commands:"logs, tasks, task, terminate-task"`
2323
}
2424

2525
func (cmd RunTaskCommand) Execute(args []string) error {

command/v7/task_command.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package v7
2+
3+
import (
4+
"strconv"
5+
6+
"code.cloudfoundry.org/cli/command/flag"
7+
"code.cloudfoundry.org/cli/util/ui"
8+
)
9+
10+
type TaskCommand struct {
11+
BaseCommand
12+
13+
RequiredArgs flag.TaskArgs `positional-args:"yes"`
14+
usage interface{} `usage:"CF_NAME task APP_NAME TASK_ID"`
15+
relatedCommands interface{} `related_commands:"apps, logs, run-task, tasks, terminate-task"`
16+
}
17+
18+
func (cmd TaskCommand) Execute(args []string) error {
19+
err := cmd.SharedActor.CheckTarget(true, true)
20+
if err != nil {
21+
return err
22+
}
23+
24+
space := cmd.Config.TargetedSpace()
25+
26+
user, err := cmd.Actor.GetCurrentUser()
27+
if err != nil {
28+
return err
29+
}
30+
31+
application, warnings, err := cmd.Actor.GetApplicationByNameAndSpace(cmd.RequiredArgs.AppName, space.GUID)
32+
cmd.UI.DisplayWarnings(warnings)
33+
if err != nil {
34+
return err
35+
}
36+
37+
cmd.UI.DisplayTextWithFlavor("Getting task {{.TaskID}} for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", map[string]interface{}{
38+
"TaskID": cmd.RequiredArgs.TaskID,
39+
"AppName": cmd.RequiredArgs.AppName,
40+
"OrgName": cmd.Config.TargetedOrganization().Name,
41+
"SpaceName": space.Name,
42+
"CurrentUser": user.Name,
43+
})
44+
cmd.UI.DisplayNewline()
45+
46+
task, warnings, err := cmd.Actor.GetTaskBySequenceIDAndApplication(cmd.RequiredArgs.TaskID, application.GUID)
47+
cmd.UI.DisplayWarnings(warnings)
48+
if err != nil {
49+
return err
50+
}
51+
52+
if task.Command == "" {
53+
task.Command = "[hidden]"
54+
}
55+
56+
table := [][]string{
57+
{cmd.UI.TranslateText("id:"), strconv.FormatInt(task.SequenceID, 10)},
58+
{cmd.UI.TranslateText("name:"), task.Name},
59+
{cmd.UI.TranslateText("state:"), string(task.State)},
60+
{cmd.UI.TranslateText("start time:"), task.CreatedAt},
61+
{cmd.UI.TranslateText("command:"), task.Command},
62+
{cmd.UI.TranslateText("memory in mb:"), strconv.FormatUint(task.MemoryInMB, 10)},
63+
{cmd.UI.TranslateText("disk in mb:"), strconv.FormatUint(task.DiskInMB, 10)},
64+
{cmd.UI.TranslateText("log rate limit:"), strconv.Itoa(task.LogRateLimitInBPS)},
65+
{cmd.UI.TranslateText("failure reason:"), task.Result.FailureReason},
66+
}
67+
68+
cmd.UI.DisplayKeyValueTable("", table, ui.DefaultTableSpacePadding)
69+
70+
return nil
71+
}

command/v7/task_command_test.go

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package v7_test
2+
3+
import (
4+
"errors"
5+
6+
"code.cloudfoundry.org/cli/actor/actionerror"
7+
"code.cloudfoundry.org/cli/actor/v7action"
8+
"code.cloudfoundry.org/cli/api/cloudcontroller/ccerror"
9+
"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant"
10+
"code.cloudfoundry.org/cli/command/commandfakes"
11+
. "code.cloudfoundry.org/cli/command/v7"
12+
"code.cloudfoundry.org/cli/command/v7/v7fakes"
13+
"code.cloudfoundry.org/cli/resources"
14+
"code.cloudfoundry.org/cli/util/configv3"
15+
"code.cloudfoundry.org/cli/util/ui"
16+
. "github.com/onsi/ginkgo/v2"
17+
. "github.com/onsi/gomega"
18+
. "github.com/onsi/gomega/gbytes"
19+
)
20+
21+
var _ = Describe("task Command", func() {
22+
var (
23+
cmd TaskCommand
24+
testUI *ui.UI
25+
fakeConfig *commandfakes.FakeConfig
26+
fakeSharedActor *commandfakes.FakeSharedActor
27+
fakeActor *v7fakes.FakeActor
28+
binaryName string
29+
executeErr error
30+
)
31+
32+
BeforeEach(func() {
33+
testUI = ui.NewTestUI(nil, NewBuffer(), NewBuffer())
34+
fakeConfig = new(commandfakes.FakeConfig)
35+
fakeSharedActor = new(commandfakes.FakeSharedActor)
36+
fakeActor = new(v7fakes.FakeActor)
37+
38+
cmd = TaskCommand{
39+
BaseCommand: BaseCommand{
40+
UI: testUI,
41+
Config: fakeConfig,
42+
SharedActor: fakeSharedActor,
43+
Actor: fakeActor,
44+
},
45+
}
46+
47+
cmd.RequiredArgs.AppName = "some-app-name"
48+
cmd.RequiredArgs.TaskID = 3
49+
50+
binaryName = "faceman"
51+
fakeConfig.BinaryNameReturns(binaryName)
52+
})
53+
54+
JustBeforeEach(func() {
55+
executeErr = cmd.Execute(nil)
56+
})
57+
58+
When("checking target fails", func() {
59+
BeforeEach(func() {
60+
fakeSharedActor.CheckTargetReturns(actionerror.NotLoggedInError{BinaryName: binaryName})
61+
})
62+
63+
It("returns an error", func() {
64+
Expect(executeErr).To(MatchError(actionerror.NotLoggedInError{BinaryName: binaryName}))
65+
66+
Expect(fakeSharedActor.CheckTargetCallCount()).To(Equal(1))
67+
checkTargetedOrg, checkTargetedSpace := fakeSharedActor.CheckTargetArgsForCall(0)
68+
Expect(checkTargetedOrg).To(BeTrue())
69+
Expect(checkTargetedSpace).To(BeTrue())
70+
})
71+
})
72+
73+
When("the user is logged in, and a space and org are targeted", func() {
74+
BeforeEach(func() {
75+
fakeConfig.HasTargetedOrganizationReturns(true)
76+
fakeConfig.TargetedOrganizationReturns(configv3.Organization{
77+
GUID: "some-org-guid",
78+
Name: "some-org",
79+
})
80+
fakeConfig.HasTargetedSpaceReturns(true)
81+
fakeConfig.TargetedSpaceReturns(configv3.Space{
82+
GUID: "some-space-guid",
83+
Name: "some-space",
84+
})
85+
})
86+
87+
When("getting the current user returns an error", func() {
88+
var expectedErr error
89+
90+
BeforeEach(func() {
91+
expectedErr = errors.New("get current user error")
92+
fakeActor.GetCurrentUserReturns(
93+
configv3.User{},
94+
expectedErr)
95+
})
96+
97+
It("returns the error", func() {
98+
Expect(executeErr).To(MatchError(expectedErr))
99+
})
100+
})
101+
102+
When("getting the current user does not return an error", func() {
103+
BeforeEach(func() {
104+
fakeActor.GetCurrentUserReturns(
105+
configv3.User{Name: "some-user"},
106+
nil)
107+
})
108+
109+
When("provided a valid application name", func() {
110+
BeforeEach(func() {
111+
fakeActor.GetApplicationByNameAndSpaceReturns(
112+
resources.Application{GUID: "some-app-guid"},
113+
v7action.Warnings{"get-application-warning-1", "get-application-warning-2"},
114+
nil)
115+
fakeActor.GetTaskBySequenceIDAndApplicationReturns(
116+
resources.Task{
117+
GUID: "task-3-guid",
118+
SequenceID: 3,
119+
Name: "task-3",
120+
State: constant.TaskRunning,
121+
CreatedAt: "2016-11-08T22:26:02Z",
122+
Command: "some-command",
123+
MemoryInMB: 100,
124+
DiskInMB: 200,
125+
LogRateLimitInBPS: 300,
126+
Result: &resources.TaskResult{
127+
FailureReason: "some failure message",
128+
},
129+
},
130+
v7action.Warnings{"get-task-warning-1"},
131+
nil)
132+
})
133+
134+
It("outputs the task and all warnings", func() {
135+
Expect(executeErr).ToNot(HaveOccurred())
136+
137+
Expect(fakeActor.GetApplicationByNameAndSpaceCallCount()).To(Equal(1))
138+
appName, spaceGUID := fakeActor.GetApplicationByNameAndSpaceArgsForCall(0)
139+
Expect(appName).To(Equal("some-app-name"))
140+
Expect(spaceGUID).To(Equal("some-space-guid"))
141+
142+
Expect(fakeActor.GetTaskBySequenceIDAndApplicationCallCount()).To(Equal(1))
143+
taskId, appGuid := fakeActor.GetTaskBySequenceIDAndApplicationArgsForCall(0)
144+
Expect(taskId).To(Equal(3))
145+
Expect(appGuid).To(Equal("some-app-guid"))
146+
147+
Expect(testUI.Out).To(Say("Getting task 3 for app some-app-name in org some-org / space some-space as some-user..."))
148+
149+
Expect(testUI.Out).To(Say(`id:\s+3`))
150+
Expect(testUI.Out).To(Say(`name:\s+task-3`))
151+
Expect(testUI.Out).To(Say(`state:\s+RUNNING`))
152+
Expect(testUI.Out).To(Say(`start time:\s+2016-11-08T22:26:02Z`))
153+
Expect(testUI.Out).To(Say(`command:\s+some-command`))
154+
Expect(testUI.Out).To(Say(`memory in mb:\s+100`))
155+
Expect(testUI.Out).To(Say(`disk in mb:\s+200`))
156+
Expect(testUI.Out).To(Say(`log rate limit:\s+300`))
157+
Expect(testUI.Out).To(Say(`failure reason:\s+some failure message`))
158+
159+
Expect(testUI.Err).To(Say("get-application-warning-1"))
160+
Expect(testUI.Err).To(Say("get-application-warning-2"))
161+
Expect(testUI.Err).To(Say("get-task-warning-1"))
162+
})
163+
164+
When("the API does not return a command", func() {
165+
BeforeEach(func() {
166+
fakeActor.GetTaskBySequenceIDAndApplicationReturns(
167+
resources.Task{
168+
GUID: "task-3-guid",
169+
SequenceID: 3,
170+
Name: "task-3",
171+
State: constant.TaskRunning,
172+
CreatedAt: "2016-11-08T22:26:02Z",
173+
Command: "",
174+
MemoryInMB: 100,
175+
DiskInMB: 200,
176+
LogRateLimitInBPS: 300,
177+
Result: &resources.TaskResult{
178+
FailureReason: "some failure message",
179+
},
180+
},
181+
v7action.Warnings{"get-task-warning-1"},
182+
nil)
183+
})
184+
It("displays [hidden] for the command", func() {
185+
Expect(executeErr).ToNot(HaveOccurred())
186+
Expect(testUI.Out).To(Say(`.*command:\s+\[hidden\]`))
187+
})
188+
})
189+
})
190+
191+
When("there are errors", func() {
192+
When("the error is translatable", func() {
193+
When("getting the application returns the error", func() {
194+
var (
195+
returnedErr error
196+
expectedErr error
197+
)
198+
199+
BeforeEach(func() {
200+
expectedErr = errors.New("request-error")
201+
returnedErr = ccerror.RequestError{Err: expectedErr}
202+
fakeActor.GetApplicationByNameAndSpaceReturns(
203+
resources.Application{GUID: "some-app-guid"},
204+
nil,
205+
returnedErr)
206+
})
207+
208+
It("returns a translatable error", func() {
209+
Expect(executeErr).To(MatchError(ccerror.RequestError{Err: expectedErr}))
210+
})
211+
})
212+
213+
When("getting the app task returns the error", func() {
214+
var returnedErr error
215+
216+
BeforeEach(func() {
217+
returnedErr = ccerror.UnverifiedServerError{URL: "some-url"}
218+
fakeActor.GetApplicationByNameAndSpaceReturns(
219+
resources.Application{GUID: "some-app-guid"},
220+
nil,
221+
nil)
222+
fakeActor.GetTaskBySequenceIDAndApplicationReturns(
223+
resources.Task{},
224+
nil,
225+
returnedErr)
226+
})
227+
228+
It("returns a translatable error", func() {
229+
Expect(executeErr).To(MatchError(returnedErr))
230+
})
231+
})
232+
})
233+
234+
When("the error is not translatable", func() {
235+
When("getting the app returns the error", func() {
236+
var expectedErr error
237+
238+
BeforeEach(func() {
239+
expectedErr = errors.New("bananapants")
240+
fakeActor.GetApplicationByNameAndSpaceReturns(
241+
resources.Application{GUID: "some-app-guid"},
242+
v7action.Warnings{"get-application-warning-1", "get-application-warning-2"},
243+
expectedErr)
244+
})
245+
246+
It("return the error and outputs all warnings", func() {
247+
Expect(executeErr).To(MatchError(expectedErr))
248+
249+
Expect(testUI.Err).To(Say("get-application-warning-1"))
250+
Expect(testUI.Err).To(Say("get-application-warning-2"))
251+
})
252+
})
253+
254+
When("getting the app task returns the error", func() {
255+
var expectedErr error
256+
257+
BeforeEach(func() {
258+
expectedErr = errors.New("bananapants??")
259+
fakeActor.GetApplicationByNameAndSpaceReturns(
260+
resources.Application{GUID: "some-app-guid"},
261+
v7action.Warnings{"get-application-warning-1", "get-application-warning-2"},
262+
nil)
263+
fakeActor.GetTaskBySequenceIDAndApplicationReturns(
264+
resources.Task{},
265+
v7action.Warnings{"get-task-warning-1", "get-task-warning-2"},
266+
expectedErr)
267+
})
268+
269+
It("returns the error and outputs all warnings", func() {
270+
Expect(executeErr).To(MatchError(expectedErr))
271+
272+
Expect(testUI.Err).To(Say("get-application-warning-1"))
273+
Expect(testUI.Err).To(Say("get-application-warning-2"))
274+
Expect(testUI.Err).To(Say("get-task-warning-1"))
275+
Expect(testUI.Err).To(Say("get-task-warning-2"))
276+
})
277+
})
278+
})
279+
})
280+
})
281+
})
282+
})

command/v7/tasks_command.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type TasksCommand struct {
1414

1515
RequiredArgs flag.AppName `positional-args:"yes"`
1616
usage interface{} `usage:"CF_NAME tasks APP_NAME"`
17-
relatedCommands interface{} `related_commands:"apps, logs, run-task, terminate-task"`
17+
relatedCommands interface{} `related_commands:"apps, logs, run-task, task, terminate-task"`
1818
}
1919

2020
func (cmd TasksCommand) Execute(args []string) error {

0 commit comments

Comments
 (0)