diff --git a/actor/v7action/application_summary.go b/actor/v7action/application_summary.go index c989c54ac6..c0b39116e1 100644 --- a/actor/v7action/application_summary.go +++ b/actor/v7action/application_summary.go @@ -2,10 +2,14 @@ package v7action import ( "errors" + "time" "code.cloudfoundry.org/cli/v8/actor/actionerror" + "code.cloudfoundry.org/cli/v8/actor/versioncheck" "code.cloudfoundry.org/cli/v8/api/cloudcontroller/ccerror" "code.cloudfoundry.org/cli/v8/api/cloudcontroller/ccv3" + "code.cloudfoundry.org/cli/v8/api/cloudcontroller/ccv3/constant" + "code.cloudfoundry.org/cli/v8/api/cloudcontroller/ccversion" "code.cloudfoundry.org/cli/v8/resources" "code.cloudfoundry.org/cli/v8/util/batcher" ) @@ -58,7 +62,15 @@ func (actor Actor) GetAppSummariesForSpace(spaceGUID string, labelSelector strin var warnings Warnings if !omitStats { - processSummariesByAppGUID, warnings, err = actor.getProcessSummariesForApps(apps) + embeddedProcessInstancesAvailable, versionErr := versioncheck.IsMinimumAPIVersionMet(actor.Config.APIVersion(), ccversion.MinVersionEmbeddedProcessInstances) + if versionErr != nil { + return nil, allWarnings, versionErr + } + if embeddedProcessInstancesAvailable { + processSummariesByAppGUID, warnings, err = actor.getProcessSummariesForSpace(spaceGUID) + } else { + processSummariesByAppGUID, warnings, err = actor.getProcessSummariesForApps(apps) + } allWarnings = append(allWarnings, warnings...) if err != nil { return nil, allWarnings, err @@ -174,6 +186,40 @@ func (actor Actor) getProcessSummariesForApps(apps []resources.Application) (map processSummariesByAppGUID[process.AppGUID] = append(processSummariesByAppGUID[process.AppGUID], processSummary) } + + return processSummariesByAppGUID, allWarnings, nil +} + +func (actor Actor) getProcessSummariesForSpace(spaceGUID string) (map[string]ProcessSummaries, Warnings, error) { + processSummariesByAppGUID := make(map[string]ProcessSummaries) + var allWarnings Warnings + var processes []resources.Process + + // use "/v3/processes?space_guids=:guid&embed=process_instances" to get processes and process instances in one request + processes, warnings, err := actor.CloudControllerClient.GetProcesses( + ccv3.Query{Key: ccv3.SpaceGUIDFilter, Values: []string{spaceGUID}}, + ccv3.Query{Key: ccv3.Embed, Values: []string{"process_instances"}}, + ) + allWarnings = append(allWarnings, warnings...) + if err != nil { + return nil, allWarnings, err + } + + for _, process := range processes { + var instanceDetails []ProcessInstance + if process.EmbeddedProcessInstances != nil { + for _, instance := range *process.EmbeddedProcessInstances { + instanceDetails = append(instanceDetails, NewProcessInstance(instance.Index, constant.ProcessInstanceState(instance.State), time.Duration(instance.Since))) + } + } + processSummary := ProcessSummary{ + Process: process, + InstanceDetails: instanceDetails, + } + + processSummariesByAppGUID[process.AppGUID] = append(processSummariesByAppGUID[process.AppGUID], processSummary) + + } return processSummariesByAppGUID, allWarnings, nil } diff --git a/actor/v7action/application_summary_test.go b/actor/v7action/application_summary_test.go index dcba2bc5c0..8c238e7ae8 100644 --- a/actor/v7action/application_summary_test.go +++ b/actor/v7action/application_summary_test.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "code.cloudfoundry.org/cli/v8/actor/actionerror" . "code.cloudfoundry.org/cli/v8/actor/v7action" "code.cloudfoundry.org/cli/v8/actor/v7action/v7actionfakes" "code.cloudfoundry.org/cli/v8/api/cloudcontroller/ccerror" @@ -13,8 +14,6 @@ import ( "code.cloudfoundry.org/cli/v8/types" "code.cloudfoundry.org/clock" - "code.cloudfoundry.org/cli/v8/actor/actionerror" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -23,11 +22,14 @@ var _ = Describe("Application Summary Actions", func() { var ( actor *Actor fakeCloudControllerClient *v7actionfakes.FakeCloudControllerClient + fakeConfig *v7actionfakes.FakeConfig ) BeforeEach(func() { fakeCloudControllerClient = new(v7actionfakes.FakeCloudControllerClient) - actor = NewActor(fakeCloudControllerClient, nil, nil, nil, nil, clock.NewClock()) + fakeConfig = new(v7actionfakes.FakeConfig) + fakeConfig.APIVersionReturns("3.210.0") + actor = NewActor(fakeCloudControllerClient, fakeConfig, nil, nil, nil, clock.NewClock()) }) Describe("ApplicationSummary", func() { @@ -287,6 +289,152 @@ var _ = Describe("Application Summary Actions", func() { Expect(fakeCloudControllerClient.GetProcessInstancesArgsForCall(0)).To(Equal("some-process-guid")) }) + Context("the cloud controller supports embedded process instances", func() { + BeforeEach(func() { + fakeConfig.APIVersionReturns("3.211.0") + + listedProcesses := []resources.Process{ + { + GUID: "some-process-guid", + Type: "some-type", + Command: *types.NewFilteredString("[Redacted Value]"), + MemoryInMB: types.NullUint64{Value: 32, IsSet: true}, + AppGUID: "some-app-guid", + EmbeddedProcessInstances: &[]resources.EmbeddedProcessInstance{ + {Index: 0, State: "RUNNING", Since: 300}, + {Index: 1, State: "CRASHED", Since: 0}, + }, + }, + { + GUID: "some-process-web-guid", + Type: "web", + Command: *types.NewFilteredString("[Redacted Value]"), + MemoryInMB: types.NullUint64{Value: 64, IsSet: true}, + AppGUID: "some-app-guid", + EmbeddedProcessInstances: &[]resources.EmbeddedProcessInstance{ + {Index: 0, State: "RUNNING", Since: 500}, + {Index: 1, State: "RUNNING", Since: 600}, + }, + }, + } + + fakeCloudControllerClient.GetProcessesReturns( + listedProcesses, + ccv3.Warnings{"get-space-processes-warning"}, + nil, + ) + }) + + It("uses the embedded process instances", func() { + Expect(executeErr).ToNot(HaveOccurred()) + Expect(summaries).To(Equal([]ApplicationSummary{ + { + Application: resources.Application{ + Name: "some-app-name", + GUID: "some-app-guid", + State: constant.ApplicationStarted, + }, + ProcessSummaries: []ProcessSummary{ + { + Process: resources.Process{ + GUID: "some-process-web-guid", + Type: "web", + Command: *types.NewFilteredString("[Redacted Value]"), + MemoryInMB: types.NullUint64{Value: 64, IsSet: true}, + AppGUID: "some-app-guid", + EmbeddedProcessInstances: &[]resources.EmbeddedProcessInstance{ + {Index: 0, State: "RUNNING", Since: 500}, + {Index: 1, State: "RUNNING", Since: 600}, + }, + }, + InstanceDetails: []ProcessInstance{ + { + Index: 0, + State: "RUNNING", + Uptime: 500, + }, + { + Index: 1, + State: "RUNNING", + Uptime: 600, + }, + }, + }, + { + Process: resources.Process{ + GUID: "some-process-guid", + MemoryInMB: types.NullUint64{Value: 32, IsSet: true}, + Type: "some-type", + Command: *types.NewFilteredString("[Redacted Value]"), + AppGUID: "some-app-guid", + EmbeddedProcessInstances: &[]resources.EmbeddedProcessInstance{ + {Index: 0, State: "RUNNING", Since: 300}, + {Index: 1, State: "CRASHED", Since: 0}, + }, + }, + InstanceDetails: []ProcessInstance{ + { + Index: 0, + State: "RUNNING", + Uptime: 300, + }, + { + Index: 1, + State: "CRASHED", + Uptime: 0, + }, + }, + }, + }, + Routes: []resources.Route{ + { + GUID: "some-route-guid", + Destinations: []resources.RouteDestination{ + { + App: resources.RouteDestinationApp{ + GUID: "some-app-guid", + }, + }, + }, + }, + { + GUID: "some-other-route-guid", + Destinations: []resources.RouteDestination{ + { + App: resources.RouteDestinationApp{ + GUID: "some-app-guid", + }, + }, + }, + }, + }, + }, + })) + + Expect(warnings).To(ConsistOf( + "get-apps-warning", + "get-space-processes-warning", + "get-routes-warning", + )) + + Expect(fakeCloudControllerClient.GetApplicationsCallCount()).To(Equal(1)) + Expect(fakeCloudControllerClient.GetApplicationsArgsForCall(0)).To(ConsistOf( + ccv3.Query{Key: ccv3.OrderBy, Values: []string{"name"}}, + ccv3.Query{Key: ccv3.SpaceGUIDFilter, Values: []string{"some-space-guid"}}, + ccv3.Query{Key: ccv3.LabelSelectorFilter, Values: []string{"some-key=some-value"}}, + ccv3.Query{Key: ccv3.PerPage, Values: []string{ccv3.MaxPerPage}}, + )) + + Expect(fakeCloudControllerClient.GetProcessesCallCount()).To(Equal(1)) + Expect(fakeCloudControllerClient.GetProcessesArgsForCall(0)).To(ConsistOf( + ccv3.Query{Key: ccv3.SpaceGUIDFilter, Values: []string{"some-space-guid"}}, + ccv3.Query{Key: ccv3.Embed, Values: []string{"process_instances"}}, + )) + + Expect(fakeCloudControllerClient.GetProcessInstancesCallCount()).To(Equal(0)) + }) + }) + When("there is no label selector", func() { BeforeEach(func() { labelSelector = "" diff --git a/actor/v7action/process_instance.go b/actor/v7action/process_instance.go index cefa44757c..c075e3e3d9 100644 --- a/actor/v7action/process_instance.go +++ b/actor/v7action/process_instance.go @@ -12,6 +12,14 @@ import ( type ProcessInstance ccv3.ProcessInstance +func NewProcessInstance(index int64, state constant.ProcessInstanceState, uptime time.Duration) ProcessInstance { + return ProcessInstance(ccv3.ProcessInstance{ + Index: index, + State: state, + Uptime: uptime, + }) +} + // Running will return true if the instance is running. func (instance ProcessInstance) Running() bool { return instance.State == constant.ProcessInstanceRunning diff --git a/api/cloudcontroller/ccv3/process_test.go b/api/cloudcontroller/ccv3/process_test.go index ec2f212728..758afcdb5f 100644 --- a/api/cloudcontroller/ccv3/process_test.go +++ b/api/cloudcontroller/ccv3/process_test.go @@ -94,6 +94,7 @@ var _ = Describe("Process", func() { "ReadinessHealthCheckEndpoint": Equal("/foo"), "ReadinessHealthCheckInvocationTimeout": BeEquivalentTo(2), "ReadinessHealthCheckInterval": BeEquivalentTo(9), + "EmbeddedProcessInstances": BeNil(), })) }) }) @@ -367,6 +368,7 @@ var _ = Describe("Process", func() { "ReadinessHealthCheckEndpoint": Equal("/foo"), "ReadinessHealthCheckInvocationTimeout": BeEquivalentTo(2), "ReadinessHealthCheckInterval": BeEquivalentTo(9), + "EmbeddedProcessInstances": BeNil(), })) }) }) @@ -524,32 +526,35 @@ var _ = Describe("Process", func() { Expect(processes).To(ConsistOf( resources.Process{ - GUID: "process-1-guid", - Type: constant.ProcessTypeWeb, - Command: types.FilteredString{IsSet: true, Value: "[PRIVATE DATA HIDDEN IN LISTS]"}, - MemoryInMB: types.NullUint64{Value: 32, IsSet: true}, - LogRateLimitInBPS: types.NullInt{Value: 64, IsSet: true}, - HealthCheckType: constant.Port, - HealthCheckTimeout: 0, + GUID: "process-1-guid", + Type: constant.ProcessTypeWeb, + Command: types.FilteredString{IsSet: true, Value: "[PRIVATE DATA HIDDEN IN LISTS]"}, + MemoryInMB: types.NullUint64{Value: 32, IsSet: true}, + LogRateLimitInBPS: types.NullInt{Value: 64, IsSet: true}, + HealthCheckType: constant.Port, + HealthCheckTimeout: 0, + EmbeddedProcessInstances: nil, }, resources.Process{ - GUID: "process-2-guid", - Type: "worker", - Command: types.FilteredString{IsSet: true, Value: "[PRIVATE DATA HIDDEN IN LISTS]"}, - MemoryInMB: types.NullUint64{Value: 64, IsSet: true}, - LogRateLimitInBPS: types.NullInt{Value: 128, IsSet: true}, - HealthCheckType: constant.HTTP, - HealthCheckEndpoint: "/health", - HealthCheckTimeout: 60, + GUID: "process-2-guid", + Type: "worker", + Command: types.FilteredString{IsSet: true, Value: "[PRIVATE DATA HIDDEN IN LISTS]"}, + MemoryInMB: types.NullUint64{Value: 64, IsSet: true}, + LogRateLimitInBPS: types.NullInt{Value: 128, IsSet: true}, + HealthCheckType: constant.HTTP, + HealthCheckEndpoint: "/health", + HealthCheckTimeout: 60, + EmbeddedProcessInstances: nil, }, resources.Process{ - GUID: "process-3-guid", - Type: "console", - Command: types.FilteredString{IsSet: true, Value: "[PRIVATE DATA HIDDEN IN LISTS]"}, - MemoryInMB: types.NullUint64{Value: 128, IsSet: true}, - LogRateLimitInBPS: types.NullInt{Value: 256, IsSet: true}, - HealthCheckType: constant.Process, - HealthCheckTimeout: 90, + GUID: "process-3-guid", + Type: "console", + Command: types.FilteredString{IsSet: true, Value: "[PRIVATE DATA HIDDEN IN LISTS]"}, + MemoryInMB: types.NullUint64{Value: 128, IsSet: true}, + LogRateLimitInBPS: types.NullInt{Value: 256, IsSet: true}, + HealthCheckType: constant.Process, + HealthCheckTimeout: 90, + EmbeddedProcessInstances: nil, }, )) Expect(warnings).To(ConsistOf("warning-1", "warning-2")) diff --git a/api/cloudcontroller/ccv3/query.go b/api/cloudcontroller/ccv3/query.go index 6c0cec48b3..99b04abad9 100644 --- a/api/cloudcontroller/ccv3/query.go +++ b/api/cloudcontroller/ccv3/query.go @@ -101,6 +101,8 @@ const ( // Include is a query parameter for specifying other resources associated with the // resource returned by the endpoint Include QueryKey = "include" + // see https://v3-apidocs.cloudfoundry.org/version/3.212.0/index.html#embed + Embed QueryKey = "embed" // GloballyEnabledStaging is the query parameter for getting only security groups that are globally enabled for staging GloballyEnabledStaging QueryKey = "globally_enabled_staging" diff --git a/api/cloudcontroller/ccversion/minimum_version.go b/api/cloudcontroller/ccversion/minimum_version.go index a092d504d0..4b07532dc5 100644 --- a/api/cloudcontroller/ccversion/minimum_version.go +++ b/api/cloudcontroller/ccversion/minimum_version.go @@ -26,4 +26,6 @@ const ( MinVersionServiceBindingStrategy = "3.205.0" MinVersionUpdateStack = "3.210.0" + + MinVersionEmbeddedProcessInstances = "3.211.0" ) diff --git a/resources/process_resource.go b/resources/process_resource.go index 30cbec45ff..96a6bcfbcf 100644 --- a/resources/process_resource.go +++ b/resources/process_resource.go @@ -27,6 +27,13 @@ type Process struct { DiskInMB types.NullUint64 LogRateLimitInBPS types.NullInt AppGUID string + EmbeddedProcessInstances *[]EmbeddedProcessInstance +} + +type EmbeddedProcessInstance struct { + Index int64 `json:"index"` + State string `json:"state"` + Since int64 `json:"since"` } func (p Process) MarshalJSON() ([]byte, error) { @@ -39,6 +46,7 @@ func (p Process) MarshalJSON() ([]byte, error) { marshalLogRateLimit(p, &ccProcess) marshalHealthCheck(p, &ccProcess) marshalReadinessHealthCheck(p, &ccProcess) + marshalEmbeddedProcessInstances(p, &ccProcess) return json.Marshal(ccProcess) } @@ -71,6 +79,8 @@ func (p *Process) UnmarshalJSON(data []byte) error { Interval int64 `json:"interval"` } `json:"data"` } `json:"readiness_health_check"` + + EmbeddedProcessInstances *[]EmbeddedProcessInstance `json:"process_instances,omitempty"` } err := cloudcontroller.DecodeJSON(data, &ccProcess) @@ -94,6 +104,9 @@ func (p *Process) UnmarshalJSON(data []byte) error { p.LogRateLimitInBPS = ccProcess.LogRateLimitInBPS p.Type = ccProcess.Type p.AppGUID = ccProcess.Relationships[constant.RelationshipTypeApplication].GUID + if ccProcess.EmbeddedProcessInstances != nil { + p.EmbeddedProcessInstances = ccProcess.EmbeddedProcessInstances + } return nil } @@ -123,8 +136,9 @@ type marshalProcess struct { DiskInMB json.Number `json:"disk_in_mb,omitempty"` LogRateLimitInBPS json.Number `json:"log_rate_limit_in_bytes_per_second,omitempty"` - HealthCheck *healthCheck `json:"health_check,omitempty"` - ReadinessHealthCheck *readinessHealthCheck `json:"readiness_health_check,omitempty"` + HealthCheck *healthCheck `json:"health_check,omitempty"` + ReadinessHealthCheck *readinessHealthCheck `json:"readiness_health_check,omitempty"` + EmbeddedProcessInstances *[]EmbeddedProcessInstance `json:"process_instances,omitempty"` } func marshalCommand(p Process, ccProcess *marshalProcess) { @@ -180,3 +194,9 @@ func marshalLogRateLimit(p Process, ccProcess *marshalProcess) { ccProcess.LogRateLimitInBPS = json.Number(fmt.Sprint(p.LogRateLimitInBPS.Value)) } } + +func marshalEmbeddedProcessInstances(p Process, ccProcess *marshalProcess) { + if p.EmbeddedProcessInstances != nil { + ccProcess.EmbeddedProcessInstances = p.EmbeddedProcessInstances + } +}