tobsch-krane 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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