From 5c2a1f4fe2a034eb7e8d3ccde26260d666a2aea6 Mon Sep 17 00:00:00 2001 From: Carson Long <12767276+ctlong@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:18:24 -0700 Subject: [PATCH] feat: Add cpu entitlement to app process table (#2840) Shows the new CPU Entitlement metric in the app process table, under the heading `cpu entitlement`. If CPU Entitlement metrics are not available (e.g. deployment does not support it, app is stopped, etc.) for a process instance, then that row will show an empty value. Signed-off-by: Rebecca Roberts --- api/cloudcontroller/ccv3/process_instance.go | 13 +- .../ccv3/process_instance_test.go | 5 + command/v7/shared/app_summary_displayer.go | 10 ++ .../v7/shared/app_summary_displayer_test.go | 67 ++++---- integration/helpers/app_instance_table.go | 51 +++--- .../helpers/app_instance_table_test.go | 42 ++--- integration/v7/isolated/app_command_test.go | 2 +- .../v7/isolated/restage_command_test.go | 4 +- .../v7/isolated/restart_command_test.go | 2 +- integration/v7/isolated/start_command_test.go | 4 +- integration/v7/push/disk_flag_test.go | 2 +- integration/v7/push/instances_flag_test.go | 2 +- integration/v7/push/memory_flag_test.go | 2 +- types/null_float64.go | 81 ++++++++++ types/null_float64_test.go | 145 ++++++++++++++++++ 15 files changed, 348 insertions(+), 84 deletions(-) create mode 100644 types/null_float64.go create mode 100644 types/null_float64_test.go diff --git a/api/cloudcontroller/ccv3/process_instance.go b/api/cloudcontroller/ccv3/process_instance.go index 56f7c221b7..17bb527794 100644 --- a/api/cloudcontroller/ccv3/process_instance.go +++ b/api/cloudcontroller/ccv3/process_instance.go @@ -8,6 +8,7 @@ import ( "code.cloudfoundry.org/cli/api/cloudcontroller" "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/internal" + "code.cloudfoundry.org/cli/types" ) // ProcessInstance represents a single process instance for a particular @@ -15,6 +16,8 @@ import ( type ProcessInstance struct { // CPU is the current CPU usage of the instance. CPU float64 + // CPU Entitlement is the current CPU entitlement usage of the instance. + CPUEntitlement types.NullFloat64 // Details is information about errors placing the instance. Details string // DiskQuota is the maximum disk the instance is allowed to use. @@ -56,10 +59,11 @@ func (instance *ProcessInstance) UnmarshalJSON(data []byte) error { Type string `json:"type"` Uptime int64 `json:"uptime"` Usage struct { - CPU float64 `json:"cpu"` - Mem uint64 `json:"mem"` - Disk uint64 `json:"disk"` - LogRate uint64 `json:"log_rate"` + CPU float64 `json:"cpu"` + CPUEntitlement types.NullFloat64 `json:"cpu_entitlement"` + Mem uint64 `json:"mem"` + Disk uint64 `json:"disk"` + LogRate uint64 `json:"log_rate"` } `json:"usage"` } @@ -69,6 +73,7 @@ func (instance *ProcessInstance) UnmarshalJSON(data []byte) error { } instance.CPU = inputInstance.Usage.CPU + instance.CPUEntitlement = inputInstance.Usage.CPUEntitlement instance.Details = inputInstance.Details instance.DiskQuota = inputInstance.DiskQuota instance.DiskUsage = inputInstance.Usage.Disk diff --git a/api/cloudcontroller/ccv3/process_instance_test.go b/api/cloudcontroller/ccv3/process_instance_test.go index 1499952f11..b47875a858 100644 --- a/api/cloudcontroller/ccv3/process_instance_test.go +++ b/api/cloudcontroller/ccv3/process_instance_test.go @@ -7,6 +7,7 @@ import ( "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" . "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" + "code.cloudfoundry.org/cli/types" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/ghttp" @@ -92,6 +93,7 @@ var _ = Describe("ProcessInstance", func() { "state": "RUNNING", "usage": { "cpu": 0.01, + "cpu_entitlement": 0.02, "mem": 1000000, "disk": 2000000, "log_rate": 5000 @@ -109,6 +111,7 @@ var _ = Describe("ProcessInstance", func() { "state": "RUNNING", "usage": { "cpu": 0.02, + "cpu_entitlement": 0.04, "mem": 8000000, "disk": 16000000, "log_rate": 32000 @@ -136,6 +139,7 @@ var _ = Describe("ProcessInstance", func() { Expect(processes).To(ConsistOf( ProcessInstance{ CPU: 0.01, + CPUEntitlement: types.NullFloat64{IsSet: true, Value: 0.02}, Details: "some details", DiskQuota: 4000000, DiskUsage: 2000000, @@ -151,6 +155,7 @@ var _ = Describe("ProcessInstance", func() { }, ProcessInstance{ CPU: 0.02, + CPUEntitlement: types.NullFloat64{IsSet: true, Value: 0.04}, DiskQuota: 32000000, DiskUsage: 16000000, Index: 1, diff --git a/command/v7/shared/app_summary_displayer.go b/command/v7/shared/app_summary_displayer.go index e9c5b41a75..98ae553742 100644 --- a/command/v7/shared/app_summary_displayer.go +++ b/command/v7/shared/app_summary_displayer.go @@ -10,6 +10,7 @@ import ( "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" "code.cloudfoundry.org/cli/command" "code.cloudfoundry.org/cli/resources" + "code.cloudfoundry.org/cli/types" "code.cloudfoundry.org/cli/util/ui" log "github.com/sirupsen/logrus" ) @@ -80,6 +81,13 @@ func formatLogRateLimit(limit int64) string { } } +func formatCPUEntitlement(cpuEntitlement types.NullFloat64) string { + if !cpuEntitlement.IsSet { + return "" + } + return fmt.Sprintf("%.1f%%", cpuEntitlement.Value*100) +} + func (display AppSummaryDisplayer) displayAppInstancesTable(processSummary v7action.ProcessSummary) { table := [][]string{ { @@ -90,6 +98,7 @@ func (display AppSummaryDisplayer) displayAppInstancesTable(processSummary v7act display.UI.TranslateText("memory"), display.UI.TranslateText("disk"), display.UI.TranslateText("logging"), + display.UI.TranslateText("cpu entitlement"), display.UI.TranslateText("details"), }, } @@ -112,6 +121,7 @@ func (display AppSummaryDisplayer) displayAppInstancesTable(processSummary v7act "LogRate": bytefmt.ByteSize(instance.LogRate), "LogRateLimit": formatLogRateLimit(instance.LogRateLimit), }), + formatCPUEntitlement(instance.CPUEntitlement), instance.Details, }) } diff --git a/command/v7/shared/app_summary_displayer_test.go b/command/v7/shared/app_summary_displayer_test.go index 8f3779805b..7caf995d80 100644 --- a/command/v7/shared/app_summary_displayer_test.go +++ b/command/v7/shared/app_summary_displayer_test.go @@ -17,7 +17,7 @@ import ( var _ = Describe("app summary displayer", func() { - const instanceStatsTitles = `state\s+since\s+cpu\s+memory\s+disk\s+logging\s+details` + const instanceStatsTitles = `state\s+since\s+cpu\s+memory\s+disk\s+logging\s+cpu entitlement\s+details` var ( appSummaryDisplayer *AppSummaryDisplayer @@ -65,39 +65,42 @@ var _ = Describe("app summary displayer", func() { Sidecars: []resources.Sidecar{}, InstanceDetails: []v7action.ProcessInstance{ v7action.ProcessInstance{ - Index: 0, - State: constant.ProcessInstanceRunning, - MemoryUsage: 1000000, - DiskUsage: 1000000, - LogRate: 1024, - MemoryQuota: 33554432, - DiskQuota: 2000000, - LogRateLimit: 1024 * 5, - Uptime: uptime, - Details: "Some Details 1", + Index: 0, + State: constant.ProcessInstanceRunning, + CPUEntitlement: types.NullFloat64{Value: 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, - 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", + Index: 1, + State: constant.ProcessInstanceRunning, + CPUEntitlement: types.NullFloat64{Value: 0, IsSet: false}, + 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, - MemoryUsage: 3000000, - DiskUsage: 3000000, - LogRate: 1024 * 3, - MemoryQuota: 33554432, - DiskQuota: 6000000, - LogRateLimit: 1024 * 5, - Uptime: time.Since(time.Unix(1277164800, 0)), + 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)), }, }, }, @@ -143,18 +146,21 @@ var _ = Describe("app summary displayer", func() { Expect(time.Parse(time.RFC3339, webProcessSummary.Instances[0].Since)).To(BeTemporally("~", time.Now().Add(-uptime), 2*time.Second)) Expect(webProcessSummary.Instances[0].Disk).To(Equal("976.6K of 1.9M")) Expect(webProcessSummary.Instances[0].CPU).To(Equal("0.0%")) + Expect(webProcessSummary.Instances[0].CPUEntitlement).To(Equal("0.0%")) Expect(webProcessSummary.Instances[0].LogRate).To(Equal("1K/s of 5K/s")) Expect(webProcessSummary.Instances[0].Details).To(Equal("Some Details 1")) Expect(webProcessSummary.Instances[1].Memory).To(Equal("1.9M of 32M")) Expect(webProcessSummary.Instances[1].Disk).To(Equal("1.9M of 3.8M")) Expect(webProcessSummary.Instances[1].CPU).To(Equal("0.0%")) + Expect(webProcessSummary.Instances[1].CPUEntitlement).To(Equal("")) Expect(webProcessSummary.Instances[1].LogRate).To(Equal("2K/s of 5K/s")) Expect(webProcessSummary.Instances[1].Details).To(Equal("Some Details 2")) Expect(webProcessSummary.Instances[2].Memory).To(Equal("2.9M of 32M")) Expect(webProcessSummary.Instances[2].Disk).To(Equal("2.9M of 5.7M")) Expect(webProcessSummary.Instances[2].CPU).To(Equal("0.0%")) + Expect(webProcessSummary.Instances[2].CPUEntitlement).To(Equal("3.0%")) Expect(webProcessSummary.Instances[2].LogRate).To(Equal("3K/s of 5K/s")) consoleProcessSummary := processTable.Processes[1] @@ -166,6 +172,7 @@ var _ = Describe("app summary displayer", func() { Expect(consoleProcessSummary.Instances[0].Memory).To(Equal("976.6K of 32M")) Expect(consoleProcessSummary.Instances[0].Disk).To(Equal("976.6K of 7.6M")) Expect(consoleProcessSummary.Instances[0].CPU).To(Equal("0.0%")) + Expect(consoleProcessSummary.Instances[0].CPUEntitlement).To(Equal("")) Expect(consoleProcessSummary.Instances[0].LogRate).To(Equal("128B/s of 256B/s")) }) }) diff --git a/integration/helpers/app_instance_table.go b/integration/helpers/app_instance_table.go index 57964f1126..5ee6c13698 100644 --- a/integration/helpers/app_instance_table.go +++ b/integration/helpers/app_instance_table.go @@ -8,14 +8,15 @@ import ( // AppInstanceRow represents an instance of a V3 app's process, // as displayed in the 'cf app' output. type AppInstanceRow struct { - Index string - State string - Since string - CPU string - Memory string - Disk string - LogRate string - Details string + Index string + State string + Since string + CPU string + Memory string + Disk string + LogRate string + CPUEntitlement string + Details string } // AppProcessTable represents a process of a V3 app, as displayed in the 'cf @@ -56,24 +57,28 @@ func ParseV3AppProcessTable(input []byte) AppTable { switch { case strings.HasPrefix(row, "#"): - const columnCount = 8 + const columnCount = 9 // instance row columns := splitColumns(row) - details := "" + cpuEntitlement, details := "", "" + if len(columns) >= columnCount-1 { + cpuEntitlement = columns[columnCount-2] + } if len(columns) >= columnCount { - details = columns[7] + details = columns[columnCount-1] } instanceRow := AppInstanceRow{ - Index: columns[0], - State: columns[1], - Since: columns[2], - CPU: columns[3], - Memory: columns[4], - Disk: columns[5], - LogRate: columns[6], - Details: details, + Index: columns[0], + State: columns[1], + Since: columns[2], + CPU: columns[3], + Memory: columns[4], + Disk: columns[5], + LogRate: columns[6], + CPUEntitlement: cpuEntitlement, + Details: details, } lastProcessIndex := len(appTable.Processes) - 1 appTable.Processes[lastProcessIndex].Instances = append( @@ -107,6 +112,12 @@ func ParseV3AppProcessTable(input []byte) AppTable { } func splitColumns(row string) []string { + s := strings.TrimSpace(row) // uses 3 spaces between columns - return regexp.MustCompile(`\s{3,}`).Split(strings.TrimSpace(row), -1) + result := regexp.MustCompile(`\s{3,}`).Split(s, -1) + // 21 spaces should only occur if cpu entitlement is empty but details is filled in + if regexp.MustCompile(`\s{21}`).MatchString(s) { + result = append(result[:len(result)-1], "", result[len(result)-1]) + } + return result } diff --git a/integration/helpers/app_instance_table_test.go b/integration/helpers/app_instance_table_test.go index 47dd9ce080..7508e0c984 100644 --- a/integration/helpers/app_instance_table_test.go +++ b/integration/helpers/app_instance_table_test.go @@ -21,17 +21,17 @@ buildpacks: ruby 1.6.44 type: web instances: 4/4 memory usage: 32M - state since cpu memory disk logging -#0 running 2017-08-02 17:12:10 PM 0.0% 21.2M of 32M 84.5M of 1G 5B/s of 1K/s -#1 running 2017-08-03 09:39:25 AM 0.2% 19.3M of 32M 84.5M of 1G 7B/s of 1K/s -#2 running 2017-08-03 03:29:25 AM 0.1% 22.8M of 32M 84.5M of 1G 10B/s of 1K/s -#3 running 2017-08-02 17:12:10 PM 0.2% 22.9M of 32M 84.5M of 1G 8B/s of 1K/s + state since cpu memory disk logging cpu entitlement +#0 running 2017-08-02 17:12:10 PM 0.0% 21.2M of 32M 84.5M of 1G 5B/s of 1K/s 0.0% +#1 running 2017-08-03 09:39:25 AM 0.2% 19.3M of 32M 84.5M of 1G 7B/s of 1K/s 0.4% +#2 running 2017-08-03 03:29:25 AM 0.1% 22.8M of 32M 84.5M of 1G 10B/s of 1K/s 0.2% +#3 running 2017-08-02 17:12:10 PM 0.2% 22.9M of 32M 84.5M of 1G 8B/s of 1K/s 0.4% type: worker instances: 1/1 memory usage: 32M - state since cpu memory disk logging -#0 stopped 2017-08-02 17:12:10 PM 0.0% 0M of 32M 0M of 1G 0B/s of 1K/s + state since cpu memory disk logging cpu entitlement +#0 stopped 2017-08-02 17:12:10 PM 0.0% 0M of 32M 0M of 1G 0B/s of 1K/s 0.0% ` appInstanceTable := ParseV3AppProcessTable([]byte(input)) Expect(appInstanceTable).To(Equal(AppTable{ @@ -41,10 +41,10 @@ memory usage: 32M InstanceCount: "4/4", MemUsage: "32M", Instances: []AppInstanceRow{ - {Index: "#0", State: "running", Since: "2017-08-02 17:12:10 PM", CPU: "0.0%", Memory: "21.2M of 32M", Disk: "84.5M of 1G", LogRate: "5B/s of 1K/s"}, - {Index: "#1", State: "running", Since: "2017-08-03 09:39:25 AM", CPU: "0.2%", Memory: "19.3M of 32M", Disk: "84.5M of 1G", LogRate: "7B/s of 1K/s"}, - {Index: "#2", State: "running", Since: "2017-08-03 03:29:25 AM", CPU: "0.1%", Memory: "22.8M of 32M", Disk: "84.5M of 1G", LogRate: "10B/s of 1K/s"}, - {Index: "#3", State: "running", Since: "2017-08-02 17:12:10 PM", CPU: "0.2%", Memory: "22.9M of 32M", Disk: "84.5M of 1G", LogRate: "8B/s of 1K/s"}, + {Index: "#0", State: "running", Since: "2017-08-02 17:12:10 PM", CPU: "0.0%", Memory: "21.2M of 32M", Disk: "84.5M of 1G", LogRate: "5B/s of 1K/s", CPUEntitlement: "0.0%"}, + {Index: "#1", State: "running", Since: "2017-08-03 09:39:25 AM", CPU: "0.2%", Memory: "19.3M of 32M", Disk: "84.5M of 1G", LogRate: "7B/s of 1K/s", CPUEntitlement: "0.4%"}, + {Index: "#2", State: "running", Since: "2017-08-03 03:29:25 AM", CPU: "0.1%", Memory: "22.8M of 32M", Disk: "84.5M of 1G", LogRate: "10B/s of 1K/s", CPUEntitlement: "0.2%"}, + {Index: "#3", State: "running", Since: "2017-08-02 17:12:10 PM", CPU: "0.2%", Memory: "22.9M of 32M", Disk: "84.5M of 1G", LogRate: "8B/s of 1K/s", CPUEntitlement: "0.4%"}, }, }, { @@ -52,7 +52,7 @@ memory usage: 32M InstanceCount: "1/1", MemUsage: "32M", Instances: []AppInstanceRow{ - {Index: "#0", State: "stopped", Since: "2017-08-02 17:12:10 PM", CPU: "0.0%", Memory: "0M of 32M", Disk: "0M of 1G", LogRate: "0B/s of 1K/s"}, + {Index: "#0", State: "stopped", Since: "2017-08-02 17:12:10 PM", CPU: "0.0%", Memory: "0M of 32M", Disk: "0M of 1G", LogRate: "0B/s of 1K/s", CPUEntitlement: "0.0%"}, }, }, }, @@ -66,11 +66,11 @@ Showing health and status for app dora in org wut / space wut as admin... type: web instances: 4/4 memory usage: 32M - state since cpu memory disk logging -#0 running 2017-08-02 17:12:10 PM 0.0% 21.2M of 32M 84.5M of 1G 1.3K/s of 5K/s -#1 running 2017-08-03 09:39:25 AM 0.2% 19.3M of 32M 84.5M of 1G 1.2K/s of 5K/s -#2 running 2017-08-03 03:29:25 AM 0.1% 22.8M of 32M 84.5M of 1G 1.1K/s of 5K/s -#3 running 2017-08-02 17:12:10 PM 0.2% 22.9M of 32M 84.5M of 1G 1.2K/s of 5K/s + state since cpu memory disk logging cpu entitlement +#0 running 2017-08-02 17:12:10 PM 0.0% 21.2M of 32M 84.5M of 1G 1.3K/s of 5K/s 0.0% +#1 running 2017-08-03 09:39:25 AM 0.2% 19.3M of 32M 84.5M of 1G 1.2K/s of 5K/s 0.4% +#2 running 2017-08-03 03:29:25 AM 0.1% 22.8M of 32M 84.5M of 1G 1.1K/s of 5K/s 0.2% +#3 running 2017-08-02 17:12:10 PM 0.2% 22.9M of 32M 84.5M of 1G 1.2K/s of 5K/s 0.4% ` appInstanceTable := ParseV3AppProcessTable([]byte(input)) Expect(appInstanceTable).To(Equal(AppTable{ @@ -80,10 +80,10 @@ memory usage: 32M InstanceCount: "4/4", MemUsage: "32M", Instances: []AppInstanceRow{ - {Index: "#0", State: "running", Since: "2017-08-02 17:12:10 PM", CPU: "0.0%", Memory: "21.2M of 32M", Disk: "84.5M of 1G", LogRate: "1.3K/s of 5K/s"}, - {Index: "#1", State: "running", Since: "2017-08-03 09:39:25 AM", CPU: "0.2%", Memory: "19.3M of 32M", Disk: "84.5M of 1G", LogRate: "1.2K/s of 5K/s"}, - {Index: "#2", State: "running", Since: "2017-08-03 03:29:25 AM", CPU: "0.1%", Memory: "22.8M of 32M", Disk: "84.5M of 1G", LogRate: "1.1K/s of 5K/s"}, - {Index: "#3", State: "running", Since: "2017-08-02 17:12:10 PM", CPU: "0.2%", Memory: "22.9M of 32M", Disk: "84.5M of 1G", LogRate: "1.2K/s of 5K/s"}, + {Index: "#0", State: "running", Since: "2017-08-02 17:12:10 PM", CPU: "0.0%", Memory: "21.2M of 32M", Disk: "84.5M of 1G", LogRate: "1.3K/s of 5K/s", CPUEntitlement: "0.0%"}, + {Index: "#1", State: "running", Since: "2017-08-03 09:39:25 AM", CPU: "0.2%", Memory: "19.3M of 32M", Disk: "84.5M of 1G", LogRate: "1.2K/s of 5K/s", CPUEntitlement: "0.4%"}, + {Index: "#2", State: "running", Since: "2017-08-03 03:29:25 AM", CPU: "0.1%", Memory: "22.8M of 32M", Disk: "84.5M of 1G", LogRate: "1.1K/s of 5K/s", CPUEntitlement: "0.2%"}, + {Index: "#3", State: "running", Since: "2017-08-02 17:12:10 PM", CPU: "0.2%", Memory: "22.9M of 32M", Disk: "84.5M of 1G", LogRate: "1.2K/s of 5K/s", CPUEntitlement: "0.4%"}, }, }, }, diff --git a/integration/v7/isolated/app_command_test.go b/integration/v7/isolated/app_command_test.go index 1be56f2fd7..4f840779ac 100644 --- a/integration/v7/isolated/app_command_test.go +++ b/integration/v7/isolated/app_command_test.go @@ -131,7 +131,7 @@ applications: Eventually(session).Should(Say(`type:\s+web`)) Eventually(session).Should(Say(`instances:\s+\d/2`)) Eventually(session).Should(Say(`memory usage:\s+128M`)) - Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+details`)) + Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+cpu entitlement\s+details`)) Eventually(session).Should(Say(`#0\s+(starting|running)\s+\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`)) Eventually(session).Should(Exit(0)) diff --git a/integration/v7/isolated/restage_command_test.go b/integration/v7/isolated/restage_command_test.go index 7f63f55e08..96860b5b2d 100644 --- a/integration/v7/isolated/restage_command_test.go +++ b/integration/v7/isolated/restage_command_test.go @@ -201,7 +201,7 @@ applications: Eventually(session).Should(Say(`sidecars:`)) Eventually(session).Should(Say(`instances:\s+\d/2`)) Eventually(session).Should(Say(`memory usage:\s+128M`)) - Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk`)) + Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+cpu entitlement`)) Eventually(session).Should(Say(`#0\s+(starting|running)\s+\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`)) Eventually(session).Should(Exit(0)) }) @@ -225,7 +225,7 @@ applications: Eventually(session).Should(Say(`sidecars:`)) Eventually(session).Should(Say(`instances:\s+\d/2`)) Eventually(session).Should(Say(`memory usage:\s+128M`)) - Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk`)) + Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+cpu entitlement`)) Eventually(session).Should(Say(`#0\s+(starting|running)\s+\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`)) Eventually(session).Should(Exit(0)) }) diff --git a/integration/v7/isolated/restart_command_test.go b/integration/v7/isolated/restart_command_test.go index 2bf370d3c0..87fd0d3d03 100644 --- a/integration/v7/isolated/restart_command_test.go +++ b/integration/v7/isolated/restart_command_test.go @@ -14,7 +14,7 @@ import ( var _ = Describe("restart command", func() { const ( - instanceStatsTitles = `\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+details` + instanceStatsTitles = `\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+cpu entitlement\s+details` instanceStatsValues = `#0\s+(starting|running)\s+\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z` ) diff --git a/integration/v7/isolated/start_command_test.go b/integration/v7/isolated/start_command_test.go index 17cbf0a3da..f6c5eb8c65 100644 --- a/integration/v7/isolated/start_command_test.go +++ b/integration/v7/isolated/start_command_test.go @@ -130,7 +130,7 @@ var _ = Describe("start command", func() { Eventually(session).Should(Say(`type:\s+web`)) Eventually(session).Should(Say(`instances:\s+1/1`)) Eventually(session).Should(Say(`memory usage:\s+1024M`)) - Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+details`)) + Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+cpu entitlement\s+details`)) Eventually(session).Should(Say(`#0\s+(starting|running)\s+\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`)) Eventually(session).Should(Exit(0)) @@ -187,7 +187,7 @@ var _ = Describe("start command", func() { Eventually(session).Should(Say(`requested state:\s+started`)) Eventually(session).Should(Say(`type:\s+web`)) Eventually(session).Should(Say(`instances:\s+1/1`)) - Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+details`)) + Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+cpu entitlement\s+details`)) Eventually(session).Should(Say(`#0\s+(starting|running)\s+\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`)) Eventually(session).Should(Exit(0)) diff --git a/integration/v7/push/disk_flag_test.go b/integration/v7/push/disk_flag_test.go index c53fb69945..0b3903aaae 100644 --- a/integration/v7/push/disk_flag_test.go +++ b/integration/v7/push/disk_flag_test.go @@ -33,7 +33,7 @@ var _ = Describe("push with disk flag", func() { Eventually(session).Should(Exit(0)) Expect(session).To(Say(`name:\s+%s`, appName)) Expect(session).To(Say(`last uploaded:\s+%s`, helpers.ReadableDateTimeRegex)) - Expect(session).To(Say(`\s+state\s+since\s+cpu\s+memory\s+disk`)) + Expect(session).To(Say(`\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+cpu entitlement`)) Expect(session).To(Say(`#0\s+running\s+\d{4}-[01]\d-[0-3]\dT[0-2][0-9]:[0-5]\d:[0-5]\dZ.+of.+of 70M`)) }) }) diff --git a/integration/v7/push/instances_flag_test.go b/integration/v7/push/instances_flag_test.go index d4387b2b32..b13a43c374 100644 --- a/integration/v7/push/instances_flag_test.go +++ b/integration/v7/push/instances_flag_test.go @@ -33,7 +33,7 @@ var _ = Describe("push with instances flag", func() { session := helpers.CF("app", appName) Eventually(session).Should(Say(`name:\s+%s`, appName)) Eventually(session).Should(Say(`last uploaded:\s+%s`, helpers.ReadableDateTimeRegex)) - Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk`)) + Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+cpu entitlement`)) Eventually(session).Should(Say(`#0\s+(running|starting)\s+\d{4}-[01]\d-[0-3]\dT[0-2][0-9]:[0-5]\d:[0-5]\dZ`)) Eventually(session).Should(Say(`#1\s+(running|starting)\s+\d{4}-[01]\d-[0-3]\dT[0-2][0-9]:[0-5]\d:[0-5]\dZ`)) Eventually(session).Should(Say(`#2\s+(running|starting)\s+\d{4}-[01]\d-[0-3]\dT[0-2][0-9]:[0-5]\d:[0-5]\dZ`)) diff --git a/integration/v7/push/memory_flag_test.go b/integration/v7/push/memory_flag_test.go index 0b577371f7..90f6bd7b7c 100644 --- a/integration/v7/push/memory_flag_test.go +++ b/integration/v7/push/memory_flag_test.go @@ -34,7 +34,7 @@ var _ = Describe("push with memory flag", func() { Eventually(session).Should(Say(`name:\s+%s`, appName)) Eventually(session).Should(Say(`last uploaded:\s+%s`, helpers.ReadableDateTimeRegex)) Eventually(session).Should(Say(`memory usage:\s+70M`)) - Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk`)) + Eventually(session).Should(Say(`\s+state\s+since\s+cpu\s+memory\s+disk\s+logging\s+cpu entitlement`)) Eventually(session).Should(Say(`#0\s+running\s+\d{4}-[01]\d-[0-3]\dT[0-2][0-9]:[0-5]\d:[0-5]\dZ`)) Eventually(session).Should(Exit(0)) }) diff --git a/types/null_float64.go b/types/null_float64.go new file mode 100644 index 0000000000..992b51811b --- /dev/null +++ b/types/null_float64.go @@ -0,0 +1,81 @@ +package types + +import ( + "fmt" + "strconv" + + "github.com/jessevdk/go-flags" +) + +// NullFloat64 is a wrapper around float64 values that can be null or a +// float64. Use IsSet to check if the value is provided, instead of checking +// against 0. +type NullFloat64 struct { + IsSet bool + Value float64 +} + +// ParseStringValue is used to parse a user provided flag argument. +func (n *NullFloat64) ParseStringValue(val string) error { + if val == "" { + n.IsSet = false + n.Value = 0 + return nil + } + + // float64Val, err := strconv.Atoi(val) + float64Val, err := strconv.ParseFloat(val, 64) + if err != nil { + n.IsSet = false + n.Value = 0 + return &flags.Error{ + Type: flags.ErrMarshal, + Message: fmt.Sprintf("invalid float64 value `%s`", val), + } + } + + n.Value = float64Val + n.IsSet = true + + return nil +} + +// IsValidValue returns an error if the input value is not a float64. +func (n *NullFloat64) IsValidValue(val string) error { + return n.ParseStringValue(val) +} + +// ParseFloat64Value is used to parse a user provided *float64 argument. +func (n *NullFloat64) ParseFloat64Value(val *float64) { + if val == nil { + n.IsSet = false + n.Value = 0 + return + } + + n.Value = *val + n.IsSet = true +} + +func (n *NullFloat64) UnmarshalFlag(val string) error { + return n.ParseStringValue(val) +} + +func (n *NullFloat64) UnmarshalJSON(rawJSON []byte) error { + stringValue := string(rawJSON) + + if stringValue == JsonNull { + n.Value = 0 + n.IsSet = false + return nil + } + + return n.ParseStringValue(stringValue) +} + +func (n NullFloat64) MarshalJSON() ([]byte, error) { + if n.IsSet { + return []byte(fmt.Sprint(n.Value)), nil + } + return []byte(JsonNull), nil +} diff --git a/types/null_float64_test.go b/types/null_float64_test.go new file mode 100644 index 0000000000..77acdfb5b8 --- /dev/null +++ b/types/null_float64_test.go @@ -0,0 +1,145 @@ +package types_test + +import ( + . "code.cloudfoundry.org/cli/types" + "github.com/jessevdk/go-flags" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("NullFloat64", func() { + var nullFloat64 NullFloat64 + + BeforeEach(func() { + nullFloat64 = NullFloat64{ + IsSet: true, + Value: 0xBAD, + } + }) + + Describe("IsValidValue", func() { + var ( + input string + executeErr error + ) + + JustBeforeEach(func() { + executeErr = nullFloat64.IsValidValue(input) + }) + + When("the value is a positive float", func() { + BeforeEach(func() { + input = "1.01" + }) + + It("does not error", func() { + Expect(executeErr).ToNot(HaveOccurred()) + }) + }) + + When("the value is a negative float", func() { + BeforeEach(func() { + input = "-21.94" + }) + + It("does not error", func() { + Expect(executeErr).ToNot(HaveOccurred()) + }) + }) + + When("the value is a non float", func() { + BeforeEach(func() { + input = "not-a-integer" + }) + + It("returns an error", func() { + Expect(executeErr).To(MatchError("invalid float64 value `not-a-integer`")) + }) + }) + }) + + Describe("ParseFloat64Value", func() { + When("nil is provided", func() { + It("sets IsSet to false", func() { + nullFloat64.ParseFloat64Value(nil) + Expect(nullFloat64).To(Equal(NullFloat64{Value: 0, IsSet: false})) + }) + }) + + When("non-nil pointer is provided", func() { + It("sets IsSet to true and Value to provided value", func() { + n := 5.04 + nullFloat64.ParseFloat64Value(&n) + Expect(nullFloat64).To(Equal(NullFloat64{Value: 5.04, IsSet: true})) + }) + }) + }) + + Describe("ParseStringValue", func() { + When("the empty string is provided", func() { + It("sets IsSet to false", func() { + err := nullFloat64.ParseStringValue("") + Expect(err).ToNot(HaveOccurred()) + Expect(nullFloat64).To(Equal(NullFloat64{Value: 0, IsSet: false})) + }) + }) + + When("an invalid float64 is provided", func() { + It("returns an error", func() { + err := nullFloat64.ParseStringValue("abcdef") + Expect(err).To(MatchError(&flags.Error{ + Type: flags.ErrMarshal, + Message: "invalid float64 value `abcdef`", + })) + Expect(nullFloat64).To(Equal(NullFloat64{Value: 0, IsSet: false})) + }) + }) + + When("a valid float64 is provided", func() { + It("stores the integer and sets IsSet to true", func() { + err := nullFloat64.ParseStringValue("0") + Expect(err).ToNot(HaveOccurred()) + Expect(nullFloat64).To(Equal(NullFloat64{Value: 0, IsSet: true})) + }) + }) + }) + + Describe("UnmarshalJSON", func() { + When("float64 value is provided", func() { + It("parses JSON number correctly", func() { + err := nullFloat64.UnmarshalJSON([]byte("42.333333")) + Expect(err).ToNot(HaveOccurred()) + Expect(nullFloat64).To(Equal(NullFloat64{Value: 42.333333, IsSet: true})) + }) + }) + + When("a null value is provided", func() { + It("returns an unset NullFloat64", func() { + err := nullFloat64.UnmarshalJSON([]byte("null")) + Expect(err).ToNot(HaveOccurred()) + Expect(nullFloat64).To(Equal(NullFloat64{Value: 0, IsSet: false})) + }) + }) + + When("an empty string is provided", func() { + It("returns an unset NullFloat64", func() { + err := nullFloat64.UnmarshalJSON([]byte("")) + Expect(err).ToNot(HaveOccurred()) + Expect(nullFloat64).To(Equal(NullFloat64{Value: 0, IsSet: false})) + }) + }) + }) + + DescribeTable("MarshalJSON", + func(nullFloat64 NullFloat64, expectedBytes []byte) { + bytes, err := nullFloat64.MarshalJSON() + Expect(err).ToNot(HaveOccurred()) + Expect(bytes).To(Equal(expectedBytes)) + }, + Entry("negative number", NullFloat64{IsSet: true, Value: -1.5}, []byte("-1.5")), + Entry("positive number", NullFloat64{IsSet: true, Value: 1.8}, []byte("1.8")), + Entry("0", NullFloat64{IsSet: true, Value: 0.0}, []byte("0")), + Entry("no value", NullFloat64{IsSet: false}, []byte("null")), + ) +})