Skip to content

Commit 192bd58

Browse files
authored
Merge pull request #496 from datev/feature/blue-green-deployment-update
adding process monitoring to blue-green deployment
2 parents 24a9f85 + c1833c5 commit 192bd58

File tree

1 file changed

+109
-26
lines changed

1 file changed

+109
-26
lines changed

operation/push.go

Lines changed: 109 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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
2631
type 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+
5467
func (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.\nNow the old application is running under name %s and is attached to state. \nThe 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.\nDuring 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

137201
func (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

Comments
 (0)