tobsch-krane 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/.buildkite/pipeline.nightly.yml +43 -0
  3. data/.github/probots.yml +2 -0
  4. data/.gitignore +20 -0
  5. data/.rubocop.yml +17 -0
  6. data/.shopify-build/VERSION +1 -0
  7. data/.shopify-build/kubernetes-deploy.yml +53 -0
  8. data/1.0-Upgrade.md +185 -0
  9. data/CHANGELOG.md +431 -0
  10. data/CODE_OF_CONDUCT.md +46 -0
  11. data/CONTRIBUTING.md +164 -0
  12. data/Gemfile +16 -0
  13. data/ISSUE_TEMPLATE.md +25 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +655 -0
  16. data/Rakefile +36 -0
  17. data/bin/ci +21 -0
  18. data/bin/setup +16 -0
  19. data/bin/test +47 -0
  20. data/dev.yml +28 -0
  21. data/dev/flamegraph-from-tests +35 -0
  22. data/exe/krane +5 -0
  23. data/krane.gemspec +44 -0
  24. data/lib/krane.rb +7 -0
  25. data/lib/krane/bindings_parser.rb +88 -0
  26. data/lib/krane/cli/deploy_command.rb +75 -0
  27. data/lib/krane/cli/global_deploy_command.rb +54 -0
  28. data/lib/krane/cli/krane.rb +91 -0
  29. data/lib/krane/cli/render_command.rb +41 -0
  30. data/lib/krane/cli/restart_command.rb +34 -0
  31. data/lib/krane/cli/run_command.rb +54 -0
  32. data/lib/krane/cli/version_command.rb +13 -0
  33. data/lib/krane/cluster_resource_discovery.rb +113 -0
  34. data/lib/krane/common.rb +23 -0
  35. data/lib/krane/concerns/template_reporting.rb +29 -0
  36. data/lib/krane/concurrency.rb +18 -0
  37. data/lib/krane/container_logs.rb +106 -0
  38. data/lib/krane/deferred_summary_logging.rb +95 -0
  39. data/lib/krane/delayed_exceptions.rb +14 -0
  40. data/lib/krane/deploy_task.rb +363 -0
  41. data/lib/krane/deploy_task_config_validator.rb +29 -0
  42. data/lib/krane/duration_parser.rb +27 -0
  43. data/lib/krane/ejson_secret_provisioner.rb +154 -0
  44. data/lib/krane/errors.rb +28 -0
  45. data/lib/krane/formatted_logger.rb +57 -0
  46. data/lib/krane/global_deploy_task.rb +210 -0
  47. data/lib/krane/global_deploy_task_config_validator.rb +12 -0
  48. data/lib/krane/kubeclient_builder.rb +156 -0
  49. data/lib/krane/kubectl.rb +120 -0
  50. data/lib/krane/kubernetes_resource.rb +621 -0
  51. data/lib/krane/kubernetes_resource/cloudsql.rb +43 -0
  52. data/lib/krane/kubernetes_resource/config_map.rb +22 -0
  53. data/lib/krane/kubernetes_resource/cron_job.rb +18 -0
  54. data/lib/krane/kubernetes_resource/custom_resource.rb +87 -0
  55. data/lib/krane/kubernetes_resource/custom_resource_definition.rb +98 -0
  56. data/lib/krane/kubernetes_resource/daemon_set.rb +90 -0
  57. data/lib/krane/kubernetes_resource/deployment.rb +213 -0
  58. data/lib/krane/kubernetes_resource/horizontal_pod_autoscaler.rb +65 -0
  59. data/lib/krane/kubernetes_resource/ingress.rb +18 -0
  60. data/lib/krane/kubernetes_resource/job.rb +60 -0
  61. data/lib/krane/kubernetes_resource/network_policy.rb +22 -0
  62. data/lib/krane/kubernetes_resource/persistent_volume_claim.rb +80 -0
  63. data/lib/krane/kubernetes_resource/pod.rb +269 -0
  64. data/lib/krane/kubernetes_resource/pod_disruption_budget.rb +23 -0
  65. data/lib/krane/kubernetes_resource/pod_set_base.rb +71 -0
  66. data/lib/krane/kubernetes_resource/pod_template.rb +20 -0
  67. data/lib/krane/kubernetes_resource/replica_set.rb +92 -0
  68. data/lib/krane/kubernetes_resource/resource_quota.rb +22 -0
  69. data/lib/krane/kubernetes_resource/role.rb +22 -0
  70. data/lib/krane/kubernetes_resource/role_binding.rb +22 -0
  71. data/lib/krane/kubernetes_resource/secret.rb +24 -0
  72. data/lib/krane/kubernetes_resource/service.rb +104 -0
  73. data/lib/krane/kubernetes_resource/service_account.rb +22 -0
  74. data/lib/krane/kubernetes_resource/stateful_set.rb +70 -0
  75. data/lib/krane/label_selector.rb +42 -0
  76. data/lib/krane/oj.rb +4 -0
  77. data/lib/krane/options_helper.rb +39 -0
  78. data/lib/krane/remote_logs.rb +60 -0
  79. data/lib/krane/render_task.rb +118 -0
  80. data/lib/krane/renderer.rb +118 -0
  81. data/lib/krane/resource_cache.rb +68 -0
  82. data/lib/krane/resource_deployer.rb +265 -0
  83. data/lib/krane/resource_watcher.rb +171 -0
  84. data/lib/krane/restart_task.rb +228 -0
  85. data/lib/krane/rollout_conditions.rb +103 -0
  86. data/lib/krane/runner_task.rb +212 -0
  87. data/lib/krane/runner_task_config_validator.rb +18 -0
  88. data/lib/krane/statsd.rb +65 -0
  89. data/lib/krane/task_config.rb +22 -0
  90. data/lib/krane/task_config_validator.rb +96 -0
  91. data/lib/krane/template_sets.rb +173 -0
  92. data/lib/krane/version.rb +4 -0
  93. data/pull_request_template.md +8 -0
  94. data/screenshots/deploy-demo.gif +0 -0
  95. data/screenshots/migrate-logs.png +0 -0
  96. data/screenshots/missing-secret-fail.png +0 -0
  97. data/screenshots/success.png +0 -0
  98. data/screenshots/test-output.png +0 -0
  99. 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