Skip to content

Commit 0fc2173

Browse files
authored
Improve cf apps with --no-stats and better error handling (#2795)
* Introduce `--no-stats` for `cf apps` Fetching process stats requires a `/stats` request for each process. If a space contains a large number apps or processes this slows down `cf apps`. In case a user is not interested in stats the `--no-stats` flag can be helpful. * Handle `ProcessNotFoundError` in `cf apps` As a user calls `cf apps` in a large space it might happen that a app was deleted while the CLI is putting all the different information (processes, stats & routes) together. This leads to a failing `cf apps` command. This change prevents this by catching `ProcessNotFoundError` errors and returning `nil` instead. Related to #2734
1 parent 3c6ad43 commit 0fc2173

File tree

6 files changed

+1173
-773
lines changed

6 files changed

+1173
-773
lines changed

actor/v7action/application_summary.go

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package v7action
22

33
import (
44
"code.cloudfoundry.org/cli/actor/actionerror"
5+
"code.cloudfoundry.org/cli/api/cloudcontroller/ccerror"
56
"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3"
67
"code.cloudfoundry.org/cli/resources"
78
"code.cloudfoundry.org/cli/util/batcher"
@@ -32,7 +33,7 @@ func (a ApplicationSummary) hasIsolationSegment() bool {
3233
len(a.ProcessSummaries[0].InstanceDetails[0].IsolationSegment) > 0
3334
}
3435

35-
func (actor Actor) GetAppSummariesForSpace(spaceGUID string, labelSelector string) ([]ApplicationSummary, Warnings, error) {
36+
func (actor Actor) GetAppSummariesForSpace(spaceGUID string, labelSelector string, omitStats bool) ([]ApplicationSummary, Warnings, error) {
3637
var allWarnings Warnings
3738
var allSummaries []ApplicationSummary
3839

@@ -43,56 +44,33 @@ func (actor Actor) GetAppSummariesForSpace(spaceGUID string, labelSelector strin
4344
if len(labelSelector) > 0 {
4445
keys = append(keys, ccv3.Query{Key: ccv3.LabelSelectorFilter, Values: []string{labelSelector}})
4546
}
46-
apps, warnings, err := actor.CloudControllerClient.GetApplications(keys...)
47-
allWarnings = append(allWarnings, warnings...)
47+
apps, ccv3Warnings, err := actor.CloudControllerClient.GetApplications(keys...)
48+
allWarnings = append(allWarnings, ccv3Warnings...)
4849
if err != nil {
4950
return nil, allWarnings, err
5051
}
51-
var processes []resources.Process
5252

53-
warnings, err = batcher.RequestByGUID(toAppGUIDs(apps), func(guids []string) (ccv3.Warnings, error) {
54-
batch, warnings, err := actor.CloudControllerClient.GetProcesses(ccv3.Query{
55-
Key: ccv3.AppGUIDFilter, Values: guids,
56-
})
57-
processes = append(processes, batch...)
58-
return warnings, err
59-
})
60-
allWarnings = append(allWarnings, warnings...)
61-
if err != nil {
62-
return nil, allWarnings, err
63-
}
53+
var processSummariesByAppGUID map[string]ProcessSummaries
54+
var warnings Warnings
6455

65-
processSummariesByAppGUID := make(map[string]ProcessSummaries, len(apps))
66-
for _, process := range processes {
67-
instances, warnings, err := actor.CloudControllerClient.GetProcessInstances(process.GUID)
68-
allWarnings = append(allWarnings, Warnings(warnings)...)
56+
if !omitStats {
57+
processSummariesByAppGUID, warnings, err = actor.getProcessSummariesForApps(apps)
58+
allWarnings = append(allWarnings, warnings...)
6959
if err != nil {
7060
return nil, allWarnings, err
7161
}
72-
73-
var instanceDetails []ProcessInstance
74-
for _, instance := range instances {
75-
instanceDetails = append(instanceDetails, ProcessInstance(instance))
76-
}
77-
78-
processSummary := ProcessSummary{
79-
Process: resources.Process(process),
80-
InstanceDetails: instanceDetails,
81-
}
82-
83-
processSummariesByAppGUID[process.AppGUID] = append(processSummariesByAppGUID[process.AppGUID], processSummary)
8462
}
8563

8664
var routes []resources.Route
8765

88-
warnings, err = batcher.RequestByGUID(toAppGUIDs(apps), func(guids []string) (ccv3.Warnings, error) {
66+
ccv3Warnings, err = batcher.RequestByGUID(toAppGUIDs(apps), func(guids []string) (ccv3.Warnings, error) {
8967
batch, warnings, err := actor.CloudControllerClient.GetRoutes(ccv3.Query{
9068
Key: ccv3.AppGUIDFilter, Values: guids,
9169
})
9270
routes = append(routes, batch...)
9371
return warnings, err
9472
})
95-
allWarnings = append(allWarnings, warnings...)
73+
allWarnings = append(allWarnings, ccv3Warnings...)
9674
if err != nil {
9775
return nil, allWarnings, err
9876
}
@@ -144,6 +122,51 @@ func (actor Actor) GetDetailedAppSummary(appName, spaceGUID string, withObfuscat
144122
return detailedSummary, allWarnings, err
145123
}
146124

125+
func (actor Actor) getProcessSummariesForApps(apps []resources.Application) (map[string]ProcessSummaries, Warnings, error) {
126+
processSummariesByAppGUID := make(map[string]ProcessSummaries)
127+
var allWarnings Warnings
128+
var processes []resources.Process
129+
130+
warnings, err := batcher.RequestByGUID(toAppGUIDs(apps), func(guids []string) (ccv3.Warnings, error) {
131+
batch, warnings, err := actor.CloudControllerClient.GetProcesses(ccv3.Query{
132+
Key: ccv3.AppGUIDFilter, Values: guids,
133+
})
134+
processes = append(processes, batch...)
135+
return warnings, err
136+
})
137+
allWarnings = append(allWarnings, warnings...)
138+
if err != nil {
139+
return nil, allWarnings, err
140+
}
141+
142+
for _, process := range processes {
143+
instances, warnings, err := actor.CloudControllerClient.GetProcessInstances(process.GUID)
144+
allWarnings = append(allWarnings, Warnings(warnings)...)
145+
146+
if err != nil {
147+
switch err.(type) {
148+
case ccerror.ProcessNotFoundError, ccerror.InstanceNotFoundError:
149+
continue
150+
default:
151+
return nil, allWarnings, err
152+
}
153+
}
154+
155+
var instanceDetails []ProcessInstance
156+
for _, instance := range instances {
157+
instanceDetails = append(instanceDetails, ProcessInstance(instance))
158+
}
159+
160+
processSummary := ProcessSummary{
161+
Process: resources.Process(process),
162+
InstanceDetails: instanceDetails,
163+
}
164+
165+
processSummariesByAppGUID[process.AppGUID] = append(processSummariesByAppGUID[process.AppGUID], processSummary)
166+
}
167+
return processSummariesByAppGUID, allWarnings, nil
168+
}
169+
147170
func (actor Actor) createSummary(app resources.Application, withObfuscatedValues bool) (ApplicationSummary, Warnings, error) {
148171
var allWarnings Warnings
149172

actor/v7action/application_summary_test.go

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ var _ = Describe("Application Summary Actions", func() {
7272
var (
7373
spaceGUID string
7474
labelSelector string
75+
omitStats bool
7576

7677
summaries []ApplicationSummary
7778
warnings Warnings
@@ -81,10 +82,11 @@ var _ = Describe("Application Summary Actions", func() {
8182
BeforeEach(func() {
8283
spaceGUID = "some-space-guid"
8384
labelSelector = "some-key=some-value"
85+
omitStats = false
8486
})
8587

8688
JustBeforeEach(func() {
87-
summaries, warnings, executeErr = actor.GetAppSummariesForSpace(spaceGUID, labelSelector)
89+
summaries, warnings, executeErr = actor.GetAppSummariesForSpace(spaceGUID, labelSelector, omitStats)
8890
})
8991

9092
When("getting the application is successful", func() {
@@ -361,6 +363,93 @@ var _ = Describe("Application Summary Actions", func() {
361363
Expect(warnings).To(ConsistOf("get-apps-warning"))
362364
})
363365
})
366+
367+
When("omitStats flag is provided", func() {
368+
BeforeEach(func() {
369+
omitStats = true
370+
371+
fakeCloudControllerClient.GetApplicationsReturns(
372+
[]resources.Application{
373+
{
374+
Name: "some-app-name",
375+
GUID: "some-app-guid",
376+
State: constant.ApplicationStarted,
377+
},
378+
},
379+
ccv3.Warnings{"get-apps-warning"},
380+
nil,
381+
)
382+
383+
listedProcesses := []resources.Process{
384+
{
385+
GUID: "some-process-web-guid",
386+
Type: "web",
387+
Command: *types.NewFilteredString("[Redacted Value]"),
388+
MemoryInMB: types.NullUint64{Value: 64, IsSet: true},
389+
AppGUID: "some-app-guid",
390+
},
391+
}
392+
393+
fakeCloudControllerClient.GetProcessesReturns(
394+
listedProcesses,
395+
ccv3.Warnings{"get-app-processes-warning"},
396+
nil,
397+
)
398+
})
399+
It("doesn't call the stats endpoint", func() {
400+
Expect(fakeCloudControllerClient.GetProcessInstancesCallCount()).To(Equal(0))
401+
})
402+
})
403+
404+
When("an application is deleted in between", func() {
405+
406+
BeforeEach(func() {
407+
fakeCloudControllerClient.GetApplicationsReturns(
408+
[]resources.Application{
409+
{
410+
Name: "some-app-name",
411+
GUID: "some-app-guid",
412+
State: constant.ApplicationStarted,
413+
},
414+
},
415+
nil,
416+
nil,
417+
)
418+
419+
fakeCloudControllerClient.GetProcessesReturns(
420+
[]resources.Process{
421+
{
422+
GUID: "some-process-web-guid",
423+
Type: "web",
424+
Command: *types.NewFilteredString("[Redacted Value]"),
425+
MemoryInMB: types.NullUint64{Value: 64, IsSet: true},
426+
AppGUID: "some-app-guid",
427+
},
428+
},
429+
nil,
430+
nil,
431+
)
432+
433+
fakeCloudControllerClient.GetProcessInstancesReturns(nil, nil, ccerror.ProcessNotFoundError{})
434+
})
435+
436+
It("does not fail and has empty ProcessSummaries & Routes", func() {
437+
Expect(executeErr).ToNot(HaveOccurred())
438+
439+
Expect(summaries).To(Equal([]ApplicationSummary{
440+
{
441+
Application: resources.Application{
442+
Name: "some-app-name",
443+
GUID: "some-app-guid",
444+
State: constant.ApplicationStarted,
445+
},
446+
ProcessSummaries: nil,
447+
Routes: nil,
448+
},
449+
}))
450+
451+
})
452+
})
364453
})
365454

366455
Describe("GetDetailedAppSummary", func() {

command/v7/actor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ type Actor interface {
8787
EnableServiceAccess(offeringName, brokerName, orgName, planName string) (v7action.SkippedPlans, v7action.Warnings, error)
8888
EntitleIsolationSegmentToOrganizationByName(isolationSegmentName string, orgName string) (v7action.Warnings, error)
8989
GetAppFeature(appGUID string, featureName string) (resources.ApplicationFeature, v7action.Warnings, error)
90-
GetAppSummariesForSpace(spaceGUID string, labels string) ([]v7action.ApplicationSummary, v7action.Warnings, error)
90+
GetAppSummariesForSpace(spaceGUID string, labels string, omitStats bool) ([]v7action.ApplicationSummary, v7action.Warnings, error)
9191
GetApplicationByNameAndSpace(appName string, spaceGUID string) (resources.Application, v7action.Warnings, error)
9292
GetApplicationMapForRoute(route resources.Route) (map[string]resources.Application, v7action.Warnings, error)
9393
GetApplicationDroplets(appName string, spaceGUID string) ([]resources.Droplet, v7action.Warnings, error)

command/v7/apps_command.go

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ type AppsCommand struct {
1313
usage interface{} `usage:"CF_NAME apps [--labels SELECTOR]\n\nEXAMPLES:\n CF_NAME apps\n CF_NAME apps --labels 'environment in (production,staging),tier in (backend)'\n CF_NAME apps --labels 'env=dev,!chargeback-code,tier in (backend,worker)'"`
1414
relatedCommands interface{} `related_commands:"events, logs, map-route, push, scale, start, stop, restart"`
1515

16-
Labels string `long:"labels" description:"Selector to filter apps by labels"`
16+
Labels string `long:"labels" description:"Selector to filter apps by labels"`
17+
OmitStats bool `long:"no-stats" description:"Do not retrieve process stats"`
1718
}
1819

1920
func (cmd AppsCommand) Execute(args []string) error {
@@ -34,7 +35,7 @@ func (cmd AppsCommand) Execute(args []string) error {
3435
})
3536
cmd.UI.DisplayNewline()
3637

37-
summaries, warnings, err := cmd.Actor.GetAppSummariesForSpace(cmd.Config.TargetedSpace().GUID, cmd.Labels)
38+
summaries, warnings, err := cmd.Actor.GetAppSummariesForSpace(cmd.Config.TargetedSpace().GUID, cmd.Labels, cmd.OmitStats)
3839
cmd.UI.DisplayWarnings(warnings)
3940
if err != nil {
4041
return err
@@ -45,22 +46,29 @@ func (cmd AppsCommand) Execute(args []string) error {
4546
return nil
4647
}
4748

48-
table := [][]string{
49-
{
50-
cmd.UI.TranslateText("name"),
51-
cmd.UI.TranslateText("requested state"),
52-
cmd.UI.TranslateText("processes"),
53-
cmd.UI.TranslateText("routes"),
54-
},
49+
fields := []string{
50+
cmd.UI.TranslateText("name"),
51+
cmd.UI.TranslateText("requested state"),
5552
}
5653

54+
if !cmd.OmitStats {
55+
fields = append(fields, cmd.UI.TranslateText("processes"))
56+
}
57+
58+
fields = append(fields, cmd.UI.TranslateText("routes"))
59+
60+
table := [][]string{fields}
61+
5762
for _, summary := range summaries {
58-
table = append(table, []string{
63+
tableRow := []string{
5964
summary.Name,
6065
cmd.UI.TranslateText(strings.ToLower(string(summary.State))),
61-
summary.ProcessSummaries.String(),
62-
getURLs(summary.Routes),
63-
})
66+
}
67+
if !cmd.OmitStats {
68+
tableRow = append(tableRow, summary.ProcessSummaries.String())
69+
}
70+
tableRow = append(tableRow, getURLs(summary.Routes))
71+
table = append(table, tableRow)
6472
}
6573

6674
cmd.UI.DisplayTableWithHeader("", table, ui.DefaultTableSpacePadding)

command/v7/apps_command_test.go

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,10 @@ var _ = Describe("apps Command", func() {
243243
Expect(testUI.Err).To(Say("warning-2"))
244244

245245
Expect(fakeActor.GetAppSummariesForSpaceCallCount()).To(Equal(1))
246-
spaceGUID, labels := fakeActor.GetAppSummariesForSpaceArgsForCall(0)
246+
spaceGUID, labels, omitStats := fakeActor.GetAppSummariesForSpaceArgsForCall(0)
247247
Expect(spaceGUID).To(Equal("some-space-guid"))
248248
Expect(labels).To(Equal(""))
249+
Expect(omitStats).To(Equal(false))
249250
})
250251
})
251252

@@ -274,9 +275,10 @@ var _ = Describe("apps Command", func() {
274275
Expect(testUI.Err).To(Say("warning"))
275276

276277
Expect(fakeActor.GetAppSummariesForSpaceCallCount()).To(Equal(1))
277-
spaceGUID, labelSelector := fakeActor.GetAppSummariesForSpaceArgsForCall(0)
278+
spaceGUID, labelSelector, omitStats := fakeActor.GetAppSummariesForSpaceArgsForCall(0)
278279
Expect(spaceGUID).To(Equal("some-space-guid"))
279280
Expect(labelSelector).To(Equal(""))
281+
Expect(omitStats).To(Equal(false))
280282
})
281283
})
282284

@@ -301,9 +303,46 @@ var _ = Describe("apps Command", func() {
301303

302304
It("passes the flag to the API", func() {
303305
Expect(fakeActor.GetAppSummariesForSpaceCallCount()).To(Equal(1))
304-
_, labelSelector := fakeActor.GetAppSummariesForSpaceArgsForCall(0)
306+
_, labelSelector, _ := fakeActor.GetAppSummariesForSpaceArgsForCall(0)
305307
Expect(labelSelector).To(Equal("fish=moose"))
306308
})
307309
})
308310

311+
Context("when '--skip-stats' flag is set", func() {
312+
BeforeEach(func() {
313+
cmd.OmitStats = true
314+
315+
appSummaries := []v7action.ApplicationSummary{
316+
{
317+
Application: resources.Application{
318+
GUID: "app-guid-1",
319+
Name: "some-app-1",
320+
State: constant.ApplicationStarted,
321+
},
322+
ProcessSummaries: []v7action.ProcessSummary{},
323+
Routes: []resources.Route{
324+
{
325+
Host: "some-app-1",
326+
URL: "some-app-1.some-domain",
327+
},
328+
},
329+
},
330+
}
331+
fakeActor.GetAppSummariesForSpaceReturns(appSummaries, v7action.Warnings{}, nil)
332+
333+
})
334+
335+
It("prints the application summary without process information", func() {
336+
Expect(executeErr).ToNot(HaveOccurred())
337+
338+
Expect(testUI.Out).To(Say(`Getting apps in org some-org / space some-space as steve\.\.\.`))
339+
340+
Expect(testUI.Out).To(Say(`name\s+requested state\s+routes`))
341+
Expect(testUI.Out).To(Say(`some-app-1\s+started\s+some-app-1.some-domain`))
342+
Expect(fakeActor.GetAppSummariesForSpaceCallCount()).To(Equal(1))
343+
_, _, omitStats := fakeActor.GetAppSummariesForSpaceArgsForCall(0)
344+
Expect(omitStats).To(Equal(true))
345+
})
346+
})
347+
309348
})

0 commit comments

Comments
 (0)