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.
- 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
|