tobsch-krane 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/.buildkite/pipeline.nightly.yml +43 -0
  3. data/.github/probots.yml +2 -0
  4. data/.gitignore +20 -0
  5. data/.rubocop.yml +17 -0
  6. data/.shopify-build/VERSION +1 -0
  7. data/.shopify-build/kubernetes-deploy.yml +53 -0
  8. data/1.0-Upgrade.md +185 -0
  9. data/CHANGELOG.md +431 -0
  10. data/CODE_OF_CONDUCT.md +46 -0
  11. data/CONTRIBUTING.md +164 -0
  12. data/Gemfile +16 -0
  13. data/ISSUE_TEMPLATE.md +25 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +655 -0
  16. data/Rakefile +36 -0
  17. data/bin/ci +21 -0
  18. data/bin/setup +16 -0
  19. data/bin/test +47 -0
  20. data/dev.yml +28 -0
  21. data/dev/flamegraph-from-tests +35 -0
  22. data/exe/krane +5 -0
  23. data/krane.gemspec +44 -0
  24. data/lib/krane.rb +7 -0
  25. data/lib/krane/bindings_parser.rb +88 -0
  26. data/lib/krane/cli/deploy_command.rb +75 -0
  27. data/lib/krane/cli/global_deploy_command.rb +54 -0
  28. data/lib/krane/cli/krane.rb +91 -0
  29. data/lib/krane/cli/render_command.rb +41 -0
  30. data/lib/krane/cli/restart_command.rb +34 -0
  31. data/lib/krane/cli/run_command.rb +54 -0
  32. data/lib/krane/cli/version_command.rb +13 -0
  33. data/lib/krane/cluster_resource_discovery.rb +113 -0
  34. data/lib/krane/common.rb +23 -0
  35. data/lib/krane/concerns/template_reporting.rb +29 -0
  36. data/lib/krane/concurrency.rb +18 -0
  37. data/lib/krane/container_logs.rb +106 -0
  38. data/lib/krane/deferred_summary_logging.rb +95 -0
  39. data/lib/krane/delayed_exceptions.rb +14 -0
  40. data/lib/krane/deploy_task.rb +363 -0
  41. data/lib/krane/deploy_task_config_validator.rb +29 -0
  42. data/lib/krane/duration_parser.rb +27 -0
  43. data/lib/krane/ejson_secret_provisioner.rb +154 -0
  44. data/lib/krane/errors.rb +28 -0
  45. data/lib/krane/formatted_logger.rb +57 -0
  46. data/lib/krane/global_deploy_task.rb +210 -0
  47. data/lib/krane/global_deploy_task_config_validator.rb +12 -0
  48. data/lib/krane/kubeclient_builder.rb +156 -0
  49. data/lib/krane/kubectl.rb +120 -0
  50. data/lib/krane/kubernetes_resource.rb +621 -0
  51. data/lib/krane/kubernetes_resource/cloudsql.rb +43 -0
  52. data/lib/krane/kubernetes_resource/config_map.rb +22 -0
  53. data/lib/krane/kubernetes_resource/cron_job.rb +18 -0
  54. data/lib/krane/kubernetes_resource/custom_resource.rb +87 -0
  55. data/lib/krane/kubernetes_resource/custom_resource_definition.rb +98 -0
  56. data/lib/krane/kubernetes_resource/daemon_set.rb +90 -0
  57. data/lib/krane/kubernetes_resource/deployment.rb +213 -0
  58. data/lib/krane/kubernetes_resource/horizontal_pod_autoscaler.rb +65 -0
  59. data/lib/krane/kubernetes_resource/ingress.rb +18 -0
  60. data/lib/krane/kubernetes_resource/job.rb +60 -0
  61. data/lib/krane/kubernetes_resource/network_policy.rb +22 -0
  62. data/lib/krane/kubernetes_resource/persistent_volume_claim.rb +80 -0
  63. data/lib/krane/kubernetes_resource/pod.rb +269 -0
  64. data/lib/krane/kubernetes_resource/pod_disruption_budget.rb +23 -0
  65. data/lib/krane/kubernetes_resource/pod_set_base.rb +71 -0
  66. data/lib/krane/kubernetes_resource/pod_template.rb +20 -0
  67. data/lib/krane/kubernetes_resource/replica_set.rb +92 -0
  68. data/lib/krane/kubernetes_resource/resource_quota.rb +22 -0
  69. data/lib/krane/kubernetes_resource/role.rb +22 -0
  70. data/lib/krane/kubernetes_resource/role_binding.rb +22 -0
  71. data/lib/krane/kubernetes_resource/secret.rb +24 -0
  72. data/lib/krane/kubernetes_resource/service.rb +104 -0
  73. data/lib/krane/kubernetes_resource/service_account.rb +22 -0
  74. data/lib/krane/kubernetes_resource/stateful_set.rb +70 -0
  75. data/lib/krane/label_selector.rb +42 -0
  76. data/lib/krane/oj.rb +4 -0
  77. data/lib/krane/options_helper.rb +39 -0
  78. data/lib/krane/remote_logs.rb +60 -0
  79. data/lib/krane/render_task.rb +118 -0
  80. data/lib/krane/renderer.rb +118 -0
  81. data/lib/krane/resource_cache.rb +68 -0
  82. data/lib/krane/resource_deployer.rb +265 -0
  83. data/lib/krane/resource_watcher.rb +171 -0
  84. data/lib/krane/restart_task.rb +228 -0
  85. data/lib/krane/rollout_conditions.rb +103 -0
  86. data/lib/krane/runner_task.rb +212 -0
  87. data/lib/krane/runner_task_config_validator.rb +18 -0
  88. data/lib/krane/statsd.rb +65 -0
  89. data/lib/krane/task_config.rb +22 -0
  90. data/lib/krane/task_config_validator.rb +96 -0
  91. data/lib/krane/template_sets.rb +173 -0
  92. data/lib/krane/version.rb +4 -0
  93. data/pull_request_template.md +8 -0
  94. data/screenshots/deploy-demo.gif +0 -0
  95. data/screenshots/migrate-logs.png +0 -0
  96. data/screenshots/missing-secret-fail.png +0 -0
  97. data/screenshots/success.png +0 -0
  98. data/screenshots/test-output.png +0 -0
  99. metadata +375 -0
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ require 'colorized_string'
3
+
4
+ module Krane
5
+ # Adds the methods krane requires to your logger class.
6
+ # These methods include helpers for logging consistent headings, as well as facilities for
7
+ # displaying key information later, in a summary section, rather than when it occurred.
8
+ module DeferredSummaryLogging
9
+ attr_reader :summary
10
+ def initialize(*args)
11
+ reset
12
+ super
13
+ end
14
+
15
+ def reset
16
+ @summary = DeferredSummary.new
17
+ @current_phase = 0
18
+ end
19
+
20
+ def blank_line(level = :info)
21
+ public_send(level, "")
22
+ end
23
+
24
+ def phase_heading(phase_name)
25
+ @current_phase += 1
26
+ heading("Phase #{@current_phase}: #{phase_name}")
27
+ end
28
+
29
+ def heading(text, secondary_msg = '', secondary_msg_color = :cyan)
30
+ padding = (100.0 - (text.length + secondary_msg.length)) / 2
31
+ blank_line
32
+ part1 = ColorizedString.new("#{'-' * padding.floor}#{text}").cyan
33
+ part2 = ColorizedString.new(secondary_msg).colorize(secondary_msg_color)
34
+ part3 = ColorizedString.new('-' * padding.ceil).cyan
35
+ info(part1 + part2 + part3)
36
+ end
37
+
38
+ # Outputs the deferred summary information saved via @logger.summary.add_action and @logger.summary.add_paragraph
39
+ def print_summary(status)
40
+ status_string = status.to_s.humanize.upcase
41
+ if status == :success
42
+ heading("Result: ", status_string, :green)
43
+ level = :info
44
+ elsif status == :timed_out
45
+ heading("Result: ", status_string, :yellow)
46
+ level = :fatal
47
+ else
48
+ heading("Result: ", status_string, :red)
49
+ level = :fatal
50
+ end
51
+
52
+ if (actions_sentence = summary.actions_sentence.presence)
53
+ public_send(level, actions_sentence)
54
+ blank_line(level)
55
+ end
56
+
57
+ summary.paragraphs.each do |para|
58
+ msg_lines = para.split("\n")
59
+ msg_lines.each { |line| public_send(level, line) }
60
+ blank_line(level) unless para == summary.paragraphs.last
61
+ end
62
+ end
63
+
64
+ class DeferredSummary
65
+ attr_reader :paragraphs
66
+
67
+ def initialize
68
+ @actions_taken = []
69
+ @paragraphs = []
70
+ end
71
+
72
+ def actions_sentence
73
+ return unless @actions_taken.present?
74
+ @actions_taken.to_sentence.capitalize
75
+ end
76
+
77
+ # Saves a sentence fragment to be displayed in the first sentence of the summary section
78
+ #
79
+ # Example:
80
+ # # The resulting summary will begin with "Created 3 secrets and failed to deploy 2 resources"
81
+ # @logger.summary.add_action("created 3 secrets")
82
+ # @logger.summary.add_action("failed to deploy 2 resources")
83
+ def add_action(sentence_fragment)
84
+ @actions_taken << sentence_fragment
85
+ end
86
+
87
+ # Adds a paragraph to be displayed in the summary section
88
+ # Paragraphs will be printed in the order they were added, separated by a blank line
89
+ # This can be used to log a block of data on a particular topic, e.g. debug info for a particular failed resource
90
+ def add_paragraph(paragraph)
91
+ paragraphs << paragraph
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DelayedExceptions
4
+ def with_delayed_exceptions(enumerable, *catch, &block)
5
+ exceptions = []
6
+ enumerable.each do |i|
7
+ begin
8
+ block.call(i)
9
+ rescue *catch => e
10
+ exceptions << e
11
+ end
12
+ end.tap { raise exceptions.first if exceptions.first }
13
+ end
14
+ end
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+ require 'yaml'
3
+ require 'shellwords'
4
+ require 'tempfile'
5
+ require 'fileutils'
6
+
7
+ require 'krane/common'
8
+ require 'krane/concurrency'
9
+ require 'krane/resource_cache'
10
+ require 'krane/kubernetes_resource'
11
+ %w(
12
+ custom_resource
13
+ cloudsql
14
+ config_map
15
+ deployment
16
+ ingress
17
+ persistent_volume_claim
18
+ pod
19
+ network_policy
20
+ service
21
+ pod_template
22
+ pod_disruption_budget
23
+ replica_set
24
+ service_account
25
+ daemon_set
26
+ resource_quota
27
+ stateful_set
28
+ cron_job
29
+ job
30
+ custom_resource_definition
31
+ horizontal_pod_autoscaler
32
+ secret
33
+ ).each do |subresource|
34
+ require "krane/kubernetes_resource/#{subresource}"
35
+ end
36
+ require 'krane/resource_watcher'
37
+ require 'krane/kubectl'
38
+ require 'krane/kubeclient_builder'
39
+ require 'krane/ejson_secret_provisioner'
40
+ require 'krane/renderer'
41
+ require 'krane/cluster_resource_discovery'
42
+ require 'krane/template_sets'
43
+ require 'krane/deploy_task_config_validator'
44
+ require 'krane/resource_deployer'
45
+ require 'krane/concerns/template_reporting'
46
+
47
+ module Krane
48
+ # Ship resources to a namespace
49
+ class DeployTask
50
+ extend Krane::StatsD::MeasureMethods
51
+ include Krane::TemplateReporting
52
+
53
+ PROTECTED_NAMESPACES = %w(
54
+ default
55
+ kube-system
56
+ kube-public
57
+ )
58
+
59
+ def predeploy_sequence
60
+ before_crs = %w(
61
+ ResourceQuota
62
+ NetworkPolicy
63
+ )
64
+ after_crs = %w(
65
+ ConfigMap
66
+ PersistentVolumeClaim
67
+ ServiceAccount
68
+ Role
69
+ RoleBinding
70
+ Secret
71
+ Pod
72
+ )
73
+
74
+ before_crs + cluster_resource_discoverer.crds.select(&:predeployed?).map(&:kind) + after_crs
75
+ end
76
+
77
+ def prune_whitelist
78
+ cluster_resource_discoverer.prunable_resources(namespaced: true)
79
+ end
80
+
81
+ def server_version
82
+ kubectl.server_version
83
+ end
84
+
85
+ # Initializes the deploy task
86
+ #
87
+ # @param namespace [String] Kubernetes namespace (*required*)
88
+ # @param context [String] Kubernetes context (*required*)
89
+ # @param current_sha [String] The SHA of the commit
90
+ # @param logger [Object] Logger object (defaults to an instance of Krane::FormattedLogger)
91
+ # @param kubectl_instance [Kubectl] Kubectl instance
92
+ # @param bindings [Hash] Bindings parsed by Krane::BindingsParser
93
+ # @param global_timeout [Integer] Timeout in seconds
94
+ # @param selector [Hash] Selector(s) parsed by Krane::LabelSelector
95
+ # @param filenames [Array<String>] An array of filenames and/or directories containing templates (*required*)
96
+ # @param protected_namespaces [Array<String>] Array of protected Kubernetes namespaces (defaults
97
+ # to Krane::DeployTask::PROTECTED_NAMESPACES)
98
+ # @param render_erb [Boolean] Enable ERB rendering
99
+ def initialize(namespace:, context:, current_sha: nil, logger: nil, kubectl_instance: nil, bindings: {},
100
+ global_timeout: nil, selector: nil, filenames: [], protected_namespaces: nil,
101
+ render_erb: false)
102
+ @logger = logger || Krane::FormattedLogger.build(namespace, context)
103
+ @template_sets = TemplateSets.from_dirs_and_files(paths: filenames, logger: @logger, render_erb: render_erb)
104
+ @task_config = Krane::TaskConfig.new(context, namespace, @logger)
105
+ @bindings = bindings
106
+ @namespace = namespace
107
+ @namespace_tags = []
108
+ @context = context
109
+ @current_sha = current_sha
110
+ @kubectl = kubectl_instance
111
+ @global_timeout = global_timeout
112
+ @selector = selector
113
+ @protected_namespaces = protected_namespaces || PROTECTED_NAMESPACES
114
+ @render_erb = render_erb
115
+ end
116
+
117
+ # Runs the task, returning a boolean representing success or failure
118
+ #
119
+ # @return [Boolean]
120
+ def run(*args)
121
+ run!(*args)
122
+ true
123
+ rescue FatalDeploymentError
124
+ false
125
+ end
126
+
127
+ # Runs the task, raising exceptions in case of issues
128
+ #
129
+ # @param verify_result [Boolean] Wait for completion and verify success
130
+ # @param prune [Boolean] Enable deletion of resources that do not appear in the template dir
131
+ #
132
+ # @return [nil]
133
+ def run!(verify_result: true, prune: true)
134
+ start = Time.now.utc
135
+ @logger.reset
136
+
137
+ @logger.phase_heading("Initializing deploy")
138
+ validate_configuration(prune: prune)
139
+ resources = discover_resources
140
+ validate_resources(resources)
141
+
142
+ @logger.phase_heading("Checking initial resource statuses")
143
+ check_initial_status(resources)
144
+
145
+ if deploy_has_priority_resources?(resources)
146
+ @logger.phase_heading("Predeploying priority resources")
147
+ resource_deployer.predeploy_priority_resources(resources, predeploy_sequence)
148
+ end
149
+
150
+ @logger.phase_heading("Deploying all resources")
151
+ if @protected_namespaces.include?(@namespace) && prune
152
+ raise FatalDeploymentError, "Refusing to deploy to protected namespace '#{@namespace}' with pruning enabled"
153
+ end
154
+
155
+ resource_deployer.deploy!(resources, verify_result, prune)
156
+
157
+ StatsD.client.event("Deployment of #{@namespace} succeeded",
158
+ "Successfully deployed all #{@namespace} resources to #{@context}",
159
+ alert_type: "success", tags: statsd_tags + %w(status:success))
160
+ StatsD.client.distribution('all_resources.duration', StatsD.duration(start),
161
+ tags: statsd_tags + %w(status:success))
162
+ @logger.print_summary(:success)
163
+ rescue DeploymentTimeoutError
164
+ @logger.print_summary(:timed_out)
165
+ StatsD.client.event("Deployment of #{@namespace} timed out",
166
+ "One or more #{@namespace} resources failed to deploy to #{@context} in time",
167
+ alert_type: "error", tags: statsd_tags + %w(status:timeout))
168
+ StatsD.client.distribution('all_resources.duration', StatsD.duration(start),
169
+ tags: statsd_tags + %w(status:timeout))
170
+ raise
171
+ rescue FatalDeploymentError => error
172
+ @logger.summary.add_action(error.message) if error.message != error.class.to_s
173
+ @logger.print_summary(:failure)
174
+ StatsD.client.event("Deployment of #{@namespace} failed",
175
+ "One or more #{@namespace} resources failed to deploy to #{@context}",
176
+ alert_type: "error", tags: statsd_tags + %w(status:failed))
177
+ StatsD.client.distribution('all_resources.duration', StatsD.duration(start),
178
+ tags: statsd_tags + %w(status:failed))
179
+ raise
180
+ end
181
+
182
+ private
183
+
184
+ def resource_deployer
185
+ @resource_deployer ||= Krane::ResourceDeployer.new(task_config: @task_config,
186
+ prune_whitelist: prune_whitelist, global_timeout: @global_timeout,
187
+ selector: @selector, statsd_tags: statsd_tags, current_sha: @current_sha)
188
+ end
189
+
190
+ def kubeclient_builder
191
+ @kubeclient_builder ||= KubeclientBuilder.new
192
+ end
193
+
194
+ def cluster_resource_discoverer
195
+ @cluster_resource_discoverer ||= ClusterResourceDiscovery.new(
196
+ task_config: @task_config,
197
+ namespace_tags: @namespace_tags
198
+ )
199
+ end
200
+
201
+ def ejson_provisioners
202
+ @ejson_provisoners ||= @template_sets.ejson_secrets_files.map do |ejson_secret_file|
203
+ EjsonSecretProvisioner.new(
204
+ task_config: @task_config,
205
+ ejson_keys_secret: ejson_keys_secret,
206
+ ejson_file: ejson_secret_file,
207
+ statsd_tags: @namespace_tags,
208
+ selector: @selector,
209
+ )
210
+ end
211
+ end
212
+
213
+ def deploy_has_priority_resources?(resources)
214
+ resources.any? { |r| predeploy_sequence.include?(r.type) }
215
+ end
216
+
217
+ def check_initial_status(resources)
218
+ cache = ResourceCache.new(@task_config)
219
+ Krane::Concurrency.split_across_threads(resources) { |r| r.sync(cache) }
220
+ resources.each { |r| @logger.info(r.pretty_status) }
221
+ end
222
+ measure_method(:check_initial_status, "initial_status.duration")
223
+
224
+ def secrets_from_ejson
225
+ ejson_provisioners.flat_map(&:resources)
226
+ end
227
+
228
+ def discover_resources
229
+ @logger.info("Discovering resources:")
230
+ resources = []
231
+ crds_by_kind = cluster_resource_discoverer.crds.group_by(&:kind)
232
+ @template_sets.with_resource_definitions(current_sha: @current_sha, bindings: @bindings) do |r_def|
233
+ crd = crds_by_kind[r_def["kind"]]&.first
234
+ r = KubernetesResource.build(namespace: @namespace, context: @context, logger: @logger, definition: r_def,
235
+ statsd_tags: @namespace_tags, crd: crd, global_names: @task_config.global_kinds)
236
+ resources << r
237
+ @logger.info(" - #{r.id}")
238
+ end
239
+
240
+ secrets_from_ejson.each do |secret|
241
+ resources << secret
242
+ @logger.info(" - #{secret.id} (from ejson)")
243
+ end
244
+
245
+ resources.sort
246
+ rescue InvalidTemplateError => e
247
+ record_invalid_template(logger: @logger, err: e.message, filename: e.filename,
248
+ content: e.content)
249
+ raise FatalDeploymentError, "Failed to render and parse template"
250
+ end
251
+ measure_method(:discover_resources)
252
+
253
+ def validate_configuration(prune:)
254
+ task_config_validator = DeployTaskConfigValidator.new(@protected_namespaces, prune,
255
+ @task_config, kubectl, kubeclient_builder)
256
+ errors = []
257
+ errors += task_config_validator.errors
258
+ errors += @template_sets.validate
259
+ unless errors.empty?
260
+ add_para_from_list(logger: @logger, action: "Configuration invalid", enum: errors)
261
+ raise Krane::TaskConfigurationError
262
+ end
263
+
264
+ confirm_ejson_keys_not_prunable if prune
265
+ @logger.info("Using resource selector #{@selector}") if @selector
266
+ @namespace_tags |= tags_from_namespace_labels
267
+ @logger.info("All required parameters and files are present")
268
+ end
269
+ measure_method(:validate_configuration)
270
+
271
+ def validate_resources(resources)
272
+ validate_globals(resources)
273
+ Krane::Concurrency.split_across_threads(resources) do |r|
274
+ r.validate_definition(kubectl, selector: @selector)
275
+ end
276
+
277
+ resources.select(&:has_warnings?).each do |resource|
278
+ record_warnings(logger: @logger, warning: resource.validation_warning_msg,
279
+ filename: File.basename(resource.file_path))
280
+ end
281
+
282
+ failed_resources = resources.select(&:validation_failed?)
283
+ if failed_resources.present?
284
+
285
+ failed_resources.each do |r|
286
+ content = File.read(r.file_path) if File.file?(r.file_path) && !r.sensitive_template_content?
287
+ record_invalid_template(logger: @logger, err: r.validation_error_msg,
288
+ filename: File.basename(r.file_path), content: content)
289
+ end
290
+ raise FatalDeploymentError, "Template validation failed"
291
+ end
292
+ end
293
+ measure_method(:validate_resources)
294
+
295
+ def validate_globals(resources)
296
+ return unless (global = resources.select(&:global?).presence)
297
+ global_names = global.map do |resource|
298
+ "#{resource.name} (#{resource.type}) in #{File.basename(resource.file_path)}"
299
+ end
300
+ global_names = FormattedLogger.indent_four(global_names.join("\n"))
301
+
302
+ @logger.summary.add_paragraph(ColorizedString.new("Global resources:\n#{global_names}").yellow)
303
+ raise FatalDeploymentError, "This command is namespaced and cannot be used to deploy global resources. "\
304
+ "Use GlobalDeployTask instead."
305
+ end
306
+
307
+ def namespace_definition
308
+ @namespace_definition ||= begin
309
+ definition, _err, st = kubectl.run("get", "namespace", @namespace, use_namespace: false,
310
+ log_failure: true, raise_if_not_found: true, attempts: 3, output: 'json')
311
+ st.success? ? JSON.parse(definition, symbolize_names: true) : nil
312
+ end
313
+ rescue Kubectl::ResourceNotFoundError
314
+ nil
315
+ end
316
+
317
+ # make sure to never prune the ejson-keys secret
318
+ def confirm_ejson_keys_not_prunable
319
+ return unless ejson_keys_secret.dig("metadata", "annotations", KubernetesResource::LAST_APPLIED_ANNOTATION)
320
+
321
+ @logger.error("Deploy cannot proceed because protected resource " \
322
+ "Secret/#{EjsonSecretProvisioner::EJSON_KEYS_SECRET} would be pruned.")
323
+ raise EjsonPrunableError
324
+ rescue Kubectl::ResourceNotFoundError => e
325
+ @logger.debug("Secret/#{EjsonSecretProvisioner::EJSON_KEYS_SECRET} does not exist: #{e}")
326
+ end
327
+
328
+ def tags_from_namespace_labels
329
+ return [] if namespace_definition.blank?
330
+ namespace_labels = namespace_definition.fetch(:metadata, {}).fetch(:labels, {})
331
+ namespace_labels.map { |key, value| "#{key}:#{value}" }
332
+ end
333
+
334
+ def kubectl
335
+ @kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
336
+ end
337
+
338
+ def ejson_keys_secret
339
+ @ejson_keys_secret ||= begin
340
+ out, err, st = kubectl.run("get", "secret", EjsonSecretProvisioner::EJSON_KEYS_SECRET, output: "json",
341
+ raise_if_not_found: true, attempts: 3, output_is_sensitive: true, log_failure: true)
342
+ unless st.success?
343
+ raise EjsonSecretError, "Error retrieving Secret/#{EjsonSecretProvisioner::EJSON_KEYS_SECRET}: #{err}"
344
+ end
345
+ JSON.parse(out)
346
+ end
347
+ end
348
+
349
+ def statsd_tags
350
+ tags = %W(namespace:#{@namespace} context:#{@context}) | @namespace_tags
351
+ @current_sha.nil? ? tags : %W(sha:#{@current_sha}) | tags
352
+ end
353
+
354
+ def with_retries(limit)
355
+ retried = 0
356
+ while retried <= limit
357
+ success = yield
358
+ break if success
359
+ retried += 1
360
+ end
361
+ end
362
+ end
363
+ end