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,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