Skip to content

Commit

Permalink
Add support for Krane for required-rollout annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianna-chang-shopify committed Aug 28, 2019
1 parent fb8ec30 commit 0361d4f
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 5 deletions.
2 changes: 1 addition & 1 deletion lib/kubernetes-deploy/kubernetes_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ def validate_timeout_annotation
end

def timeout_annotation
@definition.dig("metadata", "annotations", TIMEOUT_OVERRIDE_ANNOTATION_DEPRECATED) ||
@definition.dig("metadata", "annotations", TIMEOUT_OVERRIDE_ANNOTATION_DEPRECATED) ||
@definition.dig("metadata", "annotations", TIMEOUT_OVERRIDE_ANNOTATION)
end

Expand Down
18 changes: 14 additions & 4 deletions lib/kubernetes-deploy/kubernetes_resource/deployment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
module KubernetesDeploy
class Deployment < KubernetesResource
TIMEOUT = 7.minutes
REQUIRED_ROLLOUT_ANNOTATION = 'kubernetes-deploy.shopify.io/required-rollout'
REQUIRED_ROLLOUT_ANNOTATION_DEPRECATED = 'kubernetes-deploy.shopify.io/required-rollout'
REQUIRED_ROLLOUT_ANNOTATION = 'krane.shopify.io/required-rollout'
REQUIRED_ROLLOUT_TYPES = %w(maxUnavailable full none).freeze
DEFAULT_REQUIRED_ROLLOUT = 'full'

Expand Down Expand Up @@ -101,7 +102,7 @@ def validate_definition(*)

strategy = @definition.dig('spec', 'strategy', 'type').to_s
if required_rollout.downcase == 'maxunavailable' && strategy.present? && strategy.downcase != 'rollingupdate'
@validation_errors << "'#{REQUIRED_ROLLOUT_ANNOTATION}: #{required_rollout}' is incompatible "\
@validation_errors << "'#{rollout_annotation_prefix}: #{required_rollout}' is incompatible "\
"with strategy '#{strategy}'"
end

Expand Down Expand Up @@ -146,10 +147,18 @@ def progress_deadline
end

def rollout_annotation_err_msg
"'#{REQUIRED_ROLLOUT_ANNOTATION}: #{required_rollout}' is invalid. "\
"'#{rollout_annotation_prefix}: #{required_rollout}' is invalid. "\
"Acceptable values: #{REQUIRED_ROLLOUT_TYPES.join(', ')}"
end

def rollout_annotation_prefix
if @definition.dig("metadata", "annotations").key?(REQUIRED_ROLLOUT_ANNOTATION)
REQUIRED_ROLLOUT_ANNOTATION
elsif @definition.dig("metadata", "annotations").key?(REQUIRED_ROLLOUT_ANNOTATION_DEPRECATED)
REQUIRED_ROLLOUT_ANNOTATION_DEPRECATED
end
end

def deploy_failing_to_progress?
return false unless progress_condition.present?

Expand Down Expand Up @@ -199,7 +208,8 @@ def max_unavailable
end

def required_rollout
@definition.dig('metadata', 'annotations', REQUIRED_ROLLOUT_ANNOTATION).presence || DEFAULT_REQUIRED_ROLLOUT
@definition.dig('metadata', 'annotations', REQUIRED_ROLLOUT_ANNOTATION_DEPRECATED).presence ||
@definition.dig('metadata', 'annotations', REQUIRED_ROLLOUT_ANNOTATION).presence || DEFAULT_REQUIRED_ROLLOUT
end

def percent?(value)
Expand Down
38 changes: 38 additions & 0 deletions test/fixtures/crd/with_custom_conditions_deprecated.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: customizeds.stable.example.io
annotations:
kubernetes-deploy.shopify.io/instance-rollout-conditions: '{
"success_conditions": [
{
"path": "$.status.condition",
"value":"success_value"
},
{
"path":"$.status.test_field",
"value":"success_value"
}
],
"failure_conditions": [
{
"path":"$.status.condition",
"value":"failure_value",
"custom_error_msg":"test custom error message"
},
{
"path":"$.status.test_field",
"value":"failure_value",
"error_msg_path":"$.status.error_msg"
}
]
}'
spec:
group: stable.example.io
names:
kind: Customized
listKind: CustomizedList
plural: customizeds
singular: customized
scope: Namespaced
version: v1
15 changes: 15 additions & 0 deletions test/fixtures/crd/with_default_conditions_deprecated.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: parameterizeds.stable.example.io
annotations:
kubernetes-deploy.shopify.io/instance-rollout-conditions: "true"
spec:
group: stable.example.io
names:
kind: Parameterized
listKind: ParameterizedList
plural: parameterizeds
singular: parameterized
scope: Namespaced
version: v1
85 changes: 85 additions & 0 deletions test/unit/kubernetes-deploy/kubernetes_resource/deployment_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,18 @@ def test_deploy_succeeded_with_max_unavailable_as_percent
refute(deploy.deploy_succeeded?)
end

def test_deploy_succeeded_raises_with_invalid_rollout_annotation_deprecated
deploy = build_synced_deployment(
template: build_deployment_template_deprecated(rollout: 'bad'),
replica_sets: [build_rs_template]
)
msg = "'#{KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_ANNOTATION_DEPRECATED}: bad' is "\
"invalid. Acceptable values: #{KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_TYPES.join(', ')}"
assert_raises_message(KubernetesDeploy::FatalDeploymentError, msg) do
deploy.deploy_succeeded?
end
end

def test_deploy_succeeded_raises_with_invalid_rollout_annotation
deploy = build_synced_deployment(
template: build_deployment_template(rollout: 'bad'),
Expand All @@ -170,6 +182,20 @@ def test_deploy_succeeded_raises_with_invalid_rollout_annotation
end
end

def test_validation_fails_with_invalid_rollout_annotation_deprecated
deploy = build_synced_deployment(template: build_deployment_template_deprecated(rollout: 'bad'), replica_sets: [])
kubectl.expects(:run).with('create', '-f', anything, '--dry-run', '--output=name', anything).returns(
["", "super failed", SystemExit.new(1)]
)
refute(deploy.validate_definition(kubectl))

expected = <<~STRING.strip
super failed
'#{KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_ANNOTATION_DEPRECATED}: bad' is invalid. Acceptable values: #{KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_TYPES.join(', ')}
STRING
assert_equal(expected, deploy.validation_error_msg)
end

def test_validation_fails_with_invalid_rollout_annotation
deploy = build_synced_deployment(template: build_deployment_template(rollout: 'bad'), replica_sets: [])
kubectl.expects(:run).with('create', '-f', anything, '--dry-run', '--output=name', anything).returns(
Expand All @@ -193,6 +219,20 @@ def test_validation_with_percent_rollout_annotation
assert_empty(deploy.validation_error_msg)
end

def test_validation_with_number_rollout_annotation_deprecated
deploy = build_synced_deployment(template: build_deployment_template_deprecated(rollout: '10'), replica_sets: [])
kubectl.expects(:run).with('create', '-f', anything, '--dry-run', '--output=name', anything).returns(
["", "super failed", SystemExit.new(1)]
)

refute(deploy.validate_definition(kubectl))
expected = <<~STRING.strip
super failed
'#{KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_ANNOTATION_DEPRECATED}: 10' is invalid. Acceptable values: #{KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_TYPES.join(', ')}
STRING
assert_equal(expected, deploy.validation_error_msg)
end

def test_validation_with_number_rollout_annotation
deploy = build_synced_deployment(template: build_deployment_template(rollout: '10'), replica_sets: [])
kubectl.expects(:run).with('create', '-f', anything, '--dry-run', '--output=name', anything).returns(
Expand All @@ -207,6 +247,23 @@ def test_validation_with_number_rollout_annotation
assert_equal(expected, deploy.validation_error_msg)
end

def test_validation_fails_with_invalid_mix_of_annotation_deprecated
deploy = build_synced_deployment(
template: build_deployment_template_deprecated(rollout: 'maxUnavailable', strategy: 'Recreate'),
replica_sets: [build_rs_template]
)
kubectl.expects(:run).with('create', '-f', anything, '--dry-run', '--output=name', anything).returns(
["", "super failed", SystemExit.new(1)]
)
refute(deploy.validate_definition(kubectl))

expected = <<~STRING.strip
super failed
'#{KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_ANNOTATION_DEPRECATED}: maxUnavailable' is incompatible with strategy 'Recreate'
STRING
assert_equal(expected, deploy.validation_error_msg)
end

def test_validation_fails_with_invalid_mix_of_annotation
deploy = build_synced_deployment(
template: build_deployment_template(rollout: 'maxUnavailable', strategy: 'Recreate'),
Expand Down Expand Up @@ -365,6 +422,34 @@ def test_deploy_timed_out_based_on_progress_deadline_ignores_statuses_for_older_

private

def build_deployment_template_deprecated(status: { 'replicas' => 3 }, rollout: nil,
strategy: 'rollingUpdate', max_unavailable: nil)

base_deployment_manifest = fixtures.find { |fixture| fixture["kind"] == "Deployment" }
result = base_deployment_manifest.deep_merge("status" => status)
if rollout
result["metadata"]["annotations"][KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_ANNOTATION_DEPRECATED] = rollout
end

if (spec_override = status["replicas"].presence) # ignores possibility of surge; need a spec_replicas arg for that
result["spec"]["replicas"] = spec_override
end

if strategy == "Recreate"
result["spec"]["strategy"] = { "type" => strategy }
end

if strategy.nil?
result["spec"]["strategy"] = nil
end

if max_unavailable
result["spec"]["strategy"]["rollingUpdate"] = { "maxUnavailable" => max_unavailable }
end

result
end

def build_deployment_template(status: { 'replicas' => 3 }, rollout: nil,
strategy: 'rollingUpdate', max_unavailable: nil)

Expand Down

0 comments on commit 0361d4f

Please sign in to comment.