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
@@ -1,33 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module KubernetesDeploy
3
4
  class FatalDeploymentError < StandardError; end
4
5
  class FatalKubeAPIError < FatalDeploymentError; end
5
6
  class KubectlError < StandardError; end
6
- class TaskConfigurationError < FatalDeploymentError; end
7
-
8
- class InvalidTemplateError < FatalDeploymentError
9
- attr_reader :content
10
- attr_accessor :filename
11
- def initialize(err, filename: nil, content: nil)
12
- @filename = filename
13
- @content = content
14
- super(err)
15
- end
16
- end
17
-
18
- class NamespaceNotFoundError < FatalDeploymentError
19
- def initialize(name, context)
20
- super("Namespace `#{name}` not found in context `#{context}`")
21
- end
22
- end
23
-
24
7
  class DeploymentTimeoutError < FatalDeploymentError; end
25
-
26
- class EjsonPrunableError < FatalDeploymentError
27
- def initialize
28
- super("Found #{KubernetesResource::LAST_APPLIED_ANNOTATION} annotation on " \
29
- "#{EjsonSecretProvisioner::EJSON_KEYS_SECRET} secret. " \
30
- "kubernetes-deploy will not continue since it is extremely unlikely that this secret should be pruned.")
31
- end
32
- end
33
8
  end
@@ -1,149 +1,15 @@
1
1
  # frozen_string_literal: true
2
- require 'tempfile'
3
-
4
- require 'kubernetes-deploy/common'
5
- require 'kubernetes-deploy/renderer'
6
- require 'kubernetes-deploy/template_sets'
2
+ require 'krane/render_task'
3
+ require 'kubernetes-deploy/rescue_krane_exceptions'
7
4
 
8
5
  module KubernetesDeploy
9
- # Render templates
10
- class RenderTask
11
- # Initializes the render task
12
- #
13
- # @param logger [Object] Logger object (defaults to an instance of KubernetesDeploy::FormattedLogger)
14
- # @param current_sha [String] The SHA of the commit
15
- # @param template_dir [String] Path to a directory with templates to render (deprecated)
16
- # @param template_paths [Array<String>] An array of template paths to render
17
- # @param bindings [Hash] Bindings parsed by KubernetesDeploy::BindingsParser
18
- def initialize(logger: nil, current_sha:, template_dir: nil, template_paths: [], bindings:)
19
- @logger = logger || KubernetesDeploy::FormattedLogger.build
20
- @template_dir = template_dir
21
- @template_paths = template_paths.map { |path| File.expand_path(path) }
22
- @bindings = bindings
23
- @current_sha = current_sha
24
- end
6
+ class RenderTask < ::Krane::RenderTask
7
+ include RescueKraneExceptions
25
8
 
26
- # Runs the task, returning a boolean representing success or failure
27
- #
28
- # @return [Boolean]
29
9
  def run(*args)
30
- run!(*args)
31
- true
10
+ super(*args)
32
11
  rescue KubernetesDeploy::FatalDeploymentError
33
12
  false
34
13
  end
35
-
36
- # Runs the task, raising exceptions in case of issues
37
- #
38
- # @param stream [IO] Place to stream the output to
39
- # @param only_filenames [Array<String>] List of filenames to render
40
- #
41
- # @return [nil]
42
- def run!(stream, only_filenames = [])
43
- @logger.reset
44
- @logger.phase_heading("Initializing render task")
45
-
46
- ts = TemplateSets.from_dirs_and_files(paths: template_sets_paths(only_filenames), logger: @logger)
47
-
48
- validate_configuration(ts, only_filenames)
49
- count = render_templates(stream, ts)
50
-
51
- @logger.summary.add_action("Successfully rendered #{count} template(s)")
52
- @logger.print_summary(:success)
53
- rescue KubernetesDeploy::FatalDeploymentError
54
- @logger.print_summary(:failure)
55
- raise
56
- end
57
-
58
- private
59
-
60
- def template_sets_paths(only_filenames)
61
- if @template_paths.present?
62
- # Validation will catch @template_paths & @template_dir being present
63
- @template_paths
64
- elsif only_filenames.blank?
65
- [File.expand_path(@template_dir || '')]
66
- else
67
- absolute_template_dir = File.expand_path(@template_dir || '')
68
- only_filenames.map do |name|
69
- File.join(absolute_template_dir, name)
70
- end
71
- end
72
- end
73
-
74
- def render_templates(stream, template_sets)
75
- @logger.phase_heading("Rendering template(s)")
76
- count = 0
77
- template_sets.with_resource_definitions_and_filename(render_erb: true,
78
- current_sha: @current_sha, bindings: @bindings, raw: true) do |rendered_content, filename|
79
- write_to_stream(rendered_content, filename, stream)
80
- count += 1
81
- end
82
-
83
- count
84
- rescue KubernetesDeploy::InvalidTemplateError => exception
85
- log_invalid_template(exception)
86
- raise
87
- end
88
-
89
- def write_to_stream(rendered_content, filename, stream)
90
- file_basename = File.basename(filename)
91
- @logger.info("Rendering #{file_basename}...")
92
- implicit = []
93
- YAML.parse_stream(rendered_content, "<rendered> #{filename}") { |d| implicit << d.implicit }
94
- if rendered_content.present?
95
- stream.puts "---\n" if implicit.first
96
- stream.puts rendered_content
97
- @logger.info("Rendered #{file_basename}")
98
- else
99
- @logger.warn("Rendered #{file_basename} successfully, but the result was blank")
100
- end
101
- rescue Psych::SyntaxError => exception
102
- raise InvalidTemplateError.new("Template is not valid YAML. #{exception.message}", filename: filename)
103
- end
104
-
105
- def validate_configuration(template_sets, filenames)
106
- @logger.info("Validating configuration")
107
- errors = []
108
- if @template_dir.present? && @template_paths.present?
109
- errors << "template_dir and template_paths can not be combined"
110
- elsif @template_dir.blank? && @template_paths.blank?
111
- errors << "template_dir or template_paths must be set"
112
- end
113
-
114
- if filenames.present?
115
- if @template_dir.nil?
116
- errors << "template_dir must be set to use filenames"
117
- else
118
- absolute_template_dir = File.expand_path(@template_dir)
119
- filenames.each do |filename|
120
- absolute_file = File.expand_path(File.join(@template_dir, filename))
121
- unless absolute_file.start_with?(absolute_template_dir)
122
- errors << "Filename \"#{absolute_file}\" is outside the template directory," \
123
- " which was resolved as #{absolute_template_dir}"
124
- end
125
- end
126
- end
127
- end
128
-
129
- errors += template_sets.validate
130
-
131
- unless errors.empty?
132
- @logger.summary.add_action("Configuration invalid")
133
- @logger.summary.add_paragraph(errors.map { |err| "- #{err}" }.join("\n"))
134
- raise KubernetesDeploy::TaskConfigurationError, "Configuration invalid: #{errors.join(', ')}"
135
- end
136
- end
137
-
138
- def log_invalid_template(exception)
139
- @logger.error("Failed to render #{exception.filename}")
140
-
141
- debug_msg = ColorizedString.new("Invalid template: #{exception.filename}\n").red
142
- debug_msg += "> Error message:\n#{FormattedLogger.indent_four(exception.to_s)}"
143
- if exception.content
144
- debug_msg += "\n> Template content:\n#{FormattedLogger.indent_four(exception.content)}"
145
- end
146
- @logger.summary.add_paragraph(debug_msg)
147
- end
148
14
  end
149
15
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ require 'kubernetes-deploy/errors'
3
+
4
+ module KubernetesDeploy
5
+ module RescueKraneExceptions
6
+ def run!(*args)
7
+ super(*args)
8
+ rescue Krane::DeploymentTimeoutError => e
9
+ raise KubernetesDeploy::DeploymentTimeoutError, e.message
10
+ rescue Krane::FatalDeploymentError => e
11
+ raise KubernetesDeploy::FatalDeploymentError, e.message
12
+ rescue Krane::FatalKubeAPIError => e
13
+ raise KubernetesDeploy::FatalKubeAPIError, e.message
14
+ rescue Krane::KubectlError => e
15
+ raise KubernetesDeploy::KubectlError, e.message
16
+ end
17
+ end
18
+ end
@@ -1,224 +1,15 @@
1
1
  # frozen_string_literal: true
2
- require 'kubernetes-deploy/common'
3
- require 'kubernetes-deploy/kubernetes_resource'
4
- require 'kubernetes-deploy/kubernetes_resource/deployment'
5
- require 'kubernetes-deploy/kubeclient_builder'
6
- require 'kubernetes-deploy/resource_watcher'
7
- require 'kubernetes-deploy/kubectl'
2
+ require 'krane/restart_task'
3
+ require 'kubernetes-deploy/rescue_krane_exceptions'
8
4
 
9
5
  module KubernetesDeploy
10
- # Restart the pods in one or more deployments
11
- class RestartTask
12
- class FatalRestartError < FatalDeploymentError; end
6
+ class RestartTask < ::Krane::RestartTask
7
+ include RescueKraneExceptions
13
8
 
14
- class RestartAPIError < FatalRestartError
15
- def initialize(deployment_name, response)
16
- super("Failed to restart #{deployment_name}. " \
17
- "API returned non-200 response code (#{response.code})\n" \
18
- "Response:\n#{response.body}")
19
- end
20
- end
21
-
22
- HTTP_OK_RANGE = 200..299
23
- ANNOTATION = "shipit.shopify.io/restart"
24
-
25
- # Initializes the restart task
26
- #
27
- # @param context [String] Kubernetes context / cluster
28
- # @param namespace [String] Kubernetes namespace
29
- # @param logger [Object] Logger object (defaults to an instance of KubernetesDeploy::FormattedLogger)
30
- # @param max_watch_seconds [Integer] Timeout in seconds
31
- def initialize(context:, namespace:, logger: nil, max_watch_seconds: nil)
32
- @logger = logger || KubernetesDeploy::FormattedLogger.build(namespace, context)
33
- @task_config = KubernetesDeploy::TaskConfig.new(context, namespace, @logger)
34
- @context = context
35
- @namespace = namespace
36
- @max_watch_seconds = max_watch_seconds
37
- end
38
-
39
- # Runs the task, returning a boolean representing success or failure
40
- #
41
- # @return [Boolean]
42
9
  def run(*args)
43
- perform!(*args)
44
- true
45
- rescue FatalDeploymentError
10
+ super(*args)
11
+ rescue KubernetesDeploy::FatalDeploymentError
46
12
  false
47
13
  end
48
- alias_method :perform, :run
49
-
50
- # Runs the task, raising exceptions in case of issues
51
- #
52
- # @param deployments_names [Array<String>] Array of workload names to restart
53
- # @param selector [Hash] Selector(s) parsed by KubernetesDeploy::LabelSelector
54
- # @param verify_result [Boolean] Wait for completion and verify success
55
- #
56
- # @return [nil]
57
- def run!(deployments_names = nil, selector: nil, verify_result: true)
58
- start = Time.now.utc
59
- @logger.reset
60
-
61
- @logger.phase_heading("Initializing restart")
62
- verify_config!
63
- deployments = identify_target_deployments(deployments_names, selector: selector)
64
-
65
- @logger.phase_heading("Triggering restart by touching ENV[RESTARTED_AT]")
66
- patch_kubeclient_deployments(deployments)
67
-
68
- if verify_result
69
- @logger.phase_heading("Waiting for rollout")
70
- resources = build_watchables(deployments, start)
71
- verify_restart(resources)
72
- else
73
- warning = "Result verification is disabled for this task"
74
- @logger.summary.add_paragraph(ColorizedString.new(warning).yellow)
75
- end
76
- StatsD.distribution('restart.duration', StatsD.duration(start), tags: tags('success', deployments))
77
- @logger.print_summary(:success)
78
- rescue DeploymentTimeoutError
79
- StatsD.distribution('restart.duration', StatsD.duration(start), tags: tags('timeout', deployments))
80
- @logger.print_summary(:timed_out)
81
- raise
82
- rescue FatalDeploymentError => error
83
- StatsD.distribution('restart.duration', StatsD.duration(start), tags: tags('failure', deployments))
84
- @logger.summary.add_action(error.message) if error.message != error.class.to_s
85
- @logger.print_summary(:failure)
86
- raise
87
- end
88
- alias_method :perform!, :run!
89
-
90
- private
91
-
92
- def tags(status, deployments)
93
- %W(namespace:#{@namespace} context:#{@context} status:#{status} deployments:#{deployments.to_a.length}})
94
- end
95
-
96
- def identify_target_deployments(deployment_names, selector: nil)
97
- if deployment_names.nil?
98
- deployments = if selector.nil?
99
- @logger.info("Configured to restart all deployments with the `#{ANNOTATION}` annotation")
100
- v1beta1_kubeclient.get_deployments(namespace: @namespace)
101
- else
102
- selector_string = selector.to_s
103
- @logger.info(
104
- "Configured to restart all deployments with the `#{ANNOTATION}` annotation and #{selector_string} selector"
105
- )
106
- v1beta1_kubeclient.get_deployments(namespace: @namespace, label_selector: selector_string)
107
- end
108
- deployments.select! { |d| d.metadata.annotations[ANNOTATION] }
109
-
110
- if deployments.none?
111
- raise FatalRestartError, "no deployments with the `#{ANNOTATION}` annotation found in namespace #{@namespace}"
112
- end
113
- elsif deployment_names.empty?
114
- raise FatalRestartError, "Configured to restart deployments by name, but list of names was blank"
115
- elsif !selector.nil?
116
- raise FatalRestartError, "Can't specify deployment names and selector at the same time"
117
- else
118
- deployment_names = deployment_names.uniq
119
- list = deployment_names.join(', ')
120
- @logger.info("Configured to restart deployments by name: #{list}")
121
-
122
- deployments = fetch_deployments(deployment_names)
123
- if deployments.none?
124
- raise FatalRestartError, "no deployments with names #{list} found in namespace #{@namespace}"
125
- end
126
- end
127
- deployments
128
- end
129
-
130
- def build_watchables(kubeclient_resources, started)
131
- kubeclient_resources.map do |d|
132
- definition = d.to_h.deep_stringify_keys
133
- r = Deployment.new(namespace: @namespace, context: @context, definition: definition, logger: @logger)
134
- r.deploy_started_at = started # we don't care what happened to the resource before the restart cmd ran
135
- r
136
- end
137
- end
138
-
139
- def patch_deployment_with_restart(record)
140
- v1beta1_kubeclient.patch_deployment(
141
- record.metadata.name,
142
- build_patch_payload(record),
143
- @namespace
144
- )
145
- end
146
-
147
- def patch_kubeclient_deployments(deployments)
148
- deployments.each do |record|
149
- begin
150
- patch_deployment_with_restart(record)
151
- @logger.info("Triggered `#{record.metadata.name}` restart")
152
- rescue Kubeclient::HttpError => e
153
- raise RestartAPIError.new(record.metadata.name, e.message)
154
- end
155
- end
156
- end
157
-
158
- def fetch_deployments(list)
159
- list.map do |name|
160
- record = nil
161
- begin
162
- record = v1beta1_kubeclient.get_deployment(name, @namespace)
163
- rescue Kubeclient::ResourceNotFoundError
164
- raise FatalRestartError, "Deployment `#{name}` not found in namespace `#{@namespace}`"
165
- end
166
- record
167
- end
168
- end
169
-
170
- def build_patch_payload(deployment)
171
- containers = deployment.spec.template.spec.containers
172
- {
173
- spec: {
174
- template: {
175
- spec: {
176
- containers: containers.map do |container|
177
- {
178
- name: container.name,
179
- env: [{ name: "RESTARTED_AT", value: Time.now.to_i.to_s }],
180
- }
181
- end,
182
- },
183
- },
184
- },
185
- }
186
- end
187
-
188
- def verify_restart(resources)
189
- ResourceWatcher.new(resources: resources, operation_name: "restart",
190
- timeout: @max_watch_seconds, task_config: @task_config).run
191
- failed_resources = resources.reject(&:deploy_succeeded?)
192
- success = failed_resources.empty?
193
- if !success && failed_resources.all?(&:deploy_timed_out?)
194
- raise DeploymentTimeoutError
195
- end
196
- raise FatalDeploymentError unless success
197
- end
198
-
199
- def verify_config!
200
- task_config_validator = TaskConfigValidator.new(@task_config, kubectl, kubeclient_builder)
201
- unless task_config_validator.valid?
202
- @logger.summary.add_action("Configuration invalid")
203
- @logger.summary.add_paragraph(task_config_validator.errors.map { |err| "- #{err}" }.join("\n"))
204
- raise KubernetesDeploy::TaskConfigurationError
205
- end
206
- end
207
-
208
- def kubeclient
209
- @kubeclient ||= kubeclient_builder.build_v1_kubeclient(@context)
210
- end
211
-
212
- def kubectl
213
- @kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
214
- end
215
-
216
- def v1beta1_kubeclient
217
- @v1beta1_kubeclient ||= kubeclient_builder.build_v1beta1_kubeclient(@context)
218
- end
219
-
220
- def kubeclient_builder
221
- @kubeclient_builder ||= KubeclientBuilder.new
222
- end
223
14
  end
224
15
  end
@@ -1,212 +1,15 @@
1
1
  # frozen_string_literal: true
2
- require 'tempfile'
3
-
4
- require 'kubernetes-deploy/common'
5
- require 'kubernetes-deploy/kubeclient_builder'
6
- require 'kubernetes-deploy/kubectl'
7
- require 'kubernetes-deploy/resource_cache'
8
- require 'kubernetes-deploy/resource_watcher'
9
- require 'kubernetes-deploy/kubernetes_resource'
10
- require 'kubernetes-deploy/kubernetes_resource/pod'
11
- require 'kubernetes-deploy/runner_task_config_validator'
2
+ require 'krane/runner_task'
3
+ require 'kubernetes-deploy/rescue_krane_exceptions'
12
4
 
13
5
  module KubernetesDeploy
14
- # Run a pod that exits upon completing a task
15
- class RunnerTask
16
- class TaskTemplateMissingError < TaskConfigurationError; end
17
-
18
- attr_reader :pod_name
19
-
20
- # Initializes the runner task
21
- #
22
- # @param namespace [String] Kubernetes namespace
23
- # @param context [String] Kubernetes context / cluster
24
- # @param logger [Object] Logger object (defaults to an instance of KubernetesDeploy::FormattedLogger)
25
- # @param max_watch_seconds [Integer] Timeout in seconds
26
- def initialize(namespace:, context:, logger: nil, max_watch_seconds: nil)
27
- @logger = logger || KubernetesDeploy::FormattedLogger.build(namespace, context)
28
- @task_config = KubernetesDeploy::TaskConfig.new(context, namespace, @logger)
29
- @namespace = namespace
30
- @context = context
31
- @max_watch_seconds = max_watch_seconds
32
- end
6
+ class RunnerTask < ::Krane::RunnerTask
7
+ include RescueKraneExceptions
33
8
 
34
- # Runs the task, returning a boolean representing success or failure
35
- #
36
- # @return [Boolean]
37
9
  def run(*args)
38
- run!(*args)
39
- true
40
- rescue DeploymentTimeoutError, FatalDeploymentError
10
+ super(*args)
11
+ rescue KubernetesDeploy::DeploymentTimeoutError, KubernetesDeploy::FatalDeploymentError
41
12
  false
42
13
  end
43
-
44
- # Runs the task, raising exceptions in case of issues
45
- #
46
- # @param task_template [String] The template file you'll be rendering
47
- # @param entrypoint [Array<String>] Override the default command in the container image
48
- # @param args [Array<String>] Override the default arguments for the command
49
- # @param env_vars [Array<String>] List of env vars
50
- # @param verify_result [Boolean] Wait for completion and verify pod success
51
- #
52
- # @return [nil]
53
- def run!(task_template:, entrypoint:, args:, env_vars: [], verify_result: true)
54
- start = Time.now.utc
55
- @logger.reset
56
-
57
- @logger.phase_heading("Initializing task")
58
-
59
- @logger.info("Validating configuration")
60
- verify_config!(task_template, args)
61
- @logger.info("Using namespace '#{@namespace}' in context '#{@context}'")
62
-
63
- pod = build_pod(task_template, entrypoint, args, env_vars, verify_result)
64
- validate_pod(pod)
65
-
66
- @logger.phase_heading("Running pod")
67
- create_pod(pod)
68
-
69
- if verify_result
70
- @logger.phase_heading("Streaming logs")
71
- watch_pod(pod)
72
- else
73
- record_status_once(pod)
74
- end
75
- StatsD.distribution('task_runner.duration', StatsD.duration(start), tags: statsd_tags('success'))
76
- @logger.print_summary(:success)
77
- rescue DeploymentTimeoutError
78
- StatsD.distribution('task_runner.duration', StatsD.duration(start), tags: statsd_tags('timeout'))
79
- @logger.print_summary(:timed_out)
80
- raise
81
- rescue FatalDeploymentError
82
- StatsD.distribution('task_runner.duration', StatsD.duration(start), tags: statsd_tags('failure'))
83
- @logger.print_summary(:failure)
84
- raise
85
- end
86
-
87
- private
88
-
89
- def create_pod(pod)
90
- @logger.info("Creating pod '#{pod.name}'")
91
- pod.deploy_started_at = Time.now.utc
92
- kubeclient.create_pod(pod.to_kubeclient_resource)
93
- @pod_name = pod.name
94
- @logger.info("Pod creation succeeded")
95
- rescue Kubeclient::HttpError => e
96
- msg = "Failed to create pod: #{e.class.name}: #{e.message}"
97
- @logger.summary.add_paragraph(msg)
98
- raise FatalDeploymentError, msg
99
- end
100
-
101
- def build_pod(template_name, entrypoint, args, env_vars, verify_result)
102
- task_template = get_template(template_name)
103
- @logger.info("Using template '#{template_name}'")
104
- pod_template = build_pod_definition(task_template)
105
- set_container_overrides!(pod_template, entrypoint, args, env_vars)
106
- ensure_valid_restart_policy!(pod_template, verify_result)
107
- Pod.new(namespace: @namespace, context: @context, logger: @logger, stream_logs: true,
108
- definition: pod_template.to_hash.deep_stringify_keys, statsd_tags: [])
109
- end
110
-
111
- def validate_pod(pod)
112
- pod.validate_definition(kubectl)
113
- end
114
-
115
- def watch_pod(pod)
116
- rw = ResourceWatcher.new(resources: [pod], timeout: @max_watch_seconds,
117
- operation_name: "run", task_config: @task_config)
118
- rw.run(delay_sync: 1, reminder_interval: 30.seconds)
119
- raise DeploymentTimeoutError if pod.deploy_timed_out?
120
- raise FatalDeploymentError if pod.deploy_failed?
121
- end
122
-
123
- def record_status_once(pod)
124
- cache = ResourceCache.new(@task_config)
125
- pod.sync(cache)
126
- warning = <<~STRING
127
- #{ColorizedString.new('Result verification is disabled for this task.').yellow}
128
- The following status was observed immediately after pod creation:
129
- #{pod.pretty_status}
130
- STRING
131
- @logger.summary.add_paragraph(warning)
132
- end
133
-
134
- def verify_config!(task_template, args)
135
- task_config_validator = RunnerTaskConfigValidator.new(task_template, args, @task_config, kubectl,
136
- kubeclient_builder)
137
- unless task_config_validator.valid?
138
- @logger.summary.add_action("Configuration invalid")
139
- @logger.summary.add_paragraph([task_config_validator.errors].map { |err| "- #{err}" }.join("\n"))
140
- raise KubernetesDeploy::TaskConfigurationError
141
- end
142
- end
143
-
144
- def get_template(template_name)
145
- pod_template = kubeclient.get_pod_template(template_name, @namespace)
146
- pod_template.template
147
- rescue Kubeclient::ResourceNotFoundError
148
- msg = "Pod template `#{template_name}` not found in namespace `#{@namespace}`, context `#{@context}`"
149
- @logger.summary.add_paragraph(msg)
150
- raise TaskTemplateMissingError, msg
151
- rescue Kubeclient::HttpError => error
152
- raise FatalKubeAPIError, "Error retrieving pod template: #{error.class.name}: #{error.message}"
153
- end
154
-
155
- def build_pod_definition(base_template)
156
- pod_definition = base_template.dup
157
- pod_definition.kind = 'Pod'
158
- pod_definition.apiVersion = 'v1'
159
- pod_definition.metadata.namespace = @namespace
160
-
161
- unique_name = pod_definition.metadata.name + "-" + SecureRandom.hex(8)
162
- @logger.warn("Name is too long, using '#{unique_name[0..62]}'") if unique_name.length > 63
163
- pod_definition.metadata.name = unique_name[0..62]
164
-
165
- pod_definition
166
- end
167
-
168
- def set_container_overrides!(pod_definition, entrypoint, args, env_vars)
169
- container = pod_definition.spec.containers.find { |cont| cont.name == 'task-runner' }
170
- if container.nil?
171
- message = "Pod spec does not contain a template container called 'task-runner'"
172
- @logger.summary.add_paragraph(message)
173
- raise TaskConfigurationError, message
174
- end
175
-
176
- container.command = entrypoint if entrypoint
177
- container.args = args if args
178
-
179
- env_args = env_vars.map do |env|
180
- key, value = env.split('=', 2)
181
- { name: key, value: value }
182
- end
183
- container.env ||= []
184
- container.env = container.env.map(&:to_h) + env_args
185
- end
186
-
187
- def ensure_valid_restart_policy!(template, verify)
188
- restart_policy = template.spec.restartPolicy
189
- if verify && restart_policy != "Never"
190
- @logger.warn("Changed Pod RestartPolicy from '#{restart_policy}' to 'Never'. Disable "\
191
- "result verification to use '#{restart_policy}'.")
192
- template.spec.restartPolicy = "Never"
193
- end
194
- end
195
-
196
- def kubectl
197
- @kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
198
- end
199
-
200
- def kubeclient
201
- @kubeclient ||= kubeclient_builder.build_v1_kubeclient(@context)
202
- end
203
-
204
- def kubeclient_builder
205
- @kubeclient_builder ||= KubeclientBuilder.new
206
- end
207
-
208
- def statsd_tags(status)
209
- %W(namespace:#{@namespace} context:#{@context} status:#{status})
210
- end
211
14
  end
212
15
  end