-
Notifications
You must be signed in to change notification settings - Fork 115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Return success before fully deployed #208
Changes from all commits
8f46ab3
d6004c1
894e5af
ba66e36
480d41b
5447bc6
ee5e9cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,9 @@ | |
module KubernetesDeploy | ||
class Deployment < KubernetesResource | ||
TIMEOUT = 7.minutes | ||
REQUIRED_ROLLOUT_ANNOTATION = 'kubernetes-deploy.shopify.io/required-rollout' | ||
REQUIRED_ROLLOUT_TYPES = %w(maxUnavailable full none).freeze | ||
DEFAULT_REQUIRED_ROLLOUT = 'full' | ||
|
||
def sync | ||
raw_json, _err, st = kubectl.run("get", type, @name, "--output=json") | ||
|
@@ -19,13 +22,15 @@ def sync | |
conditions = deployment_data.fetch("status", {}).fetch("conditions", []) | ||
@progress_condition = conditions.find { |condition| condition['type'] == 'Progressing' } | ||
@progress_deadline = deployment_data['spec']['progressDeadlineSeconds'] | ||
@max_unavailable = deployment_data.dig('spec', 'strategy', 'rollingUpdate', 'maxUnavailable') | ||
else # reset | ||
@latest_rs = nil | ||
@rollout_data = { "replicas" => 0 } | ||
@status = nil | ||
@progress_condition = nil | ||
@progress_deadline = @definition['spec']['progressDeadlineSeconds'] | ||
@desired_replicas = -1 | ||
@max_unavailable = @definition.dig('spec', 'strategy', 'rollingUpdate', 'maxUnavailable') | ||
end | ||
end | ||
|
||
|
@@ -43,10 +48,23 @@ def fetch_logs | |
def deploy_succeeded? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd really like to start unit testing this now that it's pretty complex. It could reduce the number of possibly-flakey integration tests we'd need (i.e. separate %/# tests would be unnecessary). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added unit tests |
||
return false unless @latest_rs.present? | ||
|
||
@latest_rs.deploy_succeeded? && | ||
@latest_rs.desired_replicas == @desired_replicas && # latest RS fully scaled up | ||
@rollout_data["updatedReplicas"].to_i == @desired_replicas && | ||
@rollout_data["updatedReplicas"].to_i == @rollout_data["availableReplicas"].to_i | ||
case required_rollout | ||
when 'full' | ||
@latest_rs.deploy_succeeded? && | ||
@latest_rs.desired_replicas == @desired_replicas && # latest RS fully scaled up | ||
@rollout_data["updatedReplicas"].to_i == @desired_replicas && | ||
@rollout_data["updatedReplicas"].to_i == @rollout_data["availableReplicas"].to_i | ||
when 'none' | ||
true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This case is untested. I think a test using a bad readiness probe + the annotation could work nicely. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a unit test for this |
||
when 'maxUnavailable' | ||
minimum_needed = min_available_replicas | ||
|
||
@latest_rs.desired_replicas >= minimum_needed && | ||
@latest_rs.ready_replicas >= minimum_needed && | ||
@latest_rs.available_replicas >= minimum_needed | ||
else | ||
raise FatalDeploymentError, rollout_annotation_err_msg | ||
end | ||
end | ||
|
||
def deploy_failed? | ||
|
@@ -81,8 +99,29 @@ def exists? | |
@found | ||
end | ||
|
||
def validate_definition | ||
super | ||
|
||
unless REQUIRED_ROLLOUT_TYPES.include?(required_rollout) | ||
@validation_errors << rollout_annotation_err_msg | ||
end | ||
|
||
strategy = @definition.dig('spec', 'strategy', 'type').to_s | ||
if required_rollout.downcase == 'maxunavailable' && strategy.downcase != 'rollingupdate' | ||
@validation_errors << "'#{REQUIRED_ROLLOUT_ANNOTATION}: #{required_rollout}' is incompatible "\ | ||
"with strategy '#{strategy}'" | ||
end | ||
|
||
@validation_errors.empty? | ||
end | ||
|
||
private | ||
|
||
def rollout_annotation_err_msg | ||
"'#{REQUIRED_ROLLOUT_ANNOTATION}: #{required_rollout}' is invalid. "\ | ||
"Acceptable values: #{REQUIRED_ROLLOUT_TYPES.join(', ')}" | ||
end | ||
|
||
def deploy_failing_to_progress? | ||
return false unless @progress_condition.present? | ||
|
||
|
@@ -98,18 +137,22 @@ def deploy_failing_to_progress? | |
Time.parse(@progress_condition["lastUpdateTime"]).to_i >= (@deploy_started_at - 5.seconds).to_i | ||
end | ||
|
||
def find_latest_rs(deployment_data) | ||
label_string = deployment_data["spec"]["selector"]["matchLabels"].map { |k, v| "#{k}=#{v}" }.join(",") | ||
def all_rs_data(match_labels) | ||
label_string = match_labels.map { |k, v| "#{k}=#{v}" }.join(",") | ||
raw_json, _err, st = kubectl.run("get", "replicasets", "--output=json", "--selector=#{label_string}") | ||
return unless st.success? | ||
return {} unless st.success? | ||
|
||
JSON.parse(raw_json)["items"] | ||
end | ||
|
||
all_rs_data = JSON.parse(raw_json)["items"] | ||
def find_latest_rs(deployment_data) | ||
current_revision = deployment_data["metadata"]["annotations"]["deployment.kubernetes.io/revision"] | ||
|
||
latest_rs_data = all_rs_data.find do |rs| | ||
latest_rs_data = all_rs_data(deployment_data["spec"]["selector"]["matchLabels"]).find do |rs| | ||
rs["metadata"]["ownerReferences"].any? { |ref| ref["uid"] == deployment_data["metadata"]["uid"] } && | ||
rs["metadata"]["annotations"]["deployment.kubernetes.io/revision"] == current_revision | ||
end | ||
|
||
return unless latest_rs_data.present? | ||
|
||
rs = ReplicaSet.new( | ||
|
@@ -123,5 +166,18 @@ def find_latest_rs(deployment_data) | |
rs.sync(latest_rs_data) | ||
rs | ||
end | ||
|
||
def min_available_replicas | ||
if @max_unavailable =~ /%/ | ||
(@desired_replicas * (100 - @max_unavailable.to_i) / 100.0).ceil | ||
else | ||
@desired_replicas - @max_unavailable.to_i | ||
end | ||
end | ||
|
||
def required_rollout | ||
@definition.dig('metadata', 'annotations', REQUIRED_ROLLOUT_ANNOTATION).presence || | ||
DEFAULT_REQUIRED_ROLLOUT | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,13 +3,15 @@ | |
module KubernetesDeploy | ||
class ReplicaSet < PodSetBase | ||
TIMEOUT = 5.minutes | ||
attr_reader :desired_replicas, :pods | ||
attr_reader :desired_replicas, :ready_replicas, :available_replicas, :pods | ||
|
||
def initialize(namespace:, context:, definition:, logger:, parent: nil, deploy_started_at: nil) | ||
@parent = parent | ||
@deploy_started_at = deploy_started_at | ||
@rollout_data = { "replicas" => 0 } | ||
@desired_replicas = -1 | ||
@ready_replicas = -1 | ||
@available_replicas = -1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same thing should technically be done in the reset section at L35 too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
@pods = [] | ||
super(namespace: namespace, context: context, definition: definition, logger: logger) | ||
end | ||
|
@@ -26,6 +28,8 @@ def sync(rs_data = nil) | |
@rollout_data = { "replicas" => 0 }.merge( | ||
rs_data["status"].slice("replicas", "availableReplicas", "readyReplicas") | ||
) | ||
@ready_replicas = @rollout_data['readyReplicas'].to_i | ||
@available_replicas = @rollout_data["availableReplicas"].to_i | ||
@status = @rollout_data.map { |state_replicas, num| "#{num} #{state_replicas.chop.pluralize(num)}" }.join(", ") | ||
@pods = find_pods(rs_data) | ||
else # reset | ||
|
@@ -34,6 +38,8 @@ def sync(rs_data = nil) | |
@status = nil | ||
@pods = [] | ||
@desired_replicas = -1 | ||
@ready_replicas = -1 | ||
@available_replicas = -1 | ||
end | ||
end | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
--- | ||
apiVersion: apps/v1beta1 | ||
kind: Deployment | ||
metadata: | ||
name: web | ||
uid: foobar | ||
annotations: | ||
"deployment.kubernetes.io/revision": "1" | ||
spec: | ||
replicas: 3 | ||
progressDeadlineSeconds: 10 | ||
strategy: | ||
type: RollingUpdate | ||
rollingUpdate: | ||
maxSurge: 1 | ||
maxUnavailable: 1 | ||
selector: | ||
matchLabels: | ||
name: web | ||
app: hello-cloud | ||
template: | ||
metadata: | ||
labels: | ||
name: web | ||
app: hello-cloud | ||
spec: | ||
containers: | ||
- name: app | ||
image: busybox | ||
status: | ||
replicas: 3 | ||
conditions: | ||
- type: Progressing | ||
status: True | ||
lastUpdateTime: "2018-01-09 22:56:45 UTC" | ||
|
||
--- | ||
apiVersion: apps/v1beta1 | ||
kind: ReplicaSet | ||
metadata: | ||
name: web-1 | ||
annotations: | ||
"deployment.kubernetes.io/revision": "1" | ||
ownerReferences: | ||
- uid: foobar | ||
spec: | ||
replicas: 3 | ||
selector: | ||
matchLabels: | ||
name: web | ||
app: hello-cloud | ||
template: | ||
metadata: | ||
labels: | ||
name: web | ||
app: hello-cloud | ||
spec: | ||
containers: | ||
- name: app | ||
image: busybox | ||
status: | ||
replicas: 3 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
apiVersion: apps/v1beta1 | ||
kind: Deployment | ||
metadata: | ||
name: web | ||
annotations: | ||
shipit.shopify.io/restart: "true" | ||
kubernetes-deploy.shopify.io/required-rollout: maxUnavailable | ||
spec: | ||
replicas: 2 | ||
selector: | ||
matchLabels: | ||
name: web | ||
app: slow-cloud | ||
strategy: | ||
type: RollingUpdate | ||
rollingUpdate: | ||
maxSurge: 0 | ||
maxUnavailable: 1 | ||
template: | ||
metadata: | ||
labels: | ||
name: web | ||
app: slow-cloud | ||
sha: "<%= current_sha %>" | ||
spec: | ||
terminationGracePeriodSeconds: 0 | ||
containers: | ||
- name: app | ||
image: busybox | ||
imagePullPolicy: IfNotPresent | ||
command: ["tail", "-f", "/dev/null"] | ||
ports: | ||
- containerPort: 80 | ||
name: http |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be consistent about how we reference objects and whether they're in backticks. The new uses both "ReplicaSet" and "
replicaSet
", and has "deployment" and "Deployment" and "deploy". The existing text above uses "Deployment
".There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good eye. I'm going to standardize on deployment and
replicaSet