66 "fmt"
77 "io"
88 "os"
9+ "strconv"
910 "time"
1011
1112 "gopkg.in/yaml.v3"
@@ -22,6 +23,13 @@ const (
2223 StrategyRolling
2324)
2425
26+ const (
27+ AppDeployedRunningCheckIntervalSecondsDefault = 5
28+ AppDeployedRunningTimeoutMinutesDefault = 5
29+ EnvironmentVariablesConvertBaseDecimal = 10
30+ EnvironmentVariablesConvertBitsSize = 32
31+ )
32+
2533// AppPushOperation can be used to push buildpack apps
2634type AppPushOperation struct {
2735 orgName string
@@ -96,42 +104,103 @@ func (p *AppPushOperation) pushBlueGreenApp(ctx context.Context, space *resource
96104 return originalApp , err
97105 }
98106
99- tempAppName := originalApp .Name + "-venerable"
107+ originalAppName := originalApp .Name
108+ tempAppName := originalAppName + "-venerable"
100109
101110 // Check if temporary app name already exists if yes gracefully delete the app and continue
102- tempApp , err := p .findApp (ctx , tempAppName , space )
103- if err == nil {
104- err = p .gracefulDeletion (ctx , tempApp )
105- if err != nil {
106- return nil , err
111+ tempApp , tempFindError := p .findApp (ctx , tempAppName , space )
112+ if tempFindError == nil {
113+ deleteTmpErr : = p .gracefulDeletion (ctx , tempApp )
114+ if deleteTmpErr != nil {
115+ return nil , fmt . Errorf ( "failed to delete temp app due to: %w" , deleteTmpErr )
107116 }
108117 }
109118
110119 // Update the existing app's name
111- _ , err = p .client .Applications .Update (ctx , originalApp .GUID , & resource.AppUpdate {
120+ _ , updateErr : = p .client .Applications .Update (ctx , originalApp .GUID , & resource.AppUpdate {
112121 Name : tempAppName ,
113122 })
114- if err != nil {
115- return nil , fmt .Errorf ("failed to update app name failed with: %w" , err )
116- }
123+ if updateErr != nil {
124+ return nil , fmt .Errorf ("failed to update: %s, failed with: %w" , tempAppName , updateErr )
125+ }
126+
127+ newApp , pushError := p .pushApp (ctx , space , manifest , zipFile )
128+ if pushError != nil {
129+ // If push fails delete new application and change back original app name
130+ invalidNewApp , findAppErr := p .findApp (ctx , originalAppName , space )
131+ if findAppErr == nil && invalidNewApp != nil {
132+ deleteErr := p .gracefulDeletion (ctx , invalidNewApp )
133+ if deleteErr != nil {
134+ return nil , fmt .Errorf ("failed to deploy the new application and failed to revert it.\n Now the old application is running under name %s and is attached to state. \n The new application under name %s is detached from state, and failed to start correctly: %w" , tempAppName , originalApp .Name , deleteErr )
135+ }
136+ }
117137
118- // Apply the manifest
119- newApp , err := p .pushApp (ctx , space , manifest , zipFile )
120- if err != nil {
121- // If push fails change back original app name
122- _ , err = p .client .Applications .Update (ctx , originalApp .GUID , & resource.AppUpdate {
138+ _ , updateErr := p .client .Applications .Update (ctx , originalApp .GUID , & resource.AppUpdate {
123139 Name : originalApp .Name ,
124140 })
125- if err != nil {
126- return nil , fmt .Errorf ("failed to update app name back to original name: failed with %w" , err )
141+ if updateErr != nil {
142+ return nil , fmt .Errorf ("failed to push new version of application %s.\n During rollback also failed to update old app name from %s back to original name: %s. The old version of the application is currently running as: %s. The operation failed due with: %w" ,
143+ originalApp .Name , tempAppName , originalApp .Name , tempAppName , updateErr )
127144 }
128- return nil , fmt .Errorf ("blue green deployment failed with: %w" , err )
145+ return nil , fmt .Errorf ("blue- green deployment for application %s failed with: %w" , originalApp . Name , pushError )
129146 }
147+
130148 if newApp .State == "STARTED" {
131- err = p .gracefulDeletion (ctx , originalApp )
132- return newApp , err
149+
150+ healthCheckErr := p .waitForAppHealthy (ctx , newApp )
151+ // If health check fails, delete new app and change back original app name
152+ if healthCheckErr != nil {
153+ originalName := newApp .Name
154+ deleteErr := p .gracefulDeletion (ctx , newApp )
155+ if deleteErr != nil {
156+ return nil , fmt .Errorf ("failed to delete new app after health check failure: %w" , deleteErr )
157+ }
158+ _ , revertUpdateErr := p .client .Applications .Update (ctx , originalApp .GUID , & resource.AppUpdate {
159+ Name : originalName ,
160+ })
161+
162+ if revertUpdateErr != nil {
163+ return nil , fmt .Errorf ("failed to update app name back to original name: failed with: %w" , revertUpdateErr )
164+ }
165+
166+ return nil , fmt .Errorf ("new application failed health check with: %w" , healthCheckErr )
167+ }
168+ cleanUpOldErr := p .gracefulDeletion (ctx , originalApp )
169+ return newApp , cleanUpOldErr
133170 }
134- return newApp , fmt .Errorf ("failed to verify application start: %w" , err )
171+ return newApp , fmt .Errorf ("failed to verify application start: %w" , pushError )
172+ }
173+
174+ func getAppDeployedRunningPollingOptions (ctx context.Context ) (uint , uint ) {
175+ rawTimeout := ctx .Value ("app_deployed_running_timeout" )
176+ processedTimeout := uint (AppDeployedRunningTimeoutMinutesDefault )
177+
178+ rawCheckInterval := ctx .Value ("app_deployed_running_check_interval" )
179+ processedCheckInterval := uint (AppDeployedRunningCheckIntervalSecondsDefault )
180+
181+ if rawTimeout != nil {
182+ t , ok := rawTimeout .(uint )
183+ if ok == true {
184+ processedTimeout = t
185+ }
186+ } else if envTimeout := os .Getenv ("CF_APP_DEPLOYED_RUNNING_TIMEOUT" ); envTimeout != "" {
187+ if t , err := strconv .ParseUint (envTimeout , EnvironmentVariablesConvertBaseDecimal , EnvironmentVariablesConvertBitsSize ); err == nil {
188+ processedTimeout = uint (t )
189+ }
190+ }
191+
192+ if rawCheckInterval != nil {
193+ ci , ok := rawCheckInterval .(uint )
194+ if ok {
195+ processedCheckInterval = ci
196+ }
197+ } else if envInterval := os .Getenv ("CF_APP_DEPLOYED_RUNNING_CHECK_INTERVAL" ); envInterval != "" {
198+ if ci , err := strconv .ParseUint (envInterval , EnvironmentVariablesConvertBaseDecimal , EnvironmentVariablesConvertBitsSize ); err == nil {
199+ processedCheckInterval = uint (ci )
200+ }
201+ }
202+
203+ return processedTimeout , processedCheckInterval
135204}
136205
137206func (p * AppPushOperation ) pushRollingApp (ctx context.Context , space * resource.Space , manifest * AppManifest , zipFile io.Reader ) (* resource.App , error ) {
@@ -494,3 +563,25 @@ func (p *AppPushOperation) findSpace(ctx context.Context, orgGUID string) (*reso
494563 }
495564 return space , nil
496565}
566+
567+ func (p * AppPushOperation ) waitForAppHealthy (ctx context.Context , app * resource.App ) error {
568+ timeout , checkInterval := getAppDeployedRunningPollingOptions (ctx )
569+ pollOptions := client .NewPollingOptions ()
570+ pollOptions .Timeout = time .Duration (timeout ) * time .Minute
571+ pollOptions .CheckInterval = time .Duration (checkInterval ) * time .Second
572+ appPollErr := client .PollForStateOrTimeout (func () (string , string , error ) {
573+ for {
574+ procData , err := p .client .Processes .GetStatsForApp (ctx , app .GUID , "web" )
575+ if err != nil {
576+ return "FAILED" , "Failed to get processes stats for application" , err
577+ }
578+ for _ , proc := range procData .Stats {
579+ if proc .State != "RUNNING" {
580+ return proc .State , "One or more instances are not running" , nil
581+ }
582+ }
583+ return "RUNNING" , "" , nil
584+ }
585+ }, "RUNNING" , pollOptions )
586+ return appPollErr
587+ }
0 commit comments