Skip to content
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

Compare generations when available #325

Merged
merged 1 commit into from
Aug 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions lib/kubernetes-deploy/kubernetes_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ def exists?
@instance_data.present?
end

def current_generation
return -1 unless exists? # must be different default than observed_generation
@instance_data["metadata"]["generation"]
end

def observed_generation
return -2 unless exists?
# populating this is a best practice, but not all controllers actually do it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, do you have an example of a controller that doesn't populate it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since all objects have metadata generations, theoretically any controller could be setting observedGeneration, though this is only meaningful if the controller is writing a status. An example of a resource that has a status but no observedGeneration would be Job or CronJob. See also this k8s core issue. As a sidenote, in 1.11 (I think it made beta there?), CRs will have a reliable metadata.generation we can use to do this properly in our own controllers. Our generic CR success feature should make use of it when available.

@instance_data["status"]["observedGeneration"]
end

def status
exists? ? "Exists" : "Unknown"
end
Expand Down
13 changes: 2 additions & 11 deletions lib/kubernetes-deploy/kubernetes_resource/daemon_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ def deploy_succeeded?
end

def deploy_failed?
pods.present? && pods.any?(&:deploy_failed?)
pods.present? && pods.any?(&:deploy_failed?) &&
observed_generation == current_generation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this not be !=? Or maybe I just misunderstood it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand it, and @KnVerey will hopefully correct me if I'm wrong, current_generation gets bumped when we apply the config. observed_generation gets set by the controller reporting the status. When they don't match it means that we're looking at a status for the previous config not current. E.g. we don't want to say the deployment has failed or succeeded before the status reflects the current config's state.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh! Thanks for the clarification

end

def fetch_logs(kubectl)
Expand All @@ -35,16 +36,6 @@ def fetch_logs(kubectl)

private

def current_generation
return -1 unless exists? # must be different default than observed_generation
@instance_data["metadata"]["generation"]
end

def observed_generation
return -2 unless exists?
@instance_data["status"]["observedGeneration"]
end

def rollout_data
return { "currentNumberScheduled" => 0 } unless exists?
@instance_data["status"]
Expand Down
4 changes: 3 additions & 1 deletion lib/kubernetes-deploy/kubernetes_resource/deployment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def fetch_logs(kubectl)

def deploy_succeeded?
return false unless exists? && @latest_rs.present?
return false unless observed_generation == current_generation

if required_rollout == 'full'
@latest_rs.deploy_succeeded? &&
Expand All @@ -51,7 +52,8 @@ def deploy_succeeded?
end

def deploy_failed?
@latest_rs&.deploy_failed?
@latest_rs&.deploy_failed? &&
observed_generation == current_generation
end

def failure_message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def status
end

def deploy_succeeded?
exists?
exists? && observed_generation == current_generation
end

def deploy_method
Expand Down
5 changes: 4 additions & 1 deletion lib/kubernetes-deploy/kubernetes_resource/replica_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ def status
end

def deploy_succeeded?
observed_generation == current_generation &&
desired_replicas == rollout_data["availableReplicas"].to_i &&
desired_replicas == rollout_data["readyReplicas"].to_i
end

def deploy_failed?
pods.present? && pods.all?(&:deploy_failed?)
pods.present? &&
pods.all?(&:deploy_failed?) &&
observed_generation == current_generation
end

def desired_replicas
Expand Down
4 changes: 3 additions & 1 deletion lib/kubernetes-deploy/kubernetes_resource/stateful_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def deploy_succeeded?
end
true
else
observed_generation == current_generation &&
status_data['currentRevision'] == status_data['updateRevision'] &&
desired_replicas == status_data['readyReplicas'].to_i &&
desired_replicas == status_data['currentReplicas'].to_i
Expand All @@ -38,7 +39,8 @@ def deploy_succeeded?

def deploy_failed?
return false if update_strategy == ONDELETE
pods.present? && pods.any?(&:deploy_failed?)
pods.present? && pods.any?(&:deploy_failed?) &&
observed_generation == current_generation
end

private
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/for_unit_tests/deployment_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ spec:
image: busybox
status:
replicas: 3
observedGeneration: 2 # current
conditions:
- type: Progressing
status: True
Expand All @@ -43,6 +44,7 @@ apiVersion: apps/v1beta1
kind: ReplicaSet
metadata:
name: web-1
generation: 1
labels:
name: web
app: hello-cloud
Expand All @@ -66,4 +68,5 @@ spec:
- name: app
image: busybox
status:
observedGeneration: 1
replicas: 3
14 changes: 14 additions & 0 deletions test/fixtures/for_unit_tests/pod_disruption_budget_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: test
generation: 2
spec:
minAvailable: 2
selector:
matchLabels:
name: web
app: hello-cloud
status:
observedGeneration: 2
27 changes: 27 additions & 0 deletions test/fixtures/for_unit_tests/replica_set_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
name: test
generation: 2
spec:
replicas: 3
selector:
matchLabels:
app: hello-cloud
name: test
template:
metadata:
labels:
app: hello-cloud
name: test
spec:
containers:
- name: app
image: busybox
imagePullPolicy: IfNotPresent
command: ["tail", "-f", "/dev/null"]
status:
observedGeneration: 2
availableReplicas: 3
readyReplicas: 3
32 changes: 32 additions & 0 deletions test/fixtures/for_unit_tests/stateful_set_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: test-ss
generation: 2
spec:
selector:
matchLabels:
name: test-ss
app: hello-cloud
serviceName: "test-ss"
updateStrategy:
type: RollingUpdate
replicas: 2
template:
metadata:
labels:
app: hello-cloud
name: test-ss
spec:
containers:
- name: app
image: busybox
imagePullPolicy: IfNotPresent
command: ["tail", "-f", "/dev/null"]
status:
replicas: 2
readyReplicas: 2
currentReplicas: 2
observedGeneration: 2
currentRevision: 2
updateRevision: 2
25 changes: 22 additions & 3 deletions test/unit/kubernetes-deploy/kubernetes_resource/daemon_set_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,29 @@ def setup
KubernetesDeploy::Kubectl.any_instance.expects(:run).never
end

def test_deploy_fails_when_updated_available_does_not_match
def test_deploy_not_successful_when_updated_available_does_not_match
ds_template = build_ds_template
ds = build_synced_ds(template: ds_template)
refute ds.deploy_succeeded?
refute_predicate ds, :deploy_succeeded?
end

def test_deploy_succeeded_not_fooled_by_stale_status
status = {
"observedGeneration": 1,
"numberReady": 2,
"desiredNumberScheduled": 2,
"updatedNumberScheduled": 2,
}
ds_template = build_ds_template(status: status)
ds = build_synced_ds(template: ds_template)
refute_predicate ds, :deploy_succeeded?
end

def test_deploy_failed_ensures_controller_has_observed_deploy
ds_template = build_ds_template(status: { "observedGeneration": 1 })
ds = build_synced_ds(template: ds_template)
ds.stubs(:pods).returns([stub(deploy_failed?: true)])
refute_predicate ds, :deploy_failed?
end

def test_deploy_passes_when_updated_available_does_match
Expand All @@ -24,7 +43,7 @@ def test_deploy_passes_when_updated_available_does_match

ds_template = build_ds_template(status: status)
ds = build_synced_ds(template: ds_template)
assert ds.deploy_succeeded?
assert_predicate ds, :deploy_succeeded?
end

private
Expand Down
27 changes: 19 additions & 8 deletions test/unit/kubernetes-deploy/kubernetes_resource/deployment_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -238,24 +238,35 @@ def test_validation_works_with_no_strategy_and_max_unavailable_annotation
assert deploy.validate_definition(kubectl)
end

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

rs_status = {
"replicas" => 1,
"availableReplicas" => 0,
"readyReplicas" => 0
rs_status = { # points to old RS, new one not created yet
"replicas" => 3,
"availableReplicas" => 3,
"readyReplicas" => 3,
"observedGeneration" => 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?
refute_predicate deploy, :deploy_succeeded?
end

def test_deploy_failed_ensures_controller_has_observed_deploy
deploy = build_synced_deployment(
template: build_deployment_template(status: { "observedGeneration" => 1 }, rollout: 'full', max_unavailable: 1),
replica_sets: [build_rs_template]
)
KubernetesDeploy::ReplicaSet.any_instance.stubs(:pods).returns([stub(deploy_failed?: true)])
refute_predicate deploy, :deploy_failed?
end

def test_deploy_timed_out_with_hard_timeout
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'test_helper'

class PodDisruptionBudgetTest < KubernetesDeploy::TestCase
def setup
KubernetesDeploy::Kubectl.any_instance.expects(:run).never
super
end

def test_deploy_succeeded_is_true_as_soon_as_controller_observes_new_version
template = build_pdb_template(status: { "observedGeneration": 2 })
pdb = build_synced_pdb(template: template)
assert_predicate pdb, :deploy_succeeded?
end

def test_deploy_succeeded_not_fooled_by_stale_status
template = build_pdb_template(status: { "observedGeneration": 1 })
pdb = build_synced_pdb(template: template)
refute_predicate pdb, :deploy_succeeded?
end

private

def build_pdb_template(status: {})
pdb_fixture.dup.deep_merge("status" => status)
end

def build_synced_pdb(template:)
pdb = KubernetesDeploy::PodDisruptionBudget.new(namespace: "test", context: "nope",
logger: logger, definition: template)
sync_mediator = KubernetesDeploy::SyncMediator.new(namespace: 'test', context: 'minikube', logger: logger)
sync_mediator.kubectl.expects(:run).with("get", "PodDisruptionBudget", "test", "-a", "--output=json").returns(
[template.to_json, "", SystemExit.new(0)]
)
pdb.sync(sync_mediator)
pdb
end

def pdb_fixture
@pdb_fixture ||= YAML.load_stream(
File.read(File.join(fixture_path('for_unit_tests'), 'pod_disruption_budget_test.yml'))
).find { |fixture| fixture["kind"] == "PodDisruptionBudget" }
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'test_helper'

class ReplicaSetTest < KubernetesDeploy::TestCase
def setup
KubernetesDeploy::Kubectl.any_instance.expects(:run).never
super
end

def test_deploy_succeeded_is_true_when_generation_and_replica_counts_match
template = build_rs_template(status: { "observedGeneration": 2 })
rs = build_synced_rs(template: template)
assert_predicate rs, :deploy_succeeded?
end

def test_deploy_succeeded_not_fooled_by_stale_status
template = build_rs_template(status: { "observedGeneration": 1 })
rs = build_synced_rs(template: template)
refute_predicate rs, :deploy_succeeded?
end

def test_deploy_failed_ensures_controller_has_observed_deploy
template = build_rs_template(status: { "observedGeneration": 1 })
rs = build_synced_rs(template: template)
rs.stubs(:pods).returns([stub(deploy_failed?: true)])
refute_predicate rs, :deploy_failed?
end

private

def build_rs_template(status: {})
rs_fixture.dup.deep_merge("status" => status)
end

def build_synced_rs(template:)
rs = KubernetesDeploy::ReplicaSet.new(namespace: "test", context: "nope", logger: logger, definition: template)
sync_mediator = KubernetesDeploy::SyncMediator.new(namespace: 'test', context: 'minikube', logger: logger)
sync_mediator.kubectl.expects(:run).with("get", "ReplicaSet", "test", "-a", "--output=json").returns(
[template.to_json, "", SystemExit.new(0)]
)
sync_mediator.kubectl.expects(:run).with("get", "Pod", "-a", "--output=json", anything).returns(
['{ "items": [] }', "", SystemExit.new(0)]
)
rs.sync(sync_mediator)
rs
end

def rs_fixture
@rs_fixture ||= YAML.load_stream(
File.read(File.join(fixture_path('for_unit_tests'), 'replica_set_test.yml'))
).find { |fixture| fixture["kind"] == "ReplicaSet" }
end
end
Loading