kubernetes-deploy 0.29.0 → 1.0.0.pre.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.buildkite/pipeline.nightly.yml +7 -0
  3. data/.rubocop.yml +0 -12
  4. data/.shopify-build/{kubernetes-deploy.yml → krane.yml} +8 -2
  5. data/1.0-Upgrade.md +109 -0
  6. data/CHANGELOG.md +60 -0
  7. data/CONTRIBUTING.md +2 -2
  8. data/Gemfile +1 -0
  9. data/README.md +86 -2
  10. data/dev.yml +3 -1
  11. data/dev/flamegraph-from-tests +1 -1
  12. data/exe/kubernetes-deploy +12 -9
  13. data/exe/kubernetes-render +9 -7
  14. data/exe/kubernetes-restart +3 -3
  15. data/exe/kubernetes-run +1 -1
  16. data/kubernetes-deploy.gemspec +5 -5
  17. data/lib/krane.rb +5 -3
  18. data/lib/{kubernetes-deploy → krane}/bindings_parser.rb +1 -1
  19. data/lib/krane/cli/deploy_command.rb +25 -13
  20. data/lib/krane/cli/global_deploy_command.rb +55 -0
  21. data/lib/krane/cli/krane.rb +12 -3
  22. data/lib/krane/cli/render_command.rb +19 -9
  23. data/lib/krane/cli/restart_command.rb +4 -4
  24. data/lib/krane/cli/run_command.rb +4 -4
  25. data/lib/krane/cli/version_command.rb +1 -1
  26. data/lib/krane/cluster_resource_discovery.rb +113 -0
  27. data/lib/{kubernetes-deploy → krane}/common.rb +8 -9
  28. data/lib/krane/concerns/template_reporting.rb +29 -0
  29. data/lib/{kubernetes-deploy → krane}/concurrency.rb +1 -1
  30. data/lib/{kubernetes-deploy → krane}/container_logs.rb +3 -2
  31. data/lib/{kubernetes-deploy → krane}/deferred_summary_logging.rb +2 -2
  32. data/lib/{kubernetes-deploy → krane}/delayed_exceptions.rb +0 -0
  33. data/lib/krane/deploy_task.rb +16 -0
  34. data/lib/krane/deploy_task_config_validator.rb +29 -0
  35. data/lib/krane/deprecated_deploy_task.rb +404 -0
  36. data/lib/{kubernetes-deploy → krane}/duration_parser.rb +1 -3
  37. data/lib/{kubernetes-deploy → krane}/ejson_secret_provisioner.rb +10 -13
  38. data/lib/krane/errors.rb +28 -0
  39. data/lib/{kubernetes-deploy → krane}/formatted_logger.rb +2 -2
  40. data/lib/krane/global_deploy_task.rb +210 -0
  41. data/lib/krane/global_deploy_task_config_validator.rb +12 -0
  42. data/lib/{kubernetes-deploy → krane}/kubeclient_builder.rb +13 -5
  43. data/lib/{kubernetes-deploy → krane}/kubectl.rb +14 -16
  44. data/lib/{kubernetes-deploy → krane}/kubernetes_resource.rb +110 -27
  45. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/cloudsql.rb +1 -1
  46. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/config_map.rb +1 -1
  47. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/cron_job.rb +1 -1
  48. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/custom_resource.rb +2 -2
  49. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/custom_resource_definition.rb +1 -5
  50. data/lib/krane/kubernetes_resource/daemon_set.rb +90 -0
  51. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/deployment.rb +2 -2
  52. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/horizontal_pod_autoscaler.rb +1 -1
  53. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/ingress.rb +1 -1
  54. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/job.rb +1 -1
  55. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/network_policy.rb +1 -1
  56. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/persistent_volume_claim.rb +1 -1
  57. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/pod.rb +6 -2
  58. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/pod_disruption_budget.rb +2 -2
  59. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/pod_set_base.rb +3 -3
  60. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/pod_template.rb +1 -1
  61. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/replica_set.rb +2 -2
  62. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/resource_quota.rb +1 -1
  63. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/role.rb +1 -1
  64. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/role_binding.rb +1 -1
  65. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/secret.rb +1 -1
  66. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/service.rb +2 -2
  67. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/service_account.rb +1 -1
  68. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/stateful_set.rb +2 -2
  69. data/lib/{kubernetes-deploy → krane}/label_selector.rb +1 -1
  70. data/lib/{kubernetes-deploy → krane}/oj.rb +0 -0
  71. data/lib/{kubernetes-deploy → krane}/options_helper.rb +2 -2
  72. data/lib/{kubernetes-deploy → krane}/remote_logs.rb +2 -2
  73. data/lib/krane/render_task.rb +149 -0
  74. data/lib/{kubernetes-deploy → krane}/renderer.rb +1 -1
  75. data/lib/{kubernetes-deploy → krane}/resource_cache.rb +10 -9
  76. data/lib/krane/resource_deployer.rb +265 -0
  77. data/lib/{kubernetes-deploy → krane}/resource_watcher.rb +24 -25
  78. data/lib/krane/restart_task.rb +228 -0
  79. data/lib/{kubernetes-deploy → krane}/rollout_conditions.rb +1 -1
  80. data/lib/krane/runner_task.rb +212 -0
  81. data/lib/{kubernetes-deploy → krane}/runner_task_config_validator.rb +1 -1
  82. data/lib/{kubernetes-deploy → krane}/statsd.rb +13 -27
  83. data/lib/krane/task_config.rb +22 -0
  84. data/lib/{kubernetes-deploy → krane}/task_config_validator.rb +1 -1
  85. data/lib/{kubernetes-deploy → krane}/template_sets.rb +5 -5
  86. data/lib/krane/version.rb +4 -0
  87. data/lib/kubernetes-deploy/deploy_task.rb +6 -608
  88. data/lib/kubernetes-deploy/errors.rb +1 -26
  89. data/lib/kubernetes-deploy/render_task.rb +5 -122
  90. data/lib/kubernetes-deploy/rescue_krane_exceptions.rb +18 -0
  91. data/lib/kubernetes-deploy/restart_task.rb +6 -198
  92. data/lib/kubernetes-deploy/runner_task.rb +6 -184
  93. metadata +96 -70
  94. data/lib/kubernetes-deploy/cluster_resource_discovery.rb +0 -34
  95. data/lib/kubernetes-deploy/kubernetes_resource/daemon_set.rb +0 -54
  96. data/lib/kubernetes-deploy/task_config.rb +0 -16
  97. data/lib/kubernetes-deploy/version.rb +0 -4
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class RunnerTaskConfigValidator < TaskConfigValidator
4
4
  def initialize(template, args, *arguments)
5
5
  super(*arguments)
@@ -2,41 +2,27 @@
2
2
  require 'statsd-instrument'
3
3
  require 'logger'
4
4
 
5
- module KubernetesDeploy
5
+ module Krane
6
6
  class StatsD
7
- extend ::StatsD
8
-
9
7
  PREFIX = "KubernetesDeploy"
10
8
 
11
9
  def self.duration(start_time)
12
10
  (Time.now.utc - start_time).round(1)
13
11
  end
14
12
 
15
- def self.build
16
- if ENV['STATSD_DEV'].present?
17
- self.backend = ::StatsD::Instrument::Backends::LoggerBackend.new(Logger.new($stderr))
18
- elsif ENV['STATSD_ADDR'].present?
19
- statsd_impl = ENV['STATSD_IMPLEMENTATION'].present? ? ENV['STATSD_IMPLEMENTATION'] : "datadog"
20
- self.backend = ::StatsD::Instrument::Backends::UDPBackend.new(ENV['STATSD_ADDR'], statsd_impl)
21
- else
22
- self.backend = ::StatsD::Instrument::Backends::NullBackend.new
13
+ def self.client
14
+ @client ||= begin
15
+ sink = if ::StatsD::Instrument::Environment.current.env.fetch('STATSD_ENV', nil) == 'development'
16
+ ::StatsD::Instrument::LogSink.new(Logger.new($stderr))
17
+ elsif (addr = ::StatsD::Instrument::Environment.current.env.fetch('STATSD_ADDR', nil))
18
+ ::StatsD::Instrument::UDPSink.for_addr(addr)
19
+ else
20
+ ::StatsD::Instrument::NullSink.new
21
+ end
22
+ ::StatsD::Instrument::Client.new(prefix: PREFIX, sink: sink, default_sample_rate: 1.0)
23
23
  end
24
24
  end
25
25
 
26
- # It is not sufficient to set the prefix field on the KubernetesDeploy::StatsD singleton itself, since its value
27
- # is overridden in the underlying calls to the ::StatsD library, hence the need to pass it in as a custom prefix
28
- # via the metric_options hash. This is done since KubernetesDeploy may be included as a library and should not
29
- # change the global StatsD configuration of the importing application.
30
- def self.increment(key, value = 1, **metric_options)
31
- metric_options[:prefix] = PREFIX
32
- super
33
- end
34
-
35
- def self.distribution(key, value = nil, **metric_options, &block)
36
- metric_options[:prefix] = PREFIX
37
- super
38
- end
39
-
40
26
  module MeasureMethods
41
27
  def measure_method(method_name, metric = nil)
42
28
  unless method_defined?(method_name) || private_method_defined?(method_name)
@@ -64,9 +50,9 @@ module KubernetesDeploy
64
50
  dynamic_tags << "error:#{error}" if dynamic_tags.is_a?(Array)
65
51
  end
66
52
 
67
- StatsD.distribution(
53
+ Krane::StatsD.client.distribution(
68
54
  metric,
69
- KubernetesDeploy::StatsD.duration(start_time),
55
+ Krane::StatsD.duration(start_time),
70
56
  tags: dynamic_tags
71
57
  )
72
58
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'krane/cluster_resource_discovery'
4
+
5
+ module Krane
6
+ class TaskConfig
7
+ attr_reader :context, :namespace, :logger
8
+
9
+ def initialize(context, namespace, logger = nil)
10
+ @context = context
11
+ @namespace = namespace
12
+ @logger = logger || FormattedLogger.build(@namespace, @context)
13
+ end
14
+
15
+ def global_kinds
16
+ @global_kinds ||= begin
17
+ cluster_resource_discoverer = ClusterResourceDiscovery.new(task_config: self)
18
+ cluster_resource_discoverer.fetch_resources(namespaced: false).map { |g| g["kind"] }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class TaskConfigValidator
4
4
  DEFAULT_VALIDATIONS = %i(
5
5
  validate_kubeconfig
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
- require 'kubernetes-deploy/delayed_exceptions'
3
- require 'kubernetes-deploy/ejson_secret_provisioner'
2
+ require 'krane/delayed_exceptions'
3
+ require 'krane/ejson_secret_provisioner'
4
4
 
5
- module KubernetesDeploy
5
+ module Krane
6
6
  class TemplateSets
7
7
  include DelayedExceptions
8
8
  VALID_TEMPLATES = %w(.yml.erb .yml .yaml .yaml.erb)
@@ -24,7 +24,7 @@ module KubernetesDeploy
24
24
  bindings: bindings,
25
25
  )
26
26
  end
27
- with_delayed_exceptions(@files, KubernetesDeploy::InvalidTemplateError) do |filename|
27
+ with_delayed_exceptions(@files, Krane::InvalidTemplateError) do |filename|
28
28
  next if filename.end_with?(EjsonSecretProvisioner::EJSON_SECRETS_FILE)
29
29
  templates(filename: filename, raw: raw) { |r_def| yield r_def, filename }
30
30
  end
@@ -110,7 +110,7 @@ module KubernetesDeploy
110
110
  end
111
111
 
112
112
  def with_resource_definitions_and_filename(render_erb: false, current_sha: nil, bindings: nil, raw: false)
113
- with_delayed_exceptions(@template_sets, KubernetesDeploy::InvalidTemplateError) do |template_set|
113
+ with_delayed_exceptions(@template_sets, Krane::InvalidTemplateError) do |template_set|
114
114
  template_set.with_resource_definitions_and_filename(
115
115
  render_erb: render_erb,
116
116
  current_sha: current_sha,
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Krane
3
+ VERSION = "1.0.0.pre.2"
4
+ end
@@ -1,617 +1,15 @@
1
1
  # frozen_string_literal: true
2
- require 'yaml'
3
- require 'shellwords'
4
- require 'tempfile'
5
- require 'fileutils'
6
-
7
- require 'kubernetes-deploy/common'
8
- require 'kubernetes-deploy/concurrency'
9
- require 'kubernetes-deploy/resource_cache'
10
- require 'kubernetes-deploy/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 "kubernetes-deploy/kubernetes_resource/#{subresource}"
35
- end
36
- require 'kubernetes-deploy/resource_watcher'
37
- require 'kubernetes-deploy/kubectl'
38
- require 'kubernetes-deploy/kubeclient_builder'
39
- require 'kubernetes-deploy/ejson_secret_provisioner'
40
- require 'kubernetes-deploy/renderer'
41
- require 'kubernetes-deploy/cluster_resource_discovery'
42
- require 'kubernetes-deploy/template_sets'
2
+ require 'krane/deprecated_deploy_task'
3
+ require 'kubernetes-deploy/rescue_krane_exceptions'
43
4
 
44
5
  module KubernetesDeploy
45
- class DeployTask
46
- extend KubernetesDeploy::StatsD::MeasureMethods
47
-
48
- PROTECTED_NAMESPACES = %w(
49
- default
50
- kube-system
51
- kube-public
52
- )
53
- # Things removed from default prune whitelist at https://github.com/kubernetes/kubernetes/blob/0dff56b4d88ec7551084bf89028dbeebf569620e/pkg/kubectl/cmd/apply.go#L411:
54
- # core/v1/Namespace -- not namespaced
55
- # core/v1/PersistentVolume -- not namespaced
56
- # core/v1/Endpoints -- managed by services
57
- # core/v1/PersistentVolumeClaim -- would delete data
58
- # core/v1/ReplicationController -- superseded by deployments/replicasets
59
-
60
- def predeploy_sequence
61
- before_crs = %w(
62
- ResourceQuota
63
- NetworkPolicy
64
- )
65
- after_crs = %w(
66
- ConfigMap
67
- PersistentVolumeClaim
68
- ServiceAccount
69
- Role
70
- RoleBinding
71
- Secret
72
- Pod
73
- )
74
-
75
- before_crs + cluster_resource_discoverer.crds.select(&:predeployed?).map(&:kind) + after_crs
76
- end
77
-
78
- def prune_whitelist
79
- wl = %w(
80
- core/v1/ConfigMap
81
- core/v1/Pod
82
- core/v1/Service
83
- core/v1/ResourceQuota
84
- core/v1/Secret
85
- core/v1/ServiceAccount
86
- core/v1/PodTemplate
87
- batch/v1/Job
88
- extensions/v1beta1/ReplicaSet
89
- extensions/v1beta1/DaemonSet
90
- extensions/v1beta1/Deployment
91
- extensions/v1beta1/Ingress
92
- networking.k8s.io/v1/NetworkPolicy
93
- apps/v1beta1/StatefulSet
94
- autoscaling/v1/HorizontalPodAutoscaler
95
- policy/v1beta1/PodDisruptionBudget
96
- batch/v1beta1/CronJob
97
- rbac.authorization.k8s.io/v1/Role
98
- rbac.authorization.k8s.io/v1/RoleBinding
99
- )
100
- wl + cluster_resource_discoverer.crds.select(&:prunable?).map(&:group_version_kind)
101
- end
102
-
103
- def server_version
104
- kubectl.server_version
105
- end
106
-
107
- def initialize(namespace:, context:, current_sha:, logger: nil, kubectl_instance: nil, bindings: {},
108
- max_watch_seconds: nil, selector: nil, template_paths: [], template_dir: nil, protected_namespaces: nil,
109
- render_erb: true)
110
- template_dir = File.expand_path(template_dir) if template_dir
111
- template_paths = (template_paths.map { |path| File.expand_path(path) } << template_dir).compact
112
-
113
- @logger = logger || KubernetesDeploy::FormattedLogger.build(namespace, context)
114
- @template_sets = TemplateSets.from_dirs_and_files(paths: template_paths, logger: @logger)
115
- @task_config = KubernetesDeploy::TaskConfig.new(context, namespace, @logger)
116
- @bindings = bindings
117
- @namespace = namespace
118
- @namespace_tags = []
119
- @context = context
120
- @current_sha = current_sha
121
- @kubectl = kubectl_instance
122
- @max_watch_seconds = max_watch_seconds
123
- @selector = selector
124
- @protected_namespaces = protected_namespaces || PROTECTED_NAMESPACES
125
- @render_erb = render_erb
126
- end
6
+ class DeployTask < ::Krane::DeprecatedDeployTask
7
+ include RescueKraneExceptions
127
8
 
128
9
  def run(*args)
129
- run!(*args)
130
- true
131
- rescue FatalDeploymentError
10
+ super(*args)
11
+ rescue KubernetesDeploy::FatalDeploymentError
132
12
  false
133
13
  end
134
-
135
- def run!(verify_result: true, allow_protected_ns: false, prune: true)
136
- start = Time.now.utc
137
- @logger.reset
138
-
139
- @logger.phase_heading("Initializing deploy")
140
- validate_configuration(allow_protected_ns: allow_protected_ns, prune: prune)
141
- resources = discover_resources
142
- validate_resources(resources)
143
-
144
- @logger.phase_heading("Checking initial resource statuses")
145
- check_initial_status(resources)
146
-
147
- if deploy_has_priority_resources?(resources)
148
- @logger.phase_heading("Predeploying priority resources")
149
- predeploy_priority_resources(resources)
150
- end
151
-
152
- @logger.phase_heading("Deploying all resources")
153
- if @protected_namespaces.include?(@namespace) && prune
154
- raise FatalDeploymentError, "Refusing to deploy to protected namespace '#{@namespace}' with pruning enabled"
155
- end
156
-
157
- if verify_result
158
- deploy_all_resources(resources, prune: prune, verify: true)
159
- failed_resources = resources.reject(&:deploy_succeeded?)
160
- success = failed_resources.empty?
161
- if !success && failed_resources.all?(&:deploy_timed_out?)
162
- raise DeploymentTimeoutError
163
- end
164
- raise FatalDeploymentError unless success
165
- else
166
- deploy_all_resources(resources, prune: prune, verify: false)
167
- @logger.summary.add_action("deployed #{resources.length} #{'resource'.pluralize(resources.length)}")
168
- warning = <<~MSG
169
- Deploy result verification is disabled for this deploy.
170
- This means the desired changes were communicated to Kubernetes, but the deploy did not make sure they actually succeeded.
171
- MSG
172
- @logger.summary.add_paragraph(ColorizedString.new(warning).yellow)
173
- end
174
- StatsD.event("Deployment of #{@namespace} succeeded",
175
- "Successfully deployed all #{@namespace} resources to #{@context}",
176
- alert_type: "success", tags: statsd_tags << "status:success")
177
- StatsD.distribution('all_resources.duration', StatsD.duration(start), tags: statsd_tags << "status:success")
178
- @logger.print_summary(:success)
179
- rescue DeploymentTimeoutError
180
- @logger.print_summary(:timed_out)
181
- StatsD.event("Deployment of #{@namespace} timed out",
182
- "One or more #{@namespace} resources failed to deploy to #{@context} in time",
183
- alert_type: "error", tags: statsd_tags << "status:timeout")
184
- StatsD.distribution('all_resources.duration', StatsD.duration(start), tags: statsd_tags << "status:timeout")
185
- raise
186
- rescue FatalDeploymentError => error
187
- @logger.summary.add_action(error.message) if error.message != error.class.to_s
188
- @logger.print_summary(:failure)
189
- StatsD.event("Deployment of #{@namespace} failed",
190
- "One or more #{@namespace} resources failed to deploy to #{@context}",
191
- alert_type: "error", tags: statsd_tags << "status:failed")
192
- StatsD.distribution('all_resources.duration', StatsD.duration(start), tags: statsd_tags << "status:failed")
193
- raise
194
- end
195
-
196
- private
197
-
198
- def kubeclient_builder
199
- @kubeclient_builder ||= KubeclientBuilder.new
200
- end
201
-
202
- def cluster_resource_discoverer
203
- @cluster_resource_discoverer ||= ClusterResourceDiscovery.new(
204
- namespace: @namespace,
205
- context: @context,
206
- logger: @logger,
207
- namespace_tags: @namespace_tags
208
- )
209
- end
210
-
211
- def ejson_provisioners
212
- @ejson_provisoners ||= @template_sets.ejson_secrets_files.map do |ejson_secret_file|
213
- EjsonSecretProvisioner.new(
214
- namespace: @namespace,
215
- context: @context,
216
- ejson_keys_secret: ejson_keys_secret,
217
- ejson_file: ejson_secret_file,
218
- logger: @logger,
219
- statsd_tags: @namespace_tags,
220
- selector: @selector,
221
- )
222
- end
223
- end
224
-
225
- def deploy_has_priority_resources?(resources)
226
- resources.any? { |r| predeploy_sequence.include?(r.type) }
227
- end
228
-
229
- def predeploy_priority_resources(resource_list)
230
- bare_pods = resource_list.select { |resource| resource.is_a?(Pod) }
231
- if bare_pods.count == 1
232
- bare_pods.first.stream_logs = true
233
- end
234
-
235
- predeploy_sequence.each do |resource_type|
236
- matching_resources = resource_list.select { |r| r.type == resource_type }
237
- next if matching_resources.empty?
238
- deploy_resources(matching_resources, verify: true, record_summary: false)
239
-
240
- failed_resources = matching_resources.reject(&:deploy_succeeded?)
241
- fail_count = failed_resources.length
242
- if fail_count > 0
243
- KubernetesDeploy::Concurrency.split_across_threads(failed_resources) do |r|
244
- r.sync_debug_info(kubectl)
245
- end
246
- failed_resources.each { |r| @logger.summary.add_paragraph(r.debug_message) }
247
- raise FatalDeploymentError, "Failed to deploy #{fail_count} priority #{'resource'.pluralize(fail_count)}"
248
- end
249
- @logger.blank_line
250
- end
251
- end
252
- measure_method(:predeploy_priority_resources, 'priority_resources.duration')
253
-
254
- def validate_resources(resources)
255
- KubernetesDeploy::Concurrency.split_across_threads(resources) do |r|
256
- r.validate_definition(kubectl, selector: @selector)
257
- end
258
-
259
- resources.select(&:has_warnings?).each do |resource|
260
- record_warnings(warning: resource.validation_warning_msg, filename: File.basename(resource.file_path))
261
- end
262
-
263
- failed_resources = resources.select(&:validation_failed?)
264
- return unless failed_resources.present?
265
-
266
- failed_resources.each do |r|
267
- content = File.read(r.file_path) if File.file?(r.file_path) && !r.sensitive_template_content?
268
- record_invalid_template(err: r.validation_error_msg, filename: File.basename(r.file_path), content: content)
269
- end
270
- raise FatalDeploymentError, "Template validation failed"
271
- end
272
- measure_method(:validate_resources)
273
-
274
- def check_initial_status(resources)
275
- cache = ResourceCache.new(@namespace, @context, @logger)
276
- KubernetesDeploy::Concurrency.split_across_threads(resources) { |r| r.sync(cache) }
277
- resources.each { |r| @logger.info(r.pretty_status) }
278
- end
279
- measure_method(:check_initial_status, "initial_status.duration")
280
-
281
- def secrets_from_ejson
282
- ejson_provisioners.flat_map(&:resources)
283
- end
284
-
285
- def discover_resources
286
- @logger.info("Discovering resources:")
287
- resources = []
288
- crds_by_kind = cluster_resource_discoverer.crds.group_by(&:kind)
289
- @template_sets.with_resource_definitions(render_erb: @render_erb,
290
- current_sha: @current_sha, bindings: @bindings) do |r_def|
291
- crd = crds_by_kind[r_def["kind"]]&.first
292
- r = KubernetesResource.build(namespace: @namespace, context: @context, logger: @logger, definition: r_def,
293
- statsd_tags: @namespace_tags, crd: crd)
294
- resources << r
295
- @logger.info(" - #{r.id}")
296
- end
297
-
298
- secrets_from_ejson.each do |secret|
299
- resources << secret
300
- @logger.info(" - #{secret.id} (from ejson)")
301
- end
302
-
303
- if (global = resources.select(&:global?).presence)
304
- @logger.warn("Detected non-namespaced #{'resource'.pluralize(global.count)} which will never be pruned:")
305
- global.each { |r| @logger.warn(" - #{r.id}") }
306
- end
307
- resources.sort
308
- rescue InvalidTemplateError => e
309
- record_invalid_template(err: e.message, filename: e.filename, content: e.content)
310
- raise FatalDeploymentError, "Failed to render and parse template"
311
- end
312
- measure_method(:discover_resources)
313
-
314
- def record_invalid_template(err:, filename:, content: nil)
315
- debug_msg = ColorizedString.new("Invalid template: #{filename}\n").red
316
- debug_msg += "> Error message:\n#{FormattedLogger.indent_four(err)}"
317
- if content
318
- debug_msg += if content =~ /kind:\s*Secret/
319
- "\n> Template content: Suppressed because it may contain a Secret"
320
- else
321
- "\n> Template content:\n#{FormattedLogger.indent_four(content)}"
322
- end
323
- end
324
- @logger.summary.add_paragraph(debug_msg)
325
- end
326
-
327
- def record_warnings(warning:, filename:)
328
- warn_msg = "Template warning: #{filename}\n"
329
- warn_msg += "> Warning message:\n#{FormattedLogger.indent_four(warning)}"
330
- @logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow)
331
- end
332
-
333
- def validate_configuration(allow_protected_ns:, prune:)
334
- errors = []
335
- errors += kubeclient_builder.validate_config_files
336
- errors += @template_sets.validate
337
-
338
- if @namespace.blank?
339
- errors << "Namespace must be specified"
340
- elsif @protected_namespaces.include?(@namespace)
341
- if allow_protected_ns && prune
342
- errors << "Refusing to deploy to protected namespace '#{@namespace}' with pruning enabled"
343
- elsif allow_protected_ns
344
- @logger.warn("You're deploying to protected namespace #{@namespace}, which cannot be pruned.")
345
- @logger.warn("Existing resources can only be removed manually with kubectl. " \
346
- "Removing templates from the set deployed will have no effect.")
347
- @logger.warn("***Please do not deploy to #{@namespace} unless you really know what you are doing.***")
348
- else
349
- errors << "Refusing to deploy to protected namespace '#{@namespace}'"
350
- end
351
- end
352
-
353
- if @context.blank?
354
- errors << "Context must be specified"
355
- end
356
-
357
- unless errors.empty?
358
- @logger.summary.add_paragraph(errors.map { |err| "- #{err}" }.join("\n"))
359
- raise FatalDeploymentError, "Configuration invalid"
360
- end
361
-
362
- confirm_context_exists
363
- confirm_namespace_exists
364
- confirm_ejson_keys_not_prunable if prune
365
- @logger.info("Using resource selector #{@selector}") if @selector
366
- @namespace_tags |= tags_from_namespace_labels
367
- @logger.info("All required parameters and files are present")
368
- end
369
- measure_method(:validate_configuration)
370
-
371
- def deploy_resources(resources, prune: false, verify:, record_summary: true)
372
- return if resources.empty?
373
- deploy_started_at = Time.now.utc
374
-
375
- if resources.length > 1
376
- @logger.info("Deploying resources:")
377
- resources.each do |r|
378
- @logger.info("- #{r.id} (#{r.pretty_timeout_type})")
379
- end
380
- else
381
- resource = resources.first
382
- @logger.info("Deploying #{resource.id} (#{resource.pretty_timeout_type})")
383
- end
384
-
385
- # Apply can be done in one large batch, the rest have to be done individually
386
- applyables, individuals = resources.partition { |r| r.deploy_method == :apply }
387
- # Prunable resources should also applied so that they can be pruned
388
- pruneable_types = prune_whitelist.map { |t| t.split("/").last }
389
- applyables += individuals.select { |r| pruneable_types.include?(r.type) }
390
-
391
- individuals.each do |r|
392
- r.deploy_started_at = Time.now.utc
393
- case r.deploy_method
394
- when :replace
395
- _, _, replace_st = kubectl.run("replace", "-f", r.file_path, log_failure: false)
396
- when :replace_force
397
- _, _, replace_st = kubectl.run("replace", "--force", "--cascade", "-f", r.file_path,
398
- log_failure: false)
399
- else
400
- # Fail Fast! This is a programmer mistake.
401
- raise ArgumentError, "Unexpected deploy method! (#{r.deploy_method.inspect})"
402
- end
403
-
404
- next if replace_st.success?
405
- # it doesn't exist so we can't replace it
406
- _, err, create_st = kubectl.run("create", "-f", r.file_path, log_failure: false)
407
-
408
- next if create_st.success?
409
- raise FatalDeploymentError, <<~MSG
410
- Failed to replace or create resource: #{r.id}
411
- #{err}
412
- MSG
413
- end
414
-
415
- apply_all(applyables, prune)
416
-
417
- if verify
418
- watcher = ResourceWatcher.new(resources: resources, logger: @logger, deploy_started_at: deploy_started_at,
419
- timeout: @max_watch_seconds, namespace: @namespace, context: @context, sha: @current_sha)
420
- watcher.run(record_summary: record_summary)
421
- end
422
- end
423
-
424
- def deploy_all_resources(resources, prune: false, verify:, record_summary: true)
425
- deploy_resources(resources, prune: prune, verify: verify, record_summary: record_summary)
426
- end
427
- measure_method(:deploy_all_resources, 'normal_resources.duration')
428
-
429
- def apply_all(resources, prune)
430
- return unless resources.present?
431
- command = %w(apply)
432
-
433
- Dir.mktmpdir do |tmp_dir|
434
- resources.each do |r|
435
- FileUtils.symlink(r.file_path, tmp_dir)
436
- r.deploy_started_at = Time.now.utc
437
- end
438
- command.push("-f", tmp_dir)
439
-
440
- if prune
441
- command.push("--prune")
442
- if @selector
443
- command.push("--selector", @selector.to_s)
444
- else
445
- command.push("--all")
446
- end
447
- prune_whitelist.each { |type| command.push("--prune-whitelist=#{type}") }
448
- end
449
-
450
- output_is_sensitive = resources.any?(&:sensitive_template_content?)
451
- out, err, st = kubectl.run(*command, log_failure: false, output_is_sensitive: output_is_sensitive)
452
-
453
- if st.success?
454
- log_pruning(out) if prune
455
- else
456
- record_apply_failure(err, resources: resources)
457
- raise FatalDeploymentError, "Command failed: #{Shellwords.join(command)}"
458
- end
459
- end
460
- end
461
- measure_method(:apply_all)
462
-
463
- def log_pruning(kubectl_output)
464
- pruned = kubectl_output.scan(/^(.*) pruned$/)
465
- return unless pruned.present?
466
-
467
- @logger.info("The following resources were pruned: #{pruned.join(', ')}")
468
- @logger.summary.add_action("pruned #{pruned.length} #{'resource'.pluralize(pruned.length)}")
469
- end
470
-
471
- def record_apply_failure(err, resources: [])
472
- warn_msg = "WARNING: Any resources not mentioned in the error(s) below were likely created/updated. " \
473
- "You may wish to roll back this deploy."
474
- @logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow)
475
-
476
- unidentified_errors = []
477
- filenames_with_sensitive_content = resources
478
- .select(&:sensitive_template_content?)
479
- .map { |r| File.basename(r.file_path) }
480
-
481
- server_dry_run_validated_resource = resources
482
- .select(&:server_dry_run_validated?)
483
- .map { |r| File.basename(r.file_path) }
484
-
485
- err.each_line do |line|
486
- bad_files = find_bad_files_from_kubectl_output(line)
487
- unless bad_files.present?
488
- unidentified_errors << line
489
- next
490
- end
491
-
492
- bad_files.each do |f|
493
- err_msg = f[:err]
494
- if filenames_with_sensitive_content.include?(f[:filename])
495
- # Hide the error and template contents in case it has sensitive information
496
- # we display full error messages as we assume there's no sensitive info leak after server-dry-run
497
- err_msg = "SUPPRESSED FOR SECURITY" unless server_dry_run_validated_resource.include?(f[:filename])
498
- record_invalid_template(err: err_msg, filename: f[:filename], content: nil)
499
- else
500
- record_invalid_template(err: err_msg, filename: f[:filename], content: f[:content])
501
- end
502
- end
503
- end
504
- return unless unidentified_errors.any?
505
-
506
- if (filenames_with_sensitive_content - server_dry_run_validated_resource).present?
507
- warn_msg = "WARNING: There was an error applying some or all resources. The raw output may be sensitive and " \
508
- "so cannot be displayed."
509
- @logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow)
510
- else
511
- heading = ColorizedString.new('Unidentified error(s):').red
512
- msg = FormattedLogger.indent_four(unidentified_errors.join)
513
- @logger.summary.add_paragraph("#{heading}\n#{msg}")
514
- end
515
- end
516
-
517
- # Inspect the file referenced in the kubectl stderr
518
- # to make it easier for developer to understand what's going on
519
- def find_bad_files_from_kubectl_output(line)
520
- # stderr often contains one or more lines like the following, from which we can extract the file path(s):
521
- # Error from server (TypeOfError): error when creating "/path/to/service-gqq5oh.yml": Service "web" is invalid:
522
-
523
- line.scan(%r{"(/\S+\.ya?ml\S*)"}).each_with_object([]) do |matches, bad_files|
524
- matches.each do |path|
525
- content = File.read(path) if File.file?(path)
526
- bad_files << { filename: File.basename(path), err: line, content: content }
527
- end
528
- end
529
- end
530
-
531
- def confirm_context_exists
532
- out, err, st = kubectl.run("config", "get-contexts", "-o", "name",
533
- use_namespace: false, use_context: false, log_failure: false)
534
- available_contexts = out.split("\n")
535
- if !st.success?
536
- raise FatalDeploymentError, err
537
- elsif !available_contexts.include?(@context)
538
- raise FatalDeploymentError, "Context #{@context} is not available. Valid contexts: #{available_contexts}"
539
- end
540
- confirm_cluster_reachable
541
- @logger.info("Context #{@context} found")
542
- end
543
-
544
- def confirm_cluster_reachable
545
- success = false
546
- with_retries(2) do
547
- begin
548
- success = kubectl.version_info
549
- rescue KubectlError
550
- success = false
551
- end
552
- end
553
- raise FatalDeploymentError, "Failed to reach server for #{@context}" unless success
554
- TaskConfigValidator.new(@task_config, kubectl, kubeclient_builder, only: [:validate_server_version]).valid?
555
- end
556
-
557
- def confirm_namespace_exists
558
- raise FatalDeploymentError, "Namespace #{@namespace} not found" unless namespace_definition.present?
559
- @logger.info("Namespace #{@namespace} found")
560
- end
561
-
562
- def namespace_definition
563
- @namespace_definition ||= begin
564
- definition, _err, st = kubectl.run("get", "namespace", @namespace, use_namespace: false,
565
- log_failure: true, raise_if_not_found: true, attempts: 3, output: 'json')
566
- st.success? ? JSON.parse(definition, symbolize_names: true) : nil
567
- end
568
- rescue Kubectl::ResourceNotFoundError
569
- nil
570
- end
571
-
572
- # make sure to never prune the ejson-keys secret
573
- def confirm_ejson_keys_not_prunable
574
- return unless ejson_keys_secret.dig("metadata", "annotations", KubernetesResource::LAST_APPLIED_ANNOTATION)
575
-
576
- @logger.error("Deploy cannot proceed because protected resource " \
577
- "Secret/#{EjsonSecretProvisioner::EJSON_KEYS_SECRET} would be pruned.")
578
- raise EjsonPrunableError
579
- rescue Kubectl::ResourceNotFoundError => e
580
- @logger.debug("Secret/#{EjsonSecretProvisioner::EJSON_KEYS_SECRET} does not exist: #{e}")
581
- end
582
-
583
- def tags_from_namespace_labels
584
- return [] if namespace_definition.blank?
585
- namespace_labels = namespace_definition.fetch(:metadata, {}).fetch(:labels, {})
586
- namespace_labels.map { |key, value| "#{key}:#{value}" }
587
- end
588
-
589
- def kubectl
590
- @kubectl ||= Kubectl.new(namespace: @namespace, context: @context, logger: @logger, log_failure_by_default: true)
591
- end
592
-
593
- def ejson_keys_secret
594
- @ejson_keys_secret ||= begin
595
- out, err, st = kubectl.run("get", "secret", EjsonSecretProvisioner::EJSON_KEYS_SECRET, output: "json",
596
- raise_if_not_found: true, attempts: 3, output_is_sensitive: true, log_failure: true)
597
- unless st.success?
598
- raise EjsonSecretError, "Error retrieving Secret/#{EjsonSecretProvisioner::EJSON_KEYS_SECRET}: #{err}"
599
- end
600
- JSON.parse(out)
601
- end
602
- end
603
-
604
- def statsd_tags
605
- %W(namespace:#{@namespace} sha:#{@current_sha} context:#{@context}) | @namespace_tags
606
- end
607
-
608
- def with_retries(limit)
609
- retried = 0
610
- while retried <= limit
611
- success = yield
612
- break if success
613
- retried += 1
614
- end
615
- end
616
14
  end
617
15
  end