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