kubernetes-deploy 0.28.0 → 0.29.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 825f058c113d4ae975ee5937866b639ac4d705f5cf9493ab414b56a183bd862e
4
- data.tar.gz: 2a6e43245a95f182e63d9f81d085f62aede3d16c74f4184d2f07bb6f0db55563
3
+ metadata.gz: a0641a291f2f01c007051cf5001bc97ecb9216b55e7204d954c920083e0d303a
4
+ data.tar.gz: 2c2415cfd94a63f046b94e90c943a7f9e01a46207833c1d5d2a498ceeb1ff333
5
5
  SHA512:
6
- metadata.gz: dbd772b8de088a986c62075c1443069be9fed802cf04958144ff377c26f3f594e9e2a6b5e64e1570452e936016e03fdc079b7dad986019cc27473e22ebf3bec1
7
- data.tar.gz: ff2824c32e6421056842a190fe8658f0fc903b3c866919b0a83f4bb4446b76d5d5cffad739fcba0c1564bb5079c02c21eb2022df30f379f9876e190157fe1ece
6
+ metadata.gz: 18a4e86f4f144dfa23ebc191b7d06382605ada3b050e8e1c9e717cbc9db981f16b419fb27a342f4e1267663f8f82fb12bcebfe0697c9ac4ac40c15108ab73072
7
+ data.tar.gz: 314bdadd3d8f864895c13e81193845f4632fa46dc23550f12955a5ffe7a7b15ed3df1e12d769d5aefdb8ee4b7e5c3e3e13d5f8378f5c828a2e4bf3c04db8c56c
@@ -2,7 +2,7 @@ inherit_from:
2
2
  - http://shopify.github.io/ruby-style-guide/rubocop.yml
3
3
 
4
4
  AllCops:
5
- TargetRubyVersion: 2.3
5
+ TargetRubyVersion: 2.4
6
6
 
7
7
  Naming/FileName:
8
8
  Enabled: true
@@ -1,6 +1,6 @@
1
1
  containers:
2
2
  default:
3
- docker: circleci/ruby:2.5.1-node-browsers
3
+ docker: circleci/ruby:2.4.6-node-browsers
4
4
 
5
5
  steps:
6
6
  - label: Lint
@@ -3,13 +3,19 @@
3
3
  *Important!*
4
4
  - The next release will be 1.0.0, which means that master will contain breaking changes.
5
5
 
6
+ *Bug Fixes*
7
+ - Handle improper duration values more elegantly with better messaging
8
+
9
+ *Other*
10
+ - We now require Ruby 2.4.x since Ruby 2.3 is past EoL.
11
+
6
12
  ## 0.28.0
7
13
 
8
14
  *Enhancements*
9
15
  - Officially support Kubernetes 1.15 ([#546](https://github.com/Shopify/kubernetes-deploy/pull/546))
10
16
  - Make sure that we only declare a Service of type LoadBalancer as deployed after its IP address is published. [#547](https://github.com/Shopify/kubernetes-deploy/pull/547)
11
17
  - Add more validations to `RunnerTask`. [#554](https://github.com/Shopify/kubernetes-deploy/pull/554)
12
- - Validate secrets with `--server-dry-run` on supported clusters. [#553](https://github.com/Shopify/kubernetes-deploy/pull/553)
18
+ - Validate secrets with `--server-dry-run` on supported clusters. [#553](https://github.com/Shopify/kubernetes-deploy/pull/553)
13
19
  *Bug Fixes*
14
20
  - Fix a bug in rendering where we failed to add a yaml doc separator (`---`) to
15
21
  an implicit document if there are multiple documents in the file.
data/README.md CHANGED
@@ -71,7 +71,7 @@ This repo also includes related tools for [running tasks](#kubernetes-run) and [
71
71
 
72
72
  ## Prerequisites
73
73
 
74
- * Ruby 2.3+
74
+ * Ruby 2.4+
75
75
  * Your cluster must be running Kubernetes v1.11.0 or higher<sup>1</sup>
76
76
  * Each app must have a deploy directory containing its Kubernetes templates (see [Templates](#using-templates-and-variables))
77
77
 
data/bin/ci CHANGED
@@ -17,5 +17,5 @@ docker run --rm \
17
17
  -e VERBOSE=1 \
18
18
  -e PARALLELISM=$PARALLELISM \
19
19
  -w /usr/src/app \
20
- ruby:2.3 \
20
+ ruby:2.4 \
21
21
  bin/test
data/dev.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: kubernetes-deploy
3
3
  up:
4
- - ruby: 2.3.3 # Matches gemspec
4
+ - ruby: 2.4.6 # Matches gemspec
5
5
  - bundler
6
6
  - homebrew:
7
7
  - Caskroom/cask/minikube
@@ -22,13 +22,13 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = %w(lib)
24
24
 
25
- spec.required_ruby_version = '>= 2.3.0'
25
+ spec.required_ruby_version = '>= 2.4.0'
26
26
  spec.add_dependency("activesupport", ">= 5.0")
27
27
  spec.add_dependency("kubeclient", "~> 4.3")
28
28
  spec.add_dependency("googleauth", "~> 0.8.0")
29
29
  spec.add_dependency("ejson", "~> 1.0")
30
30
  spec.add_dependency("colorize", "~> 0.8")
31
- spec.add_dependency("statsd-instrument", '~> 2.3', '>= 2.3.2')
31
+ spec.add_dependency("statsd-instrument", '~> 2.3.2')
32
32
  spec.add_dependency("oj", "~> 3.0")
33
33
  spec.add_dependency("concurrent-ruby", "~> 1.1")
34
34
  spec.add_dependency("jsonpath", "~> 0.9.6")
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Krane
4
+ module CLI
5
+ class DeployCommand
6
+ DEFAULT_DEPLOY_TIMEOUT = '300s'
7
+ PROTECTED_NAMESPACES = %w(
8
+ default
9
+ kube-system
10
+ kube-public
11
+ )
12
+ OPTIONS = {
13
+ "bindings" => { type: :array, banner: "foo=bar abc=def",
14
+ desc: "Expose additional variables to ERB templates (format: k1=v1 k2=v2, JSON string or file "\
15
+ "(JSON or YAML) path prefixed by '@')" },
16
+ "filenames" => { type: :string, banner: '/tmp/my-resource.yml', aliases: :f, required: true,
17
+ desc: "Path to file or directory that contains the configuration to apply" },
18
+ "global-timeout" => { type: :string, banner: "duration", default: DEFAULT_DEPLOY_TIMEOUT,
19
+ desc: "Max duration to monitor workloads correctly deployed" },
20
+ "protected-namespaces" => { type: :array, banner: "namespace1 namespace2 namespaceN",
21
+ desc: "Enable deploys to a list of selected namespaces; set to an empty string "\
22
+ "to disable",
23
+ default: PROTECTED_NAMESPACES },
24
+ "prune" => { type: :boolean, desc: "Enable deletion of resources that do not appear in the template dir",
25
+ default: true },
26
+ "render-erb" => { type: :boolean, desc: "Enable ERB rendering", default: false },
27
+ "selector" => { type: :string, banner: "'label=value'",
28
+ desc: "Select workloads by selector(s)" },
29
+ "verbose-log-prefix" => { type: :boolean, desc: "Add [context][namespace] to the log prefix",
30
+ default: true },
31
+ "verify-result" => { type: :boolean, default: true,
32
+ desc: "Verify workloads correctly deployed" },
33
+ }
34
+
35
+ def self.from_options(namespace, context, options)
36
+ require 'kubernetes-deploy/deploy_task'
37
+ require 'kubernetes-deploy/options_helper'
38
+ require 'kubernetes-deploy/bindings_parser'
39
+ require 'kubernetes-deploy/label_selector'
40
+
41
+ bindings_parser = KubernetesDeploy::BindingsParser.new
42
+ options[:bindings]&.each { |binding_pair| bindings_parser.add(binding_pair) }
43
+
44
+ selector = KubernetesDeploy::LabelSelector.parse(options[:selector]) if options[:selector]
45
+
46
+ logger = KubernetesDeploy::FormattedLogger.build(namespace, context,
47
+ verbose_prefix: options['verbose-log-prefix'])
48
+
49
+ protected_namespaces = options['protected-namespaces']
50
+ if options['protected-namespaces'].size == 1 && %w('' "").include?(options['protected-namespaces'][0])
51
+ protected_namespaces = []
52
+ end
53
+
54
+ KubernetesDeploy::OptionsHelper.with_processed_template_paths([options[:filenames]],
55
+ require_explicit_path: true) do |paths|
56
+ deploy = KubernetesDeploy::DeployTask.new(
57
+ namespace: namespace,
58
+ context: context,
59
+ current_sha: ENV["REVISION"],
60
+ template_paths: paths,
61
+ bindings: bindings_parser.parse,
62
+ logger: logger,
63
+ max_watch_seconds: KubernetesDeploy::DurationParser.new(options["global-timeout"]).parse!.to_i,
64
+ selector: selector,
65
+ protected_namespaces: protected_namespaces,
66
+ )
67
+
68
+ deploy.run!(
69
+ verify_result: options["verify-result"],
70
+ allow_protected_ns: !protected_namespaces.empty?,
71
+ prune: options[:prune]
72
+ )
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -4,6 +4,9 @@ require 'krane'
4
4
  require 'thor'
5
5
  require 'krane/cli/version_command'
6
6
  require 'krane/cli/restart_command'
7
+ require 'krane/cli/run_command'
8
+ require 'krane/cli/render_command'
9
+ require 'krane/cli/deploy_command'
7
10
 
8
11
  module Krane
9
12
  module CLI
@@ -17,6 +20,14 @@ module Krane
17
20
  task_options.each { |option_name, config| method_option(option_name, config) }
18
21
  end
19
22
 
23
+ desc("render", "Render templates")
24
+ expand_options(RenderCommand::OPTIONS)
25
+ def render
26
+ rescue_and_exit do
27
+ RenderCommand.from_options(options)
28
+ end
29
+ end
30
+
20
31
  desc("version", "Prints the version")
21
32
  expand_options(VersionCommand::OPTIONS)
22
33
  def version
@@ -31,6 +42,22 @@ module Krane
31
42
  end
32
43
  end
33
44
 
45
+ desc("run NAMESPACE CONTEXT", "Run a pod that exits upon completing a task")
46
+ expand_options(RunCommand::OPTIONS)
47
+ def run_command(namespace, context)
48
+ rescue_and_exit do
49
+ RunCommand.from_options(namespace, context, options)
50
+ end
51
+ end
52
+
53
+ desc("deploy NAMESPACE CONTEXT", "Ship resources to a namespace")
54
+ expand_options(DeployCommand::OPTIONS)
55
+ def deploy(namespace, context)
56
+ rescue_and_exit do
57
+ DeployCommand.from_options(namespace, context, options)
58
+ end
59
+ end
60
+
34
61
  def self.exit_on_failure?
35
62
  true
36
63
  end
@@ -43,6 +70,12 @@ module Krane
43
70
  exit(TIMEOUT_EXIT_CODE)
44
71
  rescue KubernetesDeploy::FatalDeploymentError
45
72
  exit(FAILURE_EXIT_CODE)
73
+ rescue KubernetesDeploy::DurationParser::ParsingError => e
74
+ STDERR.puts(<<~ERROR_MESSAGE)
75
+ Error parsing duration
76
+ #{e.message}. Duration must be a full ISO8601 duration or time value (e.g. 300s, 10m, 1h)
77
+ ERROR_MESSAGE
78
+ exit(FAILURE_EXIT_CODE)
46
79
  end
47
80
  end
48
81
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Krane
4
+ module CLI
5
+ class RenderCommand
6
+ OPTIONS = {
7
+ bindings: { type: :array, desc: 'Bindings for erb' },
8
+ filenames: { type: :array, required: true, aliases: 'f', desc: 'Directories and files to render' },
9
+ }
10
+
11
+ def self.from_options(options)
12
+ require 'kubernetes-deploy/render_task'
13
+ require 'kubernetes-deploy/bindings_parser'
14
+ require 'kubernetes-deploy/options_helper'
15
+
16
+ bindings_parser = KubernetesDeploy::BindingsParser.new
17
+ options[:bindings]&.each { |b| bindings_parser.add(b) }
18
+
19
+ KubernetesDeploy::OptionsHelper.with_processed_template_paths(options[:filenames]) do |paths|
20
+ runner = KubernetesDeploy::RenderTask.new(
21
+ current_sha: ENV["REVISION"],
22
+ template_paths: paths,
23
+ bindings: bindings_parser.parse,
24
+ )
25
+ runner.run!(STDOUT)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Krane
4
+ module CLI
5
+ class RunCommand
6
+ DEFAULT_RUN_TIMEOUT = '300s'
7
+
8
+ OPTIONS = {
9
+ "global-timeout" => {
10
+ type: :string,
11
+ banner: "duration",
12
+ desc: "Timeout error is raised if the pod runs for longer than the specified number of seconds",
13
+ default: DEFAULT_RUN_TIMEOUT,
14
+ },
15
+ "arguments" => {
16
+ type: :string,
17
+ banner: '"ARG1 ARG2 ARG3"',
18
+ desc: "Override the default arguments for the command with a space-separated list of arguments",
19
+ },
20
+ "verify-result" => { type: :boolean, desc: "Wait for completion and verify pod success", default: true },
21
+ "command" => { type: :array, desc: "Override the default command in the container image" },
22
+ "template" => {
23
+ type: :string,
24
+ desc: "The template file you'll be rendering",
25
+ default: 'task-runner-template',
26
+ },
27
+ "env-vars" => {
28
+ type: :string,
29
+ banner: "VAR=val,FOO=bar",
30
+ desc: "A Comma-separated list of env vars",
31
+ default: '',
32
+ },
33
+ }
34
+
35
+ def self.from_options(namespace, context, options)
36
+ require "kubernetes-deploy/runner_task"
37
+ runner = KubernetesDeploy::RunnerTask.new(
38
+ namespace: namespace,
39
+ context: context,
40
+ max_watch_seconds: KubernetesDeploy::DurationParser.new(options["global-timeout"]).parse!.to_i,
41
+ )
42
+
43
+ runner.run!(
44
+ verify_result: options['verify-result'],
45
+ task_template: options['template'],
46
+ entrypoint: options['command'],
47
+ args: options['arguments']&.split(" "),
48
+ env_vars: options['env-vars'].split(','),
49
+ )
50
+ end
51
+ end
52
+ end
53
+ 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
@@ -105,7 +105,8 @@ module KubernetesDeploy
105
105
  end
106
106
 
107
107
  def initialize(namespace:, context:, current_sha:, logger: nil, kubectl_instance: nil, bindings: {},
108
- max_watch_seconds: nil, selector: nil, template_paths: [], template_dir: nil)
108
+ max_watch_seconds: nil, selector: nil, template_paths: [], template_dir: nil, protected_namespaces: nil,
109
+ render_erb: true)
109
110
  template_dir = File.expand_path(template_dir) if template_dir
110
111
  template_paths = (template_paths.map { |path| File.expand_path(path) } << template_dir).compact
111
112
 
@@ -120,6 +121,8 @@ module KubernetesDeploy
120
121
  @kubectl = kubectl_instance
121
122
  @max_watch_seconds = max_watch_seconds
122
123
  @selector = selector
124
+ @protected_namespaces = protected_namespaces || PROTECTED_NAMESPACES
125
+ @render_erb = render_erb
123
126
  end
124
127
 
125
128
  def run(*args)
@@ -147,7 +150,7 @@ module KubernetesDeploy
147
150
  end
148
151
 
149
152
  @logger.phase_heading("Deploying all resources")
150
- if PROTECTED_NAMESPACES.include?(@namespace) && prune
153
+ if @protected_namespaces.include?(@namespace) && prune
151
154
  raise FatalDeploymentError, "Refusing to deploy to protected namespace '#{@namespace}' with pruning enabled"
152
155
  end
153
156
 
@@ -283,7 +286,7 @@ module KubernetesDeploy
283
286
  @logger.info("Discovering resources:")
284
287
  resources = []
285
288
  crds_by_kind = cluster_resource_discoverer.crds.group_by(&:kind)
286
- @template_sets.with_resource_definitions(render_erb: true,
289
+ @template_sets.with_resource_definitions(render_erb: @render_erb,
287
290
  current_sha: @current_sha, bindings: @bindings) do |r_def|
288
291
  crd = crds_by_kind[r_def["kind"]]&.first
289
292
  r = KubernetesResource.build(namespace: @namespace, context: @context, logger: @logger, definition: r_def,
@@ -334,7 +337,7 @@ module KubernetesDeploy
334
337
 
335
338
  if @namespace.blank?
336
339
  errors << "Namespace must be specified"
337
- elsif PROTECTED_NAMESPACES.include?(@namespace)
340
+ elsif @protected_namespaces.include?(@namespace)
338
341
  if allow_protected_ns && prune
339
342
  errors << "Refusing to deploy to protected namespace '#{@namespace}' with pruning enabled"
340
343
  elsif allow_protected_ns
@@ -475,27 +478,36 @@ module KubernetesDeploy
475
478
  .select(&:sensitive_template_content?)
476
479
  .map { |r| File.basename(r.file_path) }
477
480
 
481
+ server_dry_run_validated_resource = resources
482
+ .select(&:server_dry_run_validated?)
483
+ .map { |r| File.basename(r.file_path) }
484
+
478
485
  err.each_line do |line|
479
486
  bad_files = find_bad_files_from_kubectl_output(line)
480
- if bad_files.present?
481
- bad_files.each do |f|
482
- if filenames_with_sensitive_content.include?(f[:filename])
483
- # Hide the error and template contents in case it has senitive information
484
- record_invalid_template(err: "SUPPRESSED FOR SECURITY", filename: f[:filename], content: nil)
485
- else
486
- record_invalid_template(err: f[:err], filename: f[:filename], content: f[:content])
487
- end
488
- end
489
- else
487
+ unless bad_files.present?
490
488
  unidentified_errors << line
489
+ next
490
+ end
491
+
492
+ bad_files.each do |f|
493
+ err_msg = f[:err]
494
+ if filenames_with_sensitive_content.include?(f[:filename])
495
+ # Hide the error and template contents in case it has sensitive information
496
+ # we display full error messages as we assume there's no sensitive info leak after server-dry-run
497
+ err_msg = "SUPPRESSED FOR SECURITY" unless server_dry_run_validated_resource.include?(f[:filename])
498
+ record_invalid_template(err: err_msg, filename: f[:filename], content: nil)
499
+ else
500
+ record_invalid_template(err: err_msg, filename: f[:filename], content: f[:content])
501
+ end
491
502
  end
492
503
  end
504
+ return unless unidentified_errors.any?
493
505
 
494
- if unidentified_errors.present? && filenames_with_sensitive_content.any?
506
+ if (filenames_with_sensitive_content - server_dry_run_validated_resource).present?
495
507
  warn_msg = "WARNING: There was an error applying some or all resources. The raw output may be sensitive and " \
496
508
  "so cannot be displayed."
497
509
  @logger.summary.add_paragraph(ColorizedString.new(warn_msg).yellow)
498
- elsif unidentified_errors.present?
510
+ else
499
511
  heading = ColorizedString.new('Unidentified error(s):').red
500
512
  msg = FormattedLogger.indent_four(unidentified_errors.join)
501
513
  @logger.summary.add_paragraph("#{heading}\n#{msg}")
@@ -9,6 +9,7 @@ module KubernetesDeploy
9
9
  }
10
10
  DEFAULT_TIMEOUT = 15
11
11
  MAX_RETRY_DELAY = 16
12
+ SERVER_DRY_RUN_MIN_VERSION = "1.13"
12
13
 
13
14
  class ResourceNotFoundError < StandardError; end
14
15
 
@@ -84,6 +85,10 @@ module KubernetesDeploy
84
85
  version_info[:server]
85
86
  end
86
87
 
88
+ def server_dry_run_enabled?
89
+ server_version >= Gem::Version.new(SERVER_DRY_RUN_MIN_VERSION)
90
+ end
91
+
87
92
  private
88
93
 
89
94
  def build_command_from_options(args, use_namespace, use_context, output)
@@ -121,6 +121,7 @@ module KubernetesDeploy
121
121
  @validation_errors = []
122
122
  @validation_warnings = []
123
123
  @instance_data = {}
124
+ @server_dry_run_validated = false
124
125
  end
125
126
 
126
127
  def to_kubeclient_resource
@@ -346,10 +347,14 @@ module KubernetesDeploy
346
347
  self.class::SENSITIVE_TEMPLATE_CONTENT
347
348
  end
348
349
 
349
- def server_dry_runnable?
350
+ def server_dry_runnable_resource?
350
351
  self.class::SERVER_DRY_RUNNABLE
351
352
  end
352
353
 
354
+ def server_dry_run_validated?
355
+ @server_dry_run_validated
356
+ end
357
+
353
358
  class Event
354
359
  EVENT_SEPARATOR = "ENDEVENT--BEGINEVENT"
355
360
  FIELD_SEPARATOR = "ENDFIELD--BEGINFIELD"
@@ -471,12 +476,15 @@ module KubernetesDeploy
471
476
  end
472
477
 
473
478
  def validate_spec_with_kubectl(kubectl)
474
- _, err, st = validate_with_dry_run_option(kubectl, "--dry-run")
475
- if st.success? && server_dry_runnable?
479
+ err = ""
480
+ if kubectl.server_dry_run_enabled? && server_dry_runnable_resource?
476
481
  _, err, st = validate_with_dry_run_option(kubectl, "--server-dry-run")
477
- if st.success? || err.match(SERVER_DRY_RUN_DISABLED_ERROR)
478
- return true
479
- end
482
+ @server_dry_run_validated = st.success?
483
+ return true if st.success?
484
+ end
485
+
486
+ if err.empty? || err.match(SERVER_DRY_RUN_DISABLED_ERROR)
487
+ _, err, st = validate_with_dry_run_option(kubectl, "--dry-run")
480
488
  end
481
489
 
482
490
  return true if st.success?
@@ -6,9 +6,9 @@ module KubernetesDeploy
6
6
 
7
7
  STDIN_TEMP_FILE = "from_stdin.yml.erb"
8
8
  class << self
9
- def with_processed_template_paths(template_paths)
9
+ def with_processed_template_paths(template_paths, require_explicit_path: false)
10
10
  validated_paths = []
11
- if template_paths.empty?
11
+ if template_paths.empty? && !require_explicit_path
12
12
  validated_paths << default_template_dir
13
13
  else
14
14
  template_paths.uniq!
@@ -3,18 +3,16 @@ require 'tempfile'
3
3
 
4
4
  require 'kubernetes-deploy/common'
5
5
  require 'kubernetes-deploy/renderer'
6
+ require 'kubernetes-deploy/template_sets'
6
7
 
7
8
  module KubernetesDeploy
8
9
  class RenderTask
9
- def initialize(logger: nil, current_sha:, template_dir:, bindings:)
10
+ def initialize(logger: nil, current_sha:, template_dir: nil, template_paths: [], bindings:)
10
11
  @logger = logger || KubernetesDeploy::FormattedLogger.build
11
12
  @template_dir = template_dir
12
- @renderer = KubernetesDeploy::Renderer.new(
13
- current_sha: current_sha,
14
- bindings: bindings,
15
- template_dir: @template_dir,
16
- logger: @logger,
17
- )
13
+ @template_paths = template_paths.map { |path| File.expand_path(path) }
14
+ @bindings = bindings
15
+ @current_sha = current_sha
18
16
  end
19
17
 
20
18
  def run(*args)
@@ -28,16 +26,12 @@ module KubernetesDeploy
28
26
  @logger.reset
29
27
  @logger.phase_heading("Initializing render task")
30
28
 
31
- filenames = if only_filenames.empty?
32
- Dir.foreach(@template_dir).select { |filename| filename.end_with?(".yml.erb", ".yml", ".yaml", ".yaml.erb") }
33
- else
34
- only_filenames
35
- end
29
+ ts = TemplateSets.from_dirs_and_files(paths: template_sets_paths(only_filenames), logger: @logger)
36
30
 
37
- validate_configuration(filenames)
38
- render_filenames(stream, filenames)
31
+ validate_configuration(ts, only_filenames)
32
+ count = render_templates(stream, ts)
39
33
 
40
- @logger.summary.add_action("Successfully rendered #{filenames.size} template(s)")
34
+ @logger.summary.add_action("Successfully rendered #{count} template(s)")
41
35
  @logger.print_summary(:success)
42
36
  rescue KubernetesDeploy::FatalDeploymentError
43
37
  @logger.print_summary(:failure)
@@ -46,29 +40,38 @@ module KubernetesDeploy
46
40
 
47
41
  private
48
42
 
49
- def render_filenames(stream, filenames)
50
- exceptions = []
51
- @logger.phase_heading("Rendering template(s)")
52
-
53
- filenames.each do |filename|
54
- begin
55
- render_filename(filename, stream)
56
- rescue KubernetesDeploy::InvalidTemplateError => exception
57
- exceptions << exception
58
- log_invalid_template(exception)
43
+ def template_sets_paths(only_filenames)
44
+ if @template_paths.present?
45
+ # Validation will catch @template_paths & @template_dir being present
46
+ @template_paths
47
+ elsif only_filenames.blank?
48
+ [File.expand_path(@template_dir || '')]
49
+ else
50
+ absolute_template_dir = File.expand_path(@template_dir || '')
51
+ only_filenames.map do |name|
52
+ File.join(absolute_template_dir, name)
59
53
  end
60
54
  end
55
+ end
61
56
 
62
- unless exceptions.empty?
63
- raise exceptions[0]
57
+ def render_templates(stream, template_sets)
58
+ @logger.phase_heading("Rendering template(s)")
59
+ count = 0
60
+ template_sets.with_resource_definitions_and_filename(render_erb: true,
61
+ current_sha: @current_sha, bindings: @bindings, raw: true) do |rendered_content, filename|
62
+ write_to_stream(rendered_content, filename, stream)
63
+ count += 1
64
64
  end
65
+
66
+ count
67
+ rescue KubernetesDeploy::InvalidTemplateError => exception
68
+ log_invalid_template(exception)
69
+ raise
65
70
  end
66
71
 
67
- def render_filename(filename, stream)
72
+ def write_to_stream(rendered_content, filename, stream)
68
73
  file_basename = File.basename(filename)
69
74
  @logger.info("Rendering #{file_basename}...")
70
- file_content = File.read(File.join(@template_dir, filename))
71
- rendered_content = @renderer.render_template(filename, file_content)
72
75
  implicit = []
73
76
  YAML.parse_stream(rendered_content, "<rendered> #{filename}") { |d| implicit << d.implicit }
74
77
  if rendered_content.present?
@@ -82,28 +85,32 @@ module KubernetesDeploy
82
85
  raise InvalidTemplateError.new("Template is not valid YAML. #{exception.message}", filename: filename)
83
86
  end
84
87
 
85
- def validate_configuration(filenames)
88
+ def validate_configuration(template_sets, filenames)
86
89
  @logger.info("Validating configuration")
87
90
  errors = []
88
-
89
- if filenames.empty?
90
- errors << "no templates found in template dir #{@template_dir}"
91
+ if @template_dir.present? && @template_paths.present?
92
+ errors << "template_dir and template_paths can not be combined"
93
+ elsif @template_dir.blank? && @template_paths.blank?
94
+ errors << "template_dir or template_paths must be set"
91
95
  end
92
96
 
93
- absolute_template_dir = File.expand_path(@template_dir)
94
-
95
- filenames.each do |filename|
96
- absolute_file = File.expand_path(File.join(@template_dir, filename))
97
- if !File.exist?(absolute_file)
98
- errors << "Filename \"#{absolute_file}\" could not be found"
99
- elsif !File.file?(absolute_file)
100
- errors << "Filename \"#{absolute_file}\" is not a file"
101
- elsif !absolute_file.start_with?(absolute_template_dir)
102
- errors << "Filename \"#{absolute_file}\" is outside the template directory," \
103
- " which was resolved as #{absolute_template_dir}"
97
+ if filenames.present?
98
+ if @template_dir.nil?
99
+ errors << "template_dir must be set to use filenames"
100
+ else
101
+ absolute_template_dir = File.expand_path(@template_dir)
102
+ filenames.each do |filename|
103
+ absolute_file = File.expand_path(File.join(@template_dir, filename))
104
+ unless absolute_file.start_with?(absolute_template_dir)
105
+ errors << "Filename \"#{absolute_file}\" is outside the template directory," \
106
+ " which was resolved as #{absolute_template_dir}"
107
+ end
108
+ end
104
109
  end
105
110
  end
106
111
 
112
+ errors += template_sets.validate
113
+
107
114
  unless errors.empty?
108
115
  @logger.summary.add_action("Configuration invalid")
109
116
  @logger.summary.add_paragraph(errors.map { |err| "- #{err}" }.join("\n"))
@@ -154,8 +154,8 @@ module KubernetesDeploy
154
154
  raise TaskConfigurationError, message
155
155
  end
156
156
 
157
- container.command = entrypoint
158
- container.args = args
157
+ container.command = entrypoint if entrypoint
158
+ container.args = args if args
159
159
 
160
160
  env_args = env_vars.map do |env|
161
161
  key, value = env.split('=', 2)
@@ -5,17 +5,11 @@ module KubernetesDeploy
5
5
  super(*arguments)
6
6
  @template = template
7
7
  @args = args
8
- @validations += %i(validate_template validate_args)
8
+ @validations += %i(validate_template)
9
9
  end
10
10
 
11
11
  private
12
12
 
13
- def validate_args
14
- if @args.blank?
15
- @errors << "Args can't be nil"
16
- end
17
- end
18
-
19
13
  def validate_template
20
14
  if @template.blank?
21
15
  @errors << "Task template name can't be nil"
@@ -1,17 +1,21 @@
1
1
  # frozen_string_literal: true
2
+ require 'kubernetes-deploy/delayed_exceptions'
3
+ require 'kubernetes-deploy/ejson_secret_provisioner'
2
4
 
3
5
  module KubernetesDeploy
4
6
  class TemplateSets
7
+ include DelayedExceptions
5
8
  VALID_TEMPLATES = %w(.yml.erb .yml .yaml .yaml.erb)
6
9
  # private inner class
7
10
  class TemplateSet
11
+ include DelayedExceptions
8
12
  def initialize(template_dir:, file_whitelist: [], logger:)
9
13
  @template_dir = template_dir
10
14
  @files = file_whitelist
11
15
  @logger = logger
12
16
  end
13
17
 
14
- def with_resource_definitions(render_erb: false, current_sha: nil, bindings: nil)
18
+ def with_resource_definitions_and_filename(render_erb: false, current_sha: nil, bindings: nil, raw: false)
15
19
  if render_erb
16
20
  @renderer = Renderer.new(
17
21
  template_dir: @template_dir,
@@ -20,11 +24,9 @@ module KubernetesDeploy
20
24
  bindings: bindings,
21
25
  )
22
26
  end
23
- @files.each do |filename|
27
+ with_delayed_exceptions(@files, KubernetesDeploy::InvalidTemplateError) do |filename|
24
28
  next if filename.end_with?(EjsonSecretProvisioner::EJSON_SECRETS_FILE)
25
- templates(filename: filename) do |r_def|
26
- yield r_def
27
- end
29
+ templates(filename: filename, raw: raw) { |r_def| yield r_def, filename }
28
30
  end
29
31
  end
30
32
 
@@ -59,7 +61,7 @@ module KubernetesDeploy
59
61
 
60
62
  private
61
63
 
62
- def templates(filename:)
64
+ def templates(filename:, raw:)
63
65
  file_content = File.read(File.join(@template_dir, filename))
64
66
  rendered_content = @renderer ? @renderer.render_template(filename, file_content) : file_content
65
67
  YAML.load_stream(rendered_content, "<rendered> #{filename}") do |doc|
@@ -68,8 +70,9 @@ module KubernetesDeploy
68
70
  raise InvalidTemplateError.new("Template is not a valid Kubernetes manifest",
69
71
  filename: filename, content: doc)
70
72
  end
71
- yield doc
73
+ yield doc unless raw
72
74
  end
75
+ yield rendered_content if raw
73
76
  rescue InvalidTemplateError => err
74
77
  err.filename ||= filename
75
78
  raise err
@@ -106,18 +109,26 @@ module KubernetesDeploy
106
109
  end
107
110
  end
108
111
 
109
- def with_resource_definitions(render_erb: false, current_sha: nil, bindings: nil)
110
- @template_sets.each do |template_set|
111
- template_set.with_resource_definitions(
112
+ def with_resource_definitions_and_filename(render_erb: false, current_sha: nil, bindings: nil, raw: false)
113
+ with_delayed_exceptions(@template_sets, KubernetesDeploy::InvalidTemplateError) do |template_set|
114
+ template_set.with_resource_definitions_and_filename(
112
115
  render_erb: render_erb,
113
116
  current_sha: current_sha,
114
- bindings: bindings
115
- ) do |r_def|
116
- yield r_def
117
+ bindings: bindings,
118
+ raw: raw
119
+ ) do |r_def, filename|
120
+ yield r_def, filename
117
121
  end
118
122
  end
119
123
  end
120
124
 
125
+ def with_resource_definitions(render_erb: false, current_sha: nil, bindings: nil, raw: false)
126
+ with_resource_definitions_and_filename(render_erb: render_erb,
127
+ current_sha: current_sha, bindings: bindings, raw: raw) do |r_def, _|
128
+ yield r_def
129
+ end
130
+ end
131
+
121
132
  def ejson_secrets_files
122
133
  @template_sets.map(&:ejson_secrets_file).compact
123
134
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module KubernetesDeploy
3
- VERSION = "0.28.0"
3
+ VERSION = "0.29.0"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kubernetes-deploy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.0
4
+ version: 0.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katrina Verey
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2019-09-20 00:00:00.000000000 Z
12
+ date: 2019-09-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -86,9 +86,6 @@ dependencies:
86
86
  requirement: !ruby/object:Gem::Requirement
87
87
  requirements:
88
88
  - - "~>"
89
- - !ruby/object:Gem::Version
90
- version: '2.3'
91
- - - ">="
92
89
  - !ruby/object:Gem::Version
93
90
  version: 2.3.2
94
91
  type: :runtime
@@ -96,9 +93,6 @@ dependencies:
96
93
  version_requirements: !ruby/object:Gem::Requirement
97
94
  requirements:
98
95
  - - "~>"
99
- - !ruby/object:Gem::Version
100
- version: '2.3'
101
- - - ">="
102
96
  - !ruby/object:Gem::Version
103
97
  version: 2.3.2
104
98
  - !ruby/object:Gem::Dependency
@@ -279,8 +273,11 @@ files:
279
273
  - exe/kubernetes-run
280
274
  - kubernetes-deploy.gemspec
281
275
  - lib/krane.rb
276
+ - lib/krane/cli/deploy_command.rb
282
277
  - lib/krane/cli/krane.rb
278
+ - lib/krane/cli/render_command.rb
283
279
  - lib/krane/cli/restart_command.rb
280
+ - lib/krane/cli/run_command.rb
284
281
  - lib/krane/cli/version_command.rb
285
282
  - lib/kubernetes-deploy.rb
286
283
  - lib/kubernetes-deploy/bindings_parser.rb
@@ -289,6 +286,7 @@ files:
289
286
  - lib/kubernetes-deploy/concurrency.rb
290
287
  - lib/kubernetes-deploy/container_logs.rb
291
288
  - lib/kubernetes-deploy/deferred_summary_logging.rb
289
+ - lib/kubernetes-deploy/delayed_exceptions.rb
292
290
  - lib/kubernetes-deploy/deploy_task.rb
293
291
  - lib/kubernetes-deploy/duration_parser.rb
294
292
  - lib/kubernetes-deploy/ejson_secret_provisioner.rb
@@ -357,7 +355,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
357
355
  requirements:
358
356
  - - ">="
359
357
  - !ruby/object:Gem::Version
360
- version: 2.3.0
358
+ version: 2.4.0
361
359
  required_rubygems_version: !ruby/object:Gem::Requirement
362
360
  requirements:
363
361
  - - ">="