diff --git a/actor/v7action/process_readiness_health_check.go b/actor/v7action/process_readiness_health_check.go new file mode 100644 index 0000000000..4dbe860325 --- /dev/null +++ b/actor/v7action/process_readiness_health_check.go @@ -0,0 +1,72 @@ +package v7action + +import ( + "sort" + + "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" +) + +type ProcessReadinessHealthCheck struct { + ProcessType string + HealthCheckType constant.HealthCheckType + Endpoint string + InvocationTimeout int64 + Interval int64 +} + +type ProcessReadinessHealthChecks []ProcessReadinessHealthCheck + +func (phs ProcessReadinessHealthChecks) Sort() { + sort.Slice(phs, func(i int, j int) bool { + var iScore int + var jScore int + + switch phs[i].ProcessType { + case constant.ProcessTypeWeb: + iScore = 0 + default: + iScore = 1 + } + + switch phs[j].ProcessType { + case constant.ProcessTypeWeb: + jScore = 0 + default: + jScore = 1 + } + + if iScore == 1 && jScore == 1 { + return phs[i].ProcessType < phs[j].ProcessType + } + return iScore < jScore + }) +} + +func (actor Actor) GetApplicationProcessReadinessHealthChecksByNameAndSpace(appName string, spaceGUID string) ([]ProcessReadinessHealthCheck, Warnings, error) { + app, allWarnings, err := actor.GetApplicationByNameAndSpace(appName, spaceGUID) + if err != nil { + return nil, allWarnings, err + } + + ccv3Processes, warnings, err := actor.CloudControllerClient.GetApplicationProcesses(app.GUID) + allWarnings = append(allWarnings, Warnings(warnings)...) + if err != nil { + return nil, allWarnings, err + } + + var processReadinessHealthChecks ProcessReadinessHealthChecks + for _, ccv3Process := range ccv3Processes { + processReadinessHealthCheck := ProcessReadinessHealthCheck{ + ProcessType: ccv3Process.Type, + HealthCheckType: ccv3Process.ReadinessHealthCheckType, + Endpoint: ccv3Process.ReadinessHealthCheckEndpoint, + InvocationTimeout: ccv3Process.ReadinessHealthCheckInvocationTimeout, + Interval: ccv3Process.ReadinessHealthCheckInterval, + } + processReadinessHealthChecks = append(processReadinessHealthChecks, processReadinessHealthCheck) + } + + processReadinessHealthChecks.Sort() + + return processReadinessHealthChecks, allWarnings, nil +} diff --git a/actor/v7action/process_readiness_health_check_test.go b/actor/v7action/process_readiness_health_check_test.go new file mode 100644 index 0000000000..056e3666c7 --- /dev/null +++ b/actor/v7action/process_readiness_health_check_test.go @@ -0,0 +1,176 @@ +package v7action_test + +import ( + "errors" + + "code.cloudfoundry.org/cli/actor/actionerror" + . "code.cloudfoundry.org/cli/actor/v7action" + "code.cloudfoundry.org/cli/actor/v7action/v7actionfakes" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" + "code.cloudfoundry.org/cli/resources" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Process Readiness Health Check Actions", func() { + var ( + actor *Actor + fakeCloudControllerClient *v7actionfakes.FakeCloudControllerClient + ) + + BeforeEach(func() { + fakeCloudControllerClient = new(v7actionfakes.FakeCloudControllerClient) + actor = NewActor(fakeCloudControllerClient, nil, nil, nil, nil, nil) + }) + + Describe("ProcessReadinessHealthChecks", func() { + var readinessHealthChecks ProcessReadinessHealthChecks + + BeforeEach(func() { + readinessHealthChecks = ProcessReadinessHealthChecks{ + { + ProcessType: "worker", + HealthCheckType: constant.Process, + }, + { + ProcessType: "console", + HealthCheckType: constant.Process, + }, + { + ProcessType: constant.ProcessTypeWeb, + HealthCheckType: constant.HTTP, + Endpoint: constant.ProcessHealthCheckEndpointDefault, + }, + } + }) + + Describe("Sort", func() { + It("sorts readiness health checks with web first and then alphabetically sorted", func() { + readinessHealthChecks.Sort() + Expect(readinessHealthChecks[0].ProcessType).To(Equal(constant.ProcessTypeWeb)) + Expect(readinessHealthChecks[1].ProcessType).To(Equal("console")) + Expect(readinessHealthChecks[2].ProcessType).To(Equal("worker")) + }) + }) + }) + + Describe("GetApplicationProcessReadinessHealthChecksByNameAndSpace", func() { + var ( + warnings Warnings + executeErr error + processReadinessHealthChecks []ProcessReadinessHealthCheck + ) + + JustBeforeEach(func() { + processReadinessHealthChecks, warnings, executeErr = actor.GetApplicationProcessReadinessHealthChecksByNameAndSpace("some-app-name", "some-space-guid") + }) + + When("application does not exist", func() { + BeforeEach(func() { + fakeCloudControllerClient.GetApplicationsReturns( + []resources.Application{}, + ccv3.Warnings{"some-warning"}, + nil, + ) + }) + + It("returns the error and warnings", func() { + Expect(executeErr).To(Equal(actionerror.ApplicationNotFoundError{Name: "some-app-name"})) + Expect(warnings).To(ConsistOf("some-warning")) + }) + }) + + When("getting application returns an error", func() { + var expectedErr error + + BeforeEach(func() { + expectedErr = errors.New("some-error") + fakeCloudControllerClient.GetApplicationsReturns( + []resources.Application{}, + ccv3.Warnings{"some-warning"}, + expectedErr, + ) + }) + + It("returns the error and warnings", func() { + Expect(executeErr).To(Equal(expectedErr)) + Expect(warnings).To(ConsistOf("some-warning")) + }) + }) + + When("application exists", func() { + BeforeEach(func() { + fakeCloudControllerClient.GetApplicationsReturns( + []resources.Application{ + { + GUID: "some-app-guid", + }, + }, + ccv3.Warnings{"some-warning"}, + nil, + ) + }) + + When("getting application processes returns an error", func() { + var expectedErr error + + BeforeEach(func() { + expectedErr = errors.New("some-error") + fakeCloudControllerClient.GetApplicationProcessesReturns( + []resources.Process{}, + ccv3.Warnings{"some-process-warning"}, + expectedErr, + ) + }) + + It("returns the error and warnings", func() { + Expect(executeErr).To(Equal(expectedErr)) + Expect(warnings).To(ConsistOf("some-warning", "some-process-warning")) + }) + }) + + When("application has processes", func() { + BeforeEach(func() { + fakeCloudControllerClient.GetApplicationProcessesReturns( + []resources.Process{ + { + GUID: "process-guid-1", + Type: "process-type-1", + ReadinessHealthCheckType: "readiness-health-check-type-1", + ReadinessHealthCheckEndpoint: "readiness-health-check-endpoint-1", + ReadinessHealthCheckInvocationTimeout: 42, + }, + { + GUID: "process-guid-2", + Type: "process-type-2", + ReadinessHealthCheckType: "readiness-health-check-type-2", + ReadinessHealthCheckInvocationTimeout: 0, + }, + }, + ccv3.Warnings{"some-process-warning"}, + nil, + ) + }) + + It("returns health checks", func() { + Expect(executeErr).NotTo(HaveOccurred()) + Expect(warnings).To(ConsistOf("some-warning", "some-process-warning")) + Expect(processReadinessHealthChecks).To(Equal([]ProcessReadinessHealthCheck{ + { + ProcessType: "process-type-1", + HealthCheckType: "readiness-health-check-type-1", + Endpoint: "readiness-health-check-endpoint-1", + InvocationTimeout: 42, + }, + { + ProcessType: "process-type-2", + HealthCheckType: "readiness-health-check-type-2", + InvocationTimeout: 0, + }, + })) + }) + }) + }) + }) +}) diff --git a/api/cloudcontroller/ccv3/process_test.go b/api/cloudcontroller/ccv3/process_test.go index fc584e4f37..7450143749 100644 --- a/api/cloudcontroller/ccv3/process_test.go +++ b/api/cloudcontroller/ccv3/process_test.go @@ -56,6 +56,14 @@ var _ = Describe("Process", func() { "endpoint": "/health", "invocation_timeout": 42 } + }, + "readiness_health_check": { + "type": "http", + "data": { + "interval": 9, + "endpoint": "/foo", + "invocation_timeout": 2 + } } }` server.AppendHandlers( @@ -70,18 +78,22 @@ var _ = Describe("Process", func() { Expect(err).NotTo(HaveOccurred()) Expect(warnings).To(ConsistOf("this is a warning")) Expect(process).To(MatchAllFields(Fields{ - "GUID": Equal("process-1-guid"), - "Type": Equal("some-type"), - "AppGUID": Equal("some-app-guid"), - "Command": Equal(types.FilteredString{IsSet: true, Value: "start-command-1"}), - "Instances": Equal(types.NullInt{Value: 22, IsSet: true}), - "MemoryInMB": Equal(types.NullUint64{Value: 32, IsSet: true}), - "DiskInMB": Equal(types.NullUint64{Value: 1024, IsSet: true}), - "LogRateLimitInBPS": Equal(types.NullInt{Value: 512, IsSet: true}), - "HealthCheckType": Equal(constant.HTTP), - "HealthCheckEndpoint": Equal("/health"), - "HealthCheckInvocationTimeout": BeEquivalentTo(42), - "HealthCheckTimeout": BeEquivalentTo(90), + "GUID": Equal("process-1-guid"), + "Type": Equal("some-type"), + "AppGUID": Equal("some-app-guid"), + "Command": Equal(types.FilteredString{IsSet: true, Value: "start-command-1"}), + "Instances": Equal(types.NullInt{Value: 22, IsSet: true}), + "MemoryInMB": Equal(types.NullUint64{Value: 32, IsSet: true}), + "DiskInMB": Equal(types.NullUint64{Value: 1024, IsSet: true}), + "LogRateLimitInBPS": Equal(types.NullInt{Value: 512, IsSet: true}), + "HealthCheckType": Equal(constant.HTTP), + "HealthCheckEndpoint": Equal("/health"), + "HealthCheckInvocationTimeout": BeEquivalentTo(42), + "HealthCheckTimeout": BeEquivalentTo(90), + "ReadinessHealthCheckType": Equal(constant.HTTP), + "ReadinessHealthCheckEndpoint": Equal("/foo"), + "ReadinessHealthCheckInvocationTimeout": BeEquivalentTo(2), + "ReadinessHealthCheckInterval": BeEquivalentTo(9), })) }) }) @@ -317,6 +329,14 @@ var _ = Describe("Process", func() { "endpoint": "/health", "invocation_timeout": 42 } + }, + "readiness_health_check": { + "type": "http", + "data": { + "interval": 9, + "endpoint": "/foo", + "invocation_timeout": 2 + } } }` server.AppendHandlers( @@ -331,18 +351,22 @@ var _ = Describe("Process", func() { Expect(err).NotTo(HaveOccurred()) Expect(warnings).To(ConsistOf("this is a warning")) Expect(process).To(MatchAllFields(Fields{ - "GUID": Equal("process-1-guid"), - "Type": Equal("some-type"), - "AppGUID": Equal("some-app-guid"), - "Command": Equal(types.FilteredString{IsSet: true, Value: "start-command-1"}), - "Instances": Equal(types.NullInt{Value: 22, IsSet: true}), - "MemoryInMB": Equal(types.NullUint64{Value: 32, IsSet: true}), - "DiskInMB": Equal(types.NullUint64{Value: 1024, IsSet: true}), - "LogRateLimitInBPS": Equal(types.NullInt{Value: 64, IsSet: true}), - "HealthCheckType": Equal(constant.HTTP), - "HealthCheckEndpoint": Equal("/health"), - "HealthCheckInvocationTimeout": BeEquivalentTo(42), - "HealthCheckTimeout": BeEquivalentTo(90), + "GUID": Equal("process-1-guid"), + "Type": Equal("some-type"), + "AppGUID": Equal("some-app-guid"), + "Command": Equal(types.FilteredString{IsSet: true, Value: "start-command-1"}), + "Instances": Equal(types.NullInt{Value: 22, IsSet: true}), + "MemoryInMB": Equal(types.NullUint64{Value: 32, IsSet: true}), + "DiskInMB": Equal(types.NullUint64{Value: 1024, IsSet: true}), + "LogRateLimitInBPS": Equal(types.NullInt{Value: 64, IsSet: true}), + "HealthCheckType": Equal(constant.HTTP), + "HealthCheckEndpoint": Equal("/health"), + "HealthCheckInvocationTimeout": BeEquivalentTo(42), + "HealthCheckTimeout": BeEquivalentTo(90), + "ReadinessHealthCheckType": Equal(constant.HTTP), + "ReadinessHealthCheckEndpoint": Equal("/foo"), + "ReadinessHealthCheckInvocationTimeout": BeEquivalentTo(2), + "ReadinessHealthCheckInterval": BeEquivalentTo(9), })) }) }) diff --git a/command/common/command_list_v7.go b/command/common/command_list_v7.go index da3513fd39..d7f4796b47 100644 --- a/command/common/command_list_v7.go +++ b/command/common/command_list_v7.go @@ -87,6 +87,7 @@ type commandList struct { FeatureFlag v7.FeatureFlagCommand `command:"feature-flag" description:"Retrieve an individual feature flag with status"` FeatureFlags v7.FeatureFlagsCommand `command:"feature-flags" description:"Retrieve list of feature flags with status"` GetHealthCheck v7.GetHealthCheckCommand `command:"get-health-check" description:"Show the type of health check performed on an app"` + GetReadinessHealthCheck v7.GetReadinessHealthCheckCommand `command:"get-readiness-health-check" description:"Show the type of readiness health check performed on an app"` Help HelpCommand `command:"help" alias:"h" description:"Show help"` InstallPlugin InstallPluginCommand `command:"install-plugin" description:"Install CLI plugin"` IsolationSegments v7.IsolationSegmentsCommand `command:"isolation-segments" description:"List all isolation segments"` diff --git a/command/common/internal/help_all_display.go b/command/common/internal/help_all_display.go index 5de55da528..9bbd28714a 100644 --- a/command/common/internal/help_all_display.go +++ b/command/common/internal/help_all_display.go @@ -23,7 +23,8 @@ var HelpCategoryList = []HelpCategory{ {"env", "set-env", "unset-env"}, {"stacks", "stack"}, {"copy-source", "create-app-manifest"}, - {"get-health-check", "set-health-check", "enable-ssh", "disable-ssh", "ssh-enabled", "ssh"}, + {"get-health-check", "set-health-check", "get-readiness-health-check"}, + {"enable-ssh", "disable-ssh", "ssh-enabled", "ssh"}, }, }, { diff --git a/command/v7/actor.go b/command/v7/actor.go index 026669f8d6..2f45f47dff 100644 --- a/command/v7/actor.go +++ b/command/v7/actor.go @@ -94,6 +94,7 @@ type Actor interface { GetApplicationLabels(appName string, spaceGUID string) (map[string]types.NullString, v7action.Warnings, error) GetApplicationPackages(appName string, spaceGUID string) ([]resources.Package, v7action.Warnings, error) GetApplicationProcessHealthChecksByNameAndSpace(appName string, spaceGUID string) ([]v7action.ProcessHealthCheck, v7action.Warnings, error) + GetApplicationProcessReadinessHealthChecksByNameAndSpace(appName string, spaceGUID string) ([]v7action.ProcessReadinessHealthCheck, v7action.Warnings, error) GetApplicationRevisionsDeployed(appGUID string) ([]resources.Revision, v7action.Warnings, error) GetApplicationRoutes(appGUID string) ([]resources.Route, v7action.Warnings, error) GetApplicationTasks(appName string, sortOrder v7action.SortOrder) ([]resources.Task, v7action.Warnings, error) diff --git a/command/v7/get_readiness_health_check_command.go b/command/v7/get_readiness_health_check_command.go new file mode 100644 index 0000000000..83cbabc12a --- /dev/null +++ b/command/v7/get_readiness_health_check_command.go @@ -0,0 +1,85 @@ +package v7 + +import ( + "fmt" + "strconv" + + "code.cloudfoundry.org/cli/actor/v7action" + "code.cloudfoundry.org/cli/command/flag" + "code.cloudfoundry.org/cli/util/ui" +) + +type GetReadinessHealthCheckCommand struct { + BaseCommand + + RequiredArgs flag.AppName `positional-args:"yes"` + usage interface{} `usage:"CF_NAME get-readiness-health-check APP_NAME"` +} + +func (cmd GetReadinessHealthCheckCommand) Execute(args []string) error { + err := cmd.SharedActor.CheckTarget(true, true) + if err != nil { + return err + } + + user, err := cmd.Actor.GetCurrentUser() + if err != nil { + return err + } + + cmd.UI.DisplayTextWithFlavor("Getting readiness health check type 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, + }) + + processReadinessHealthChecks, warnings, err := cmd.Actor.GetApplicationProcessReadinessHealthChecksByNameAndSpace(cmd.RequiredArgs.AppName, cmd.Config.TargetedSpace().GUID) + cmd.UI.DisplayWarnings(warnings) + if err != nil { + return err + } + + cmd.UI.DisplayNewline() + + if len(processReadinessHealthChecks) == 0 { + cmd.UI.DisplayText("App has no processes") + return nil + } + + return cmd.DisplayProcessTable(processReadinessHealthChecks) +} + +func (cmd GetReadinessHealthCheckCommand) DisplayProcessTable(processReadinessHealthChecks []v7action.ProcessReadinessHealthCheck) error { + table := [][]string{ + { + cmd.UI.TranslateText("process"), + cmd.UI.TranslateText("type"), + cmd.UI.TranslateText("endpoint (for http)"), + cmd.UI.TranslateText("invocation timeout"), + cmd.UI.TranslateText("interval"), + }, + } + + for _, healthCheck := range processReadinessHealthChecks { + var invocationTimeout, interval string + if healthCheck.InvocationTimeout != 0 { + invocationTimeout = strconv.FormatInt(healthCheck.InvocationTimeout, 10) + } + if healthCheck.Interval != 0 { + interval = strconv.FormatInt(healthCheck.Interval, 10) + } + + table = append(table, []string{ + healthCheck.ProcessType, + string(healthCheck.HealthCheckType), + healthCheck.Endpoint, + fmt.Sprint(invocationTimeout), + fmt.Sprint(interval), + }) + } + + cmd.UI.DisplayTableWithHeader("", table, ui.DefaultTableSpacePadding) + + return nil +} diff --git a/command/v7/get_readiness_health_check_command_test.go b/command/v7/get_readiness_health_check_command_test.go new file mode 100644 index 0000000000..b3c5137ca6 --- /dev/null +++ b/command/v7/get_readiness_health_check_command_test.go @@ -0,0 +1,161 @@ +package v7_test + +import ( + "errors" + + "code.cloudfoundry.org/cli/actor/actionerror" + "code.cloudfoundry.org/cli/actor/v7action" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant" + "code.cloudfoundry.org/cli/command/commandfakes" + "code.cloudfoundry.org/cli/command/flag" + . "code.cloudfoundry.org/cli/command/v7" + "code.cloudfoundry.org/cli/command/v7/v7fakes" + "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("get-readiness-health-check Command", func() { + var ( + cmd GetReadinessHealthCheckCommand + testUI *ui.UI + fakeConfig *commandfakes.FakeConfig + fakeSharedActor *commandfakes.FakeSharedActor + fakeActor *v7fakes.FakeActor + binaryName string + executeErr error + app string + ) + + BeforeEach(func() { + testUI = ui.NewTestUI(nil, NewBuffer(), NewBuffer()) + fakeConfig = new(commandfakes.FakeConfig) + fakeSharedActor = new(commandfakes.FakeSharedActor) + fakeActor = new(v7fakes.FakeActor) + + binaryName = "faceman" + fakeConfig.BinaryNameReturns(binaryName) + app = "some-app" + + cmd = GetReadinessHealthCheckCommand{ + RequiredArgs: flag.AppName{AppName: app}, + + BaseCommand: BaseCommand{ + UI: testUI, + Config: fakeConfig, + SharedActor: fakeSharedActor, + Actor: fakeActor, + }, + } + + fakeConfig.TargetedOrganizationReturns(configv3.Organization{ + Name: "some-org", + GUID: "some-org-guid", + }) + fakeConfig.TargetedSpaceReturns(configv3.Space{ + Name: "some-space", + GUID: "some-space-guid", + }) + + fakeActor.GetCurrentUserReturns(configv3.User{Name: "steve"}, nil) + }) + + JustBeforeEach(func() { + executeErr = cmd.Execute(nil) + }) + + When("checking target fails", func() { + BeforeEach(func() { + fakeSharedActor.CheckTargetReturns(actionerror.NoOrganizationTargetedError{BinaryName: binaryName}) + }) + + It("returns an error", func() { + Expect(executeErr).To(MatchError(actionerror.NoOrganizationTargetedError{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 not logged in", func() { + var expectedErr error + + BeforeEach(func() { + expectedErr = errors.New("some current user error") + fakeActor.GetCurrentUserReturns(configv3.User{}, expectedErr) + }) + + It("return an error", func() { + Expect(executeErr).To(Equal(expectedErr)) + }) + }) + + When("getting the application process readiness health checks returns an error", func() { + var expectedErr error + + BeforeEach(func() { + expectedErr = actionerror.ApplicationNotFoundError{Name: app} + fakeActor.GetApplicationProcessReadinessHealthChecksByNameAndSpaceReturns(nil, v7action.Warnings{"warning-1", "warning-2"}, expectedErr) + }) + + It("returns the error and prints warnings", func() { + Expect(executeErr).To(Equal(actionerror.ApplicationNotFoundError{Name: app})) + + Expect(testUI.Out).To(Say("Getting readiness health check type for app some-app in org some-org / space some-space as steve...")) + + Expect(testUI.Err).To(Say("warning-1")) + Expect(testUI.Err).To(Say("warning-2")) + }) + }) + + When("app has no processes", func() { + BeforeEach(func() { + fakeActor.GetApplicationProcessReadinessHealthChecksByNameAndSpaceReturns( + []v7action.ProcessReadinessHealthCheck{}, + v7action.Warnings{"warning-1", "warning-2"}, + nil) + }) + + It("displays a message that there are no processes", func() { + Expect(executeErr).ToNot(HaveOccurred()) + + Expect(testUI.Out).To(Say("Getting readiness health check type for app some-app in org some-org / space some-space as steve...")) + Expect(testUI.Out).To(Say("App has no processes")) + + Expect(fakeActor.GetApplicationProcessReadinessHealthChecksByNameAndSpaceCallCount()).To(Equal(1)) + appName, spaceGUID := fakeActor.GetApplicationProcessReadinessHealthChecksByNameAndSpaceArgsForCall(0) + Expect(appName).To(Equal("some-app")) + Expect(spaceGUID).To(Equal("some-space-guid")) + }) + }) + + When("app has processes", func() { + BeforeEach(func() { + appProcessReadinessHealthChecks := []v7action.ProcessReadinessHealthCheck{ + {ProcessType: constant.ProcessTypeWeb, HealthCheckType: constant.HTTP, Endpoint: "/foo", InvocationTimeout: 10, Interval: 2}, + {ProcessType: "queue", HealthCheckType: constant.Port, Endpoint: "", InvocationTimeout: 0}, + {ProcessType: "timer", HealthCheckType: constant.Process, Endpoint: "", InvocationTimeout: 5}, + } + fakeActor.GetApplicationProcessReadinessHealthChecksByNameAndSpaceReturns(appProcessReadinessHealthChecks, v7action.Warnings{"warning-1", "warning-2"}, nil) + }) + + It("prints the readiness health check type of each process and warnings", func() { + Expect(executeErr).ToNot(HaveOccurred()) + + Expect(testUI.Out).To(Say("Getting readiness health check type for app some-app in org some-org / space some-space as steve...")) + Expect(testUI.Out).To(Say(`process\s+type\s+endpoint\s+\(for http\)\s+invocation timeout\s+interval\n`)) + Expect(testUI.Out).To(Say(`web\s+http\s+/foo\s+10\s+2\n`)) + Expect(testUI.Out).To(Say(`queue\s+port\s+\n`)) + Expect(testUI.Out).To(Say(`timer\s+process\s+5\s+\n`)) + + Expect(fakeActor.GetApplicationProcessReadinessHealthChecksByNameAndSpaceCallCount()).To(Equal(1)) + appName, spaceGUID := fakeActor.GetApplicationProcessReadinessHealthChecksByNameAndSpaceArgsForCall(0) + Expect(appName).To(Equal("some-app")) + Expect(spaceGUID).To(Equal("some-space-guid")) + }) + }) +}) diff --git a/command/v7/v7fakes/fake_actor.go b/command/v7/v7fakes/fake_actor.go index 8783e2a5e3..a74d755bab 100644 --- a/command/v7/v7fakes/fake_actor.go +++ b/command/v7/v7fakes/fake_actor.go @@ -1116,6 +1116,22 @@ type FakeActor struct { result2 v7action.Warnings result3 error } + GetApplicationProcessReadinessHealthChecksByNameAndSpaceStub func(string, string) ([]v7action.ProcessReadinessHealthCheck, v7action.Warnings, error) + getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex sync.RWMutex + getApplicationProcessReadinessHealthChecksByNameAndSpaceArgsForCall []struct { + arg1 string + arg2 string + } + getApplicationProcessReadinessHealthChecksByNameAndSpaceReturns struct { + result1 []v7action.ProcessReadinessHealthCheck + result2 v7action.Warnings + result3 error + } + getApplicationProcessReadinessHealthChecksByNameAndSpaceReturnsOnCall map[int]struct { + result1 []v7action.ProcessReadinessHealthCheck + result2 v7action.Warnings + result3 error + } GetApplicationRevisionsDeployedStub func(string) ([]resources.Revision, v7action.Warnings, error) getApplicationRevisionsDeployedMutex sync.RWMutex getApplicationRevisionsDeployedArgsForCall []struct { @@ -8498,6 +8514,74 @@ func (fake *FakeActor) GetApplicationProcessHealthChecksByNameAndSpaceReturnsOnC }{result1, result2, result3} } +func (fake *FakeActor) GetApplicationProcessReadinessHealthChecksByNameAndSpace(arg1 string, arg2 string) ([]v7action.ProcessReadinessHealthCheck, v7action.Warnings, error) { + fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.Lock() + ret, specificReturn := fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceReturnsOnCall[len(fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceArgsForCall)] + fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceArgsForCall = append(fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + stub := fake.GetApplicationProcessReadinessHealthChecksByNameAndSpaceStub + fakeReturns := fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceReturns + fake.recordInvocation("GetApplicationProcessReadinessHealthChecksByNameAndSpace", []interface{}{arg1, arg2}) + fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeActor) GetApplicationProcessReadinessHealthChecksByNameAndSpaceCallCount() int { + fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.RLock() + defer fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.RUnlock() + return len(fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceArgsForCall) +} + +func (fake *FakeActor) GetApplicationProcessReadinessHealthChecksByNameAndSpaceCalls(stub func(string, string) ([]v7action.ProcessReadinessHealthCheck, v7action.Warnings, error)) { + fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.Lock() + defer fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.Unlock() + fake.GetApplicationProcessReadinessHealthChecksByNameAndSpaceStub = stub +} + +func (fake *FakeActor) GetApplicationProcessReadinessHealthChecksByNameAndSpaceArgsForCall(i int) (string, string) { + fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.RLock() + defer fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.RUnlock() + argsForCall := fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeActor) GetApplicationProcessReadinessHealthChecksByNameAndSpaceReturns(result1 []v7action.ProcessReadinessHealthCheck, result2 v7action.Warnings, result3 error) { + fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.Lock() + defer fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.Unlock() + fake.GetApplicationProcessReadinessHealthChecksByNameAndSpaceStub = nil + fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceReturns = struct { + result1 []v7action.ProcessReadinessHealthCheck + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeActor) GetApplicationProcessReadinessHealthChecksByNameAndSpaceReturnsOnCall(i int, result1 []v7action.ProcessReadinessHealthCheck, result2 v7action.Warnings, result3 error) { + fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.Lock() + defer fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.Unlock() + fake.GetApplicationProcessReadinessHealthChecksByNameAndSpaceStub = nil + if fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceReturnsOnCall == nil { + fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceReturnsOnCall = make(map[int]struct { + result1 []v7action.ProcessReadinessHealthCheck + result2 v7action.Warnings + result3 error + }) + } + fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceReturnsOnCall[i] = struct { + result1 []v7action.ProcessReadinessHealthCheck + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + func (fake *FakeActor) GetApplicationRevisionsDeployed(arg1 string) ([]resources.Revision, v7action.Warnings, error) { fake.getApplicationRevisionsDeployedMutex.Lock() ret, specificReturn := fake.getApplicationRevisionsDeployedReturnsOnCall[len(fake.getApplicationRevisionsDeployedArgsForCall)] @@ -19795,6 +19879,8 @@ func (fake *FakeActor) Invocations() map[string][][]interface{} { defer fake.getApplicationPackagesMutex.RUnlock() fake.getApplicationProcessHealthChecksByNameAndSpaceMutex.RLock() defer fake.getApplicationProcessHealthChecksByNameAndSpaceMutex.RUnlock() + fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.RLock() + defer fake.getApplicationProcessReadinessHealthChecksByNameAndSpaceMutex.RUnlock() fake.getApplicationRevisionsDeployedMutex.RLock() defer fake.getApplicationRevisionsDeployedMutex.RUnlock() fake.getApplicationRoutesMutex.RLock() diff --git a/integration/v7/isolated/get_readiness_health_check_command_test.go b/integration/v7/isolated/get_readiness_health_check_command_test.go new file mode 100644 index 0000000000..39fb50c382 --- /dev/null +++ b/integration/v7/isolated/get_readiness_health_check_command_test.go @@ -0,0 +1,197 @@ +package isolated + +import ( + . "code.cloudfoundry.org/cli/cf/util/testhelpers/matchers" + "code.cloudfoundry.org/cli/integration/helpers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gbytes" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("get-readiness-health-check command", func() { + var ( + orgName string + spaceName string + appName string + ) + + BeforeEach(func() { + orgName = helpers.NewOrgName() + spaceName = helpers.NewSpaceName() + appName = helpers.PrefixedRandomName("app") + }) + + Describe("help", func() { + When("--help flag is set", func() { + It("appears in cf help -a", func() { + session := helpers.CF("help", "-a") + Eventually(session).Should(Exit(0)) + Expect(session).To(HaveCommandInCategoryWithDescription("get-readiness-health-check", "APPS", "Show the type of readiness health check performed on an app")) + }) + + It("Displays command usage to output", func() { + session := helpers.CF("get-readiness-health-check", "--help") + + Eventually(session).Should(Say("NAME:")) + Eventually(session).Should(Say("get-readiness-health-check - Show the type of readiness health check performed on an app")) + Eventually(session).Should(Say("USAGE:")) + Eventually(session).Should(Say("cf get-readiness-health-check APP_NAME")) + + Eventually(session).Should(Exit(0)) + }) + }) + }) + + When("the app name is not provided", func() { + It("tells the user that the app name is required, prints help text, and exits 1", func() { + session := helpers.CF("get-health-check") + + Eventually(session.Err).Should(Say("Incorrect Usage: the required argument `APP_NAME` was not provided")) + Eventually(session).Should(Say("NAME:")) + Eventually(session).Should(Exit(1)) + }) + }) + + When("the environment is not setup correctly", func() { + It("fails with the appropriate errors", func() { + helpers.CheckEnvironmentTargetedCorrectly(true, true, ReadOnlyOrg, "get-readiness-health-check", appName) + }) + }) + + When("the environment is set up correctly", func() { + var username string + + BeforeEach(func() { + helpers.SetupCF(orgName, spaceName) + username, _ = helpers.GetCredentials() + }) + + AfterEach(func() { + helpers.QuickDeleteOrg(orgName) + }) + + When("the input is invalid", func() { + When("there are not enough arguments", func() { + It("outputs the usage and exits 1", func() { + session := helpers.CF("get-readiness-health-check") + + Eventually(session.Err).Should(Say("Incorrect Usage:")) + Eventually(session).Should(Say("NAME:")) + Eventually(session).Should(Exit(1)) + }) + }) + }) + + When("the app exists", func() { + BeforeEach(func() { + helpers.WithProcfileApp(func(appDir string) { + Eventually(helpers.CustomCF(helpers.CFEnv{WorkingDirectory: appDir}, "push", appName, "--no-start")).Should(Exit(0)) + }) + }) + + It("displays the readiness health check types for each process", func() { + session := helpers.CF("get-readiness-health-check", appName) + + Eventually(session).Should(Say(`Getting readiness health check type for app %s in org %s / space %s as %s\.\.\.`, appName, orgName, spaceName, username)) + Eventually(session).Should(Say(`process\s+type\s+endpoint \(for http\)\s+invocation timeout\s+interval\n`)) + Eventually(session).Should(Say(`web\s+process\s+\n`)) + + Eventually(session).Should(Exit(0)) + }) + + // TODO: Implement these when the set-readiness-health-check-command is implemented + // When("the health check type is http", func() { + // BeforeEach(func() { + // Eventually(helpers.CF("set-health-check", appName, "http")).Should(Exit(0)) + // }) + + // It("shows the health check type is http with an endpoint of `/`", func() { + // session := helpers.CF("get-health-check", appName) + + // Eventually(session).Should(Say(`Getting health check type for app %s in org %s / space %s as %s\.\.\.`, appName, orgName, spaceName, username)) + // Eventually(session).Should(Say(`process\s+health check\s+endpoint \(for http\)\s+invocation timeout\n`)) + // Eventually(session).Should(Say(`web\s+http\s+/\s+1\n`)) + + // Eventually(session).Should(Exit(0)) + // }) + // }) + + // When("the health check type is http with a custom endpoint", func() { + // BeforeEach(func() { + // Eventually(helpers.CF("set-health-check", appName, "http", "--endpoint", "/some-endpoint")).Should(Exit(0)) + // }) + + // It("shows the health check type is http with the custom endpoint", func() { + // session := helpers.CF("get-health-check", appName) + + // Eventually(session).Should(Say(`Getting health check type for app %s in org %s / space %s as %s\.\.\.`, appName, orgName, spaceName, username)) + // Eventually(session).Should(Say(`process\s+health check\s+endpoint \(for http\)\s+invocation timeout\n`)) + // Eventually(session).Should(Say(`web\s+http\s+/some-endpoint\s+1\n`)) + + // Eventually(session).Should(Exit(0)) + // }) + // }) + + // When("the health check type is port", func() { + // BeforeEach(func() { + // Eventually(helpers.CF("set-health-check", appName, "port")).Should(Exit(0)) + // }) + + // It("shows that the health check type is port", func() { + // session := helpers.CF("get-health-check", appName) + + // Eventually(session).Should(Say(`Getting health check type for app %s in org %s / space %s as %s\.\.\.`, appName, orgName, spaceName, username)) + // Eventually(session).Should(Say(`web\s+port\s+\d+`)) + + // Eventually(session).Should(Exit(0)) + // }) + // }) + + // When("the health check type is process", func() { + // BeforeEach(func() { + // Eventually(helpers.CF("set-health-check", appName, "process")).Should(Exit(0)) + // }) + + // It("shows that the health check type is process", func() { + // session := helpers.CF("get-health-check", appName) + + // Eventually(session).Should(Say(`Getting health check type for app %s in org %s / space %s as %s\.\.\.`, appName, orgName, spaceName, username)) + // Eventually(session).Should(Say(`web\s+process\s+\d+`)) + + // Eventually(session).Should(Exit(0)) + // }) + // }) + + // When("the health check type changes from http to another type", func() { + // BeforeEach(func() { + // Eventually(helpers.CF("set-health-check", appName, "http", "--endpoint", "/some-endpoint")).Should(Exit(0)) + // Eventually(helpers.CF("set-health-check", appName, "process")).Should(Exit(0)) + // }) + + // It("does not show an endpoint", func() { + // session := helpers.CF("get-health-check", appName) + + // Consistently(session).ShouldNot(Say("/some-endpoint")) + // Eventually(session).Should(Say(`Getting health check type for app %s in org %s / space %s as %s\.\.\.`, appName, orgName, spaceName, username)) + // Eventually(session).Should(Say("\n\n")) + // Eventually(session).Should(Say(`web\s+process\s+\d+`)) + + // Eventually(session).Should(Exit(0)) + // }) + // }) + }) + + When("the app does not exist", func() { + It("displays app not found and exits 1", func() { + session := helpers.CF("get-readiness-health-check", appName) + + Eventually(session).Should(Say(`Getting readiness health check type for app %s in org %s / space %s as %s\.\.\.`, appName, orgName, spaceName, username)) + Eventually(session.Err).Should(Say("App '%s' not found", appName)) + Eventually(session).Should(Say("FAILED")) + + Eventually(session).Should(Exit(1)) + }) + }) + }) +}) diff --git a/resources/process_resource.go b/resources/process_resource.go index afbf20a09a..8eb5916998 100644 --- a/resources/process_resource.go +++ b/resources/process_resource.go @@ -13,16 +13,20 @@ type Process struct { GUID string Type string // Command is the process start command. Note: This value will be obfuscated when obtained from listing. - Command types.FilteredString - HealthCheckType constant.HealthCheckType - HealthCheckEndpoint string - HealthCheckInvocationTimeout int64 - HealthCheckTimeout int64 - Instances types.NullInt - MemoryInMB types.NullUint64 - DiskInMB types.NullUint64 - LogRateLimitInBPS types.NullInt - AppGUID string + Command types.FilteredString + HealthCheckType constant.HealthCheckType + HealthCheckEndpoint string + HealthCheckInvocationTimeout int64 + HealthCheckTimeout int64 + ReadinessHealthCheckType constant.HealthCheckType + ReadinessHealthCheckEndpoint string + ReadinessHealthCheckInvocationTimeout int64 + ReadinessHealthCheckInterval int64 + Instances types.NullInt + MemoryInMB types.NullUint64 + DiskInMB types.NullUint64 + LogRateLimitInBPS types.NullInt + AppGUID string } func (p Process) MarshalJSON() ([]byte, error) { @@ -34,6 +38,7 @@ func (p Process) MarshalJSON() ([]byte, error) { marshalDisk(p, &ccProcess) marshalLogRateLimit(p, &ccProcess) marshalHealthCheck(p, &ccProcess) + marshalReadinessHealthCheck(p, &ccProcess) return json.Marshal(ccProcess) } @@ -57,6 +62,15 @@ func (p *Process) UnmarshalJSON(data []byte) error { Timeout int64 `json:"timeout"` } `json:"data"` } `json:"health_check"` + + ReadinessHealthCheck struct { + Type constant.HealthCheckType `json:"type"` + Data struct { + Endpoint string `json:"endpoint"` + InvocationTimeout int64 `json:"invocation_timeout"` + Interval int64 `json:"interval"` + } `json:"data"` + } `json:"readiness_health_check"` } err := cloudcontroller.DecodeJSON(data, &ccProcess) @@ -71,6 +85,10 @@ func (p *Process) UnmarshalJSON(data []byte) error { p.HealthCheckInvocationTimeout = ccProcess.HealthCheck.Data.InvocationTimeout p.HealthCheckTimeout = ccProcess.HealthCheck.Data.Timeout p.HealthCheckType = ccProcess.HealthCheck.Type + p.ReadinessHealthCheckEndpoint = ccProcess.ReadinessHealthCheck.Data.Endpoint + p.ReadinessHealthCheckType = ccProcess.ReadinessHealthCheck.Type + p.ReadinessHealthCheckInvocationTimeout = ccProcess.ReadinessHealthCheck.Data.InvocationTimeout + p.ReadinessHealthCheckInterval = ccProcess.ReadinessHealthCheck.Data.Interval p.Instances = ccProcess.Instances p.MemoryInMB = ccProcess.MemoryInMB p.LogRateLimitInBPS = ccProcess.LogRateLimitInBPS @@ -89,6 +107,15 @@ type healthCheck struct { } `json:"data"` } +type readinessHealthCheck struct { + Type constant.HealthCheckType `json:"type,omitempty"` + Data struct { + Endpoint interface{} `json:"endpoint,omitempty"` + InvocationTimeout int64 `json:"invocation_timeout,omitempty"` + Interval int64 `json:"interval,omitempty"` + } `json:"data"` +} + type marshalProcess struct { Command interface{} `json:"command,omitempty"` Instances json.Number `json:"instances,omitempty"` @@ -96,7 +123,8 @@ 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"` + HealthCheck *healthCheck `json:"health_check,omitempty"` + ReadinessHealthCheck *readinessHealthCheck `json:"readiness_health_check,omitempty"` } func marshalCommand(p Process, ccProcess *marshalProcess) { @@ -123,6 +151,18 @@ func marshalHealthCheck(p Process, ccProcess *marshalProcess) { } } +func marshalReadinessHealthCheck(p Process, ccProcess *marshalProcess) { + if p.ReadinessHealthCheckType != "" || p.ReadinessHealthCheckEndpoint != "" || p.ReadinessHealthCheckInvocationTimeout != 0 { + ccProcess.ReadinessHealthCheck = new(readinessHealthCheck) + ccProcess.ReadinessHealthCheck.Type = p.ReadinessHealthCheckType + ccProcess.ReadinessHealthCheck.Data.InvocationTimeout = p.ReadinessHealthCheckInvocationTimeout + ccProcess.ReadinessHealthCheck.Data.Interval = p.ReadinessHealthCheckInterval + if p.ReadinessHealthCheckEndpoint != "" { + ccProcess.ReadinessHealthCheck.Data.Endpoint = p.ReadinessHealthCheckEndpoint + } + } +} + func marshalInstances(p Process, ccProcess *marshalProcess) { if p.Instances.IsSet { ccProcess.Instances = json.Number(fmt.Sprint(p.Instances.Value)) diff --git a/resources/process_resource_test.go b/resources/process_resource_test.go index ff4a36d978..b9c03dc51b 100644 --- a/resources/process_resource_test.go +++ b/resources/process_resource_test.go @@ -75,6 +75,7 @@ var _ = Describe("Process", func() { Expect(string(processBytes)).To(MatchJSON(`{"log_rate_limit_in_bytes_per_second": 1024}`)) }) }) + When("health check type http is provided", func() { BeforeEach(func() { process = resources.Process{ @@ -112,6 +113,43 @@ var _ = Describe("Process", func() { }) }) + When("readiness health check type http is provided", func() { + BeforeEach(func() { + process = resources.Process{ + ReadinessHealthCheckType: constant.HTTP, + ReadinessHealthCheckEndpoint: "some-endpoint", + } + }) + + It("sets the readiness health check type to http and has an endpoint", func() { + Expect(string(processBytes)).To(MatchJSON(`{"readiness_health_check":{"type":"http", "data": {"endpoint": "some-endpoint"}}}`)) + }) + }) + + When("readiness health check type port is provided", func() { + BeforeEach(func() { + process = resources.Process{ + ReadinessHealthCheckType: constant.Port, + } + }) + + It("sets the readiness health check type to port", func() { + Expect(string(processBytes)).To(MatchJSON(`{"readiness_health_check":{"type":"port", "data": {}}}`)) + }) + }) + + When("readiness health check type process is provided", func() { + BeforeEach(func() { + process = resources.Process{ + ReadinessHealthCheckType: constant.Process, + } + }) + + It("sets the readiness health check type to process", func() { + Expect(string(processBytes)).To(MatchJSON(`{"readiness_health_check":{"type":"process", "data": {}}}`)) + }) + }) + When("process has no fields provided", func() { BeforeEach(func() { process = resources.Process{} @@ -184,5 +222,42 @@ var _ = Describe("Process", func() { })) }) }) + + When("readiness health check type http is provided", func() { + BeforeEach(func() { + processBytes = []byte(`{"readiness_health_check":{"type":"http", "data": {"endpoint": "some-endpoint"}}}`) + }) + + It("sets the readiness health check type to http and has an endpoint", func() { + Expect(process).To(MatchFields(IgnoreExtras, Fields{ + "ReadinessHealthCheckType": Equal(constant.HTTP), + "ReadinessHealthCheckEndpoint": Equal("some-endpoint"), + })) + }) + }) + + When("readiness health check type port is provided", func() { + BeforeEach(func() { + processBytes = []byte(`{"readiness_health_check":{"type":"port", "data": {"endpoint": null}}}`) + }) + + It("sets the readiness health check type to port", func() { + Expect(process).To(MatchFields(IgnoreExtras, Fields{ + "ReadinessHealthCheckType": Equal(constant.Port), + })) + }) + }) + + When("readiness health check type process is provided", func() { + BeforeEach(func() { + processBytes = []byte(`{"readiness_health_check":{"type":"process", "data": {"endpoint": null}}}`) + }) + + It("sets the readiness health check type to process", func() { + Expect(process).To(MatchFields(IgnoreExtras, Fields{ + "ReadinessHealthCheckType": Equal(constant.Process), + })) + }) + }) }) })