Skip to content

Commit 886a824

Browse files
Samzesethboyles
authored andcommitted
wip canary step model and actions
1 parent 503e65e commit 886a824

File tree

7 files changed

+431
-41
lines changed

7 files changed

+431
-41
lines changed

app/actions/deployment_continue.rb

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,34 @@ def continue(deployment:, user_audit_info:)
1212
reject_invalid_state!(deployment) unless deployment.continuable?
1313

1414
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-
)
15+
16+
# state = if deployment.canary_steps && deployment.canary_current_step != deployment.canary_steps.length
17+
# DeploymentModel::PREPAUSED_STATE
18+
# else
19+
# DeploymentModel::DEPLOYING_STATE
20+
# end
21+
22+
# deployment.update(
23+
# state: state,
24+
# status_value: DeploymentModel::ACTIVE_STATUS_VALUE,
25+
# status_reason: DeploymentModel::DEPLOYING_STATUS_REASON
26+
# )
27+
28+
# TODO: Should we increment step here or in the PAUSE action?
29+
if deployment.canary_steps && deployment.canary_current_step < deployment.canary_steps.length
30+
deployment.update(
31+
state: DeploymentModel::PREPAUSED_STATE,
32+
status_value: DeploymentModel::ACTIVE_STATUS_VALUE,
33+
status_reason: DeploymentModel::DEPLOYING_STATUS_REASON,
34+
canary_current_step: deployment.canary_current_step + 1
35+
)
36+
else
37+
deployment.update(
38+
state: DeploymentModel::DEPLOYING_STATE,
39+
status_value: DeploymentModel::ACTIVE_STATUS_VALUE,
40+
status_reason: DeploymentModel::DEPLOYING_STATUS_REASON
41+
)
42+
end
2043
end
2144
end
2245

app/actions/deployment_create.rb

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,21 @@ def create(app:, user_audit_info:, message:)
5656
strategy: message.strategy,
5757
max_in_flight: message.max_in_flight
5858
)
59+
60+
# TODO: should this dig go into a model func
61+
canary_steps = message.options&.dig(:canary, :steps)
62+
if canary_steps && canary_steps.any?
63+
deployment.update(
64+
canary_steps: canary_steps.map(&:stringify_keys),
65+
canary_current_step: 1
66+
)
67+
end
68+
5969
MetadataUpdate.update(deployment, message)
6070

6171
supersede_deployment(previous_deployment)
6272

63-
process_instances = starting_process_instances(message, desired_instances)
73+
process_instances = starting_process_instances(deployment, desired_instances)
6474

6575
process = create_deployment_process(app, deployment.guid, revision, process_instances)
6676
# Need to transition from STOPPED to STARTED to engage the ProcessObserver to desire the LRP.
@@ -165,6 +175,8 @@ def deployment_for_stopped_app(app, message, previous_deployment, previous_dropl
165175
# Do not create a revision here because AppStart will not handle the rollback case
166176
AppStart.start(app: app, user_audit_info: user_audit_info, create_revision: false)
167177

178+
# TODO, do we update this for canary steps?
179+
168180
deployment = DeploymentModel.create(
169181
app: app,
170182
state: DeploymentModel::DEPLOYED_STATE,
@@ -217,14 +229,14 @@ def starting_state(message)
217229
end
218230
end
219231

220-
def starting_process_instances(message, desired_instances)
221-
if message.strategy == DeploymentModel::CANARY_STRATEGY
222-
# TODO: replace this with [message.max_in_flight, deployment.next_canary_step].min
223-
# or remove this 'if' and make `desired_instances` do this work
224-
1
225-
else
226-
[message.max_in_flight, desired_instances].min
227-
end
232+
def starting_process_instances(deployment, desired_instances)
233+
starting_process_count = if deployment.strategy == DeploymentModel::CANARY_STRATEGY
234+
deployment.canary_step[:canary]
235+
else
236+
desired_instances
237+
end
238+
239+
[deployment.max_in_flight, starting_process_count].min
228240
end
229241

230242
def log_rollback_event(app_guid, user_id, revision_id, strategy)

app/models/runtime/deployment_model.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module VCAP::CloudController
22
class DeploymentModel < Sequel::Model(:deployments)
3+
plugin :serialization
4+
35
DEPLOYMENT_STATES = [
46
DEPLOYING_STATE = 'DEPLOYING'.freeze,
57
PREPAUSED_STATE = 'PREPAUSED'.freeze,
@@ -76,6 +78,8 @@ class DeploymentModel < Sequel::Model(:deployments)
7678
add_association_dependencies labels: :destroy
7779
add_association_dependencies annotations: :destroy
7880

81+
serialize_attributes :json, :canary_steps
82+
7983
dataset_module do
8084
def deploying_count
8185
where(state: DeploymentModel::PROGRESSING_STATES).count
@@ -99,6 +103,40 @@ def continuable?
99103
state == DeploymentModel::PAUSED_STATE
100104
end
101105

106+
# TODO: index 0 for visibility??
107+
def canary_step
108+
# TODO: when strategy is not canary
109+
plan = canary_step_plan
110+
111+
current_step = canary_current_step || 1
112+
plan[current_step - 1]
113+
end
114+
115+
# TODO: to be called by continue deployment. Should this also set the state to pre-paused conditionally (if not last step)
116+
# def set_canary_step_complete
117+
# self.canary_current_step = canary_current_step + 1
118+
# # TODO: do we need to do something for model to save here or is it automatic?
119+
120+
# # if (more steps)
121+
# # set state to prepaused
122+
# # else
123+
# # set state to deploying
124+
# end
125+
126+
# TODO: would be used to populate any API.
127+
def canary_step_plan
128+
# TODO: when strategy is not canary
129+
return [{ canary: 1, original: original_web_process_instance_count }] if canary_steps.nil?
130+
131+
canary_steps.map do |step|
132+
weight = step['instance_weight']
133+
target_canary = (original_web_process_instance_count * (weight.to_f / 100)).round.to_i # TODO: this rounds up at mid point 5.5 (is this an issue(?))
134+
target_canary = 1 if target_canary.zero?
135+
target_original = original_web_process_instance_count - target_canary + 1
136+
{ canary: target_canary, original: target_original }
137+
end
138+
end
139+
102140
private
103141

104142
def set_status_updated_at
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Sequel.migration do
2+
up do
3+
alter_table(:deployments) do
4+
# TODO: what size?
5+
add_column :canary_steps, String, size: 4096
6+
add_column :canary_current_step, :integer
7+
end
8+
end
9+
down do
10+
alter_table(:deployments) do
11+
drop_column :canary_steps
12+
drop_column :canary_current_step
13+
end
14+
end
15+
end

spec/unit/actions/deployment_continue_spec.rb

Lines changed: 130 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,36 +30,140 @@ module VCAP::CloudController
3030
let(:status_value) { DeploymentModel::ACTIVE_STATUS_VALUE }
3131
let(:status_reason) { DeploymentModel::DEPLOYING_STATUS_REASON }
3232

33-
it 'sets the deployments status to DEPLOYING' do
34-
expect(deployment.state).not_to eq(DeploymentModel::DEPLOYING_STATE)
33+
context 'there are no steps defined' do
34+
it 'sets the deployments status to DEPLOYING' do
35+
expect(deployment.state).not_to eq(DeploymentModel::DEPLOYING_STATE)
3536

36-
DeploymentContinue.continue(deployment:, user_audit_info:)
37-
deployment.reload
37+
DeploymentContinue.continue(deployment:, user_audit_info:)
38+
deployment.reload
39+
40+
expect(deployment.state).to eq(DeploymentModel::DEPLOYING_STATE)
41+
expect(deployment.status_value).to eq(DeploymentModel::ACTIVE_STATUS_VALUE)
42+
expect(deployment.status_reason).to eq(DeploymentModel::DEPLOYING_STATUS_REASON)
43+
end
44+
45+
it 'records an audit event for the continue deployment' do
46+
DeploymentContinue.continue(deployment:, user_audit_info:)
47+
48+
event = VCAP::CloudController::Event.find(type: 'audit.app.deployment.continue')
49+
expect(event).not_to be_nil
50+
expect(event.actor).to eq('1234')
51+
expect(event.actor_type).to eq('user')
52+
expect(event.actor_name).to eq('[email protected]')
53+
expect(event.actor_username).to eq('eric')
54+
expect(event.actee).to eq(app.guid)
55+
expect(event.actee_type).to eq('app')
56+
expect(event.actee_name).to eq(app.name)
57+
expect(event.timestamp).to be
58+
expect(event.space_guid).to eq(app.space_guid)
59+
expect(event.organization_guid).to eq(app.space.organization.guid)
60+
expect(event.metadata).to eq({
61+
'droplet_guid' => droplet.guid,
62+
'deployment_guid' => deployment.guid
63+
})
64+
end
65+
end
66+
67+
context 'and there are no remaining steps' do
68+
let!(:deployment) do
69+
VCAP::CloudController::DeploymentModel.make(
70+
state: state,
71+
status_value: status_value,
72+
status_reason: status_reason,
73+
droplet: droplet,
74+
app: original_web_process.app,
75+
deploying_web_process: deploying_web_process,
76+
canary_current_step: 2
77+
)
78+
end
79+
80+
it 'sets the deployments status to DEPLOYING' do
81+
expect(deployment.state).not_to eq(DeploymentModel::DEPLOYING_STATE)
82+
83+
DeploymentContinue.continue(deployment:, user_audit_info:)
84+
deployment.reload
85+
86+
expect(deployment.state).to eq(DeploymentModel::DEPLOYING_STATE)
87+
expect(deployment.status_value).to eq(DeploymentModel::ACTIVE_STATUS_VALUE)
88+
expect(deployment.status_reason).to eq(DeploymentModel::DEPLOYING_STATUS_REASON)
89+
end
90+
91+
it 'records an audit event for the continue deployment' do
92+
DeploymentContinue.continue(deployment:, user_audit_info:)
3893

39-
expect(deployment.state).to eq(DeploymentModel::DEPLOYING_STATE)
40-
expect(deployment.status_value).to eq(DeploymentModel::ACTIVE_STATUS_VALUE)
41-
expect(deployment.status_reason).to eq(DeploymentModel::DEPLOYING_STATUS_REASON)
94+
event = VCAP::CloudController::Event.find(type: 'audit.app.deployment.continue')
95+
expect(event).not_to be_nil
96+
expect(event.actor).to eq('1234')
97+
expect(event.actor_type).to eq('user')
98+
expect(event.actor_name).to eq('[email protected]')
99+
expect(event.actor_username).to eq('eric')
100+
expect(event.actee).to eq(app.guid)
101+
expect(event.actee_type).to eq('app')
102+
expect(event.actee_name).to eq(app.name)
103+
expect(event.timestamp).to be
104+
expect(event.space_guid).to eq(app.space_guid)
105+
expect(event.organization_guid).to eq(app.space.organization.guid)
106+
expect(event.metadata).to eq({
107+
'droplet_guid' => droplet.guid,
108+
'deployment_guid' => deployment.guid
109+
})
110+
end
42111
end
43112

44-
it 'records an audit event for the continue deployment' do
45-
DeploymentContinue.continue(deployment:, user_audit_info:)
46-
47-
event = VCAP::CloudController::Event.find(type: 'audit.app.deployment.continue')
48-
expect(event).not_to be_nil
49-
expect(event.actor).to eq('1234')
50-
expect(event.actor_type).to eq('user')
51-
expect(event.actor_name).to eq('[email protected]')
52-
expect(event.actor_username).to eq('eric')
53-
expect(event.actee).to eq(app.guid)
54-
expect(event.actee_type).to eq('app')
55-
expect(event.actee_name).to eq(app.name)
56-
expect(event.timestamp).to be
57-
expect(event.space_guid).to eq(app.space_guid)
58-
expect(event.organization_guid).to eq(app.space.organization.guid)
59-
expect(event.metadata).to eq({
60-
'droplet_guid' => droplet.guid,
61-
'deployment_guid' => deployment.guid
62-
})
113+
context 'and there are remaining steps' do
114+
let!(:deployment) do
115+
VCAP::CloudController::DeploymentModel.make(
116+
state: state,
117+
status_value: status_value,
118+
status_reason: status_reason,
119+
droplet: droplet,
120+
app: original_web_process.app,
121+
deploying_web_process: deploying_web_process,
122+
canary_current_step: 1,
123+
canary_steps: [{ 'instance_weight' => 10 }, { 'instance_weight' => 40 }]
124+
)
125+
end
126+
127+
it 'sets the deployments status to PREPAUSED' do
128+
expect(deployment.state).not_to eq(DeploymentModel::DEPLOYING_STATE)
129+
130+
DeploymentContinue.continue(deployment:, user_audit_info:)
131+
deployment.reload
132+
133+
expect(deployment.state).to eq(DeploymentModel::PREPAUSED_STATE)
134+
expect(deployment.status_value).to eq(DeploymentModel::ACTIVE_STATUS_VALUE)
135+
expect(deployment.status_reason).to eq(DeploymentModel::DEPLOYING_STATUS_REASON)
136+
end
137+
138+
it 'increments the current canary step' do
139+
expect(deployment.state).not_to eq(DeploymentModel::DEPLOYING_STATE)
140+
141+
DeploymentContinue.continue(deployment:, user_audit_info:)
142+
deployment.reload
143+
144+
expect(deployment.canary_current_step).to eq(2)
145+
end
146+
147+
it 'records an audit event for the continue deployment' do
148+
DeploymentContinue.continue(deployment:, user_audit_info:)
149+
150+
event = VCAP::CloudController::Event.find(type: 'audit.app.deployment.continue')
151+
expect(event).not_to be_nil
152+
expect(event.actor).to eq('1234')
153+
expect(event.actor_type).to eq('user')
154+
expect(event.actor_name).to eq('[email protected]')
155+
expect(event.actor_username).to eq('eric')
156+
expect(event.actee).to eq(app.guid)
157+
expect(event.actee_type).to eq('app')
158+
expect(event.actee_name).to eq(app.name)
159+
expect(event.timestamp).to be
160+
expect(event.space_guid).to eq(app.space_guid)
161+
expect(event.organization_guid).to eq(app.space.organization.guid)
162+
expect(event.metadata).to eq({
163+
'droplet_guid' => droplet.guid,
164+
'deployment_guid' => deployment.guid
165+
})
166+
end
63167
end
64168
end
65169

spec/unit/actions/deployment_create_spec.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,58 @@ module VCAP::CloudController
11241124

11251125
expect(deployment.state).to eq(DeploymentModel::PREPAUSED_STATE)
11261126
end
1127+
1128+
context 'and there are canary options with steps' do
1129+
let(:message) do
1130+
DeploymentCreateMessage.new({
1131+
relationships: { app: { data: { guid: app.guid } } },
1132+
droplet: { guid: next_droplet.guid },
1133+
strategy: strategy,
1134+
options: {
1135+
max_in_flight: max_in_flight,
1136+
canary: { steps: [{ 'instance_weight' => 66 }, { 'instance_weight' => 99 }] }
1137+
}
1138+
})
1139+
end
1140+
1141+
it 'sets the deployment canary steps' do
1142+
deployment = nil
1143+
1144+
expect do
1145+
deployment = DeploymentCreate.create(app:, message:, user_audit_info:)
1146+
end.to change(DeploymentModel, :count).by(1)
1147+
1148+
expect(deployment.canary_steps).to eq([{ instance_weight: 66 }, { instance_weight: 99 }])
1149+
end
1150+
1151+
it 'sets the deployment canary current step' do
1152+
deployment = nil
1153+
1154+
expect do
1155+
deployment = DeploymentCreate.create(app:, message:, user_audit_info:)
1156+
end.to change(DeploymentModel, :count).by(1)
1157+
1158+
expect(deployment.canary_current_step).to eq(1)
1159+
end
1160+
1161+
it 'sets starting instances to max in flight' do
1162+
DeploymentCreate.create(app:, message:, user_audit_info:)
1163+
1164+
deploying_web_process = app.reload.newest_web_process
1165+
expect(deploying_web_process.instances).to eq(1)
1166+
end
1167+
1168+
context 'when max_in_flight is more than first canary step' do
1169+
let!(:max_in_flight) { 100 }
1170+
1171+
it 'creates a process with first canary step' do
1172+
DeploymentCreate.create(app:, message:, user_audit_info:)
1173+
1174+
deploying_web_process = app.reload.newest_web_process
1175+
expect(deploying_web_process.instances).to eq(2)
1176+
end
1177+
end
1178+
end
11271179
end
11281180

11291181
context 'when the strategy is nil' do

0 commit comments

Comments
 (0)