tobsch-krane 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.buildkite/pipeline.nightly.yml +43 -0
- data/.github/probots.yml +2 -0
- data/.gitignore +20 -0
- data/.rubocop.yml +17 -0
- data/.shopify-build/VERSION +1 -0
- data/.shopify-build/kubernetes-deploy.yml +53 -0
- data/1.0-Upgrade.md +185 -0
- data/CHANGELOG.md +431 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +164 -0
- data/Gemfile +16 -0
- data/ISSUE_TEMPLATE.md +25 -0
- data/LICENSE.txt +21 -0
- data/README.md +655 -0
- data/Rakefile +36 -0
- data/bin/ci +21 -0
- data/bin/setup +16 -0
- data/bin/test +47 -0
- data/dev.yml +28 -0
- data/dev/flamegraph-from-tests +35 -0
- data/exe/krane +5 -0
- data/krane.gemspec +44 -0
- data/lib/krane.rb +7 -0
- data/lib/krane/bindings_parser.rb +88 -0
- data/lib/krane/cli/deploy_command.rb +75 -0
- data/lib/krane/cli/global_deploy_command.rb +54 -0
- data/lib/krane/cli/krane.rb +91 -0
- data/lib/krane/cli/render_command.rb +41 -0
- data/lib/krane/cli/restart_command.rb +34 -0
- data/lib/krane/cli/run_command.rb +54 -0
- data/lib/krane/cli/version_command.rb +13 -0
- data/lib/krane/cluster_resource_discovery.rb +113 -0
- data/lib/krane/common.rb +23 -0
- data/lib/krane/concerns/template_reporting.rb +29 -0
- data/lib/krane/concurrency.rb +18 -0
- data/lib/krane/container_logs.rb +106 -0
- data/lib/krane/deferred_summary_logging.rb +95 -0
- data/lib/krane/delayed_exceptions.rb +14 -0
- data/lib/krane/deploy_task.rb +363 -0
- data/lib/krane/deploy_task_config_validator.rb +29 -0
- data/lib/krane/duration_parser.rb +27 -0
- data/lib/krane/ejson_secret_provisioner.rb +154 -0
- data/lib/krane/errors.rb +28 -0
- data/lib/krane/formatted_logger.rb +57 -0
- data/lib/krane/global_deploy_task.rb +210 -0
- data/lib/krane/global_deploy_task_config_validator.rb +12 -0
- data/lib/krane/kubeclient_builder.rb +156 -0
- data/lib/krane/kubectl.rb +120 -0
- data/lib/krane/kubernetes_resource.rb +621 -0
- data/lib/krane/kubernetes_resource/cloudsql.rb +43 -0
- data/lib/krane/kubernetes_resource/config_map.rb +22 -0
- data/lib/krane/kubernetes_resource/cron_job.rb +18 -0
- data/lib/krane/kubernetes_resource/custom_resource.rb +87 -0
- data/lib/krane/kubernetes_resource/custom_resource_definition.rb +98 -0
- data/lib/krane/kubernetes_resource/daemon_set.rb +90 -0
- data/lib/krane/kubernetes_resource/deployment.rb +213 -0
- data/lib/krane/kubernetes_resource/horizontal_pod_autoscaler.rb +65 -0
- data/lib/krane/kubernetes_resource/ingress.rb +18 -0
- data/lib/krane/kubernetes_resource/job.rb +60 -0
- data/lib/krane/kubernetes_resource/network_policy.rb +22 -0
- data/lib/krane/kubernetes_resource/persistent_volume_claim.rb +80 -0
- data/lib/krane/kubernetes_resource/pod.rb +269 -0
- data/lib/krane/kubernetes_resource/pod_disruption_budget.rb +23 -0
- data/lib/krane/kubernetes_resource/pod_set_base.rb +71 -0
- data/lib/krane/kubernetes_resource/pod_template.rb +20 -0
- data/lib/krane/kubernetes_resource/replica_set.rb +92 -0
- data/lib/krane/kubernetes_resource/resource_quota.rb +22 -0
- data/lib/krane/kubernetes_resource/role.rb +22 -0
- data/lib/krane/kubernetes_resource/role_binding.rb +22 -0
- data/lib/krane/kubernetes_resource/secret.rb +24 -0
- data/lib/krane/kubernetes_resource/service.rb +104 -0
- data/lib/krane/kubernetes_resource/service_account.rb +22 -0
- data/lib/krane/kubernetes_resource/stateful_set.rb +70 -0
- data/lib/krane/label_selector.rb +42 -0
- data/lib/krane/oj.rb +4 -0
- data/lib/krane/options_helper.rb +39 -0
- data/lib/krane/remote_logs.rb +60 -0
- data/lib/krane/render_task.rb +118 -0
- data/lib/krane/renderer.rb +118 -0
- data/lib/krane/resource_cache.rb +68 -0
- data/lib/krane/resource_deployer.rb +265 -0
- data/lib/krane/resource_watcher.rb +171 -0
- data/lib/krane/restart_task.rb +228 -0
- data/lib/krane/rollout_conditions.rb +103 -0
- data/lib/krane/runner_task.rb +212 -0
- data/lib/krane/runner_task_config_validator.rb +18 -0
- data/lib/krane/statsd.rb +65 -0
- data/lib/krane/task_config.rb +22 -0
- data/lib/krane/task_config_validator.rb +96 -0
- data/lib/krane/template_sets.rb +173 -0
- data/lib/krane/version.rb +4 -0
- data/pull_request_template.md +8 -0
- data/screenshots/deploy-demo.gif +0 -0
- data/screenshots/migrate-logs.png +0 -0
- data/screenshots/missing-secret-fail.png +0 -0
- data/screenshots/success.png +0 -0
- data/screenshots/test-output.png +0 -0
- metadata +375 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Krane
|
3
|
+
class Cloudsql < KubernetesResource
|
4
|
+
TIMEOUT = 10.minutes
|
5
|
+
|
6
|
+
def sync(cache)
|
7
|
+
super
|
8
|
+
@proxy_deployment = cache.get_instance(Deployment.kind, "cloudsql-#{cloudsql_resource_uuid}")
|
9
|
+
@proxy_service = cache.get_instance(Service.kind, "cloudsql-#{@name}")
|
10
|
+
end
|
11
|
+
|
12
|
+
def status
|
13
|
+
deploy_succeeded? ? "Provisioned" : "Unknown"
|
14
|
+
end
|
15
|
+
|
16
|
+
def deploy_succeeded?
|
17
|
+
proxy_deployment_ready? && proxy_service_ready?
|
18
|
+
end
|
19
|
+
|
20
|
+
def deploy_failed?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def proxy_deployment_ready?
|
27
|
+
return false unless (status = @proxy_deployment["status"])
|
28
|
+
# all cloudsql-proxy pods are running
|
29
|
+
status.fetch("availableReplicas", -1) == status.fetch("replicas", 0)
|
30
|
+
end
|
31
|
+
|
32
|
+
def proxy_service_ready?
|
33
|
+
return false unless @proxy_service.present?
|
34
|
+
# the service has an assigned cluster IP and is therefore functioning
|
35
|
+
@proxy_service.dig("spec", "clusterIP").present?
|
36
|
+
end
|
37
|
+
|
38
|
+
def cloudsql_resource_uuid
|
39
|
+
return unless @instance_data
|
40
|
+
@instance_data.dig("metadata", "uid")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Krane
|
3
|
+
class ConfigMap < KubernetesResource
|
4
|
+
TIMEOUT = 30.seconds
|
5
|
+
|
6
|
+
def deploy_succeeded?
|
7
|
+
exists?
|
8
|
+
end
|
9
|
+
|
10
|
+
def status
|
11
|
+
exists? ? "Available" : "Not Found"
|
12
|
+
end
|
13
|
+
|
14
|
+
def deploy_failed?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def timeout_message
|
19
|
+
UNUSUAL_FAILURE_MESSAGE
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Krane
|
3
|
+
class CronJob < KubernetesResource
|
4
|
+
TIMEOUT = 30.seconds
|
5
|
+
|
6
|
+
def deploy_succeeded?
|
7
|
+
exists?
|
8
|
+
end
|
9
|
+
|
10
|
+
def deploy_failed?
|
11
|
+
!exists?
|
12
|
+
end
|
13
|
+
|
14
|
+
def timeout_message
|
15
|
+
UNUSUAL_FAILURE_MESSAGE
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'jsonpath'
|
3
|
+
|
4
|
+
module Krane
|
5
|
+
class CustomResource < KubernetesResource
|
6
|
+
TIMEOUT_MESSAGE_DIFFERENT_GENERATIONS = <<~MSG
|
7
|
+
This resource's status could not be used to determine rollout success because it is not up-to-date
|
8
|
+
(.metadata.generation != .status.observedGeneration).
|
9
|
+
MSG
|
10
|
+
|
11
|
+
def initialize(namespace:, context:, definition:, logger:, statsd_tags: [], crd:)
|
12
|
+
super(namespace: namespace, context: context, definition: definition,
|
13
|
+
logger: logger, statsd_tags: statsd_tags)
|
14
|
+
@crd = crd
|
15
|
+
end
|
16
|
+
|
17
|
+
def timeout
|
18
|
+
timeout_override || @crd.timeout_for_instance || TIMEOUT
|
19
|
+
end
|
20
|
+
|
21
|
+
def deploy_succeeded?
|
22
|
+
return super unless rollout_conditions
|
23
|
+
return false unless observed_generation == current_generation
|
24
|
+
|
25
|
+
rollout_conditions.rollout_successful?(@instance_data)
|
26
|
+
end
|
27
|
+
|
28
|
+
def deploy_failed?
|
29
|
+
return super unless rollout_conditions
|
30
|
+
return false unless observed_generation == current_generation
|
31
|
+
|
32
|
+
rollout_conditions.rollout_failed?(@instance_data)
|
33
|
+
end
|
34
|
+
|
35
|
+
def failure_message
|
36
|
+
return super unless rollout_conditions
|
37
|
+
messages = rollout_conditions.failure_messages(@instance_data)
|
38
|
+
messages.join("\n") if messages.present?
|
39
|
+
end
|
40
|
+
|
41
|
+
def timeout_message
|
42
|
+
if rollout_conditions && current_generation != observed_generation
|
43
|
+
TIMEOUT_MESSAGE_DIFFERENT_GENERATIONS
|
44
|
+
else
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def status
|
50
|
+
if !exists? || rollout_conditions.nil?
|
51
|
+
super
|
52
|
+
elsif deploy_succeeded?
|
53
|
+
"Healthy"
|
54
|
+
elsif deploy_failed?
|
55
|
+
"Unhealthy"
|
56
|
+
else
|
57
|
+
"Unknown"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def type
|
62
|
+
kind
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_definition(*)
|
66
|
+
super
|
67
|
+
|
68
|
+
@crd.validate_rollout_conditions
|
69
|
+
rescue RolloutConditionsError => e
|
70
|
+
@validation_errors << "The CRD that specifies this resource is using invalid rollout conditions. " \
|
71
|
+
"Krane will not be able to continue until those rollout conditions are fixed.\n" \
|
72
|
+
"Rollout conditions can be found on the CRD that defines this resource (#{@crd.name}), " \
|
73
|
+
"under the annotation #{CustomResourceDefinition::ROLLOUT_CONDITIONS_ANNOTATION}.\n" \
|
74
|
+
"Validation failed with: #{e}"
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def kind
|
80
|
+
@definition["kind"]
|
81
|
+
end
|
82
|
+
|
83
|
+
def rollout_conditions
|
84
|
+
@crd.rollout_conditions
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Krane
|
3
|
+
class CustomResourceDefinition < KubernetesResource
|
4
|
+
TIMEOUT = 2.minutes
|
5
|
+
ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX = "instance-rollout-conditions"
|
6
|
+
ROLLOUT_CONDITIONS_ANNOTATION = "krane.shopify.io/#{ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX}"
|
7
|
+
TIMEOUT_FOR_INSTANCE_ANNOTATION = "krane.shopify.io/instance-timeout"
|
8
|
+
GLOBAL = true
|
9
|
+
|
10
|
+
def deploy_succeeded?
|
11
|
+
names_accepted_status == "True"
|
12
|
+
end
|
13
|
+
|
14
|
+
def deploy_failed?
|
15
|
+
names_accepted_status == "False"
|
16
|
+
end
|
17
|
+
|
18
|
+
def timeout_message
|
19
|
+
"The names this CRD is attempting to register were neither accepted nor rejected in time"
|
20
|
+
end
|
21
|
+
|
22
|
+
def timeout_for_instance
|
23
|
+
timeout = krane_annotation_value("instance-timeout")
|
24
|
+
DurationParser.new(timeout).parse!.to_i
|
25
|
+
rescue DurationParser::ParsingError
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def status
|
30
|
+
if !exists?
|
31
|
+
super
|
32
|
+
elsif deploy_succeeded?
|
33
|
+
"Names accepted"
|
34
|
+
else
|
35
|
+
"#{names_accepted_condition['reason']} (#{names_accepted_condition['message']})"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def group_version_kind
|
40
|
+
group = @definition.dig("spec", "group")
|
41
|
+
version = @definition.dig("spec", "version")
|
42
|
+
"#{group}/#{version}/#{kind}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def kind
|
46
|
+
@definition.dig("spec", "names", "kind")
|
47
|
+
end
|
48
|
+
|
49
|
+
def prunable?
|
50
|
+
prunable = krane_annotation_value("prunable")
|
51
|
+
prunable == "true"
|
52
|
+
end
|
53
|
+
|
54
|
+
def predeployed?
|
55
|
+
predeployed = krane_annotation_value("predeployed")
|
56
|
+
predeployed.nil? || predeployed == "true"
|
57
|
+
end
|
58
|
+
|
59
|
+
def rollout_conditions
|
60
|
+
return @rollout_conditions if defined?(@rollout_conditions)
|
61
|
+
|
62
|
+
@rollout_conditions = if krane_annotation_value(ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX)
|
63
|
+
RolloutConditions.from_annotation(krane_annotation_value(ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX))
|
64
|
+
end
|
65
|
+
rescue RolloutConditionsError
|
66
|
+
@rollout_conditions = nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_definition(*)
|
70
|
+
super
|
71
|
+
|
72
|
+
validate_rollout_conditions
|
73
|
+
rescue RolloutConditionsError => e
|
74
|
+
@validation_errors << "Annotation #{krane_annotation_key(ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX)} "\
|
75
|
+
"on #{name} is invalid: #{e}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate_rollout_conditions
|
79
|
+
if krane_annotation_value(ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX) && @rollout_conditions_validated.nil?
|
80
|
+
conditions = RolloutConditions.from_annotation(krane_annotation_value(ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX))
|
81
|
+
conditions.validate!
|
82
|
+
end
|
83
|
+
|
84
|
+
@rollout_conditions_validated = true
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def names_accepted_condition
|
90
|
+
conditions = @instance_data.dig("status", "conditions") || []
|
91
|
+
conditions.detect { |c| c["type"] == "NamesAccepted" } || {}
|
92
|
+
end
|
93
|
+
|
94
|
+
def names_accepted_status
|
95
|
+
names_accepted_condition["status"]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "krane/kubernetes_resource/pod_set_base"
|
3
|
+
module Krane
|
4
|
+
class DaemonSet < PodSetBase
|
5
|
+
TIMEOUT = 5.minutes
|
6
|
+
attr_reader :pods
|
7
|
+
|
8
|
+
def sync(cache)
|
9
|
+
super
|
10
|
+
@pods = exists? ? find_pods(cache) : []
|
11
|
+
@nodes = find_nodes(cache) if @nodes.blank?
|
12
|
+
end
|
13
|
+
|
14
|
+
def status
|
15
|
+
return super unless exists?
|
16
|
+
rollout_data.map { |state_replicas, num| "#{num} #{state_replicas}" }.join(", ")
|
17
|
+
end
|
18
|
+
|
19
|
+
def deploy_succeeded?
|
20
|
+
return false unless exists?
|
21
|
+
current_generation == observed_generation &&
|
22
|
+
rollout_data["desiredNumberScheduled"].to_i == rollout_data["updatedNumberScheduled"].to_i &&
|
23
|
+
relevant_pods_ready?
|
24
|
+
end
|
25
|
+
|
26
|
+
def deploy_failed?
|
27
|
+
pods.present? && pods.any?(&:deploy_failed?) &&
|
28
|
+
observed_generation == current_generation
|
29
|
+
end
|
30
|
+
|
31
|
+
def fetch_debug_logs
|
32
|
+
most_useful_pod = pods.find(&:deploy_failed?) || pods.find(&:deploy_timed_out?) || pods.first
|
33
|
+
most_useful_pod.fetch_debug_logs
|
34
|
+
end
|
35
|
+
|
36
|
+
def print_debug_logs?
|
37
|
+
pods.present? # the kubectl command times out if no pods exist
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
class Node
|
43
|
+
attr_reader :name
|
44
|
+
|
45
|
+
class << self
|
46
|
+
def kind
|
47
|
+
name.demodulize
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(definition:)
|
52
|
+
@name = definition.dig("metadata", "name").to_s
|
53
|
+
@definition = definition
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def relevant_pods_ready?
|
58
|
+
return true if rollout_data["desiredNumberScheduled"].to_i == rollout_data["numberReady"].to_i # all pods ready
|
59
|
+
relevant_node_names = @nodes.map(&:name)
|
60
|
+
considered_pods = @pods.select { |p| relevant_node_names.include?(p.node_name) }
|
61
|
+
@logger.debug("DaemonSet is reporting #{rollout_data['numberReady']} pods ready." \
|
62
|
+
" Considered #{considered_pods.size} pods out of #{@pods.size} for #{@nodes.size} nodes.")
|
63
|
+
considered_pods.present? &&
|
64
|
+
considered_pods.all?(&:deploy_succeeded?) &&
|
65
|
+
rollout_data["numberReady"].to_i >= considered_pods.length
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_nodes(cache)
|
69
|
+
all_nodes = cache.get_all(Node.kind)
|
70
|
+
all_nodes.map { |node_data| Node.new(definition: node_data) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def rollout_data
|
74
|
+
return { "currentNumberScheduled" => 0 } unless exists?
|
75
|
+
@instance_data["status"]
|
76
|
+
.slice("updatedNumberScheduled", "desiredNumberScheduled", "numberReady")
|
77
|
+
end
|
78
|
+
|
79
|
+
def parent_of_pod?(pod_data)
|
80
|
+
return false unless pod_data.dig("metadata", "ownerReferences")
|
81
|
+
|
82
|
+
template_generation = @instance_data.dig("spec", "templateGeneration") ||
|
83
|
+
@instance_data.dig("metadata", "annotations", "deprecated.daemonset.template.generation")
|
84
|
+
return false unless template_generation.present?
|
85
|
+
|
86
|
+
pod_data["metadata"]["ownerReferences"].any? { |ref| ref["uid"] == @instance_data["metadata"]["uid"] } &&
|
87
|
+
pod_data["metadata"]["labels"]["pod-template-generation"].to_i == template_generation.to_i
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'krane/kubernetes_resource/replica_set'
|
3
|
+
|
4
|
+
module Krane
|
5
|
+
class Deployment < KubernetesResource
|
6
|
+
TIMEOUT = 7.minutes
|
7
|
+
REQUIRED_ROLLOUT_ANNOTATION_SUFFIX = "required-rollout"
|
8
|
+
REQUIRED_ROLLOUT_ANNOTATION_DEPRECATED = "kubernetes-deploy.shopify.io/#{REQUIRED_ROLLOUT_ANNOTATION_SUFFIX}"
|
9
|
+
REQUIRED_ROLLOUT_ANNOTATION = "krane.shopify.io/#{REQUIRED_ROLLOUT_ANNOTATION_SUFFIX}"
|
10
|
+
REQUIRED_ROLLOUT_TYPES = %w(maxUnavailable full none).freeze
|
11
|
+
DEFAULT_REQUIRED_ROLLOUT = 'full'
|
12
|
+
|
13
|
+
def sync(cache)
|
14
|
+
super
|
15
|
+
@latest_rs = exists? ? find_latest_rs(cache) : nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def status
|
19
|
+
return super unless exists?
|
20
|
+
rollout_data.map { |state_replicas, num| "#{num} #{state_replicas.chop.pluralize(num)}" }.join(", ")
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch_events(kubectl)
|
24
|
+
own_events = super
|
25
|
+
return own_events unless @latest_rs.present?
|
26
|
+
own_events.merge(@latest_rs.fetch_events(kubectl))
|
27
|
+
end
|
28
|
+
|
29
|
+
def print_debug_logs?
|
30
|
+
@latest_rs.present?
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch_debug_logs
|
34
|
+
@latest_rs.fetch_debug_logs
|
35
|
+
end
|
36
|
+
|
37
|
+
def deploy_succeeded?
|
38
|
+
return false unless exists? && @latest_rs.present?
|
39
|
+
return false unless observed_generation == current_generation
|
40
|
+
|
41
|
+
if required_rollout == 'full'
|
42
|
+
@latest_rs.deploy_succeeded? &&
|
43
|
+
@latest_rs.desired_replicas == desired_replicas && # latest RS fully scaled up
|
44
|
+
rollout_data["updatedReplicas"].to_i == desired_replicas &&
|
45
|
+
rollout_data["updatedReplicas"].to_i == rollout_data["availableReplicas"].to_i
|
46
|
+
elsif required_rollout == 'none'
|
47
|
+
true
|
48
|
+
elsif required_rollout == 'maxUnavailable' || percent?(required_rollout)
|
49
|
+
minimum_needed = min_available_replicas
|
50
|
+
|
51
|
+
@latest_rs.desired_replicas >= minimum_needed &&
|
52
|
+
@latest_rs.ready_replicas >= minimum_needed &&
|
53
|
+
@latest_rs.available_replicas >= minimum_needed
|
54
|
+
else
|
55
|
+
raise FatalDeploymentError, rollout_annotation_err_msg
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def deploy_failed?
|
60
|
+
@latest_rs&.deploy_failed? &&
|
61
|
+
observed_generation == current_generation
|
62
|
+
end
|
63
|
+
|
64
|
+
def failure_message
|
65
|
+
return unless @latest_rs.present?
|
66
|
+
"Latest ReplicaSet: #{@latest_rs.name}\n\n#{@latest_rs.failure_message}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def timeout_message
|
70
|
+
reason_msg = if timeout_override
|
71
|
+
STANDARD_TIMEOUT_MESSAGE
|
72
|
+
elsif progress_condition.present?
|
73
|
+
"Timeout reason: #{progress_condition['reason']}"
|
74
|
+
else
|
75
|
+
"Timeout reason: hard deadline for #{type}"
|
76
|
+
end
|
77
|
+
return reason_msg unless @latest_rs.present?
|
78
|
+
"#{reason_msg}\nLatest ReplicaSet: #{@latest_rs.name}\n\n#{@latest_rs.timeout_message}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def pretty_timeout_type
|
82
|
+
if timeout_override
|
83
|
+
"timeout override: #{timeout_override}s"
|
84
|
+
elsif progress_deadline.present?
|
85
|
+
"progress deadline: #{progress_deadline}s"
|
86
|
+
else
|
87
|
+
super
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def deploy_timed_out?
|
92
|
+
return false if deploy_failed?
|
93
|
+
return super if timeout_override
|
94
|
+
|
95
|
+
# Do not use the hard timeout if progress deadline is set
|
96
|
+
progress_condition.present? ? deploy_failing_to_progress? : super
|
97
|
+
end
|
98
|
+
|
99
|
+
def validate_definition(*)
|
100
|
+
super
|
101
|
+
|
102
|
+
unless REQUIRED_ROLLOUT_TYPES.include?(required_rollout) || percent?(required_rollout)
|
103
|
+
@validation_errors << rollout_annotation_err_msg
|
104
|
+
end
|
105
|
+
|
106
|
+
strategy = @definition.dig('spec', 'strategy', 'type').to_s
|
107
|
+
if required_rollout.downcase == 'maxunavailable' && strategy.present? && strategy.downcase != 'rollingupdate'
|
108
|
+
@validation_errors << "'#{krane_annotation_key(REQUIRED_ROLLOUT_ANNOTATION_SUFFIX)}: #{required_rollout}' "\
|
109
|
+
"is incompatible with strategy '#{strategy}'"
|
110
|
+
end
|
111
|
+
|
112
|
+
@validation_errors.empty?
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def current_generation
|
118
|
+
return -2 unless exists? # different default than observed
|
119
|
+
@instance_data.dig('metadata', 'generation')
|
120
|
+
end
|
121
|
+
|
122
|
+
def observed_generation
|
123
|
+
return -1 unless exists? # different default than current
|
124
|
+
@instance_data.dig('status', 'observedGeneration')
|
125
|
+
end
|
126
|
+
|
127
|
+
def desired_replicas
|
128
|
+
return -1 unless exists?
|
129
|
+
@instance_data["spec"]["replicas"].to_i
|
130
|
+
end
|
131
|
+
|
132
|
+
def rollout_data
|
133
|
+
return { "replicas" => 0 } unless exists?
|
134
|
+
{ "replicas" => 0 }.merge(@instance_data["status"]
|
135
|
+
.slice("replicas", "updatedReplicas", "availableReplicas", "unavailableReplicas"))
|
136
|
+
end
|
137
|
+
|
138
|
+
def progress_condition
|
139
|
+
return unless exists?
|
140
|
+
conditions = @instance_data.fetch("status", {}).fetch("conditions", [])
|
141
|
+
conditions.find { |condition| condition['type'] == 'Progressing' }
|
142
|
+
end
|
143
|
+
|
144
|
+
def progress_deadline
|
145
|
+
if exists?
|
146
|
+
@instance_data['spec']['progressDeadlineSeconds']
|
147
|
+
else
|
148
|
+
@definition['spec']['progressDeadlineSeconds']
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def rollout_annotation_err_msg
|
153
|
+
"'#{krane_annotation_key(REQUIRED_ROLLOUT_ANNOTATION_SUFFIX)}: #{required_rollout}' is invalid. "\
|
154
|
+
"Acceptable values: #{REQUIRED_ROLLOUT_TYPES.join(', ')}"
|
155
|
+
end
|
156
|
+
|
157
|
+
def deploy_failing_to_progress?
|
158
|
+
return false unless progress_condition.present?
|
159
|
+
|
160
|
+
# This assumes that when the controller bumps the observed generation, it also updates/clears all the status
|
161
|
+
# conditions. Specifically, it assumes the progress condition is immediately set to True if a rollout is starting.
|
162
|
+
deploy_started? &&
|
163
|
+
current_generation == observed_generation &&
|
164
|
+
progress_condition["status"] == 'False'
|
165
|
+
end
|
166
|
+
|
167
|
+
def find_latest_rs(cache)
|
168
|
+
all_rs_data = cache.get_all(ReplicaSet.kind, @instance_data["spec"]["selector"]["matchLabels"])
|
169
|
+
current_revision = @instance_data["metadata"]["annotations"]["deployment.kubernetes.io/revision"]
|
170
|
+
|
171
|
+
latest_rs_data = all_rs_data.find do |rs|
|
172
|
+
rs["metadata"]["ownerReferences"].any? { |ref| ref["uid"] == @instance_data["metadata"]["uid"] } &&
|
173
|
+
rs["metadata"]["annotations"]["deployment.kubernetes.io/revision"] == current_revision
|
174
|
+
end
|
175
|
+
|
176
|
+
return unless latest_rs_data.present?
|
177
|
+
|
178
|
+
rs = ReplicaSet.new(
|
179
|
+
namespace: namespace,
|
180
|
+
context: context,
|
181
|
+
definition: latest_rs_data,
|
182
|
+
logger: @logger,
|
183
|
+
parent: "#{@name.capitalize} deployment",
|
184
|
+
deploy_started_at: @deploy_started_at
|
185
|
+
)
|
186
|
+
rs.sync(cache)
|
187
|
+
rs
|
188
|
+
end
|
189
|
+
|
190
|
+
def min_available_replicas
|
191
|
+
if percent?(required_rollout)
|
192
|
+
(desired_replicas * required_rollout.to_i / 100.0).ceil
|
193
|
+
elsif max_unavailable =~ /%/
|
194
|
+
(desired_replicas * (100 - max_unavailable.to_i) / 100.0).ceil
|
195
|
+
else
|
196
|
+
desired_replicas - max_unavailable.to_i
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def max_unavailable
|
201
|
+
source = exists? ? @instance_data : @definition
|
202
|
+
source.dig('spec', 'strategy', 'rollingUpdate', 'maxUnavailable')
|
203
|
+
end
|
204
|
+
|
205
|
+
def required_rollout
|
206
|
+
krane_annotation_value("required-rollout") || DEFAULT_REQUIRED_ROLLOUT
|
207
|
+
end
|
208
|
+
|
209
|
+
def percent?(value)
|
210
|
+
value =~ /\d+%/
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|