diff --git a/cf/cmd/cmd.go b/cf/cmd/cmd.go index d2c1a38264c..787a1b36c63 100644 --- a/cf/cmd/cmd.go +++ b/cf/cmd/cmd.go @@ -226,3 +226,22 @@ func handleVerbose(args []string) ([]string, bool) { return args, verbose } + +func handleJSON(args []string) ([]string, bool) { + var isJSON bool + idx := -1 + + for i, arg := range args { + if arg == "--json" { + idx = i + break + } + } + + if idx != -1 && len(args) > 1 { + isJSON = true + args = append(args[:idx], args[idx+1:]...) + } + + return args, isJSON +} diff --git a/command/v7/app_command.go b/command/v7/app_command.go index d431a71ff55..34c03e994f5 100644 --- a/command/v7/app_command.go +++ b/command/v7/app_command.go @@ -1,10 +1,15 @@ package v7 import ( + "code.cloudfoundry.org/cli/actor/v7action" "code.cloudfoundry.org/cli/command/flag" "code.cloudfoundry.org/cli/command/v7/shared" ) +type AppDisplayer interface { + AppDisplay(summary v7action.DetailedApplicationSummary, displayStartCommand bool) +} + type AppCommand struct { BaseCommand @@ -12,6 +17,7 @@ type AppCommand struct { GUID bool `long:"guid" description:"Retrieve and display the given app's guid. All other health and status output for the app is suppressed."` usage interface{} `usage:"CF_NAME app APP_NAME [--guid]"` relatedCommands interface{} `related_commands:"apps, events, logs, map-route, unmap-route, push"` + JSONOutput bool `long:"json" description:"Output in json form"` } func (cmd AppCommand) Execute(args []string) error { @@ -29,15 +35,18 @@ func (cmd AppCommand) Execute(args []string) error { return err } - cmd.UI.DisplayTextWithFlavor("Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", map[string]interface{}{ - "AppName": cmd.RequiredArgs.AppName, - "OrgName": cmd.Config.TargetedOrganization().Name, - "SpaceName": cmd.Config.TargetedSpace().Name, - "Username": user.Name, - }) - cmd.UI.DisplayNewline() + var appSummaryDisplayer AppDisplayer = shared.NewAppSummaryJSONDisplayer(cmd.UI) + if !cmd.JSONOutput { + cmd.UI.DisplayTextWithFlavor("Showing health and status for app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", map[string]interface{}{ + "AppName": cmd.RequiredArgs.AppName, + "OrgName": cmd.Config.TargetedOrganization().Name, + "SpaceName": cmd.Config.TargetedSpace().Name, + "Username": user.Name, + }) + cmd.UI.DisplayNewline() + appSummaryDisplayer = shared.NewAppSummaryDisplayer(cmd.UI) + } - appSummaryDisplayer := shared.NewAppSummaryDisplayer(cmd.UI) summary, warnings, err := cmd.Actor.GetDetailedAppSummary(cmd.RequiredArgs.AppName, cmd.Config.TargetedSpace().GUID, false) cmd.UI.DisplayWarnings(warnings) if err != nil { @@ -55,6 +64,10 @@ func (cmd AppCommand) displayAppGUID() error { return err } + if cmd.JSONOutput { + return cmd.UI.DisplayJSON("", map[string]string{"guid": app.GUID}) + } + cmd.UI.DisplayText(app.GUID) return nil } diff --git a/command/v7/apps_command.go b/command/v7/apps_command.go index 84d936652bd..9850a70a0ca 100644 --- a/command/v7/apps_command.go +++ b/command/v7/apps_command.go @@ -3,6 +3,7 @@ package v7 import ( "strings" + "code.cloudfoundry.org/cli/actor/v7action" "code.cloudfoundry.org/cli/resources" "code.cloudfoundry.org/cli/util/ui" ) @@ -13,8 +14,9 @@ type AppsCommand struct { 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)'"` relatedCommands interface{} `related_commands:"events, logs, map-route, push, scale, start, stop, restart"` - Labels string `long:"labels" description:"Selector to filter apps by labels"` - OmitStats bool `long:"no-stats" description:"Do not retrieve process stats"` + Labels string `long:"labels" description:"Selector to filter apps by labels"` + OmitStats bool `long:"no-stats" description:"Do not retrieve process stats"` + JSONOutput bool `long:"json" description:"Output in json form"` } func (cmd AppsCommand) Execute(args []string) error { @@ -41,6 +43,10 @@ func (cmd AppsCommand) Execute(args []string) error { return err } + if cmd.JSONOutput { + return cmd.UI.DisplayJSON("", map[string][]v7action.ApplicationSummary{"apps": summaries}) + } + if len(summaries) == 0 { cmd.UI.DisplayText("No apps found") return nil diff --git a/command/v7/get_health_check_command.go b/command/v7/get_health_check_command.go index 050f6a568de..4b610d08810 100644 --- a/command/v7/get_health_check_command.go +++ b/command/v7/get_health_check_command.go @@ -13,6 +13,7 @@ type GetHealthCheckCommand struct { RequiredArgs flag.AppName `positional-args:"yes"` usage interface{} `usage:"CF_NAME get-health-check APP_NAME"` + JSONOutput bool `long:"json" description:"Output in json form"` } func (cmd GetHealthCheckCommand) Execute(args []string) error { @@ -39,6 +40,10 @@ func (cmd GetHealthCheckCommand) Execute(args []string) error { return err } + if cmd.JSONOutput { + return cmd.UI.DisplayJSON("", map[string][]v7action.ProcessHealthCheck{"app": processHealthChecks}) + } + cmd.UI.DisplayNewline() if len(processHealthChecks) == 0 { diff --git a/command/v7/shared/app_summary_displayer.go b/command/v7/shared/app_summary_displayer.go index f7d386a66b1..491a71ffd06 100644 --- a/command/v7/shared/app_summary_displayer.go +++ b/command/v7/shared/app_summary_displayer.go @@ -238,3 +238,17 @@ func (display AppSummaryDisplayer) displayBuildpackTable(buildpacks []resources. display.UI.DisplayTableWithHeader("\t", keyValueTable, ui.DefaultTableSpacePadding) } } + +type AppSummaryJSONDisplayer struct { + UI command.UI +} + +func NewAppSummaryJSONDisplayer(ui command.UI) *AppSummaryJSONDisplayer { + return &AppSummaryJSONDisplayer{ + UI: ui, + } +} + +func (display AppSummaryJSONDisplayer) AppDisplay(summary v7action.DetailedApplicationSummary, displayStartCommand bool) { + display.UI.DisplayJSON("", summary) +} diff --git a/command/v7/shared/app_summary_displayer_test.go b/command/v7/shared/app_summary_displayer_test.go index 833dc595cd3..c41c672f338 100644 --- a/command/v7/shared/app_summary_displayer_test.go +++ b/command/v7/shared/app_summary_displayer_test.go @@ -1,6 +1,7 @@ package shared_test import ( + "encoding/json" "time" "code.cloudfoundry.org/cli/actor/v7action" @@ -1051,4 +1052,279 @@ var _ = Describe("app summary displayer", func() { }) }) }) + + Describe("AppDisplayJSON", func() { + + var ( + summary v7action.DetailedApplicationSummary + displayStartCommand bool + ) + + JustBeforeEach(func() { + testUI.IsJson = true + appSummaryDisplayer := NewAppSummaryJSONDisplayer(testUI) + appSummaryDisplayer.AppDisplay(summary, displayStartCommand) + }) + + When("the app has instances", func() { + When("the process instances are running", func() { + var uptime time.Duration + + BeforeEach(func() { + uptime = time.Since(time.Unix(267321600, 0)) + summary = v7action.DetailedApplicationSummary{ + ApplicationSummary: v7action.ApplicationSummary{ + Application: resources.Application{ + GUID: "some-app-guid", + State: constant.ApplicationStarted, + }, + ProcessSummaries: v7action.ProcessSummaries{ + { + Process: resources.Process{ + Type: constant.ProcessTypeWeb, + MemoryInMB: types.NullUint64{Value: 32, IsSet: true}, + DiskInMB: types.NullUint64{Value: 1024, IsSet: true}, + LogRateLimitInBPS: types.NullInt{Value: 1024 * 5, IsSet: true}, + }, + Sidecars: []resources.Sidecar{}, + InstanceDetails: []v7action.ProcessInstance{ + v7action.ProcessInstance{ + Index: 0, + State: constant.ProcessInstanceRunning, + CPUEntitlement: types.NullFloat64{Value: 0.0, IsSet: true}, + MemoryUsage: 1000000, + DiskUsage: 1000000, + LogRate: 1024, + MemoryQuota: 33554432, + DiskQuota: 2000000, + LogRateLimit: 1024 * 5, + Uptime: uptime, + Details: "Some Details 1", + }, + v7action.ProcessInstance{ + Index: 1, + State: constant.ProcessInstanceRunning, + CPUEntitlement: types.NullFloat64{Value: 1.0, IsSet: true}, + MemoryUsage: 2000000, + DiskUsage: 2000000, + LogRate: 1024 * 2, + MemoryQuota: 33554432, + DiskQuota: 4000000, + LogRateLimit: 1024 * 5, + Uptime: time.Since(time.Unix(330480000, 0)), + Details: "Some Details 2", + }, + v7action.ProcessInstance{ + Index: 2, + State: constant.ProcessInstanceRunning, + CPUEntitlement: types.NullFloat64{Value: 0.03, IsSet: true}, + MemoryUsage: 3000000, + DiskUsage: 3000000, + LogRate: 1024 * 3, + MemoryQuota: 33554432, + DiskQuota: 6000000, + LogRateLimit: 1024 * 5, + Uptime: time.Since(time.Unix(1277164800, 0)), + }, + }, + }, + { + Process: resources.Process{ + Type: "console", + MemoryInMB: types.NullUint64{Value: 16, IsSet: true}, + DiskInMB: types.NullUint64{Value: 512, IsSet: true}, + LogRateLimitInBPS: types.NullInt{Value: 256, IsSet: true}, + }, + Sidecars: []resources.Sidecar{}, + InstanceDetails: []v7action.ProcessInstance{ + v7action.ProcessInstance{ + Index: 0, + State: constant.ProcessInstanceRunning, + CPUEntitlement: types.NullFloat64{Value: 0.0, IsSet: true}, + MemoryUsage: 1000000, + DiskUsage: 1000000, + LogRate: 128, + MemoryQuota: 33554432, + DiskQuota: 8000000, + LogRateLimit: 256, + Uptime: time.Since(time.Unix(167572800, 0)), + }, + }, + }, + }, + }, + } + }) + + It("lists information for each of the processes", func() { + jsonStr, err := json.Marshal(summary) + Expect(err).ToNot(HaveOccurred()) + Expect(output.Contents()).To(MatchJSON(jsonStr)) + }) + }) + + When("CPU entitlement is not available", func() { + BeforeEach(func() { + summary = v7action.DetailedApplicationSummary{ + ApplicationSummary: v7action.ApplicationSummary{ + Application: resources.Application{ + GUID: "some-app-guid", + State: constant.ApplicationStarted, + }, + ProcessSummaries: v7action.ProcessSummaries{ + { + InstanceDetails: []v7action.ProcessInstance{ + v7action.ProcessInstance{ + Index: 0, + State: constant.ProcessInstanceRunning, + }, + v7action.ProcessInstance{ + Index: 1, + State: constant.ProcessInstanceRunning, + }, + v7action.ProcessInstance{ + Index: 2, + State: constant.ProcessInstanceRunning, + }, + }, + }, + }, + }, + } + }) + + It("outputs an empty value for the CPU entitlement column", func() { + jsonStr, err := json.Marshal(summary) + Expect(err).ToNot(HaveOccurred()) + Expect(output.Contents()).To(MatchJSON(jsonStr)) + }) + }) + + When("all the instances for a process are down (but scaled to > 0 instances)", func() { + BeforeEach(func() { + summary = v7action.DetailedApplicationSummary{ + ApplicationSummary: v7action.ApplicationSummary{ + ProcessSummaries: []v7action.ProcessSummary{ + { + Process: resources.Process{ + Type: constant.ProcessTypeWeb, + MemoryInMB: types.NullUint64{Value: 32, IsSet: true}, + }, + Sidecars: []resources.Sidecar{}, + InstanceDetails: []v7action.ProcessInstance{{State: constant.ProcessInstanceDown}}, + }}, + }, + } + }) + + It("displays the instances table", func() { + jsonStr, err := json.Marshal(summary) + Expect(err).ToNot(HaveOccurred()) + Expect(output.Contents()).To(MatchJSON(jsonStr)) + }) + }) + + When("the app is stopped", func() { + BeforeEach(func() { + summary = v7action.DetailedApplicationSummary{ + ApplicationSummary: v7action.ApplicationSummary{ + Application: resources.Application{ + GUID: "some-app-guid", + State: constant.ApplicationStopped, + }, + ProcessSummaries: v7action.ProcessSummaries{ + { + Process: resources.Process{ + Type: constant.ProcessTypeWeb, + }, + Sidecars: []resources.Sidecar{}, + }, + { + Process: resources.Process{ + Type: "console", + }, + Sidecars: []resources.Sidecar{}, + }, + }, + }, + } + }) + + It("lists information for each of the processes", func() { + jsonStr, err := json.Marshal(summary) + Expect(err).ToNot(HaveOccurred()) + Expect(output.Contents()).To(MatchJSON(jsonStr)) + }) + }) + + When("the application is a docker app", func() { + BeforeEach(func() { + summary = v7action.DetailedApplicationSummary{ + ApplicationSummary: v7action.ApplicationSummary{ + Application: resources.Application{ + GUID: "some-guid", + Name: "some-app", + State: constant.ApplicationStarted, + LifecycleType: constant.AppLifecycleTypeDocker, + }, + }, + CurrentDroplet: resources.Droplet{ + Image: "docker/some-image", + }, + } + }) + + It("displays the app information", func() { + jsonStr, err := json.Marshal(summary) + Expect(err).ToNot(HaveOccurred()) + Expect(output.Contents()).To(MatchJSON(jsonStr)) + }) + }) + + When("the application is a buildpack app", func() { + BeforeEach(func() { + summary = v7action.DetailedApplicationSummary{ + ApplicationSummary: v7action.ApplicationSummary{ + Application: resources.Application{ + LifecycleType: constant.AppLifecycleTypeBuildpack, + }, + }, + CurrentDroplet: resources.Droplet{ + Stack: "cflinuxfs4", + Buildpacks: []resources.DropletBuildpack{ + { + Name: "ruby_buildpack", + BuildpackName: "ruby_buildpack_name", + DetectOutput: "some-detect-output", + Version: "0.0.1", + }, + { + Name: "go_buildpack_without_detect_output", + BuildpackName: "go_buildpack_name", + DetectOutput: "", + Version: "0.0.2", + }, + { + Name: "go_buildpack_without_version", + BuildpackName: "go_buildpack_name", + DetectOutput: "", + Version: "", + }, + { + Name: "some-buildpack", + DetectOutput: "", + }, + }, + }, + } + }) + + It("displays stack and buildpacks", func() { + jsonStr, err := json.Marshal(summary) + Expect(err).ToNot(HaveOccurred()) + Expect(output.Contents()).To(MatchJSON(jsonStr)) + }) + }) + }) + }) }) diff --git a/util/command_parser/command_parser.go b/util/command_parser/command_parser.go index 5a847731b62..c326eedd699 100644 --- a/util/command_parser/command_parser.go +++ b/util/command_parser/command_parser.go @@ -195,6 +195,9 @@ func (p *CommandParser) handleFlagErrorAndCommandHelp(flagErr *flags.Error, flag case flags.ErrUnknownCommand: if containsHelpFlag(originalArgs) { return p.parse([]string{"help", originalArgs[0]}, commandList) + } else if containsJSONFlag(originalArgs) { + p.UI.IsJson = true + return 0, nil } else { return 0, UnknownCommandError{CommandName: originalArgs[0]} } @@ -214,6 +217,9 @@ func (p *CommandParser) handleFlagErrorAndCommandHelp(flagErr *flags.Error, flag } func (p *CommandParser) parse(args []string, commandList interface{}) (int, error) { + if containsJSONFlag(args) { + p.UI.IsJson = true + } flagsParser := flags.NewParser(commandList, flags.HelpFlag) flagsParser.CommandHandler = p.executionWrapper extraArgs, err := flagsParser.ParseArgs(args) @@ -248,6 +254,15 @@ func containsHelpFlag(args []string) bool { return false } +func containsJSONFlag(args []string) bool { + for _, arg := range args { + if arg == "--json" { + return true + } + } + return false +} + func isCommand(s string) bool { _, found := reflect.TypeOf(common.Commands).FieldByNameFunc( func(fieldName string) bool { diff --git a/util/ui/ui.go b/util/ui/ui.go index 435dc4da50d..6797ca3fb6a 100644 --- a/util/ui/ui.go +++ b/util/ui/ui.go @@ -93,6 +93,8 @@ type UI struct { IsTTY bool TerminalWidth int + IsJson bool + TimezoneLocation *time.Location deferred []string @@ -225,6 +227,9 @@ func (ui *UI) DisplayHeader(text string) { // DisplayNewline outputs a newline to UI.Out. func (ui *UI) DisplayNewline() { + if ui.IsJson { + return + } ui.terminalLock.Lock() defer ui.terminalLock.Unlock() @@ -242,6 +247,9 @@ func (ui *UI) DisplayOK() { // DisplayText translates the template, substitutes in templateValues, and // outputs the result to ui.Out. Only the first map in templateValues is used. func (ui *UI) DisplayText(template string, templateValues ...map[string]interface{}) { + if ui.IsJson { + return + } ui.terminalLock.Lock() defer ui.terminalLock.Unlock() @@ -275,6 +283,9 @@ func (ui *UI) DisplayTextWithBold(template string, templateValues ...map[string] // templateValues, substitutes templateValues into the template, and outputs // the result to ui.Out. Only the first map in templateValues is used. func (ui *UI) DisplayTextWithFlavor(template string, templateValues ...map[string]interface{}) { + if ui.IsJson { + return + } ui.terminalLock.Lock() defer ui.terminalLock.Unlock()