Skip to content

Commit d66f46a

Browse files
Samzesethboyles
andauthored
Add Canary deployments (#3892)
* Introduce Canary deployments as an experimental feature. Closes #3837 Co-authored-by: Sam Gunaratne <[email protected]> Co-authored-by: Seth Boyles <[email protected]>
1 parent 3d09a70 commit d66f46a

File tree

31 files changed

+1044
-41
lines changed

31 files changed

+1044
-41
lines changed

app/actions/deployment_continue.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module VCAP::CloudController
2+
class DeploymentContinue
3+
class Error < StandardError
4+
end
5+
class InvalidStatus < Error
6+
end
7+
8+
class << self
9+
def continue(deployment:, user_audit_info:)
10+
deployment.db.transaction do
11+
deployment.lock!
12+
reject_invalid_state!(deployment) unless deployment.continuable?
13+
14+
record_audit_event(deployment, user_audit_info)
15+
deployment.update(
16+
state: DeploymentModel::DEPLOYING_STATE,
17+
status_value: DeploymentModel::ACTIVE_STATUS_VALUE,
18+
status_reason: DeploymentModel::DEPLOYING_STATUS_REASON
19+
)
20+
end
21+
end
22+
23+
private
24+
25+
def reject_invalid_state!(deployment)
26+
raise InvalidStatus.new("Cannot continue a deployment with status: #{deployment.status_value} and reason: #{deployment.status_reason}")
27+
end
28+
29+
def record_audit_event(deployment, user_audit_info)
30+
app = deployment.app
31+
Repositories::DeploymentEventRepository.record_continue(
32+
deployment,
33+
deployment.droplet,
34+
user_audit_info,
35+
app.name,
36+
app.space_guid,
37+
app.space.organization_guid
38+
)
39+
end
40+
end
41+
end
42+
end

app/actions/deployment_create.rb

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ def create(app:, user_audit_info:, message:)
1313
DeploymentModel.db.transaction do
1414
app.lock!
1515

16+
message.strategy ||= DeploymentModel::ROLLING_STRATEGY
17+
1618
target_state = DeploymentTargetState.new(app, message)
1719

1820
previous_droplet = app.droplet
1921
target_state.apply_to_app(app, user_audit_info)
2022

2123
if target_state.rollback_target_revision
2224
revision = RevisionResolver.rollback_app_revision(app, target_state.rollback_target_revision, user_audit_info)
23-
log_rollback_event(app.guid, user_audit_info.user_guid, target_state.rollback_target_revision.guid)
25+
log_rollback_event(app.guid, user_audit_info.user_guid, target_state.rollback_target_revision.guid, message.strategy)
2426
else
2527
revision = RevisionResolver.update_app_revision(app, user_audit_info)
2628
end
@@ -41,15 +43,15 @@ def create(app:, user_audit_info:, message:)
4143

4244
deployment = DeploymentModel.create(
4345
app: app,
44-
state: DeploymentModel::DEPLOYING_STATE,
46+
state: starting_state(message),
4547
status_value: DeploymentModel::ACTIVE_STATUS_VALUE,
4648
status_reason: DeploymentModel::DEPLOYING_STATUS_REASON,
4749
droplet: target_state.droplet,
4850
previous_droplet: previous_droplet,
4951
original_web_process_instance_count: desired_instances(app.oldest_web_process, previous_deployment),
5052
revision_guid: revision&.guid,
5153
revision_version: revision&.version,
52-
strategy: DeploymentModel::ROLLING_STRATEGY
54+
strategy: message.strategy
5355
)
5456
MetadataUpdate.update(deployment, message)
5557

@@ -201,15 +203,23 @@ def supersede_deployment(previous_deployment)
201203
)
202204
end
203205

204-
def log_rollback_event(app_guid, user_id, revision_id)
206+
def starting_state(message)
207+
if message.strategy == DeploymentModel::CANARY_STRATEGY
208+
DeploymentModel::PREPAUSED_STATE
209+
else
210+
DeploymentModel::DEPLOYING_STATE
211+
end
212+
end
213+
214+
def log_rollback_event(app_guid, user_id, revision_id, strategy)
205215
TelemetryLogger.v3_emit(
206216
'rolled-back-app',
207217
{
208218
'app-id' => app_guid,
209219
'user-id' => user_id,
210220
'revision-id' => revision_id
211221
},
212-
{ 'strategy' => 'rolling' }
222+
{ 'strategy' => strategy }
213223
)
214224
end
215225
end

app/controllers/v3/deployments_controller.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require 'actions/deployment_create'
77
require 'actions/deployment_update'
88
require 'actions/deployment_cancel'
9+
require 'actions/deployment_continue'
910
require 'cloud_controller/telemetry_logger'
1011

1112
class DeploymentsController < ApplicationController
@@ -57,7 +58,7 @@ def create
5758
'app-id' => app.guid,
5859
'user-id' => current_user.guid
5960
},
60-
{ 'strategy' => 'rolling' }
61+
{ 'strategy' => deployment.strategy }
6162
)
6263
rescue DeploymentCreate::Error => e
6364
unprocessable!(e.message)
@@ -97,6 +98,22 @@ def cancel
9798
head :ok
9899
end
99100

101+
def continue
102+
deployment = DeploymentModel.find(guid: hashed_params[:guid])
103+
104+
resource_not_found!(:deployment) unless deployment && permission_queryer.can_manage_apps_in_active_space?(deployment.app.space.id) &&
105+
permission_queryer.is_space_active?(deployment.app.space.id)
106+
107+
begin
108+
DeploymentContinue.continue(deployment:, user_audit_info:)
109+
logger.info("Continued deployment #{deployment.guid} for app #{deployment.app_guid}")
110+
rescue DeploymentContinue::Error => e
111+
unprocessable!(e.message)
112+
end
113+
114+
head :ok
115+
end
116+
100117
private
101118

102119
def deployments_not_enabled!

app/jobs/runtime/prune_completed_deployments.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def perform
2525
select(:id)
2626

2727
deployments_to_delete = deployments_dataset.
28-
exclude(state: [DeploymentModel::DEPLOYING_STATE, DeploymentModel::CANCELING_STATE]).
28+
exclude(state: DeploymentModel::ACTIVE_STATES).
2929
exclude(id: deployments_to_keep)
3030

3131
delete_count = DeploymentDelete.delete(deployments_to_delete)

app/messages/deployment_create_message.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class DeploymentCreateMessage < MetadataBaseMessage
1111

1212
validates_with NoAdditionalKeysValidator
1313
validates :strategy,
14-
inclusion: { in: %w[rolling], message: "'%<value>s' is not a supported deployment strategy" },
14+
inclusion: { in: %w[rolling canary], message: "'%<value>s' is not a supported deployment strategy" },
1515
allow_nil: true
1616
validate :mutually_exclusive_droplet_sources
1717

app/models/runtime/app_model.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def stopped?
132132
end
133133

134134
def deploying?
135-
deployments_dataset.where(state: DeploymentModel::DEPLOYING_STATE).any?
135+
deployments_dataset.where(state: DeploymentModel::PROGRESSING_STATES).any?
136136
end
137137

138138
def self.user_visibility_filter(user)

app/models/runtime/deployment_model.rb

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ module VCAP::CloudController
22
class DeploymentModel < Sequel::Model(:deployments)
33
DEPLOYMENT_STATES = [
44
DEPLOYING_STATE = 'DEPLOYING'.freeze,
5+
PREPAUSED_STATE = 'PREPAUSED'.freeze,
6+
PAUSED_STATE = 'PAUSED'.freeze,
57
DEPLOYED_STATE = 'DEPLOYED'.freeze,
68
CANCELING_STATE = 'CANCELING'.freeze,
79
CANCELED_STATE = 'CANCELED'.freeze
@@ -13,15 +15,28 @@ class DeploymentModel < Sequel::Model(:deployments)
1315
].freeze
1416

1517
STATUS_REASONS = [
16-
DEPLOYED_STATUS_REASON = 'DEPLOYED'.freeze,
1718
DEPLOYING_STATUS_REASON = 'DEPLOYING'.freeze,
19+
PAUSED_STATUS_REASON = 'PAUSED'.freeze,
20+
DEPLOYED_STATUS_REASON = 'DEPLOYED'.freeze,
1821
CANCELED_STATUS_REASON = 'CANCELED'.freeze,
1922
CANCELING_STATUS_REASON = 'CANCELING'.freeze,
2023
SUPERSEDED_STATUS_REASON = 'SUPERSEDED'.freeze
2124
].freeze
2225

2326
DEPLOYMENT_STRATEGIES = [
24-
ROLLING_STRATEGY = 'rolling'.freeze
27+
ROLLING_STRATEGY = 'rolling'.freeze,
28+
CANARY_STRATEGY = 'canary'.freeze
29+
].freeze
30+
31+
PROGRESSING_STATES = [
32+
DEPLOYING_STATE,
33+
PREPAUSED_STATE,
34+
PAUSED_STATE
35+
].freeze
36+
37+
ACTIVE_STATES = [
38+
*PROGRESSING_STATES,
39+
CANCELING_STATE
2540
].freeze
2641

2742
many_to_one :app,
@@ -63,7 +78,7 @@ class DeploymentModel < Sequel::Model(:deployments)
6378

6479
dataset_module do
6580
def deploying_count
66-
where(state: DeploymentModel::DEPLOYING_STATE).count
81+
where(state: DeploymentModel::PROGRESSING_STATES).count
6782
end
6883
end
6984

@@ -73,13 +88,15 @@ def before_update
7388
end
7489

7590
def deploying?
76-
state == DEPLOYING_STATE
91+
DeploymentModel::PROGRESSING_STATES.include?(state)
7792
end
7893

7994
def cancelable?
80-
valid_states_for_cancel = [DeploymentModel::DEPLOYING_STATE,
81-
DeploymentModel::CANCELING_STATE]
82-
valid_states_for_cancel.include?(state)
95+
DeploymentModel::ACTIVE_STATES.include?(state)
96+
end
97+
98+
def continuable?
99+
state == DeploymentModel::PAUSED_STATE
83100
end
84101

85102
private

app/presenters/v3/deployment_presenter.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ def build_links
7676
method: 'POST'
7777
}
7878
end
79+
end.tap do |links|
80+
if deployment.continuable?
81+
links[:continue] = {
82+
href: url_builder.build_url(path: "/v3/deployments/#{deployment.guid}/actions/continue"),
83+
method: 'POST'
84+
}
85+
end
7986
end
8087
end
8188
end

app/repositories/deployment_event_repository.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ def self.record_create(deployment, droplet, user_audit_info, v3_app_name, space_
1111
droplet_guid: droplet&.guid,
1212
type: type,
1313
revision_guid: deployment.revision_guid,
14-
request: params
14+
request: params,
15+
strategy: deployment.strategy
1516
}
1617

1718
Event.create(
@@ -53,6 +54,30 @@ def self.record_cancel(deployment, droplet, user_audit_info, v3_app_name, space_
5354
organization_guid: org_guid
5455
)
5556
end
57+
58+
def self.record_continue(deployment, droplet, user_audit_info, v3_app_name, space_guid, org_guid)
59+
VCAP::AppLogEmitter.emit(deployment.app_guid, "Continuing deployment for app with guid #{deployment.app_guid}")
60+
61+
metadata = {
62+
deployment_guid: deployment.guid,
63+
droplet_guid: droplet&.guid
64+
}
65+
66+
Event.create(
67+
type: EventTypes::APP_DEPLOYMENT_CONTINUE,
68+
actor: user_audit_info.user_guid,
69+
actor_type: 'user',
70+
actor_name: user_audit_info.user_email,
71+
actor_username: user_audit_info.user_name,
72+
actee: deployment.app_guid,
73+
actee_type: 'app',
74+
actee_name: v3_app_name,
75+
timestamp: Sequel::CURRENT_TIMESTAMP,
76+
metadata: metadata,
77+
space_guid: space_guid,
78+
organization_guid: org_guid
79+
)
80+
end
5681
end
5782
end
5883
end

app/repositories/event_types.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class EventTypesError < StandardError
4747
APP_REVISION_ENV_VARS_SHOW = 'audit.app.revision.environment_variables.show'.freeze,
4848
APP_DEPLOYMENT_CANCEL = 'audit.app.deployment.cancel'.freeze,
4949
APP_DEPLOYMENT_CREATE = 'audit.app.deployment.create'.freeze,
50+
APP_DEPLOYMENT_CONTINUE = 'audit.app.deployment.continue'.freeze,
5051
APP_COPY_BITS = 'audit.app.copy-bits'.freeze,
5152
APP_UPLOAD_BITS = 'audit.app.upload-bits'.freeze,
5253
APP_APPLY_MANIFEST = 'audit.app.apply_manifest'.freeze,

0 commit comments

Comments
 (0)