Skip to content

Commit

Permalink
Add cf task <app> <id> command
Browse files Browse the repository at this point in the history
  • Loading branch information
Samze committed Nov 22, 2024
1 parent dfe2201 commit 87db63e
Show file tree
Hide file tree
Showing 12 changed files with 499 additions and 6 deletions.
1 change: 1 addition & 0 deletions command/common/command_list_v7.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ type commandList struct {
Start v7.StartCommand `command:"start" alias:"st" description:"Start an app"`
Stop v7.StopCommand `command:"stop" alias:"sp" description:"Stop an app"`
Target v7.TargetCommand `command:"target" alias:"t" description:"Set or view the targeted org or space"`
Task v7.TaskCommand `command:"task" description:"Display a task of an app"`
Tasks v7.TasksCommand `command:"tasks" description:"List tasks of an app"`
TerminateTask v7.TerminateTaskCommand `command:"terminate-task" description:"Terminate a running task of an app"`
MoveRoute v7.MoveRouteCommand `command:"move-route" description:"Assign a route to a different space"`
Expand Down
2 changes: 1 addition & 1 deletion command/common/internal/help_all_display.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var HelpCategoryList = []HelpCategory{
{"push", "scale", "delete", "rename"},
{"cancel-deployment", "continue-deployment"},
{"start", "stop", "restart", "stage-package", "restage", "restart-app-instance"},
{"run-task", "tasks", "terminate-task"},
{"run-task", "task", "tasks", "terminate-task"},
{"packages", "create-package"},
{"revisions", "rollback"},
{"droplets", "set-droplet", "download-droplet"},
Expand Down
5 changes: 5 additions & 0 deletions command/flag/arguments.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,8 @@ type RemoveNetworkPolicyArgsV7 struct {
SourceApp string `positional-arg-name:"SOURCE_APP" required:"true" description:"The source app"`
DestApp string `positional-arg-name:"DESTINATION_APP" required:"true" description:"The destination app"`
}

type TaskArgs struct {
AppName string `positional-arg-name:"APP_NAME" required:"true" description:"The application name"`
TaskID int `positional-arg-name:"TASK_ID" required:"true" description:"The Task ID for the application"`
}
2 changes: 1 addition & 1 deletion command/v7/run_task_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type RunTaskCommand struct {
Process string `long:"process" description:"Process type to use as a template for command, memory, and disk for the created task."`
Wait bool `long:"wait" short:"w" description:"Wait for the task to complete before exiting"`
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"`
relatedCommands interface{} `related_commands:"logs, tasks, terminate-task"`
relatedCommands interface{} `related_commands:"logs, tasks, task, terminate-task"`
}

func (cmd RunTaskCommand) Execute(args []string) error {
Expand Down
71 changes: 71 additions & 0 deletions command/v7/task_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package v7

import (
"strconv"

"code.cloudfoundry.org/cli/command/flag"
"code.cloudfoundry.org/cli/util/ui"
)

type TaskCommand struct {
BaseCommand

RequiredArgs flag.TaskArgs `positional-args:"yes"`
usage interface{} `usage:"CF_NAME task APP_NAME TASK_ID"`
relatedCommands interface{} `related_commands:"apps, logs, run-task, tasks, terminate-task"`
}

func (cmd TaskCommand) Execute(args []string) error {
err := cmd.SharedActor.CheckTarget(true, true)
if err != nil {
return err
}

space := cmd.Config.TargetedSpace()

user, err := cmd.Actor.GetCurrentUser()
if err != nil {
return err
}

application, warnings, err := cmd.Actor.GetApplicationByNameAndSpace(cmd.RequiredArgs.AppName, space.GUID)
cmd.UI.DisplayWarnings(warnings)
if err != nil {
return err
}

cmd.UI.DisplayTextWithFlavor("Getting task {{.TaskID}} for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.CurrentUser}}...", map[string]interface{}{
"TaskID": cmd.RequiredArgs.TaskID,
"AppName": cmd.RequiredArgs.AppName,
"OrgName": cmd.Config.TargetedOrganization().Name,
"SpaceName": space.Name,
"CurrentUser": user.Name,
})
cmd.UI.DisplayNewline()

task, warnings, err := cmd.Actor.GetTaskBySequenceIDAndApplication(cmd.RequiredArgs.TaskID, application.GUID)
cmd.UI.DisplayWarnings(warnings)
if err != nil {
return err
}

if task.Command == "" {
task.Command = "[hidden]"
}

table := [][]string{
{cmd.UI.TranslateText("id:"), strconv.FormatInt(task.SequenceID, 10)},
{cmd.UI.TranslateText("name:"), task.Name},
{cmd.UI.TranslateText("state:"), string(task.State)},
{cmd.UI.TranslateText("start time:"), task.CreatedAt},
{cmd.UI.TranslateText("command:"), task.Command},
{cmd.UI.TranslateText("memory in mb:"), strconv.FormatUint(task.MemoryInMB, 10)},
{cmd.UI.TranslateText("disk in mb:"), strconv.FormatUint(task.DiskInMB, 10)},
{cmd.UI.TranslateText("log rate limit:"), strconv.Itoa(task.LogRateLimitInBPS)},
{cmd.UI.TranslateText("failure reason:"), task.Result.FailureReason},
}

cmd.UI.DisplayKeyValueTable("", table, ui.DefaultTableSpacePadding)

return nil
}
282 changes: 282 additions & 0 deletions command/v7/task_command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
package v7_test

import (
"errors"

"code.cloudfoundry.org/cli/actor/actionerror"
"code.cloudfoundry.org/cli/actor/v7action"
"code.cloudfoundry.org/cli/api/cloudcontroller/ccerror"
"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant"
"code.cloudfoundry.org/cli/command/commandfakes"
. "code.cloudfoundry.org/cli/command/v7"
"code.cloudfoundry.org/cli/command/v7/v7fakes"
"code.cloudfoundry.org/cli/resources"
"code.cloudfoundry.org/cli/util/configv3"
"code.cloudfoundry.org/cli/util/ui"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gbytes"
)

var _ = Describe("task Command", func() {
var (
cmd TaskCommand
testUI *ui.UI
fakeConfig *commandfakes.FakeConfig
fakeSharedActor *commandfakes.FakeSharedActor
fakeActor *v7fakes.FakeActor
binaryName string
executeErr error
)

BeforeEach(func() {
testUI = ui.NewTestUI(nil, NewBuffer(), NewBuffer())
fakeConfig = new(commandfakes.FakeConfig)
fakeSharedActor = new(commandfakes.FakeSharedActor)
fakeActor = new(v7fakes.FakeActor)

cmd = TaskCommand{
BaseCommand: BaseCommand{
UI: testUI,
Config: fakeConfig,
SharedActor: fakeSharedActor,
Actor: fakeActor,
},
}

cmd.RequiredArgs.AppName = "some-app-name"
cmd.RequiredArgs.TaskID = 3

binaryName = "faceman"
fakeConfig.BinaryNameReturns(binaryName)
})

JustBeforeEach(func() {
executeErr = cmd.Execute(nil)
})

When("checking target fails", func() {
BeforeEach(func() {
fakeSharedActor.CheckTargetReturns(actionerror.NotLoggedInError{BinaryName: binaryName})
})

It("returns an error", func() {
Expect(executeErr).To(MatchError(actionerror.NotLoggedInError{BinaryName: binaryName}))

Expect(fakeSharedActor.CheckTargetCallCount()).To(Equal(1))
checkTargetedOrg, checkTargetedSpace := fakeSharedActor.CheckTargetArgsForCall(0)
Expect(checkTargetedOrg).To(BeTrue())
Expect(checkTargetedSpace).To(BeTrue())
})
})

When("the user is logged in, and a space and org are targeted", func() {
BeforeEach(func() {
fakeConfig.HasTargetedOrganizationReturns(true)
fakeConfig.TargetedOrganizationReturns(configv3.Organization{
GUID: "some-org-guid",
Name: "some-org",
})
fakeConfig.HasTargetedSpaceReturns(true)
fakeConfig.TargetedSpaceReturns(configv3.Space{
GUID: "some-space-guid",
Name: "some-space",
})
})

When("getting the current user returns an error", func() {
var expectedErr error

BeforeEach(func() {
expectedErr = errors.New("get current user error")
fakeActor.GetCurrentUserReturns(
configv3.User{},
expectedErr)
})

It("returns the error", func() {
Expect(executeErr).To(MatchError(expectedErr))
})
})

When("getting the current user does not return an error", func() {
BeforeEach(func() {
fakeActor.GetCurrentUserReturns(
configv3.User{Name: "some-user"},
nil)
})

When("provided a valid application name", func() {
BeforeEach(func() {
fakeActor.GetApplicationByNameAndSpaceReturns(
resources.Application{GUID: "some-app-guid"},
v7action.Warnings{"get-application-warning-1", "get-application-warning-2"},
nil)
fakeActor.GetTaskBySequenceIDAndApplicationReturns(
resources.Task{
GUID: "task-3-guid",
SequenceID: 3,
Name: "task-3",
State: constant.TaskRunning,
CreatedAt: "2016-11-08T22:26:02Z",
Command: "some-command",
MemoryInMB: 100,
DiskInMB: 200,
LogRateLimitInBPS: 300,
Result: &resources.TaskResult{
FailureReason: "some failure message",
},
},
v7action.Warnings{"get-task-warning-1"},
nil)
})

It("outputs the task and all warnings", func() {
Expect(executeErr).ToNot(HaveOccurred())

Expect(fakeActor.GetApplicationByNameAndSpaceCallCount()).To(Equal(1))
appName, spaceGUID := fakeActor.GetApplicationByNameAndSpaceArgsForCall(0)
Expect(appName).To(Equal("some-app-name"))
Expect(spaceGUID).To(Equal("some-space-guid"))

Expect(fakeActor.GetTaskBySequenceIDAndApplicationCallCount()).To(Equal(1))
taskId, appGuid := fakeActor.GetTaskBySequenceIDAndApplicationArgsForCall(0)
Expect(taskId).To(Equal(3))
Expect(appGuid).To(Equal("some-app-guid"))

Expect(testUI.Out).To(Say("Getting task 3 for app some-app-name in org some-org / space some-space as some-user..."))

Expect(testUI.Out).To(Say(`id:\s+3`))
Expect(testUI.Out).To(Say(`name:\s+task-3`))
Expect(testUI.Out).To(Say(`state:\s+RUNNING`))
Expect(testUI.Out).To(Say(`start time:\s+2016-11-08T22:26:02Z`))
Expect(testUI.Out).To(Say(`command:\s+some-command`))
Expect(testUI.Out).To(Say(`memory in mb:\s+100`))
Expect(testUI.Out).To(Say(`disk in mb:\s+200`))
Expect(testUI.Out).To(Say(`log rate limit:\s+300`))
Expect(testUI.Out).To(Say(`failure reason:\s+some failure message`))

Expect(testUI.Err).To(Say("get-application-warning-1"))
Expect(testUI.Err).To(Say("get-application-warning-2"))
Expect(testUI.Err).To(Say("get-task-warning-1"))
})

When("the API does not return a command", func() {
BeforeEach(func() {
fakeActor.GetTaskBySequenceIDAndApplicationReturns(
resources.Task{
GUID: "task-3-guid",
SequenceID: 3,
Name: "task-3",
State: constant.TaskRunning,
CreatedAt: "2016-11-08T22:26:02Z",
Command: "",
MemoryInMB: 100,
DiskInMB: 200,
LogRateLimitInBPS: 300,
Result: &resources.TaskResult{
FailureReason: "some failure message",
},
},
v7action.Warnings{"get-task-warning-1"},
nil)
})
It("displays [hidden] for the command", func() {
Expect(executeErr).ToNot(HaveOccurred())
Expect(testUI.Out).To(Say(`.*command:\s+\[hidden\]`))
})
})
})

When("there are errors", func() {
When("the error is translatable", func() {
When("getting the application returns the error", func() {
var (
returnedErr error
expectedErr error
)

BeforeEach(func() {
expectedErr = errors.New("request-error")
returnedErr = ccerror.RequestError{Err: expectedErr}
fakeActor.GetApplicationByNameAndSpaceReturns(
resources.Application{GUID: "some-app-guid"},
nil,
returnedErr)
})

It("returns a translatable error", func() {
Expect(executeErr).To(MatchError(ccerror.RequestError{Err: expectedErr}))
})
})

When("getting the app task returns the error", func() {
var returnedErr error

BeforeEach(func() {
returnedErr = ccerror.UnverifiedServerError{URL: "some-url"}
fakeActor.GetApplicationByNameAndSpaceReturns(
resources.Application{GUID: "some-app-guid"},
nil,
nil)
fakeActor.GetTaskBySequenceIDAndApplicationReturns(
resources.Task{},
nil,
returnedErr)
})

It("returns a translatable error", func() {
Expect(executeErr).To(MatchError(returnedErr))
})
})
})

When("the error is not translatable", func() {
When("getting the app returns the error", func() {
var expectedErr error

BeforeEach(func() {
expectedErr = errors.New("bananapants")
fakeActor.GetApplicationByNameAndSpaceReturns(
resources.Application{GUID: "some-app-guid"},
v7action.Warnings{"get-application-warning-1", "get-application-warning-2"},
expectedErr)
})

It("return the error and outputs all warnings", func() {
Expect(executeErr).To(MatchError(expectedErr))

Expect(testUI.Err).To(Say("get-application-warning-1"))
Expect(testUI.Err).To(Say("get-application-warning-2"))
})
})

When("getting the app task returns the error", func() {
var expectedErr error

BeforeEach(func() {
expectedErr = errors.New("bananapants??")
fakeActor.GetApplicationByNameAndSpaceReturns(
resources.Application{GUID: "some-app-guid"},
v7action.Warnings{"get-application-warning-1", "get-application-warning-2"},
nil)
fakeActor.GetTaskBySequenceIDAndApplicationReturns(
resources.Task{},
v7action.Warnings{"get-task-warning-1", "get-task-warning-2"},
expectedErr)
})

It("returns the error and outputs all warnings", func() {
Expect(executeErr).To(MatchError(expectedErr))

Expect(testUI.Err).To(Say("get-application-warning-1"))
Expect(testUI.Err).To(Say("get-application-warning-2"))
Expect(testUI.Err).To(Say("get-task-warning-1"))
Expect(testUI.Err).To(Say("get-task-warning-2"))
})
})
})
})
})
})
})
2 changes: 1 addition & 1 deletion command/v7/tasks_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type TasksCommand struct {

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

func (cmd TasksCommand) Execute(args []string) error {
Expand Down
Loading

0 comments on commit 87db63e

Please sign in to comment.