kubernetes-deploy 0.30.0 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +15 -0
  4. data/CONTRIBUTING.md +2 -2
  5. data/README.md +1 -1
  6. data/dev.yml +1 -1
  7. data/dev/flamegraph-from-tests +1 -1
  8. data/exe/kubernetes-deploy +11 -9
  9. data/exe/kubernetes-render +9 -7
  10. data/exe/kubernetes-restart +3 -3
  11. data/exe/kubernetes-run +1 -1
  12. data/kubernetes-deploy.gemspec +3 -3
  13. data/lib/krane.rb +5 -3
  14. data/lib/{kubernetes-deploy → krane}/bindings_parser.rb +1 -1
  15. data/lib/krane/cli/deploy_command.rb +14 -11
  16. data/lib/krane/cli/global_deploy_command.rb +47 -0
  17. data/lib/krane/cli/krane.rb +12 -3
  18. data/lib/krane/cli/render_command.rb +11 -9
  19. data/lib/krane/cli/restart_command.rb +4 -4
  20. data/lib/krane/cli/run_command.rb +3 -3
  21. data/lib/krane/cli/version_command.rb +1 -1
  22. data/lib/krane/cluster_resource_discovery.rb +102 -0
  23. data/lib/{kubernetes-deploy → krane}/common.rb +8 -9
  24. data/lib/krane/concerns/template_reporting.rb +29 -0
  25. data/lib/{kubernetes-deploy → krane}/concurrency.rb +1 -1
  26. data/lib/{kubernetes-deploy → krane}/container_logs.rb +1 -1
  27. data/lib/{kubernetes-deploy → krane}/deferred_summary_logging.rb +2 -2
  28. data/lib/{kubernetes-deploy → krane}/delayed_exceptions.rb +0 -0
  29. data/lib/krane/deploy_task.rb +2 -2
  30. data/lib/{kubernetes-deploy → krane}/deploy_task_config_validator.rb +1 -1
  31. data/lib/krane/deprecated_deploy_task.rb +404 -0
  32. data/lib/{kubernetes-deploy → krane}/duration_parser.rb +1 -1
  33. data/lib/{kubernetes-deploy → krane}/ejson_secret_provisioner.rb +3 -3
  34. data/lib/krane/errors.rb +28 -0
  35. data/lib/{kubernetes-deploy → krane}/formatted_logger.rb +2 -2
  36. data/lib/krane/global_deploy_task.rb +210 -0
  37. data/lib/krane/global_deploy_task_config_validator.rb +12 -0
  38. data/lib/{kubernetes-deploy → krane}/kubeclient_builder.rb +11 -3
  39. data/lib/{kubernetes-deploy → krane}/kubectl.rb +2 -2
  40. data/lib/{kubernetes-deploy → krane}/kubernetes_resource.rb +54 -22
  41. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/cloudsql.rb +1 -1
  42. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/config_map.rb +1 -1
  43. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/cron_job.rb +1 -1
  44. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/custom_resource.rb +2 -2
  45. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/custom_resource_definition.rb +1 -5
  46. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/daemon_set.rb +7 -4
  47. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/deployment.rb +2 -2
  48. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/horizontal_pod_autoscaler.rb +1 -1
  49. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/ingress.rb +1 -1
  50. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/job.rb +1 -1
  51. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/network_policy.rb +1 -1
  52. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/persistent_volume_claim.rb +1 -1
  53. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/pod.rb +2 -2
  54. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/pod_disruption_budget.rb +2 -2
  55. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/pod_set_base.rb +3 -3
  56. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/pod_template.rb +1 -1
  57. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/replica_set.rb +2 -2
  58. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/resource_quota.rb +1 -1
  59. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/role.rb +1 -1
  60. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/role_binding.rb +1 -1
  61. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/secret.rb +1 -1
  62. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/service.rb +2 -2
  63. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/service_account.rb +1 -1
  64. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/stateful_set.rb +2 -2
  65. data/lib/{kubernetes-deploy → krane}/label_selector.rb +1 -1
  66. data/lib/{kubernetes-deploy → krane}/oj.rb +0 -0
  67. data/lib/{kubernetes-deploy → krane}/options_helper.rb +2 -2
  68. data/lib/{kubernetes-deploy → krane}/remote_logs.rb +2 -2
  69. data/lib/krane/render_task.rb +149 -0
  70. data/lib/{kubernetes-deploy → krane}/renderer.rb +1 -1
  71. data/lib/{kubernetes-deploy → krane}/resource_cache.rb +4 -3
  72. data/lib/krane/resource_deployer.rb +265 -0
  73. data/lib/{kubernetes-deploy → krane}/resource_watcher.rb +6 -6
  74. data/lib/krane/restart_task.rb +224 -0
  75. data/lib/{kubernetes-deploy → krane}/rollout_conditions.rb +1 -1
  76. data/lib/krane/runner_task.rb +212 -0
  77. data/lib/{kubernetes-deploy → krane}/runner_task_config_validator.rb +1 -1
  78. data/lib/{kubernetes-deploy → krane}/statsd.rb +13 -27
  79. data/lib/krane/task_config.rb +19 -0
  80. data/lib/{kubernetes-deploy → krane}/task_config_validator.rb +1 -1
  81. data/lib/{kubernetes-deploy → krane}/template_sets.rb +5 -5
  82. data/lib/krane/version.rb +4 -0
  83. data/lib/kubernetes-deploy/deploy_task.rb +6 -603
  84. data/lib/kubernetes-deploy/errors.rb +1 -26
  85. data/lib/kubernetes-deploy/render_task.rb +5 -139
  86. data/lib/kubernetes-deploy/rescue_krane_exceptions.rb +18 -0
  87. data/lib/kubernetes-deploy/restart_task.rb +6 -215
  88. data/lib/kubernetes-deploy/runner_task.rb +6 -203
  89. metadata +75 -58
  90. data/lib/kubernetes-deploy/cluster_resource_discovery.rb +0 -57
  91. data/lib/kubernetes-deploy/task_config.rb +0 -16
  92. data/lib/kubernetes-deploy/version.rb +0 -4
@@ -16,12 +16,12 @@ module Krane
16
16
  }
17
17
 
18
18
  def self.from_options(namespace, context, options)
19
- require 'kubernetes-deploy/restart_task'
20
- selector = KubernetesDeploy::LabelSelector.parse(options[:selector]) if options[:selector]
21
- restart = KubernetesDeploy::RestartTask.new(
19
+ require 'krane/restart_task'
20
+ selector = ::Krane::LabelSelector.parse(options[:selector]) if options[:selector]
21
+ restart = ::Krane::RestartTask.new(
22
22
  namespace: namespace,
23
23
  context: context,
24
- max_watch_seconds: KubernetesDeploy::DurationParser.new(options["global-timeout"]).parse!.to_i,
24
+ max_watch_seconds: ::Krane::DurationParser.new(options["global-timeout"]).parse!.to_i,
25
25
  )
26
26
  restart.run!(
27
27
  options[:deployments],
@@ -33,11 +33,11 @@ module Krane
33
33
  }
34
34
 
35
35
  def self.from_options(namespace, context, options)
36
- require "kubernetes-deploy/runner_task"
37
- runner = KubernetesDeploy::RunnerTask.new(
36
+ require "krane/runner_task"
37
+ runner = ::Krane::RunnerTask.new(
38
38
  namespace: namespace,
39
39
  context: context,
40
- max_watch_seconds: KubernetesDeploy::DurationParser.new(options["global-timeout"]).parse!.to_i,
40
+ max_watch_seconds: ::Krane::DurationParser.new(options["global-timeout"]).parse!.to_i,
41
41
  )
42
42
 
43
43
  runner.run!(
@@ -6,7 +6,7 @@ module Krane
6
6
  OPTIONS = {}
7
7
 
8
8
  def self.from_options(_)
9
- puts("krane #{KubernetesDeploy::VERSION}")
9
+ puts("krane #{::Krane::VERSION}")
10
10
  end
11
11
  end
12
12
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Krane
4
+ class ClusterResourceDiscovery
5
+ delegate :namespace, :context, :logger, to: :@task_config
6
+
7
+ def initialize(task_config:, namespace_tags: [])
8
+ @task_config = task_config
9
+ @namespace_tags = namespace_tags
10
+ end
11
+
12
+ def crds
13
+ @crds ||= fetch_crds.map do |cr_def|
14
+ CustomResourceDefinition.new(namespace: namespace, context: context, logger: logger,
15
+ definition: cr_def, statsd_tags: @namespace_tags)
16
+ end
17
+ end
18
+
19
+ def global_resource_kinds
20
+ @globals ||= fetch_resources(namespaced: false).map { |g| g["kind"] }
21
+ end
22
+
23
+ def prunable_resources(namespaced:)
24
+ black_list = %w(Namespace Node)
25
+ api_versions = fetch_api_versions
26
+
27
+ fetch_resources(namespaced: namespaced).map do |resource|
28
+ next unless resource['verbs'].one? { |v| v == "delete" }
29
+ next if black_list.include?(resource['kind'])
30
+ version = api_versions[resource['apigroup'].to_s].last
31
+ [resource['apigroup'], version, resource['kind']].compact.join("/")
32
+ end.compact
33
+ end
34
+
35
+ private
36
+
37
+ # kubectl api-versions returns a list of group/version strings e.g. autoscaling/v2beta2
38
+ # A kind may not exist in all versions of the group.
39
+ def fetch_api_versions
40
+ raw, _, st = kubectl.run("api-versions", attempts: 5, use_namespace: false)
41
+ # The "core" group is represented by an empty string
42
+ versions = { "" => %w(v1) }
43
+ if st.success?
44
+ rows = raw.split("\n")
45
+ rows.each do |group_version|
46
+ group, version = group_version.split("/")
47
+ versions[group] ||= []
48
+ versions[group] << version
49
+ end
50
+ end
51
+ versions
52
+ end
53
+
54
+ # kubectl api-resources -o wide returns 5 columns
55
+ # NAME SHORTNAMES APIGROUP NAMESPACED KIND VERBS
56
+ # SHORTNAMES and APIGROUP may be blank
57
+ # VERBS is an array
58
+ # serviceaccounts sa <blank> true ServiceAccount [create delete deletecollection get list patch update watch]
59
+ def fetch_resources(namespaced: false)
60
+ command = %w(api-resources)
61
+ command << "--namespaced=#{namespaced}"
62
+ raw, _, st = kubectl.run(*command, output: "wide", attempts: 5,
63
+ use_namespace: false)
64
+ if st.success?
65
+ rows = raw.split("\n")
66
+ header = rows[0]
67
+ resources = rows[1..-1]
68
+ full_width_field_names = header.downcase.scan(/[a-z]+[\W]*/)
69
+ cursor = 0
70
+ fields = full_width_field_names.each_with_object({}) do |name, hash|
71
+ start = cursor
72
+ cursor = start + name.length
73
+ # Last field should consume the remainder of the line
74
+ cursor = 0 if full_width_field_names.last == name.strip
75
+ hash[name.strip] = [start, cursor - 1]
76
+ end
77
+ resources.map do |resource|
78
+ resource = fields.map { |k, (s, e)| [k.strip, resource[s..e].strip] }.to_h
79
+ # Manually parse verbs: "[get list]" into %w(get list)
80
+ resource["verbs"] = resource["verbs"][1..-2].split
81
+ resource
82
+ end
83
+ else
84
+ []
85
+ end
86
+ end
87
+
88
+ def fetch_crds
89
+ raw_json, _, st = kubectl.run("get", "CustomResourceDefinition", output: "json", attempts: 5,
90
+ use_namespace: false)
91
+ if st.success?
92
+ JSON.parse(raw_json)["items"]
93
+ else
94
+ []
95
+ end
96
+ end
97
+
98
+ def kubectl
99
+ @kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
100
+ end
101
+ end
102
+ end
@@ -10,15 +10,14 @@ require 'active_support/core_ext/hash/keys'
10
10
  require 'active_support/core_ext/array/conversions'
11
11
  require 'colorized_string'
12
12
 
13
- require 'kubernetes-deploy/version'
14
- require 'kubernetes-deploy/oj'
15
- require 'kubernetes-deploy/errors'
16
- require 'kubernetes-deploy/formatted_logger'
17
- require 'kubernetes-deploy/statsd'
18
- require 'kubernetes-deploy/task_config'
19
- require 'kubernetes-deploy/task_config_validator'
13
+ require 'krane/version'
14
+ require 'krane/oj'
15
+ require 'krane/errors'
16
+ require 'krane/formatted_logger'
17
+ require 'krane/statsd'
18
+ require 'krane/task_config'
19
+ require 'krane/task_config_validator'
20
20
 
21
- module KubernetesDeploy
21
+ module Krane
22
22
  MIN_KUBE_VERSION = '1.11.0'
23
- StatsD.build
24
23
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Krane
4
+ module TemplateReporting
5
+ def record_invalid_template(logger:, err:, filename:, content: nil)
6
+ debug_msg = ColorizedString.new("Invalid template: #{filename}\n").red
7
+ debug_msg += "> Error message:\n#{Krane::FormattedLogger.indent_four(err)}"
8
+ if content
9
+ debug_msg += if content =~ /kind:\s*Secret/
10
+ "\n> Template content: Suppressed because it may contain a Secret"
11
+ else
12
+ "\n> Template content:\n#{Krane::FormattedLogger.indent_four(content)}"
13
+ end
14
+ end
15
+ logger.summary.add_paragraph(debug_msg)
16
+ end
17
+
18
+ def record_warnings(logger:, warning:, filename:)
19
+ warn_msg = "Template warning: #{filename}\n"
20
+ warn_msg += "> Warning message:\n#{Krane::FormattedLogger.indent_four(warning)}"
21
+ logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow)
22
+ end
23
+
24
+ def add_para_from_list(logger:, action:, enum:)
25
+ logger.summary.add_action(action)
26
+ logger.summary.add_paragraph(enum.map { |e| "- #{e}" }.join("\n"))
27
+ end
28
+ end
29
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  module Concurrency
4
4
  MAX_THREADS = 8
5
5
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class ContainerLogs
4
4
  attr_reader :lines, :container_name
5
5
 
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  require 'colorized_string'
3
3
 
4
- module KubernetesDeploy
5
- # Adds the methods kubernetes-deploy requires to your logger class.
4
+ module Krane
5
+ # Adds the methods krane requires to your logger class.
6
6
  # These methods include helpers for logging consistent headings, as well as facilities for
7
7
  # displaying key information later, in a summary section, rather than when it occurred.
8
8
  module DeferredSummaryLogging
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'kubernetes-deploy/deploy_task'
3
+ require 'krane/deprecated_deploy_task'
4
4
 
5
5
  module Krane
6
- class DeployTask < KubernetesDeploy::DeployTask
6
+ class DeployTask < Krane::DeprecatedDeployTask
7
7
  def initialize(**args)
8
8
  raise "Use Krane::DeployGlobalTask to deploy global resources" if args[:allow_globals]
9
9
  super(args.merge(allow_globals: false))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class DeployTaskConfigValidator < TaskConfigValidator
4
4
  def initialize(protected_namespaces, allow_protected_ns, prune, *arguments)
5
5
  super(*arguments)
@@ -0,0 +1,404 @@
1
+ # frozen_string_literal: true
2
+ require 'yaml'
3
+ require 'shellwords'
4
+ require 'tempfile'
5
+ require 'fileutils'
6
+
7
+ require 'krane/common'
8
+ require 'krane/concurrency'
9
+ require 'krane/resource_cache'
10
+ require 'krane/kubernetes_resource'
11
+ %w(
12
+ custom_resource
13
+ cloudsql
14
+ config_map
15
+ deployment
16
+ ingress
17
+ persistent_volume_claim
18
+ pod
19
+ network_policy
20
+ service
21
+ pod_template
22
+ pod_disruption_budget
23
+ replica_set
24
+ service_account
25
+ daemon_set
26
+ resource_quota
27
+ stateful_set
28
+ cron_job
29
+ job
30
+ custom_resource_definition
31
+ horizontal_pod_autoscaler
32
+ secret
33
+ ).each do |subresource|
34
+ require "krane/kubernetes_resource/#{subresource}"
35
+ end
36
+ require 'krane/resource_watcher'
37
+ require 'krane/kubectl'
38
+ require 'krane/kubeclient_builder'
39
+ require 'krane/ejson_secret_provisioner'
40
+ require 'krane/renderer'
41
+ require 'krane/cluster_resource_discovery'
42
+ require 'krane/template_sets'
43
+ require 'krane/deploy_task_config_validator'
44
+ require 'krane/resource_deployer'
45
+ require 'krane/concerns/template_reporting'
46
+
47
+ module Krane
48
+ # Ship resources to a namespace
49
+ class DeprecatedDeployTask
50
+ extend Krane::StatsD::MeasureMethods
51
+ include Krane::TemplateReporting
52
+
53
+ PROTECTED_NAMESPACES = %w(
54
+ default
55
+ kube-system
56
+ kube-public
57
+ )
58
+ # Things removed from default prune whitelist at https://github.com/kubernetes/kubernetes/blob/0dff56b4d88ec7551084bf89028dbeebf569620e/pkg/kubectl/cmd/apply.go#L411:
59
+ # core/v1/Namespace -- not namespaced
60
+ # core/v1/PersistentVolume -- not namespaced
61
+ # core/v1/Endpoints -- managed by services
62
+ # core/v1/PersistentVolumeClaim -- would delete data
63
+ # core/v1/ReplicationController -- superseded by deployments/replicasets
64
+
65
+ def predeploy_sequence
66
+ before_crs = %w(
67
+ ResourceQuota
68
+ NetworkPolicy
69
+ )
70
+ after_crs = %w(
71
+ ConfigMap
72
+ PersistentVolumeClaim
73
+ ServiceAccount
74
+ Role
75
+ RoleBinding
76
+ Secret
77
+ Pod
78
+ )
79
+
80
+ before_crs + cluster_resource_discoverer.crds.select(&:predeployed?).map(&:kind) + after_crs
81
+ end
82
+
83
+ def prune_whitelist
84
+ wl = %w(
85
+ core/v1/ConfigMap
86
+ core/v1/Pod
87
+ core/v1/Service
88
+ core/v1/ResourceQuota
89
+ core/v1/Secret
90
+ core/v1/ServiceAccount
91
+ core/v1/PodTemplate
92
+ core/v1/PersistentVolumeClaim
93
+ batch/v1/Job
94
+ apps/v1/ReplicaSet
95
+ apps/v1/DaemonSet
96
+ apps/v1/Deployment
97
+ extensions/v1beta1/Ingress
98
+ networking.k8s.io/v1/NetworkPolicy
99
+ apps/v1/StatefulSet
100
+ autoscaling/v1/HorizontalPodAutoscaler
101
+ policy/v1beta1/PodDisruptionBudget
102
+ batch/v1beta1/CronJob
103
+ rbac.authorization.k8s.io/v1/Role
104
+ rbac.authorization.k8s.io/v1/RoleBinding
105
+ )
106
+ wl + cluster_resource_discoverer.crds.select(&:prunable?).map(&:group_version_kind)
107
+ end
108
+
109
+ def server_version
110
+ kubectl.server_version
111
+ end
112
+
113
+ # Initializes the deploy task
114
+ #
115
+ # @param namespace [String] Kubernetes namespace
116
+ # @param context [String] Kubernetes context
117
+ # @param current_sha [String] The SHA of the commit
118
+ # @param logger [Object] Logger object (defaults to an instance of Krane::FormattedLogger)
119
+ # @param kubectl_instance [Kubectl] Kubectl instance
120
+ # @param bindings [Hash] Bindings parsed by Krane::BindingsParser
121
+ # @param max_watch_seconds [Integer] Timeout in seconds
122
+ # @param selector [Hash] Selector(s) parsed by Krane::LabelSelector
123
+ # @param template_paths [Array<String>] An array of template paths
124
+ # @param template_dir [String] Path to a directory with templates (deprecated)
125
+ # @param protected_namespaces [Array<String>] Array of protected Kubernetes namespaces (defaults
126
+ # to Krane::DeployTask::PROTECTED_NAMESPACES)
127
+ # @param render_erb [Boolean] Enable ERB rendering
128
+ def initialize(namespace:, context:, current_sha:, logger: nil, kubectl_instance: nil, bindings: {},
129
+ max_watch_seconds: nil, selector: nil, template_paths: [], template_dir: nil, protected_namespaces: nil,
130
+ render_erb: true, allow_globals: false)
131
+ template_dir = File.expand_path(template_dir) if template_dir
132
+ template_paths = (template_paths.map { |path| File.expand_path(path) } << template_dir).compact
133
+
134
+ @logger = logger || Krane::FormattedLogger.build(namespace, context)
135
+ @template_sets = TemplateSets.from_dirs_and_files(paths: template_paths, logger: @logger)
136
+ @task_config = Krane::TaskConfig.new(context, namespace, @logger)
137
+ @bindings = bindings
138
+ @namespace = namespace
139
+ @namespace_tags = []
140
+ @context = context
141
+ @current_sha = current_sha
142
+ @kubectl = kubectl_instance
143
+ @max_watch_seconds = max_watch_seconds
144
+ @selector = selector
145
+ @protected_namespaces = protected_namespaces || PROTECTED_NAMESPACES
146
+ @render_erb = render_erb
147
+ @allow_globals = allow_globals
148
+ end
149
+
150
+ # Runs the task, returning a boolean representing success or failure
151
+ #
152
+ # @return [Boolean]
153
+ def run(*args)
154
+ run!(*args)
155
+ true
156
+ rescue FatalDeploymentError
157
+ false
158
+ end
159
+
160
+ # Runs the task, raising exceptions in case of issues
161
+ #
162
+ # @param verify_result [Boolean] Wait for completion and verify success
163
+ # @param allow_protected_ns [Boolean] Enable deploying to protected namespaces
164
+ # @param prune [Boolean] Enable deletion of resources that do not appear in the template dir
165
+ #
166
+ # @return [nil]
167
+ def run!(verify_result: true, allow_protected_ns: false, prune: true)
168
+ start = Time.now.utc
169
+ @logger.reset
170
+
171
+ @logger.phase_heading("Initializing deploy")
172
+ validate_configuration(allow_protected_ns: allow_protected_ns, prune: prune)
173
+ resources = discover_resources
174
+ validate_resources(resources)
175
+
176
+ @logger.phase_heading("Checking initial resource statuses")
177
+ check_initial_status(resources)
178
+
179
+ if deploy_has_priority_resources?(resources)
180
+ @logger.phase_heading("Predeploying priority resources")
181
+ resource_deployer.predeploy_priority_resources(resources, predeploy_sequence)
182
+ end
183
+
184
+ @logger.phase_heading("Deploying all resources")
185
+ if @protected_namespaces.include?(@namespace) && prune
186
+ raise FatalDeploymentError, "Refusing to deploy to protected namespace '#{@namespace}' with pruning enabled"
187
+ end
188
+
189
+ resource_deployer.deploy!(resources, verify_result, prune)
190
+
191
+ StatsD.client.event("Deployment of #{@namespace} succeeded",
192
+ "Successfully deployed all #{@namespace} resources to #{@context}",
193
+ alert_type: "success", tags: statsd_tags + %w(status:success))
194
+ StatsD.client.distribution('all_resources.duration', StatsD.duration(start),
195
+ tags: statsd_tags + %w(status:success))
196
+ @logger.print_summary(:success)
197
+ rescue DeploymentTimeoutError
198
+ @logger.print_summary(:timed_out)
199
+ StatsD.client.event("Deployment of #{@namespace} timed out",
200
+ "One or more #{@namespace} resources failed to deploy to #{@context} in time",
201
+ alert_type: "error", tags: statsd_tags + %w(status:timeout))
202
+ StatsD.client.distribution('all_resources.duration', StatsD.duration(start),
203
+ tags: statsd_tags + %w(status:timeout))
204
+ raise
205
+ rescue FatalDeploymentError => error
206
+ @logger.summary.add_action(error.message) if error.message != error.class.to_s
207
+ @logger.print_summary(:failure)
208
+ StatsD.client.event("Deployment of #{@namespace} failed",
209
+ "One or more #{@namespace} resources failed to deploy to #{@context}",
210
+ alert_type: "error", tags: statsd_tags + %w(status:failed))
211
+ StatsD.client.distribution('all_resources.duration', StatsD.duration(start),
212
+ tags: statsd_tags + %w(status:failed))
213
+ raise
214
+ end
215
+
216
+ private
217
+
218
+ def resource_deployer
219
+ @resource_deployer ||= Krane::ResourceDeployer.new(task_config: @task_config,
220
+ prune_whitelist: prune_whitelist, max_watch_seconds: @max_watch_seconds,
221
+ selector: @selector, statsd_tags: statsd_tags, current_sha: @current_sha)
222
+ end
223
+
224
+ def kubeclient_builder
225
+ @kubeclient_builder ||= KubeclientBuilder.new
226
+ end
227
+
228
+ def cluster_resource_discoverer
229
+ @cluster_resource_discoverer ||= ClusterResourceDiscovery.new(
230
+ task_config: @task_config,
231
+ namespace_tags: @namespace_tags
232
+ )
233
+ end
234
+
235
+ def ejson_provisioners
236
+ @ejson_provisoners ||= @template_sets.ejson_secrets_files.map do |ejson_secret_file|
237
+ EjsonSecretProvisioner.new(
238
+ task_config: @task_config,
239
+ ejson_keys_secret: ejson_keys_secret,
240
+ ejson_file: ejson_secret_file,
241
+ statsd_tags: @namespace_tags,
242
+ selector: @selector,
243
+ )
244
+ end
245
+ end
246
+
247
+ def deploy_has_priority_resources?(resources)
248
+ resources.any? { |r| predeploy_sequence.include?(r.type) }
249
+ end
250
+
251
+ def check_initial_status(resources)
252
+ cache = ResourceCache.new(@task_config)
253
+ Krane::Concurrency.split_across_threads(resources) { |r| r.sync(cache) }
254
+ resources.each { |r| @logger.info(r.pretty_status) }
255
+ end
256
+ measure_method(:check_initial_status, "initial_status.duration")
257
+
258
+ def secrets_from_ejson
259
+ ejson_provisioners.flat_map(&:resources)
260
+ end
261
+
262
+ def discover_resources
263
+ @logger.info("Discovering resources:")
264
+ resources = []
265
+ crds_by_kind = cluster_resource_discoverer.crds.group_by(&:kind)
266
+ @template_sets.with_resource_definitions(render_erb: @render_erb,
267
+ current_sha: @current_sha, bindings: @bindings) do |r_def|
268
+ crd = crds_by_kind[r_def["kind"]]&.first
269
+ r = KubernetesResource.build(namespace: @namespace, context: @context, logger: @logger, definition: r_def,
270
+ statsd_tags: @namespace_tags, crd: crd, global_names: @task_config.global_kinds)
271
+ resources << r
272
+ @logger.info(" - #{r.id}")
273
+ end
274
+
275
+ secrets_from_ejson.each do |secret|
276
+ resources << secret
277
+ @logger.info(" - #{secret.id} (from ejson)")
278
+ end
279
+
280
+ resources.sort
281
+ rescue InvalidTemplateError => e
282
+ record_invalid_template(logger: @logger, err: e.message, filename: e.filename,
283
+ content: e.content)
284
+ raise FatalDeploymentError, "Failed to render and parse template"
285
+ end
286
+ measure_method(:discover_resources)
287
+
288
+ def validate_configuration(allow_protected_ns:, prune:)
289
+ task_config_validator = DeployTaskConfigValidator.new(@protected_namespaces, allow_protected_ns, prune,
290
+ @task_config, kubectl, kubeclient_builder)
291
+ errors = []
292
+ errors += task_config_validator.errors
293
+ errors += @template_sets.validate
294
+ unless errors.empty?
295
+ add_para_from_list(logger: @logger, action: "Configuration invalid", enum: errors)
296
+ raise Krane::TaskConfigurationError
297
+ end
298
+
299
+ confirm_ejson_keys_not_prunable if prune
300
+ @logger.info("Using resource selector #{@selector}") if @selector
301
+ @namespace_tags |= tags_from_namespace_labels
302
+ @logger.info("All required parameters and files are present")
303
+ end
304
+ measure_method(:validate_configuration)
305
+
306
+ def validate_resources(resources)
307
+ validate_globals(resources)
308
+ Krane::Concurrency.split_across_threads(resources) do |r|
309
+ r.validate_definition(kubectl, selector: @selector)
310
+ end
311
+
312
+ resources.select(&:has_warnings?).each do |resource|
313
+ record_warnings(logger: @logger, warning: resource.validation_warning_msg,
314
+ filename: File.basename(resource.file_path))
315
+ end
316
+
317
+ failed_resources = resources.select(&:validation_failed?)
318
+ if failed_resources.present?
319
+
320
+ failed_resources.each do |r|
321
+ content = File.read(r.file_path) if File.file?(r.file_path) && !r.sensitive_template_content?
322
+ record_invalid_template(logger: @logger, err: r.validation_error_msg,
323
+ filename: File.basename(r.file_path), content: content)
324
+ end
325
+ raise FatalDeploymentError, "Template validation failed"
326
+ end
327
+ end
328
+ measure_method(:validate_resources)
329
+
330
+ def validate_globals(resources)
331
+ return unless (global = resources.select(&:global?).presence)
332
+ global_names = global.map do |resource|
333
+ "#{resource.name} (#{resource.type}) in #{File.basename(resource.file_path)}"
334
+ end
335
+ global_names = FormattedLogger.indent_four(global_names.join("\n"))
336
+
337
+ if @allow_globals
338
+ msg = "The ability for this task to deploy global resources will be removed in the next version,"\
339
+ " which will affect the following resources:"
340
+ msg += "\n#{global_names}"
341
+ @logger.summary.add_paragraph(ColorizedString.new(msg).yellow)
342
+ else
343
+ @logger.summary.add_paragraph(ColorizedString.new("Global resources:\n#{global_names}").yellow)
344
+ raise FatalDeploymentError, "This command is namespaced and cannot be used to deploy global resources."
345
+ end
346
+ end
347
+
348
+ def namespace_definition
349
+ @namespace_definition ||= begin
350
+ definition, _err, st = kubectl.run("get", "namespace", @namespace, use_namespace: false,
351
+ log_failure: true, raise_if_not_found: true, attempts: 3, output: 'json')
352
+ st.success? ? JSON.parse(definition, symbolize_names: true) : nil
353
+ end
354
+ rescue Kubectl::ResourceNotFoundError
355
+ nil
356
+ end
357
+
358
+ # make sure to never prune the ejson-keys secret
359
+ def confirm_ejson_keys_not_prunable
360
+ return unless ejson_keys_secret.dig("metadata", "annotations", KubernetesResource::LAST_APPLIED_ANNOTATION)
361
+
362
+ @logger.error("Deploy cannot proceed because protected resource " \
363
+ "Secret/#{EjsonSecretProvisioner::EJSON_KEYS_SECRET} would be pruned.")
364
+ raise EjsonPrunableError
365
+ rescue Kubectl::ResourceNotFoundError => e
366
+ @logger.debug("Secret/#{EjsonSecretProvisioner::EJSON_KEYS_SECRET} does not exist: #{e}")
367
+ end
368
+
369
+ def tags_from_namespace_labels
370
+ return [] if namespace_definition.blank?
371
+ namespace_labels = namespace_definition.fetch(:metadata, {}).fetch(:labels, {})
372
+ namespace_labels.map { |key, value| "#{key}:#{value}" }
373
+ end
374
+
375
+ def kubectl
376
+ @kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
377
+ end
378
+
379
+ def ejson_keys_secret
380
+ @ejson_keys_secret ||= begin
381
+ out, err, st = kubectl.run("get", "secret", EjsonSecretProvisioner::EJSON_KEYS_SECRET, output: "json",
382
+ raise_if_not_found: true, attempts: 3, output_is_sensitive: true, log_failure: true)
383
+ unless st.success?
384
+ raise EjsonSecretError, "Error retrieving Secret/#{EjsonSecretProvisioner::EJSON_KEYS_SECRET}: #{err}"
385
+ end
386
+ JSON.parse(out)
387
+ end
388
+ end
389
+
390
+ def statsd_tags
391
+ tags = %W(namespace:#{@namespace} context:#{@context}) | @namespace_tags
392
+ @current_sha.nil? ? tags : %W(sha:#{@current_sha}) | tags
393
+ end
394
+
395
+ def with_retries(limit)
396
+ retried = 0
397
+ while retried <= limit
398
+ success = yield
399
+ break if success
400
+ retried += 1
401
+ end
402
+ end
403
+ end
404
+ end