diff --git a/.github/ops-files/replace-redis.yml b/.github/ops-files/replace-redis.yml new file mode 100644 index 00000000000..677e09ca8d6 --- /dev/null +++ b/.github/ops-files/replace-redis.yml @@ -0,0 +1,6 @@ +--- +- type: replace + path: /instance_groups/name=api/jobs/name=redis? + value: + name: valkey + release: capi \ No newline at end of file diff --git a/.github/workflows/tests-integration-reusable.yml b/.github/workflows/tests-integration-reusable.yml index f48692c70eb..dd3a3ea50c0 100644 --- a/.github/workflows/tests-integration-reusable.yml +++ b/.github/workflows/tests-integration-reusable.yml @@ -179,6 +179,7 @@ jobs: bosh -d cf manifest > /tmp/manifest.yml bosh interpolate /tmp/manifest.yml \ -o .github/ops-files/use-cflinuxfs3.yml \ + -o .github/ops-files/replace-redis.yml \ -o cf-deployment/operations/use-internal-lookup-for-route-services.yml \ -o cf-deployment/operations/add-persistent-isolation-segment-diego-cell.yml \ -o cli-ci/ci/infrastructure/operations/use-latest-capi.yml \ diff --git a/actor/v7action/application_summary.go b/actor/v7action/application_summary.go index d33be9dd4e9..783720538af 100644 --- a/actor/v7action/application_summary.go +++ b/actor/v7action/application_summary.go @@ -1,6 +1,8 @@ package v7action import ( + "errors" + "code.cloudfoundry.org/cli/actor/actionerror" "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" "code.cloudfoundry.org/cli/api/cloudcontroller/ccv3" @@ -18,6 +20,7 @@ type ApplicationSummary struct { type DetailedApplicationSummary struct { ApplicationSummary CurrentDroplet resources.Droplet + Deployment resources.Deployment } func (a ApplicationSummary) GetIsolationSegmentName() (string, bool) { @@ -120,6 +123,12 @@ func (actor Actor) GetDetailedAppSummary(appName, spaceGUID string, withObfuscat return DetailedApplicationSummary{}, allWarnings, err } + detailedSummary, warnings, err = actor.addDeployment(detailedSummary) + allWarnings = append(allWarnings, warnings...) + if err != nil { + return DetailedApplicationSummary{}, allWarnings, err + } + return detailedSummary, allWarnings, err } @@ -206,6 +215,19 @@ func (actor Actor) addDroplet(summary ApplicationSummary) (DetailedApplicationSu }, allWarnings, nil } +func (actor Actor) addDeployment(detailedSummary DetailedApplicationSummary) (DetailedApplicationSummary, Warnings, error) { + var allWarnings Warnings + + deployment, warnings, err := actor.GetLatestActiveDeploymentForApp(detailedSummary.GUID) + allWarnings = append(allWarnings, warnings...) + if err != nil && !errors.Is(err, actionerror.ActiveDeploymentNotFoundError{}) { + return DetailedApplicationSummary{}, allWarnings, err + } + + detailedSummary.Deployment = deployment + return detailedSummary, allWarnings, nil +} + func toAppGUIDs(apps []resources.Application) []string { guids := make([]string, len(apps)) diff --git a/actor/v7action/application_summary_test.go b/actor/v7action/application_summary_test.go index 351a06bb753..bf0a1e17259 100644 --- a/actor/v7action/application_summary_test.go +++ b/actor/v7action/application_summary_test.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" - "code.cloudfoundry.org/cli/actor/v7action" . "code.cloudfoundry.org/cli/actor/v7action" "code.cloudfoundry.org/cli/actor/v7action/v7actionfakes" "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" @@ -574,6 +573,115 @@ var _ = Describe("Application Summary Actions", func() { ) }) + When("getting application deployment succeeds", func() { + When("the deployment is active", func() { + When("the deployment strategy is rolling", func() { + When("the deployment is in progress", func() { + BeforeEach(func() { + fakeCloudControllerClient.GetDeploymentsReturns( + []resources.Deployment{ + { + GUID: "some-deployment-guid", + Strategy: "rolling", + StatusValue: "ACTIVE", + StatusReason: "DEPLOYING", + }, + }, + nil, + nil, + ) + }) + It("returns the deployment information", func() { + Expect(summary.Deployment).To(Equal(resources.Deployment{ + GUID: "some-deployment-guid", + Strategy: "rolling", + StatusValue: "ACTIVE", + StatusReason: "DEPLOYING", + })) + }) + }) + + When("the deployment is canceled", func() { + When("the deployment is in progress", func() { + BeforeEach(func() { + fakeCloudControllerClient.GetDeploymentsReturns( + []resources.Deployment{ + { + GUID: "some-deployment-guid", + Strategy: "rolling", + StatusValue: "ACTIVE", + StatusReason: "CANCELLING", + }, + }, + nil, + nil, + ) + }) + It("returns the deployment information", func() { + Expect(summary.Deployment).To(Equal(resources.Deployment{ + GUID: "some-deployment-guid", + Strategy: "rolling", + StatusValue: "ACTIVE", + StatusReason: "CANCELLING", + })) + }) + }) + }) + }) + }) + + When("the deployment is not active", func() { + BeforeEach(func() { + fakeCloudControllerClient.GetDeploymentsReturns( + []resources.Deployment{ + { + GUID: "", + Strategy: "", + StatusValue: "", + StatusReason: "", + }, + }, + nil, + nil, + ) + }) + It("returns no deployment information", func() { + Expect(summary.Deployment).To(Equal(resources.Deployment{ + GUID: "", + Strategy: "", + StatusValue: "", + StatusReason: "", + })) + }) + }) + }) + + When("getting application deployment fails", func() { + BeforeEach(func() { + fakeCloudControllerClient.GetDeploymentsReturns( + nil, + ccv3.Warnings{"get-deployments-warning"}, + errors.New("some-error"), + ) + }) + + It("returns the warnings and error", func() { + Expect(executeErr).To(MatchError("some-error")) + Expect(warnings).To(ConsistOf( + "get-apps-warning", + "get-app-processes-warning", + "get-process-by-type-warning", + "get-process-sidecars-warning", + "get-process-instances-warning", + "get-process-by-type-warning", + "get-process-sidecars-warning", + "get-process-instances-warning", + "get-app-droplet-warning", + "get-deployments-warning", + )) + }) + }) + When("getting application routes succeeds", func() { BeforeEach(func() { fakeCloudControllerClient.GetApplicationRoutesReturns( @@ -589,7 +697,7 @@ var _ = Describe("Application Summary Actions", func() { It("returns the summary and warnings with droplet information", func() { Expect(executeErr).ToNot(HaveOccurred()) Expect(summary).To(Equal(DetailedApplicationSummary{ - ApplicationSummary: v7action.ApplicationSummary{ + ApplicationSummary: ApplicationSummary{ Application: resources.Application{ Name: "some-app-name", GUID: "some-app-guid", @@ -733,7 +841,7 @@ var _ = Describe("Application Summary Actions", func() { It("returns the summary and warnings without droplet information", func() { Expect(executeErr).ToNot(HaveOccurred()) Expect(summary).To(Equal(DetailedApplicationSummary{ - ApplicationSummary: v7action.ApplicationSummary{ + ApplicationSummary: ApplicationSummary{ Application: resources.Application{ Name: "some-app-name", GUID: "some-app-guid", diff --git a/api/cloudcontroller/ccv3/constant/deployment.go b/api/cloudcontroller/ccv3/constant/deployment.go index d324cb9aafb..7927e3d1d7b 100644 --- a/api/cloudcontroller/ccv3/constant/deployment.go +++ b/api/cloudcontroller/ccv3/constant/deployment.go @@ -28,6 +28,12 @@ const ( type DeploymentStatusReason string const ( + // DeploymentStatusReasonDeploying means the deployment is in state 'DEPLOYING' + DeploymentStatusReasonDeploying DeploymentStatusReason = "DEPLOYING" + + // DeploymentCanceling means the deployment is in state 'CANCELING' + DeploymentStatusReasonCanceling DeploymentStatusReason = "CANCELING" + // DeploymentStatusReasonDeployed means the deployment's status.value is // 'DEPLOYED' DeploymentStatusReasonDeployed DeploymentStatusReason = "DEPLOYED" diff --git a/command/v7/shared/app_summary_displayer.go b/command/v7/shared/app_summary_displayer.go index 98ae553742a..6b3e71725fc 100644 --- a/command/v7/shared/app_summary_displayer.go +++ b/command/v7/shared/app_summary_displayer.go @@ -13,6 +13,8 @@ import ( "code.cloudfoundry.org/cli/types" "code.cloudfoundry.org/cli/util/ui" log "github.com/sirupsen/logrus" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) type AppSummaryDisplayer struct { @@ -159,6 +161,13 @@ func (display AppSummaryDisplayer) displayProcessTable(summary v7action.Detailed } display.displayAppInstancesTable(process) } + + if summary.Deployment.StatusValue == constant.DeploymentStatusValueActive { + display.UI.DisplayNewline() + display.UI.DisplayText(fmt.Sprintf("%s deployment currently %s.", + cases.Title(language.English, cases.NoLower).String(string(summary.Deployment.Strategy)), + summary.Deployment.StatusReason)) + } } func (display AppSummaryDisplayer) getCreatedTime(summary v7action.DetailedApplicationSummary) string { diff --git a/command/v7/shared/app_summary_displayer_test.go b/command/v7/shared/app_summary_displayer_test.go index 7caf995d800..78da40d2e00 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 ( + "fmt" "time" "code.cloudfoundry.org/cli/actor/v7action" @@ -13,6 +14,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gbytes" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) var _ = Describe("app summary displayer", func() { @@ -652,5 +655,59 @@ var _ = Describe("app summary displayer", func() { Expect(testUI.Out).To(Say(`some-buildpack`)) }) }) + + When("there is an active deployment", func() { + When("the deployment strategy is rolling", func() { + When("the deployment is in progress", func() { + BeforeEach(func() { + summary = v7action.DetailedApplicationSummary{ + Deployment: resources.Deployment{ + Strategy: constant.DeploymentStrategyRolling, + StatusValue: constant.DeploymentStatusValueActive, + StatusReason: constant.DeploymentStatusReasonDeploying, + }, + } + }) + + It("displays the message", func() { + Expect(testUI.Out).To(Say("Rolling deployment currently DEPLOYING.")) + }) + }) + + When("the deployment is cancelled", func() { + BeforeEach(func() { + summary = v7action.DetailedApplicationSummary{ + Deployment: resources.Deployment{ + Strategy: constant.DeploymentStrategyRolling, + StatusValue: constant.DeploymentStatusValueActive, + StatusReason: constant.DeploymentStatusReasonCanceling, + }, + } + }) + + It("displays the message", func() { + Expect(testUI.Out).To(Say("Rolling deployment currently CANCELING.")) + }) + }) + }) + }) + + When("there is no active deployment", func() { + BeforeEach(func() { + summary = v7action.DetailedApplicationSummary{ + Deployment: resources.Deployment{ + Strategy: "", + StatusValue: "", + StatusReason: "", + }, + } + }) + + It("does not display deployment info", func() { + Expect(testUI.Out).NotTo(Say(fmt.Sprintf("%s deployment currently %s", + cases.Title(language.English, cases.NoLower).String(string(summary.Deployment.Strategy)), + summary.Deployment.StatusReason))) + }) + }) }) }) diff --git a/integration/v7/isolated/app_command_test.go b/integration/v7/isolated/app_command_test.go index a3109ceca54..b7e89fb1fad 100644 --- a/integration/v7/isolated/app_command_test.go +++ b/integration/v7/isolated/app_command_test.go @@ -253,6 +253,53 @@ applications: Eventually(session).Should(Exit(0)) }) }) + + When("there is an active deployment", func() { + BeforeEach(func() { + helpers.WithHelloWorldApp(func(appDir string) { + Eventually(helpers.CF("push", appName, "-p", appDir, "-b", "staticfile_buildpack")).Should(Exit(0)) + }) + }) + + When("the deployment strategy is rolling", func() { + When("the deployment is in progress", func() { + It("displays the message", func() { + session := helpers.CF("restart", appName, "--strategy", "rolling") + + session1 := helpers.CF("app", appName) + Eventually(session1).Should(Say("Rolling deployment currently DEPLOYING.")) + Eventually(session).Should(Exit(0)) + Eventually(session1).Should(Exit(0)) + }) + }) + When("the deployment is cancelled", func() { + It("displays the message", func() { + helpers.CF("restart", appName, "--strategy", "rolling") + Eventually(func() *Session { + return helpers.CF("cancel-deployment", appName).Wait() + }).Should(Exit(0)) + + session2 := helpers.CF("app", appName) + Eventually(session2).Should(Say("Rolling deployment currently CANCELING.")) + Eventually(session2).Should(Exit(0)) + }) + }) + }) + }) + + When("there is no active deployment", func() { + BeforeEach(func() { + helpers.WithHelloWorldApp(func(appDir string) { + Eventually(helpers.CF("push", appName, "-p", appDir, "-b", "staticfile_buildpack")).Should(Exit(0)) + }) + }) + + It("does not display the message", func() { + session := helpers.CF("app", appName) + Eventually(session).Should(Exit(0)) + Eventually(session).ShouldNot(Say(`\w+ deployment currently \w+`)) + }) + }) }) Describe("version independent display", func() { diff --git a/resources/deployment_resource.go b/resources/deployment_resource.go index 144ac076426..742b47429f4 100644 --- a/resources/deployment_resource.go +++ b/resources/deployment_resource.go @@ -18,6 +18,7 @@ type Deployment struct { UpdatedAt string Relationships Relationships NewProcesses []Process + Strategy constant.DeploymentStrategy } // MarshalJSON converts a Deployment into a Cloud Controller Deployment. @@ -59,9 +60,11 @@ func (d *Deployment) UnmarshalJSON(data []byte) error { Value constant.DeploymentStatusValue `json:"value"` Reason constant.DeploymentStatusReason `json:"reason"` } `json:"status"` - Droplet Droplet `json:"droplet,omitempty"` - NewProcesses []Process `json:"new_processes,omitempty"` + Droplet Droplet `json:"droplet,omitempty"` + NewProcesses []Process `json:"new_processes,omitempty"` + Strategy constant.DeploymentStrategy `json:"strategy"` } + err := cloudcontroller.DecodeJSON(data, &ccDeployment) if err != nil { return err @@ -75,6 +78,7 @@ func (d *Deployment) UnmarshalJSON(data []byte) error { d.StatusReason = ccDeployment.Status.Reason d.DropletGUID = ccDeployment.Droplet.GUID d.NewProcesses = ccDeployment.NewProcesses + d.Strategy = ccDeployment.Strategy return nil }