@@ -22,13 +22,20 @@ const (
2222 StrategyRolling
2323)
2424
25+ const (
26+ AppDeployedRunningCheckIntervalSecondsDefault = 5
27+ AppDeployedRunningTimeoutMinutesDefault = 5
28+ )
29+
2530// AppPushOperation can be used to push buildpack apps
2631type AppPushOperation struct {
27- orgName string
28- spaceName string
29- client * client.Client
30- strategy StrategyMode
31- stopped bool
32+ orgName string
33+ spaceName string
34+ client * client.Client
35+ strategy StrategyMode
36+ stopped bool
37+ timeout uint
38+ checkInterval uint
3239}
3340
3441// NewAppPushOperation creates a new AppPushOperation
@@ -51,6 +58,12 @@ func (p *AppPushOperation) WithStrategy(s StrategyMode) {
5158 }
5259}
5360
61+ func (p * AppPushOperation ) WithBlueGreenStrategy (timeout uint , checkInterval uint ) {
62+ p .strategy = StrategyBlueGreen
63+ p .timeout = timeout
64+ p .checkInterval = checkInterval
65+ }
66+
5467func (p * AppPushOperation ) WithNoStart (stopped bool ) {
5568 p .stopped = stopped
5669}
@@ -96,42 +109,93 @@ func (p *AppPushOperation) pushBlueGreenApp(ctx context.Context, space *resource
96109 return originalApp , err
97110 }
98111
99- tempAppName := originalApp .Name + "-venerable"
112+ originalAppName := originalApp .Name
113+ tempAppName := originalAppName + "-venerable"
100114
101115 // 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
116+ tempApp , tempFindError := p .findApp (ctx , tempAppName , space )
117+ if tempFindError == nil {
118+ deleteTmpErr : = p .gracefulDeletion (ctx , tempApp )
119+ if deleteTmpErr != nil {
120+ return nil , fmt . Errorf ( "failed to delete temp app due to: %w" , deleteTmpErr )
107121 }
108122 }
109123
110124 // Update the existing app's name
111- _ , err = p .client .Applications .Update (ctx , originalApp .GUID , & resource.AppUpdate {
125+ _ , updateErr : = p .client .Applications .Update (ctx , originalApp .GUID , & resource.AppUpdate {
112126 Name : tempAppName ,
113127 })
114- if err != nil {
115- return nil , fmt .Errorf ("failed to update app name failed with: %w" , err )
116- }
128+ if updateErr != nil {
129+ return nil , fmt .Errorf ("failed to update: %s, failed with: %w" , tempAppName , updateErr )
130+ }
131+
132+ newApp , pushError := p .pushApp (ctx , space , manifest , zipFile )
133+ if pushError != nil {
134+ // If push fails delete new application and change back original app name
135+ invalidNewApp , findAppErr := p .findApp (ctx , originalAppName , space )
136+ if findAppErr == nil && invalidNewApp != nil {
137+ deleteErr := p .gracefulDeletion (ctx , invalidNewApp )
138+ if deleteErr != nil {
139+ 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 )
140+ }
141+ }
117142
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 {
143+ _ , updateErr := p .client .Applications .Update (ctx , originalApp .GUID , & resource.AppUpdate {
123144 Name : originalApp .Name ,
124145 })
125- if err != nil {
126- return nil , fmt .Errorf ("failed to update app name back to original name: failed with %w" , err )
146+ if updateErr != nil {
147+ 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" ,
148+ originalApp .Name , tempAppName , originalApp .Name , tempAppName , updateErr )
127149 }
128- return nil , fmt .Errorf ("blue green deployment failed with: %w" , err )
150+ return nil , fmt .Errorf ("blue- green deployment for application %s failed with: %w" , originalApp . Name , pushError )
129151 }
152+
130153 if newApp .State == "STARTED" {
131- err = p .gracefulDeletion (ctx , originalApp )
132- return newApp , err
154+ if p .isWaitForAppDeployedRunningEnabled () {
155+ pollOptions := p .getAppDeployedRunningPollingOptions ()
156+ healthCheckErr := p .waitForAppHealthy (ctx , newApp , pollOptions )
157+ // If health check fails, delete new app and change back original app name
158+ if healthCheckErr != nil {
159+ originalName := newApp .Name
160+ deleteErr := p .gracefulDeletion (ctx , newApp )
161+ if deleteErr != nil {
162+ return nil , fmt .Errorf ("failed to delete new app after health check failure: %w" , deleteErr )
163+ }
164+ _ , revertUpdateErr := p .client .Applications .Update (ctx , originalApp .GUID , & resource.AppUpdate {
165+ Name : originalName ,
166+ })
167+
168+ if revertUpdateErr != nil {
169+ return nil , fmt .Errorf ("failed to update app name back to original name: failed with: %w" , revertUpdateErr )
170+ }
171+
172+ return nil , fmt .Errorf ("new application failed health check with: %w" , healthCheckErr )
173+ }
174+ }
175+ cleanUpOldErr := p .gracefulDeletion (ctx , originalApp )
176+ return newApp , cleanUpOldErr
133177 }
134- return newApp , fmt .Errorf ("failed to verify application start: %w" , err )
178+ return newApp , fmt .Errorf ("failed to verify application start: %w" , pushError )
179+ }
180+
181+ func (p * AppPushOperation ) isWaitForAppDeployedRunningEnabled () bool {
182+ return p .timeout != 0 || p .checkInterval != 0
183+ }
184+
185+ func (p * AppPushOperation ) getAppDeployedRunningPollingOptions () * client.PollingOptions {
186+
187+ if p .timeout == 0 {
188+ p .timeout = uint (AppDeployedRunningTimeoutMinutesDefault )
189+ }
190+
191+ if p .checkInterval == 0 {
192+ p .checkInterval = uint (AppDeployedRunningCheckIntervalSecondsDefault )
193+ }
194+
195+ pollOptions := client .NewPollingOptions ()
196+ pollOptions .Timeout = time .Duration (p .timeout ) * time .Minute
197+ pollOptions .CheckInterval = time .Duration (p .checkInterval ) * time .Second
198+ return pollOptions
135199}
136200
137201func (p * AppPushOperation ) pushRollingApp (ctx context.Context , space * resource.Space , manifest * AppManifest , zipFile io.Reader ) (* resource.App , error ) {
@@ -494,3 +558,22 @@ func (p *AppPushOperation) findSpace(ctx context.Context, orgGUID string) (*reso
494558 }
495559 return space , nil
496560}
561+
562+ func (p * AppPushOperation ) waitForAppHealthy (ctx context.Context , app * resource.App , pollOptions * client.PollingOptions ) error {
563+
564+ appPollErr := client .PollForStateOrTimeout (func () (string , string , error ) {
565+ for {
566+ procData , err := p .client .Processes .GetStatsForApp (ctx , app .GUID , "web" )
567+ if err != nil {
568+ return "FAILED" , "Failed to get processes stats for application" , err
569+ }
570+ for _ , proc := range procData .Stats {
571+ if proc .State != "RUNNING" {
572+ return proc .State , "One or more instances are not running" , nil
573+ }
574+ }
575+ return "RUNNING" , "" , nil
576+ }
577+ }, "RUNNING" , pollOptions )
578+ return appPollErr
579+ }
0 commit comments