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,29 @@
1
+ # frozen_string_literal: true
2
+ module Krane
3
+ class DeployTaskConfigValidator < TaskConfigValidator
4
+ def initialize(protected_namespaces, prune, *arguments)
5
+ super(*arguments)
6
+ @protected_namespaces = protected_namespaces
7
+ @allow_protected_ns = !protected_namespaces.empty?
8
+ @prune = prune
9
+ @validations += %i(validate_protected_namespaces)
10
+ end
11
+
12
+ private
13
+
14
+ def validate_protected_namespaces
15
+ if @protected_namespaces.include?(namespace)
16
+ if @allow_protected_ns && @prune
17
+ @errors << "Refusing to deploy to protected namespace '#{namespace}' with pruning enabled"
18
+ elsif @allow_protected_ns
19
+ logger.warn("You're deploying to protected namespace #{namespace}, which cannot be pruned.")
20
+ logger.warn("Existing resources can only be removed manually with kubectl. " \
21
+ "Removing templates from the set deployed will have no effect.")
22
+ logger.warn("***Please do not deploy to #{namespace} unless you really know what you are doing.***")
23
+ else
24
+ @errors << "Refusing to deploy to protected namespace '#{namespace}'"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/duration'
4
+
5
+ module Krane
6
+ # This class is a less strict extension of ActiveSupport::Duration::ISO8601Parser.
7
+ # In addition to full ISO8601 durations, it can parse unprefixed ISO8601 time components (e.g. '1H').
8
+ # It is also case-insensitive.
9
+ # For example, this class considers the values "1H", "1h" and "PT1H" to be valid and equivalent.
10
+ class DurationParser
11
+ class ParsingError < ArgumentError; end
12
+
13
+ def initialize(value)
14
+ @iso8601_str = value.to_s.strip.upcase
15
+ end
16
+
17
+ def parse!
18
+ ActiveSupport::Duration.parse("PT#{@iso8601_str}") # By default assume it is just a time component
19
+ rescue ActiveSupport::Duration::ISO8601Parser::ParsingError
20
+ begin
21
+ ActiveSupport::Duration.parse(@iso8601_str)
22
+ rescue ActiveSupport::Duration::ISO8601Parser::ParsingError => e
23
+ raise ParsingError, e.message
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+ require 'base64'
4
+ require 'open3'
5
+ require 'krane/kubectl'
6
+
7
+ module Krane
8
+ class EjsonSecretError < FatalDeploymentError
9
+ def initialize(msg)
10
+ super("Generation of Kubernetes secrets from ejson failed: #{msg}")
11
+ end
12
+ end
13
+
14
+ class EjsonSecretProvisioner
15
+ EJSON_SECRET_ANNOTATION = "kubernetes-deploy.shopify.io/ejson-secret"
16
+ EJSON_SECRET_KEY = "kubernetes_secrets"
17
+ EJSON_SECRETS_FILE = "secrets.ejson"
18
+ EJSON_KEYS_SECRET = "ejson-keys"
19
+ delegate :namespace, :context, :logger, to: :@task_config
20
+
21
+ def initialize(task_config:, ejson_keys_secret:, ejson_file:, statsd_tags:, selector: nil)
22
+ @ejson_keys_secret = ejson_keys_secret
23
+ @ejson_file = ejson_file
24
+ @statsd_tags = statsd_tags
25
+ @selector = selector
26
+ @task_config = task_config
27
+ @kubectl = Kubectl.new(
28
+ task_config: @task_config,
29
+ log_failure_by_default: false,
30
+ output_is_sensitive_default: true # output may contain ejson secrets
31
+ )
32
+ end
33
+
34
+ def resources
35
+ @resources ||= build_secrets
36
+ end
37
+
38
+ private
39
+
40
+ def build_secrets
41
+ unless @ejson_keys_secret
42
+ raise EjsonSecretError, "Secret #{EJSON_KEYS_SECRET} not provided, cannot decrypt secrets"
43
+ end
44
+ return [] unless File.exist?(@ejson_file)
45
+ with_decrypted_ejson do |decrypted|
46
+ secrets = decrypted[EJSON_SECRET_KEY]
47
+ unless secrets.present?
48
+ logger.warn("#{EJSON_SECRETS_FILE} does not have key #{EJSON_SECRET_KEY}."\
49
+ "No secrets will be created.")
50
+ return []
51
+ end
52
+
53
+ secrets.map do |secret_name, secret_spec|
54
+ validate_secret_spec(secret_name, secret_spec)
55
+ resource = generate_secret_resource(secret_name, secret_spec["_type"], secret_spec["data"])
56
+ resource.validate_definition(@kubectl)
57
+ if resource.validation_failed?
58
+ raise EjsonSecretError, "Resulting resource Secret/#{secret_name} failed validation"
59
+ end
60
+ resource
61
+ end
62
+ end
63
+ end
64
+
65
+ def encrypted_ejson
66
+ @encrypted_ejson ||= load_ejson_from_file
67
+ end
68
+
69
+ def public_key
70
+ encrypted_ejson["_public_key"]
71
+ end
72
+
73
+ def private_key
74
+ @private_key ||= fetch_private_key_from_secret
75
+ end
76
+
77
+ def validate_secret_spec(secret_name, spec)
78
+ errors = []
79
+ errors << "secret type unspecified" if spec["_type"].blank?
80
+ errors << "no data provided" if spec["data"].blank?
81
+
82
+ unless errors.empty?
83
+ raise EjsonSecretError, "Ejson incomplete for secret #{secret_name}: #{errors.join(', ')}"
84
+ end
85
+ end
86
+
87
+ def generate_secret_resource(secret_name, secret_type, data)
88
+ unless data.is_a?(Hash) && data.values.all? { |v| v.is_a?(String) } # Secret data is map[string]string
89
+ raise EjsonSecretError, "Data for secret #{secret_name} was invalid. Only key-value pairs are permitted."
90
+ end
91
+ encoded_data = data.each_with_object({}) do |(key, value), encoded|
92
+ # Leading underscores in ejson keys are used to skip encryption of the associated value
93
+ # To support this ejson feature, we need to exclude these leading underscores from the secret's keys
94
+ secret_key = key.sub(/\A_/, '')
95
+ encoded[secret_key] = Base64.strict_encode64(value)
96
+ end
97
+
98
+ labels = { "name" => secret_name }
99
+ labels.reverse_merge!(@selector.to_h) if @selector
100
+
101
+ secret = {
102
+ 'kind' => 'Secret',
103
+ 'apiVersion' => 'v1',
104
+ 'type' => secret_type,
105
+ 'metadata' => {
106
+ "name" => secret_name,
107
+ "labels" => labels,
108
+ "namespace" => namespace,
109
+ "annotations" => { EJSON_SECRET_ANNOTATION => "true" },
110
+ },
111
+ "data" => encoded_data,
112
+ }
113
+
114
+ Krane::Secret.build(
115
+ namespace: namespace, context: context, logger: logger, definition: secret, statsd_tags: @statsd_tags,
116
+ )
117
+ end
118
+
119
+ def load_ejson_from_file
120
+ return {} unless File.exist?(@ejson_file)
121
+ JSON.parse(File.read(@ejson_file))
122
+ end
123
+
124
+ def with_decrypted_ejson
125
+ return unless File.exist?(@ejson_file)
126
+
127
+ Dir.mktmpdir("ejson_keydir") do |key_dir|
128
+ File.write(File.join(key_dir, public_key), private_key)
129
+ decrypted = decrypt_ejson(key_dir)
130
+ yield decrypted
131
+ end
132
+ end
133
+
134
+ def decrypt_ejson(key_dir)
135
+ # ejson seems to dump both errors and output to STDOUT
136
+ out_err, st = Open3.capture2e("EJSON_KEYDIR=#{key_dir} ejson decrypt #{@ejson_file}")
137
+ raise EjsonSecretError, out_err unless st.success?
138
+ JSON.parse(out_err)
139
+ rescue JSON::ParserError
140
+ raise EjsonSecretError, "Failed to parse decrypted ejson"
141
+ end
142
+
143
+ def fetch_private_key_from_secret
144
+ encoded_private_key = @ejson_keys_secret["data"][public_key]
145
+ unless encoded_private_key
146
+ raise EjsonSecretError, "Private key for #{public_key} not found in #{EJSON_KEYS_SECRET} secret"
147
+ end
148
+
149
+ Base64.decode64(encoded_private_key)
150
+ rescue Kubectl::ResourceNotFoundError
151
+ raise EjsonSecretError, "Secret/#{EJSON_KEYS_SECRET} is required to decrypt EJSON and could not be found"
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Krane
4
+ class FatalDeploymentError < StandardError; end
5
+ class FatalKubeAPIError < FatalDeploymentError; end
6
+ class KubectlError < StandardError; end
7
+ class TaskConfigurationError < FatalDeploymentError; end
8
+
9
+ class InvalidTemplateError < FatalDeploymentError
10
+ attr_reader :content
11
+ attr_accessor :filename
12
+ def initialize(err, filename: nil, content: nil)
13
+ @filename = filename
14
+ @content = content
15
+ super(err)
16
+ end
17
+ end
18
+
19
+ class DeploymentTimeoutError < FatalDeploymentError; end
20
+
21
+ class EjsonPrunableError < FatalDeploymentError
22
+ def initialize
23
+ super("Found #{KubernetesResource::LAST_APPLIED_ANNOTATION} annotation on " \
24
+ "#{EjsonSecretProvisioner::EJSON_KEYS_SECRET} secret. " \
25
+ "krane will not continue since it is extremely unlikely that this secret should be pruned.")
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ require 'logger'
3
+ require 'colorized_string'
4
+ require 'krane/deferred_summary_logging'
5
+
6
+ module Krane
7
+ class FormattedLogger < Logger
8
+ include DeferredSummaryLogging
9
+
10
+ def self.indent_four(str)
11
+ " " + str.to_s.gsub("\n", "\n ")
12
+ end
13
+
14
+ def self.build(namespace = nil, context = nil, stream = $stderr, verbose_prefix: false)
15
+ l = new(stream)
16
+ l.level = level_from_env
17
+
18
+ middle = if verbose_prefix
19
+ if namespace.blank?
20
+ raise ArgumentError, 'Must pass a namespace if logging verbosely'
21
+ end
22
+ if context.blank?
23
+ raise ArgumentError, 'Must pass a context if logging verbosely'
24
+ end
25
+
26
+ "[#{context}][#{namespace}]"
27
+ end
28
+
29
+ l.formatter = proc do |severity, datetime, _progname, msg|
30
+ colorized_line = ColorizedString.new("[#{severity}][#{datetime}]#{middle}\t#{msg}\n")
31
+
32
+ case severity
33
+ when "FATAL"
34
+ ColorizedString.new("[#{severity}][#{datetime}]#{middle}\t").red + "#{msg}\n"
35
+ when "ERROR"
36
+ colorized_line.red
37
+ when "WARN"
38
+ colorized_line.yellow
39
+ else
40
+ colorized_line
41
+ end
42
+ end
43
+ l
44
+ end
45
+
46
+ def self.level_from_env
47
+ return ::Logger::DEBUG if ENV["DEBUG"]
48
+
49
+ if ENV["LEVEL"]
50
+ ::Logger.const_get(ENV["LEVEL"].upcase)
51
+ else
52
+ ::Logger::INFO
53
+ end
54
+ end
55
+ private_class_method :level_from_env
56
+ end
57
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+ require 'tempfile'
3
+
4
+ require 'krane/common'
5
+ require 'krane/concurrency'
6
+ require 'krane/resource_cache'
7
+ require 'krane/kubectl'
8
+ require 'krane/kubeclient_builder'
9
+ require 'krane/cluster_resource_discovery'
10
+ require 'krane/template_sets'
11
+ require 'krane/resource_deployer'
12
+ require 'krane/kubernetes_resource'
13
+ require 'krane/global_deploy_task_config_validator'
14
+ require 'krane/concerns/template_reporting'
15
+
16
+ %w(
17
+ custom_resource
18
+ custom_resource_definition
19
+ ).each do |subresource|
20
+ require "krane/kubernetes_resource/#{subresource}"
21
+ end
22
+
23
+ module Krane
24
+ # Ship global resources to a context
25
+ class GlobalDeployTask
26
+ extend Krane::StatsD::MeasureMethods
27
+ include TemplateReporting
28
+ delegate :context, :logger, :global_kinds, to: :@task_config
29
+
30
+ # Initializes the deploy task
31
+ #
32
+ # @param context [String] Kubernetes context (*required*)
33
+ # @param global_timeout [Integer] Timeout in seconds
34
+ # @param selector [Hash] Selector(s) parsed by Krane::LabelSelector (*required*)
35
+ # @param filenames [Array<String>] An array of filenames and/or directories containing templates (*required*)
36
+ def initialize(context:, global_timeout: nil, selector: nil, filenames: [], logger: nil)
37
+ template_paths = filenames.map { |path| File.expand_path(path) }
38
+
39
+ @task_config = TaskConfig.new(context, nil, logger)
40
+ @template_sets = TemplateSets.from_dirs_and_files(paths: template_paths,
41
+ logger: @task_config.logger, render_erb: false)
42
+ @global_timeout = global_timeout
43
+ @selector = selector
44
+ end
45
+
46
+ # Runs the task, returning a boolean representing success or failure
47
+ #
48
+ # @return [Boolean]
49
+ def run(*args)
50
+ run!(*args)
51
+ true
52
+ rescue FatalDeploymentError
53
+ false
54
+ end
55
+
56
+ # Runs the task, raising exceptions in case of issues
57
+ #
58
+ # @param verify_result [Boolean] Wait for completion and verify success
59
+ # @param prune [Boolean] Enable deletion of resources that match the provided
60
+ # selector and do not appear in the template dir
61
+ #
62
+ # @return [nil]
63
+ def run!(verify_result: true, prune: true)
64
+ start = Time.now.utc
65
+ logger.reset
66
+
67
+ logger.phase_heading("Initializing deploy")
68
+ validate_configuration
69
+ resources = discover_resources
70
+ validate_resources(resources)
71
+
72
+ logger.phase_heading("Checking initial resource statuses")
73
+ check_initial_status(resources)
74
+
75
+ logger.phase_heading("Deploying all resources")
76
+ deploy!(resources, verify_result, prune)
77
+
78
+ StatsD.client.event("Deployment succeeded",
79
+ "Successfully deployed all resources to #{context}",
80
+ alert_type: "success", tags: statsd_tags + %w(status:success))
81
+ StatsD.client.distribution('all_resources.duration', StatsD.duration(start),
82
+ tags: statsd_tags << "status:success")
83
+ logger.print_summary(:success)
84
+ rescue Krane::DeploymentTimeoutError
85
+ logger.print_summary(:timed_out)
86
+ StatsD.client.event("Deployment timed out",
87
+ "One or more resources failed to deploy to #{context} in time",
88
+ alert_type: "error", tags: statsd_tags + %w(status:timeout))
89
+ StatsD.client.distribution('all_resources.duration', StatsD.duration(start),
90
+ tags: statsd_tags << "status:timeout")
91
+ raise
92
+ rescue Krane::FatalDeploymentError => error
93
+ logger.summary.add_action(error.message) if error.message != error.class.to_s
94
+ logger.print_summary(:failure)
95
+ StatsD.client.event("Deployment failed",
96
+ "One or more resources failed to deploy to #{context}",
97
+ alert_type: "error", tags: statsd_tags + %w(status:failed))
98
+ StatsD.client.distribution('all_resources.duration', StatsD.duration(start),
99
+ tags: statsd_tags << "status:failed")
100
+ raise
101
+ end
102
+
103
+ private
104
+
105
+ def deploy!(resources, verify_result, prune)
106
+ resource_deployer = ResourceDeployer.new(task_config: @task_config,
107
+ prune_whitelist: prune_whitelist, global_timeout: @global_timeout,
108
+ selector: @selector, statsd_tags: statsd_tags)
109
+ resource_deployer.deploy!(resources, verify_result, prune)
110
+ end
111
+
112
+ def validate_configuration
113
+ task_config_validator = GlobalDeployTaskConfigValidator.new(@task_config,
114
+ kubectl, kubeclient_builder)
115
+ errors = []
116
+ errors += task_config_validator.errors
117
+ errors += @template_sets.validate
118
+ errors << "Selector is required" unless @selector.to_h.present?
119
+ unless errors.empty?
120
+ add_para_from_list(logger: logger, action: "Configuration invalid", enum: errors)
121
+ raise TaskConfigurationError
122
+ end
123
+
124
+ logger.info("Using resource selector #{@selector}")
125
+ logger.info("All required parameters and files are present")
126
+ end
127
+ measure_method(:validate_configuration)
128
+
129
+ def validate_resources(resources)
130
+ validate_globals(resources)
131
+
132
+ Concurrency.split_across_threads(resources) do |r|
133
+ r.validate_definition(@kubectl, selector: @selector)
134
+ end
135
+
136
+ resources.select(&:has_warnings?).each do |resource|
137
+ record_warnings(logger: logger, warning: resource.validation_warning_msg,
138
+ filename: File.basename(resource.file_path))
139
+ end
140
+
141
+ failed_resources = resources.select(&:validation_failed?)
142
+ if failed_resources.present?
143
+ failed_resources.each do |r|
144
+ content = File.read(r.file_path) if File.file?(r.file_path) && !r.sensitive_template_content?
145
+ record_invalid_template(logger: logger, err: r.validation_error_msg,
146
+ filename: File.basename(r.file_path), content: content)
147
+ end
148
+ raise FatalDeploymentError, "Template validation failed"
149
+ end
150
+ end
151
+ measure_method(:validate_resources)
152
+
153
+ def validate_globals(resources)
154
+ return unless (namespaced = resources.reject(&:global?).presence)
155
+ namespaced_names = namespaced.map do |resource|
156
+ "#{resource.name} (#{resource.type}) in #{File.basename(resource.file_path)}"
157
+ end
158
+ namespaced_names = FormattedLogger.indent_four(namespaced_names.join("\n"))
159
+
160
+ logger.summary.add_paragraph(ColorizedString.new("Namespaced resources:\n#{namespaced_names}").yellow)
161
+ raise FatalDeploymentError, "This command cannot deploy namespaced resources. Use DeployTask instead."
162
+ end
163
+
164
+ def discover_resources
165
+ logger.info("Discovering resources:")
166
+ resources = []
167
+ crds_by_kind = cluster_resource_discoverer.crds.map { |crd| [crd.name, crd] }.to_h
168
+ @template_sets.with_resource_definitions do |r_def|
169
+ crd = crds_by_kind[r_def["kind"]]&.first
170
+ r = KubernetesResource.build(context: context, logger: logger, definition: r_def,
171
+ crd: crd, global_names: global_kinds, statsd_tags: statsd_tags)
172
+ resources << r
173
+ logger.info(" - #{r.id}")
174
+ end
175
+
176
+ resources.sort
177
+ rescue InvalidTemplateError => e
178
+ record_invalid_template(logger: logger, err: e.message, filename: e.filename, content: e.content)
179
+ raise FatalDeploymentError, "Failed to parse template"
180
+ end
181
+ measure_method(:discover_resources)
182
+
183
+ def cluster_resource_discoverer
184
+ @cluster_resource_discoverer ||= ClusterResourceDiscovery.new(task_config: @task_config)
185
+ end
186
+
187
+ def statsd_tags
188
+ %W(context:#{context})
189
+ end
190
+
191
+ def kubectl
192
+ @kubectl ||= Kubectl.new(task_config: @task_config, log_failure_by_default: true)
193
+ end
194
+
195
+ def kubeclient_builder
196
+ @kubeclient_builder ||= KubeclientBuilder.new
197
+ end
198
+
199
+ def prune_whitelist
200
+ cluster_resource_discoverer.prunable_resources(namespaced: false)
201
+ end
202
+
203
+ def check_initial_status(resources)
204
+ cache = ResourceCache.new(@task_config)
205
+ Concurrency.split_across_threads(resources) { |r| r.sync(cache) }
206
+ resources.each { |r| logger.info(r.pretty_status) }
207
+ end
208
+ measure_method(:check_initial_status, "initial_status.duration")
209
+ end
210
+ end