kubernetes-deploy 0.30.0 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +15 -0
  4. data/CONTRIBUTING.md +2 -2
  5. data/README.md +1 -1
  6. data/dev.yml +1 -1
  7. data/dev/flamegraph-from-tests +1 -1
  8. data/exe/kubernetes-deploy +11 -9
  9. data/exe/kubernetes-render +9 -7
  10. data/exe/kubernetes-restart +3 -3
  11. data/exe/kubernetes-run +1 -1
  12. data/kubernetes-deploy.gemspec +3 -3
  13. data/lib/krane.rb +5 -3
  14. data/lib/{kubernetes-deploy → krane}/bindings_parser.rb +1 -1
  15. data/lib/krane/cli/deploy_command.rb +14 -11
  16. data/lib/krane/cli/global_deploy_command.rb +47 -0
  17. data/lib/krane/cli/krane.rb +12 -3
  18. data/lib/krane/cli/render_command.rb +11 -9
  19. data/lib/krane/cli/restart_command.rb +4 -4
  20. data/lib/krane/cli/run_command.rb +3 -3
  21. data/lib/krane/cli/version_command.rb +1 -1
  22. data/lib/krane/cluster_resource_discovery.rb +102 -0
  23. data/lib/{kubernetes-deploy → krane}/common.rb +8 -9
  24. data/lib/krane/concerns/template_reporting.rb +29 -0
  25. data/lib/{kubernetes-deploy → krane}/concurrency.rb +1 -1
  26. data/lib/{kubernetes-deploy → krane}/container_logs.rb +1 -1
  27. data/lib/{kubernetes-deploy → krane}/deferred_summary_logging.rb +2 -2
  28. data/lib/{kubernetes-deploy → krane}/delayed_exceptions.rb +0 -0
  29. data/lib/krane/deploy_task.rb +2 -2
  30. data/lib/{kubernetes-deploy → krane}/deploy_task_config_validator.rb +1 -1
  31. data/lib/krane/deprecated_deploy_task.rb +404 -0
  32. data/lib/{kubernetes-deploy → krane}/duration_parser.rb +1 -1
  33. data/lib/{kubernetes-deploy → krane}/ejson_secret_provisioner.rb +3 -3
  34. data/lib/krane/errors.rb +28 -0
  35. data/lib/{kubernetes-deploy → krane}/formatted_logger.rb +2 -2
  36. data/lib/krane/global_deploy_task.rb +210 -0
  37. data/lib/krane/global_deploy_task_config_validator.rb +12 -0
  38. data/lib/{kubernetes-deploy → krane}/kubeclient_builder.rb +11 -3
  39. data/lib/{kubernetes-deploy → krane}/kubectl.rb +2 -2
  40. data/lib/{kubernetes-deploy → krane}/kubernetes_resource.rb +54 -22
  41. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/cloudsql.rb +1 -1
  42. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/config_map.rb +1 -1
  43. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/cron_job.rb +1 -1
  44. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/custom_resource.rb +2 -2
  45. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/custom_resource_definition.rb +1 -5
  46. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/daemon_set.rb +7 -4
  47. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/deployment.rb +2 -2
  48. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/horizontal_pod_autoscaler.rb +1 -1
  49. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/ingress.rb +1 -1
  50. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/job.rb +1 -1
  51. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/network_policy.rb +1 -1
  52. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/persistent_volume_claim.rb +1 -1
  53. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/pod.rb +2 -2
  54. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/pod_disruption_budget.rb +2 -2
  55. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/pod_set_base.rb +3 -3
  56. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/pod_template.rb +1 -1
  57. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/replica_set.rb +2 -2
  58. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/resource_quota.rb +1 -1
  59. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/role.rb +1 -1
  60. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/role_binding.rb +1 -1
  61. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/secret.rb +1 -1
  62. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/service.rb +2 -2
  63. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/service_account.rb +1 -1
  64. data/lib/{kubernetes-deploy → krane}/kubernetes_resource/stateful_set.rb +2 -2
  65. data/lib/{kubernetes-deploy → krane}/label_selector.rb +1 -1
  66. data/lib/{kubernetes-deploy → krane}/oj.rb +0 -0
  67. data/lib/{kubernetes-deploy → krane}/options_helper.rb +2 -2
  68. data/lib/{kubernetes-deploy → krane}/remote_logs.rb +2 -2
  69. data/lib/krane/render_task.rb +149 -0
  70. data/lib/{kubernetes-deploy → krane}/renderer.rb +1 -1
  71. data/lib/{kubernetes-deploy → krane}/resource_cache.rb +4 -3
  72. data/lib/krane/resource_deployer.rb +265 -0
  73. data/lib/{kubernetes-deploy → krane}/resource_watcher.rb +6 -6
  74. data/lib/krane/restart_task.rb +224 -0
  75. data/lib/{kubernetes-deploy → krane}/rollout_conditions.rb +1 -1
  76. data/lib/krane/runner_task.rb +212 -0
  77. data/lib/{kubernetes-deploy → krane}/runner_task_config_validator.rb +1 -1
  78. data/lib/{kubernetes-deploy → krane}/statsd.rb +13 -27
  79. data/lib/krane/task_config.rb +19 -0
  80. data/lib/{kubernetes-deploy → krane}/task_config_validator.rb +1 -1
  81. data/lib/{kubernetes-deploy → krane}/template_sets.rb +5 -5
  82. data/lib/krane/version.rb +4 -0
  83. data/lib/kubernetes-deploy/deploy_task.rb +6 -603
  84. data/lib/kubernetes-deploy/errors.rb +1 -26
  85. data/lib/kubernetes-deploy/render_task.rb +5 -139
  86. data/lib/kubernetes-deploy/rescue_krane_exceptions.rb +18 -0
  87. data/lib/kubernetes-deploy/restart_task.rb +6 -215
  88. data/lib/kubernetes-deploy/runner_task.rb +6 -203
  89. metadata +75 -58
  90. data/lib/kubernetes-deploy/cluster_resource_discovery.rb +0 -57
  91. data/lib/kubernetes-deploy/task_config.rb +0 -16
  92. data/lib/kubernetes-deploy/version.rb +0 -4
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class Cloudsql < KubernetesResource
4
4
  TIMEOUT = 10.minutes
5
5
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class ConfigMap < KubernetesResource
4
4
  TIMEOUT = 30.seconds
5
5
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class CronJob < KubernetesResource
4
4
  TIMEOUT = 30.seconds
5
5
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require 'jsonpath'
3
3
 
4
- module KubernetesDeploy
4
+ module Krane
5
5
  class CustomResource < KubernetesResource
6
6
  TIMEOUT_MESSAGE_DIFFERENT_GENERATIONS = <<~MSG
7
7
  This resource's status could not be used to determine rollout success because it is not up-to-date
@@ -68,7 +68,7 @@ module KubernetesDeploy
68
68
  @crd.validate_rollout_conditions
69
69
  rescue RolloutConditionsError => e
70
70
  @validation_errors << "The CRD that specifies this resource is using invalid rollout conditions. " \
71
- "Kubernetes-deploy will not be able to continue until those rollout conditions are fixed.\n" \
71
+ "Krane will not be able to continue until those rollout conditions are fixed.\n" \
72
72
  "Rollout conditions can be found on the CRD that defines this resource (#{@crd.name}), " \
73
73
  "under the annotation #{CustomResourceDefinition::ROLLOUT_CONDITIONS_ANNOTATION}.\n" \
74
74
  "Validation failed with: #{e}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class CustomResourceDefinition < KubernetesResource
4
4
  TIMEOUT = 2.minutes
5
5
  ROLLOUT_CONDITIONS_ANNOTATION_SUFFIX = "instance-rollout-conditions"
@@ -46,10 +46,6 @@ module KubernetesDeploy
46
46
  @definition.dig("spec", "names", "kind")
47
47
  end
48
48
 
49
- def name
50
- @definition.dig("metadata", "name")
51
- end
52
-
53
49
  def prunable?
54
50
  prunable = krane_annotation_value("prunable")
55
51
  prunable == "true"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require 'kubernetes-deploy/kubernetes_resource/pod_set_base'
3
- module KubernetesDeploy
2
+ require 'krane/kubernetes_resource/pod_set_base'
3
+ module Krane
4
4
  class DaemonSet < PodSetBase
5
5
  TIMEOUT = 5.minutes
6
6
  attr_reader :pods
@@ -58,8 +58,11 @@ module KubernetesDeploy
58
58
  return true if rollout_data["desiredNumberScheduled"].to_i == rollout_data["numberReady"].to_i # all pods ready
59
59
  relevant_node_names = @nodes.map(&:name)
60
60
  considered_pods = @pods.select { |p| relevant_node_names.include?(p.node_name) }
61
- @logger.debug("Considered #{considered_pods.size} pods out of #{@pods.size} for #{@nodes.size} nodes")
62
- considered_pods.present? && considered_pods.all?(&:deploy_succeeded?)
61
+ @logger.debug("DaemonSet is reporting #{rollout_data['numberReady']} pods ready." \
62
+ " Considered #{considered_pods.size} pods out of #{@pods.size} for #{@nodes.size} nodes.")
63
+ considered_pods.present? &&
64
+ considered_pods.all?(&:deploy_succeeded?) &&
65
+ rollout_data["numberReady"].to_i >= considered_pods.length
63
66
  end
64
67
 
65
68
  def find_nodes(cache)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
- require 'kubernetes-deploy/kubernetes_resource/replica_set'
2
+ require 'krane/kubernetes_resource/replica_set'
3
3
 
4
- module KubernetesDeploy
4
+ module Krane
5
5
  class Deployment < KubernetesResource
6
6
  TIMEOUT = 7.minutes
7
7
  REQUIRED_ROLLOUT_ANNOTATION_SUFFIX = "required-rollout"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class HorizontalPodAutoscaler < KubernetesResource
4
4
  TIMEOUT = 3.minutes
5
5
  RECOVERABLE_CONDITION_PREFIX = "FailedGet"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class Ingress < KubernetesResource
4
4
  TIMEOUT = 30.seconds
5
5
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class Job < KubernetesResource
4
4
  TIMEOUT = 10.minutes
5
5
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class NetworkPolicy < KubernetesResource
4
4
  TIMEOUT = 30.seconds
5
5
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class PersistentVolumeClaim < KubernetesResource
4
4
  TIMEOUT = 5.minutes
5
5
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class Pod < KubernetesResource
4
4
  TIMEOUT = 10.minutes
5
5
 
@@ -141,7 +141,7 @@ module KubernetesDeploy
141
141
  end
142
142
 
143
143
  def logs
144
- @logs ||= KubernetesDeploy::RemoteLogs.new(
144
+ @logs ||= Krane::RemoteLogs.new(
145
145
  logger: @logger,
146
146
  parent_id: id,
147
147
  container_names: @containers.map(&:name),
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class PodDisruptionBudget < KubernetesResource
4
4
  TIMEOUT = 10.seconds
5
5
 
@@ -13,7 +13,7 @@ module KubernetesDeploy
13
13
 
14
14
  def deploy_method
15
15
  # Required until https://github.com/kubernetes/kubernetes/issues/45398 changes
16
- :replace_force
16
+ uses_generate_name? ? :create : :replace_force
17
17
  end
18
18
 
19
19
  def timeout_message
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
- require 'kubernetes-deploy/kubernetes_resource/pod'
2
+ require 'krane/kubernetes_resource/pod'
3
3
 
4
- module KubernetesDeploy
4
+ module Krane
5
5
  class PodSetBase < KubernetesResource
6
6
  def failure_message
7
7
  pods.map(&:failure_message).compact.uniq.join("\n")
@@ -19,7 +19,7 @@ module KubernetesDeploy
19
19
  end
20
20
 
21
21
  def fetch_debug_logs
22
- logs = KubernetesDeploy::RemoteLogs.new(
22
+ logs = Krane::RemoteLogs.new(
23
23
  logger: @logger,
24
24
  parent_id: id,
25
25
  container_names: container_names,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class PodTemplate < KubernetesResource
4
4
  def status
5
5
  exists? ? "Available" : "Not Found"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
- require 'kubernetes-deploy/kubernetes_resource/pod_set_base'
2
+ require 'krane/kubernetes_resource/pod_set_base'
3
3
 
4
- module KubernetesDeploy
4
+ module Krane
5
5
  class ReplicaSet < PodSetBase
6
6
  TIMEOUT = 5.minutes
7
7
  attr_reader :pods
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class ResourceQuota < KubernetesResource
4
4
  TIMEOUT = 30.seconds
5
5
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class Role < KubernetesResource
4
4
  TIMEOUT = 30.seconds
5
5
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class RoleBinding < KubernetesResource
4
4
  TIMEOUT = 30.seconds
5
5
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class Secret < KubernetesResource
4
4
  TIMEOUT = 30.seconds
5
5
  SENSITIVE_TEMPLATE_CONTENT = true
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
- require 'kubernetes-deploy/kubernetes_resource/pod'
2
+ require 'krane/kubernetes_resource/pod'
3
3
 
4
- module KubernetesDeploy
4
+ module Krane
5
5
  class Service < KubernetesResource
6
6
  TIMEOUT = 7.minutes
7
7
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module KubernetesDeploy
2
+ module Krane
3
3
  class ServiceAccount < KubernetesResource
4
4
  TIMEOUT = 30.seconds
5
5
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require 'kubernetes-deploy/kubernetes_resource/pod_set_base'
3
- module KubernetesDeploy
2
+ require 'krane/kubernetes_resource/pod_set_base'
3
+ module Krane
4
4
  class StatefulSet < PodSetBase
5
5
  TIMEOUT = 10.minutes
6
6
  ONDELETE = 'OnDelete'
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module KubernetesDeploy
3
+ module Krane
4
4
  class LabelSelector
5
5
  def self.parse(string)
6
6
  selector = {}
File without changes
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module KubernetesDeploy
3
+ module Krane
4
4
  module OptionsHelper
5
5
  class OptionsError < StandardError; end
6
6
 
@@ -19,7 +19,7 @@ module KubernetesDeploy
19
19
  end
20
20
 
21
21
  if template_paths.include?("-")
22
- Dir.mktmpdir("kubernetes-deploy") do |dir|
22
+ Dir.mktmpdir("krane") do |dir|
23
23
  template_dir_from_stdin(temp_dir: dir)
24
24
  validated_paths << dir
25
25
  yield validated_paths
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
- require 'kubernetes-deploy/container_logs'
2
+ require 'krane/container_logs'
3
3
 
4
- module KubernetesDeploy
4
+ module Krane
5
5
  class RemoteLogs
6
6
  attr_reader :container_logs
7
7
 
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+ require 'tempfile'
3
+
4
+ require 'krane/common'
5
+ require 'krane/renderer'
6
+ require 'krane/template_sets'
7
+
8
+ module Krane
9
+ # Render templates
10
+ class RenderTask
11
+ # Initializes the render task
12
+ #
13
+ # @param logger [Object] Logger object (defaults to an instance of Krane::FormattedLogger)
14
+ # @param current_sha [String] The SHA of the commit
15
+ # @param template_dir [String] Path to a directory with templates to render (deprecated)
16
+ # @param template_paths [Array<String>] An array of template paths to render
17
+ # @param bindings [Hash] Bindings parsed by Krane::BindingsParser
18
+ def initialize(logger: nil, current_sha:, template_dir: nil, template_paths: [], bindings:)
19
+ @logger = logger || Krane::FormattedLogger.build
20
+ @template_dir = template_dir
21
+ @template_paths = template_paths.map { |path| File.expand_path(path) }
22
+ @bindings = bindings
23
+ @current_sha = current_sha
24
+ end
25
+
26
+ # Runs the task, returning a boolean representing success or failure
27
+ #
28
+ # @return [Boolean]
29
+ def run(*args)
30
+ run!(*args)
31
+ true
32
+ rescue Krane::FatalDeploymentError
33
+ false
34
+ end
35
+
36
+ # Runs the task, raising exceptions in case of issues
37
+ #
38
+ # @param stream [IO] Place to stream the output to
39
+ # @param only_filenames [Array<String>] List of filenames to render
40
+ #
41
+ # @return [nil]
42
+ def run!(stream, only_filenames = [])
43
+ @logger.reset
44
+ @logger.phase_heading("Initializing render task")
45
+
46
+ ts = TemplateSets.from_dirs_and_files(paths: template_sets_paths(only_filenames), logger: @logger)
47
+
48
+ validate_configuration(ts, only_filenames)
49
+ count = render_templates(stream, ts)
50
+
51
+ @logger.summary.add_action("Successfully rendered #{count} template(s)")
52
+ @logger.print_summary(:success)
53
+ rescue Krane::FatalDeploymentError
54
+ @logger.print_summary(:failure)
55
+ raise
56
+ end
57
+
58
+ private
59
+
60
+ def template_sets_paths(only_filenames)
61
+ if @template_paths.present?
62
+ # Validation will catch @template_paths & @template_dir being present
63
+ @template_paths
64
+ elsif only_filenames.blank?
65
+ [File.expand_path(@template_dir || '')]
66
+ else
67
+ absolute_template_dir = File.expand_path(@template_dir || '')
68
+ only_filenames.map do |name|
69
+ File.join(absolute_template_dir, name)
70
+ end
71
+ end
72
+ end
73
+
74
+ def render_templates(stream, template_sets)
75
+ @logger.phase_heading("Rendering template(s)")
76
+ count = 0
77
+ template_sets.with_resource_definitions_and_filename(render_erb: true,
78
+ current_sha: @current_sha, bindings: @bindings, raw: true) do |rendered_content, filename|
79
+ write_to_stream(rendered_content, filename, stream)
80
+ count += 1
81
+ end
82
+
83
+ count
84
+ rescue Krane::InvalidTemplateError => exception
85
+ log_invalid_template(exception)
86
+ raise
87
+ end
88
+
89
+ def write_to_stream(rendered_content, filename, stream)
90
+ file_basename = File.basename(filename)
91
+ @logger.info("Rendering #{file_basename}...")
92
+ implicit = []
93
+ YAML.parse_stream(rendered_content, "<rendered> #{filename}") { |d| implicit << d.implicit }
94
+ if rendered_content.present?
95
+ stream.puts "---\n" if implicit.first
96
+ stream.puts rendered_content
97
+ @logger.info("Rendered #{file_basename}")
98
+ else
99
+ @logger.warn("Rendered #{file_basename} successfully, but the result was blank")
100
+ end
101
+ rescue Psych::SyntaxError => exception
102
+ raise InvalidTemplateError.new("Template is not valid YAML. #{exception.message}", filename: filename)
103
+ end
104
+
105
+ def validate_configuration(template_sets, filenames)
106
+ @logger.info("Validating configuration")
107
+ errors = []
108
+ if @template_dir.present? && @template_paths.present?
109
+ errors << "template_dir and template_paths can not be combined"
110
+ elsif @template_dir.blank? && @template_paths.blank?
111
+ errors << "template_dir or template_paths must be set"
112
+ end
113
+
114
+ if filenames.present?
115
+ if @template_dir.nil?
116
+ errors << "template_dir must be set to use filenames"
117
+ else
118
+ absolute_template_dir = File.expand_path(@template_dir)
119
+ filenames.each do |filename|
120
+ absolute_file = File.expand_path(File.join(@template_dir, filename))
121
+ unless absolute_file.start_with?(absolute_template_dir)
122
+ errors << "Filename \"#{absolute_file}\" is outside the template directory," \
123
+ " which was resolved as #{absolute_template_dir}"
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ errors += template_sets.validate
130
+
131
+ unless errors.empty?
132
+ @logger.summary.add_action("Configuration invalid")
133
+ @logger.summary.add_paragraph(errors.map { |err| "- #{err}" }.join("\n"))
134
+ raise Krane::TaskConfigurationError, "Configuration invalid: #{errors.join(', ')}"
135
+ end
136
+ end
137
+
138
+ def log_invalid_template(exception)
139
+ @logger.error("Failed to render #{exception.filename}")
140
+
141
+ debug_msg = ColorizedString.new("Invalid template: #{exception.filename}\n").red
142
+ debug_msg += "> Error message:\n#{FormattedLogger.indent_four(exception.to_s)}"
143
+ if exception.content
144
+ debug_msg += "\n> Template content:\n#{FormattedLogger.indent_four(exception.content)}"
145
+ end
146
+ @logger.summary.add_paragraph(debug_msg)
147
+ end
148
+ end
149
+ end