Skip to content

Commit 9ec7f7b

Browse files
adding process monitoring to blue-green deployment
1 parent 24a9f85 commit 9ec7f7b

File tree

1 file changed

+112
-21
lines changed

1 file changed

+112
-21
lines changed

operation/push.go

Lines changed: 112 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
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
2634
type 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.\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)
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.\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",
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

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

Comments
 (0)