Skip to content

Commit

Permalink
Improve helper responsibilities
Browse files Browse the repository at this point in the history
  • Loading branch information
KnVerey committed Jan 10, 2018
1 parent 0ad2256 commit ffb648b
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 71 deletions.
2 changes: 1 addition & 1 deletion lib/kubernetes-deploy/kubernetes_resource/deployment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def validate_definition

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

Expand Down
195 changes: 125 additions & 70 deletions test/unit/kubernetes-deploy/kubernetes_resource/deployment_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,150 +8,188 @@ def setup
end

def test_deploy_succeeded_with_none_annotation
status = {
deployment_status = {
"replicas" => 3,
"updatedReplicas" => 1,
"unavailableReplicas" => 1,
"availableReplicas" => 0
}

new_rs_status = {
rs_status = {
"replicas" => 3,
"availableReplicas" => 0,
"readyReplicas" => 0
}
deploy = build_synced_deployment(status: status, new_rs_status: new_rs_status, rollout: 'none', max_unavail: 1)
dep_template = build_deployment_template(status: deployment_status, rollout: 'none',
strategy: 'RollingUpdate', max_unavailable: 1)
deploy = build_synced_deployment(template: dep_template, replica_sets: [build_rs_template(status: rs_status)])
assert deploy.deploy_succeeded?
end

def test_deploy_succeeded_is_false_with_none_annotation_before_new_rs_created
status = {
deployment_status = {
"replicas" => 3,
"updatedReplicas" => 3,
"unavailableReplicas" => 0,
"availableReplicas" => 3
}
deploy = build_synced_deployment(status: status, new_rs_status: nil, rollout: 'none', max_unavail: 1)
deploy = build_synced_deployment(
template: build_deployment_template(status: deployment_status, rollout: 'none'),
replica_sets: []
)
refute deploy.deploy_succeeded?
end

def test_deploy_succeeded_with_max_unavailable
status = {
deployment_status = {
"replicas" => 3, # one terminating in old rs, one starting in new rs, one up in new rs
"updatedReplicas" => 2,
"unavailableReplicas" => 2,
"availableReplicas" => 1
}

new_rs_status = {
rs_status = {
"replicas" => 2,
"availableReplicas" => 1,
"readyReplicas" => 1
}
deploy = build_synced_deployment(status: status, new_rs_status: new_rs_status,
rollout: 'maxUnavailable', max_unavail: 3)
replica_sets = [build_rs_template(status: rs_status)]

deploy = build_synced_deployment(
template: build_deployment_template(status: deployment_status, rollout: 'maxUnavailable', max_unavailable: 3),
replica_sets: replica_sets
)
assert deploy.deploy_succeeded?

deploy = build_synced_deployment(status: status, new_rs_status: new_rs_status,
rollout: 'maxUnavailable', max_unavail: 2)
deploy = build_synced_deployment(
template: build_deployment_template(status: deployment_status, rollout: 'maxUnavailable', max_unavailable: 2),
replica_sets: replica_sets
)
assert deploy.deploy_succeeded?

deploy = build_synced_deployment(status: status, new_rs_status: new_rs_status,
rollout: 'maxUnavailable', max_unavail: 1)
deploy = build_synced_deployment(
template: build_deployment_template(status: deployment_status, rollout: 'maxUnavailable', max_unavailable: 1),
replica_sets: replica_sets
)
refute deploy.deploy_succeeded?

deploy = build_synced_deployment(status: status, new_rs_status: new_rs_status,
rollout: 'maxUnavailable', max_unavail: 0)
deploy = build_synced_deployment(
template: build_deployment_template(status: deployment_status, rollout: 'maxUnavailable', max_unavailable: 0),
replica_sets: replica_sets
)
refute deploy.deploy_succeeded?
end

def test_deploy_succeeded_with_max_unavailable_as_percent
status = {
deployment_status = {
"replicas" => 3,
"updatedReplicas" => 2,
"unavailableReplicas" => 2,
"availableReplicas" => 1
}

new_rs_status = {
rs_status = {
"replicas" => 2,
"availableReplicas" => 1,
"readyReplicas" => 1
}
deploy = build_synced_deployment(status: status, new_rs_status: new_rs_status,
rollout: 'maxUnavailable', max_unavail: "100%")
replica_sets = [build_rs_template(status: rs_status)]

dep_template = build_deployment_template(status: deployment_status,
rollout: 'maxUnavailable', max_unavailable: '100%')
deploy = build_synced_deployment(template: dep_template, replica_sets: replica_sets)
assert deploy.deploy_succeeded?

deploy = build_synced_deployment(status: status, new_rs_status: new_rs_status,
rollout: 'maxUnavailable', max_unavail: "67%") # rounds up to two max
# rounds up to two max
deploy = build_synced_deployment(
template: build_deployment_template(status: deployment_status, rollout: 'maxUnavailable', max_unavailable: '67%'),
replica_sets: replica_sets
)
assert deploy.deploy_succeeded?

deploy = build_synced_deployment(status: status, new_rs_status: new_rs_status,
rollout: 'maxUnavailable', max_unavail: "66%") # rounds down to one max
# rounds down to one max
deploy = build_synced_deployment(
template: build_deployment_template(status: deployment_status, rollout: 'maxUnavailable', max_unavailable: '66%'),
replica_sets: replica_sets
)
refute deploy.deploy_succeeded?

deploy = build_synced_deployment(status: status, new_rs_status: new_rs_status,
rollout: 'maxUnavailable', max_unavail: "0%")
deploy = build_synced_deployment(
template: build_deployment_template(status: deployment_status, rollout: 'maxUnavailable', max_unavailable: '0%'),
replica_sets: replica_sets
)
refute deploy.deploy_succeeded?
end

def test_deploy_succeeded_raises_with_invalid_rollout_annotation
deploy = build_synced_deployment(status: {}, new_rs_status: {}, rollout: 'bad', max_unavail: "33%")
m = "'kubernetes-deploy.shopify.io/required-rollout: bad' is invalid. Acceptable values: maxUnavailable, full, none"
assert_raises_message(KubernetesDeploy::FatalDeploymentError, m) do
deploy = build_synced_deployment(
template: build_deployment_template(rollout: 'bad'),
replica_sets: [build_rs_template]
)
msg = "'#{KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_ANNOTATION}: bad' is "\
"invalid. Acceptable values: maxUnavailable, full, none"
assert_raises_message(KubernetesDeploy::FatalDeploymentError, msg) do
deploy.deploy_succeeded?
end
end

def test_validation_fails_with_invalid_rollout_annotation
deploy = build_synced_deployment(status: {}, new_rs_status: {}, rollout: 'bad', max_unavail: false)
deploy = build_synced_deployment(template: build_deployment_template(rollout: 'bad'), replica_sets: [])
deploy.kubectl.expects(:run).with('create', '-f', anything, '--dry-run', '--output=name', anything).returns(
["", "super failed", SystemExit.new(1)]
)
refute deploy.validate_definition

expected = <<~STRING.strip
super failed
'kubernetes-deploy.shopify.io/required-rollout: bad' is invalid. Acceptable values: maxUnavailable, full, none
'#{KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_ANNOTATION}: bad' is invalid. Acceptable values: maxUnavailable, full, none
STRING
assert_equal expected, deploy.validation_error_msg
end

def test_validation_fails_with_invalid_mix_of_annotation
deploy = build_synced_deployment(status: {}, new_rs_status: {}, rollout: 'maxUnavailable', max_unavail: false)
deploy = build_synced_deployment(
template: build_deployment_template(rollout: 'maxUnavailable', strategy: 'Recreate'),
replica_sets: [build_rs_template]
)
deploy.kubectl.expects(:run).with('create', '-f', anything, '--dry-run', '--output=name', anything).returns(
["", "super failed", SystemExit.new(1)]
)
refute deploy.validate_definition

expected = <<~STRING.strip
super failed
'kubernetes-deploy.shopify.io/required-rollout: maxUnavailable' is invalid with strategy 'Recreate'
'#{KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_ANNOTATION}: maxUnavailable' is incompatible with strategy 'Recreate'
STRING
assert_equal expected, deploy.validation_error_msg
end

def test_deploy_succeeded_not_fooled_by_stale_rs_data_in_deploy_status
status = {
deployment_status = {
"replicas" => 3,
"updatedReplicas" => 3, # stale -- hasn't been updated since new RS was created
"unavailableReplicas" => 0,
"availableReplicas" => 3
}

new_rs_status = {
rs_status = {
"replicas" => 1,
"availableReplicas" => 0,
"readyReplicas" => 0
}
deploy = build_synced_deployment(status: status, new_rs_status: new_rs_status, max_unavail: 1)
deploy = build_synced_deployment(
template: build_deployment_template(status: deployment_status, rollout: 'full', max_unavailable: 1),
replica_sets: [build_rs_template(status: rs_status)]
)
refute deploy.deploy_succeeded?
end

def test_deploy_timed_out_with_hard_timeout
Timecop.freeze do
deploy = build_synced_deployment(status: { "replicas" => 3, "conditions" => [] },
new_rs_status: { "replicas" => 1 }, max_unavail: 1)
deploy = build_synced_deployment(
template: build_deployment_template(status: { "replicas" => 3, "conditions" => [] }),
replica_sets: [build_rs_template(status: { "replica" => 1 })]
)
deploy.deploy_started_at = Time.now.utc - KubernetesDeploy::Deployment::TIMEOUT
refute deploy.deploy_timed_out?

Expand All @@ -164,7 +202,7 @@ def test_deploy_timed_out_with_hard_timeout

def test_deploy_timed_out_based_on_progress_deadline
Timecop.freeze do
status = {
deployment_status = {
"replicas" => 3,
"conditions" => [{
"type" => "Progressing",
Expand All @@ -173,7 +211,10 @@ def test_deploy_timed_out_based_on_progress_deadline
"reason" => "Failed to progress"
}]
}
deploy = build_synced_deployment(status: status, new_rs_status: { "replicas" => 1 }, max_unavail: 1)
deploy = build_synced_deployment(
template: build_deployment_template(status: deployment_status),
replica_sets: [build_rs_template(status: { "replica" => 1 })]
)
deploy.deploy_started_at = Time.now.utc - 3.minutes
deploy.kubectl.expects(:server_version).returns(Gem::Version.new("1.8"))

Expand All @@ -184,7 +225,7 @@ def test_deploy_timed_out_based_on_progress_deadline

def test_deploy_timed_out_based_on_progress_deadline_ignores_conditions_older_than_the_deploy
Timecop.freeze do
status = {
deployment_status = {
"replicas" => 3,
"conditions" => [{
"type" => "Progressing",
Expand All @@ -193,7 +234,10 @@ def test_deploy_timed_out_based_on_progress_deadline_ignores_conditions_older_th
"reason" => "Failed to progress"
}]
}
deploy = build_synced_deployment(status: status, new_rs_status: { "replicas" => 1 }, max_unavail: 1)
deploy = build_synced_deployment(
template: build_deployment_template(status: deployment_status),
replica_sets: [build_rs_template(status: { "replica" => 1 })]
)
deploy.kubectl.expects(:server_version).returns(Gem::Version.new("1.8")).at_least_once

deploy.deploy_started_at = nil # not started yet
Expand All @@ -209,7 +253,7 @@ def test_deploy_timed_out_based_on_progress_deadline_ignores_conditions_older_th

def test_deploy_timed_out_based_on_progress_deadline_accommodates_stale_conditions_bug_in_k8s_176_and_lower
Timecop.freeze do
status = {
deployment_status = {
"replicas" => 3,
"conditions" => [{
"type" => "Progressing",
Expand All @@ -218,7 +262,10 @@ def test_deploy_timed_out_based_on_progress_deadline_accommodates_stale_conditio
"reason" => "Failed to progress"
}]
}
deploy = build_synced_deployment(status: status, new_rs_status: { "replicas" => 1 }, max_unavail: 1)
deploy = build_synced_deployment(
template: build_deployment_template(status: deployment_status),
replica_sets: [build_rs_template(status: { "replica" => 1 })]
)
deploy.deploy_started_at = Time.now.utc - 5.seconds # progress deadline of 10s has not elapsed
deploy.kubectl.expects(:server_version).returns(Gem::Version.new("1.7.6"))

Expand All @@ -228,50 +275,58 @@ def test_deploy_timed_out_based_on_progress_deadline_accommodates_stale_conditio

private

def build_synced_deployment(status:, new_rs_status:, rollout: nil, max_unavail:)
spec = base_deployment_manifest.deep_merge(
"spec" => { "replicas" => status["replicas"] }, # note: this ignores the possibility of surging
"status" => status
)
spec["metadata"]["annotations"][KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_ANNOTATION] = rollout if rollout
if max_unavail
spec["spec"]["strategy"]["rollingUpdate"]["maxUnavailable"] = max_unavail
else
spec["spec"]["strategy"] = { "type" => "Recreate" }
def build_deployment_template(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)
result["metadata"]["annotations"][KubernetesDeploy::Deployment::REQUIRED_ROLLOUT_ANNOTATION] = rollout if rollout

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

deploy = KubernetesDeploy::Deployment.new(namespace: "test", context: "nope", logger: logger, definition: spec)
if strategy == "Recreate"
result["spec"]["strategy"] = { "type" => strategy }
end

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

result
end

def build_rs_template(status: { 'replicas' => 3 })
base_rs_manifest = fixtures.find { |fixture| fixture["kind"] == "ReplicaSet" }
result = base_rs_manifest.deep_merge("status" => status)

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

def build_synced_deployment(template:, replica_sets:)
deploy = KubernetesDeploy::Deployment.new(namespace: "test", context: "nope", logger: logger, definition: template)
deploy.kubectl.expects(:run).with("get", "Deployment", "web", "--output=json").returns(
[spec.to_json, "", SystemExit.new(0)]
[template.to_json, "", SystemExit.new(0)]
)

replicasets = { "items" => [] }
if new_rs_status
replicasets["items"] << base_rs_manifest.deep_merge(
"spec" => { "replicas" => new_rs_status["replicas"] },
"status" => new_rs_status
)
if replica_sets.present?
KubernetesDeploy::ReplicaSet.any_instance.expects(:kubectl).returns(deploy.kubectl)
deploy.kubectl.expects(:run).with("get", "pods", "-a", "--output=json", anything).returns(
['{ "items": [] }', "", SystemExit.new(0)]
)
end

deploy.kubectl.expects(:run).with("get", "replicasets", "--output=json", anything).returns(
[replicasets.to_json, "", SystemExit.new(0)]
[{ "items" => replica_sets }.to_json, "", SystemExit.new(0)]
)
deploy.sync
deploy
end

def base_rs_manifest
fixtures.find { |fixture| fixture["kind"] == "ReplicaSet" }
end

def base_deployment_manifest
fixtures.find { |fixture| fixture["kind"] == "Deployment" }
end

def fixtures
@fixtures ||= YAML.load_stream(File.read(File.join(fixture_path('for_unit_tests'), 'deployment_test.yml')))
end
Expand Down

0 comments on commit ffb648b

Please sign in to comment.