kubernetes-deploy 0.26.7 → 0.27.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/CHANGELOG.md +16 -0
  4. data/CONTRIBUTING.md +1 -1
  5. data/README.md +1 -0
  6. data/exe/kubernetes-deploy +22 -6
  7. data/exe/kubernetes-render +7 -6
  8. data/exe/kubernetes-restart +3 -3
  9. data/exe/kubernetes-run +1 -3
  10. data/lib/kubernetes-deploy.rb +3 -27
  11. data/lib/kubernetes-deploy/common.rb +24 -0
  12. data/lib/kubernetes-deploy/deferred_summary_logging.rb +2 -0
  13. data/lib/kubernetes-deploy/deploy_task.rb +58 -65
  14. data/lib/kubernetes-deploy/duration_parser.rb +2 -0
  15. data/lib/kubernetes-deploy/ejson_secret_provisioner.rb +7 -14
  16. data/lib/kubernetes-deploy/errors.rb +0 -8
  17. data/lib/kubernetes-deploy/formatted_logger.rb +1 -0
  18. data/lib/kubernetes-deploy/kubeclient_builder.rb +2 -2
  19. data/lib/kubernetes-deploy/kubectl.rb +1 -0
  20. data/lib/kubernetes-deploy/kubernetes_resource.rb +3 -1
  21. data/lib/kubernetes-deploy/kubernetes_resource/custom_resource_definition.rb +0 -2
  22. data/lib/kubernetes-deploy/kubernetes_resource/deployment.rb +2 -0
  23. data/lib/kubernetes-deploy/kubernetes_resource/pod_set_base.rb +2 -0
  24. data/lib/kubernetes-deploy/kubernetes_resource/replica_set.rb +1 -0
  25. data/lib/kubernetes-deploy/kubernetes_resource/service.rb +21 -8
  26. data/lib/kubernetes-deploy/options_helper.rb +25 -12
  27. data/lib/kubernetes-deploy/render_task.rb +4 -4
  28. data/lib/kubernetes-deploy/resource_watcher.rb +4 -0
  29. data/lib/kubernetes-deploy/restart_task.rb +17 -13
  30. data/lib/kubernetes-deploy/runner_task.rb +9 -5
  31. data/lib/kubernetes-deploy/task_config.rb +16 -0
  32. data/lib/kubernetes-deploy/task_config_validator.rb +96 -0
  33. data/lib/kubernetes-deploy/template_sets.rb +135 -0
  34. data/lib/kubernetes-deploy/version.rb +1 -1
  35. metadata +7 -7
  36. data/lib/kubernetes-deploy/kubernetes_resource/memcached.rb +0 -43
  37. data/lib/kubernetes-deploy/kubernetes_resource/redis.rb +0 -56
  38. data/lib/kubernetes-deploy/template_discovery.rb +0 -15
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/duration'
4
+
3
5
  module KubernetesDeploy
4
6
  ##
5
7
  # This class is a less strict extension of ActiveSupport::Duration::ISO8601Parser.
@@ -17,10 +17,11 @@ module KubernetesDeploy
17
17
  EJSON_SECRETS_FILE = "secrets.ejson"
18
18
  EJSON_KEYS_SECRET = "ejson-keys"
19
19
 
20
- def initialize(namespace:, context:, template_dir:, logger:, statsd_tags:, selector: nil)
20
+ def initialize(namespace:, context:, ejson_keys_secret:, ejson_file:, logger:, statsd_tags:, selector: nil)
21
21
  @namespace = namespace
22
22
  @context = context
23
- @ejson_file = "#{template_dir}/#{EJSON_SECRETS_FILE}"
23
+ @ejson_keys_secret = ejson_keys_secret
24
+ @ejson_file = ejson_file
24
25
  @logger = logger
25
26
  @statsd_tags = statsd_tags
26
27
  @selector = selector
@@ -37,20 +38,12 @@ module KubernetesDeploy
37
38
  @resources ||= build_secrets
38
39
  end
39
40
 
40
- def ejson_keys_secret
41
- @ejson_keys_secret ||= begin
42
- out, err, st = @kubectl.run("get", "secret", EJSON_KEYS_SECRET, output: "json",
43
- raise_if_not_found: true, attempts: 3, output_is_sensitive: true, log_failure: true)
44
- unless st.success?
45
- raise EjsonSecretError, "Error retrieving Secret/#{EJSON_KEYS_SECRET}: #{err}"
46
- end
47
- JSON.parse(out)
48
- end
49
- end
50
-
51
41
  private
52
42
 
53
43
  def build_secrets
44
+ unless @ejson_keys_secret
45
+ raise EjsonSecretError, "Secret #{EJSON_KEYS_SECRET} not provided, cannot decrypt secrets"
46
+ end
54
47
  return [] unless File.exist?(@ejson_file)
55
48
  with_decrypted_ejson do |decrypted|
56
49
  secrets = decrypted[EJSON_SECRET_KEY]
@@ -153,7 +146,7 @@ module KubernetesDeploy
153
146
  end
154
147
 
155
148
  def fetch_private_key_from_secret
156
- encoded_private_key = ejson_keys_secret["data"][public_key]
149
+ encoded_private_key = @ejson_keys_secret["data"][public_key]
157
150
  unless encoded_private_key
158
151
  raise EjsonSecretError, "Private key for #{public_key} not found in #{EJSON_KEYS_SECRET} secret"
159
152
  end
@@ -30,12 +30,4 @@ module KubernetesDeploy
30
30
  "kubernetes-deploy will not continue since it is extremely unlikely that this secret should be pruned.")
31
31
  end
32
32
  end
33
-
34
- module Errors
35
- extend self
36
- def server_version_warning(server_version)
37
- "Minimum cluster version requirement of #{MIN_KUBE_VERSION} not met. "\
38
- "Using #{server_version} could result in unexpected behavior as it is no longer tested against"
39
- end
40
- end
41
33
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require 'logger'
3
+ require 'colorized_string'
3
4
  require 'kubernetes-deploy/deferred_summary_logging'
4
5
 
5
6
  module KubernetesDeploy
@@ -106,11 +106,11 @@ module KubernetesDeploy
106
106
  def validate_config_files
107
107
  errors = []
108
108
  if @kubeconfig_files.empty?
109
- errors << "Kube config file name(s) not set in $KUBECONFIG"
109
+ errors << "Kubeconfig file name(s) not set in $KUBECONFIG"
110
110
  else
111
111
  @kubeconfig_files.each do |f|
112
112
  # If any files in the list are not valid, we can't be sure the merged context list is what the user intended
113
- errors << "Kube config not found at #{f}" unless File.file?(f)
113
+ errors << "Kubeconfig not found at #{f}" unless File.file?(f)
114
114
  end
115
115
  end
116
116
  errors
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'open3'
2
3
 
3
4
  module KubernetesDeploy
4
5
  class Kubectl
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
  require 'json'
3
- require 'open3'
4
3
  require 'shellwords'
5
4
 
6
5
  require 'kubernetes-deploy/remote_logs'
6
+ require 'kubernetes-deploy/duration_parser'
7
+ require 'kubernetes-deploy/label_selector'
8
+ require 'kubernetes-deploy/rollout_conditions'
7
9
 
8
10
  module KubernetesDeploy
9
11
  class KubernetesResource
@@ -1,6 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require 'kubernetes-deploy/rollout_conditions'
3
-
4
2
  module KubernetesDeploy
5
3
  class CustomResourceDefinition < KubernetesResource
6
4
  TIMEOUT = 2.minutes
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require 'kubernetes-deploy/kubernetes_resource/replica_set'
3
+
2
4
  module KubernetesDeploy
3
5
  class Deployment < KubernetesResource
4
6
  TIMEOUT = 7.minutes
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require 'kubernetes-deploy/kubernetes_resource/pod'
3
+
2
4
  module KubernetesDeploy
3
5
  class PodSetBase < KubernetesResource
4
6
  def failure_message
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require 'kubernetes-deploy/kubernetes_resource/pod_set_base'
3
+
3
4
  module KubernetesDeploy
4
5
  class ReplicaSet < PodSetBase
5
6
  TIMEOUT = 5.minutes
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require 'kubernetes-deploy/kubernetes_resource/pod'
3
+
2
4
  module KubernetesDeploy
3
5
  class Service < KubernetesResource
4
6
  TIMEOUT = 7.minutes
@@ -6,11 +8,11 @@ module KubernetesDeploy
6
8
  def sync(cache)
7
9
  super
8
10
  if exists? && selector.present?
9
- @related_deployments = cache.get_all(Deployment.kind, selector)
10
11
  @related_pods = cache.get_all(Pod.kind, selector)
12
+ @related_workloads = fetch_related_workloads(cache)
11
13
  else
12
- @related_deployments = []
13
14
  @related_pods = []
15
+ @related_workloads = []
14
16
  end
15
17
  end
16
18
 
@@ -30,7 +32,7 @@ module KubernetesDeploy
30
32
  return false unless exists?
31
33
  return exists? unless requires_endpoints?
32
34
  # We can't use endpoints if we want the service to be able to fail fast when the pods are down
33
- exposes_zero_replica_deployment? || selects_some_pods?
35
+ exposes_zero_replica_workload? || selects_some_pods?
34
36
  end
35
37
 
36
38
  def deploy_failed?
@@ -38,18 +40,27 @@ module KubernetesDeploy
38
40
  end
39
41
 
40
42
  def timeout_message
41
- "This service does not seem to select any pods. This means its spec.selector is probably incorrect."
43
+ "This service does not seem to select any pods and this is likely invalid. "\
44
+ "Please confirm the spec.selector is correct and the targeted workload is healthy."
42
45
  end
43
46
 
44
47
  private
45
48
 
46
- def exposes_zero_replica_deployment?
49
+ def fetch_related_workloads(cache)
50
+ related_deployments = cache.get_all(Deployment.kind)
51
+ related_statefulsets = cache.get_all(StatefulSet.kind)
52
+ (related_deployments + related_statefulsets).select do |workload|
53
+ selector.all? { |k, v| workload['spec']['template']['metadata']['labels'][k] == v }
54
+ end
55
+ end
56
+
57
+ def exposes_zero_replica_workload?
47
58
  return false unless related_replica_count
48
59
  related_replica_count == 0
49
60
  end
50
61
 
51
62
  def requires_endpoints?
52
- # service of type External don't have endpoints
63
+ # services of type External don't have endpoints
53
64
  return false if external_name_svc?
54
65
 
55
66
  # problem counting replicas - by default, assume endpoints are required
@@ -69,8 +80,10 @@ module KubernetesDeploy
69
80
 
70
81
  def related_replica_count
71
82
  return 0 unless selector.present?
72
- return unless @related_deployments.length == 1
73
- @related_deployments.first["spec"]["replicas"].to_i
83
+
84
+ if @related_workloads.present?
85
+ @related_workloads.inject(0) { |sum, d| sum + d["spec"]["replicas"].to_i }
86
+ end
74
87
  end
75
88
 
76
89
  def external_name_svc?
@@ -6,30 +6,43 @@ module KubernetesDeploy
6
6
 
7
7
  STDIN_TEMP_FILE = "from_stdin.yml.erb"
8
8
  class << self
9
- def with_validated_template_dir(template_dir)
10
- if template_dir == '-'
9
+ def with_processed_template_paths(template_paths)
10
+ validated_paths = []
11
+ if template_paths.empty?
12
+ validated_paths << default_template_dir
13
+ else
14
+ template_paths.uniq!
15
+ template_paths.each do |template_path|
16
+ next if template_path == '-'
17
+ validated_paths << template_path
18
+ end
19
+ end
20
+
21
+ if template_paths.include?("-")
11
22
  Dir.mktmpdir("kubernetes-deploy") do |dir|
12
23
  template_dir_from_stdin(temp_dir: dir)
13
- yield dir
24
+ validated_paths << dir
25
+ yield validated_paths
14
26
  end
15
- elsif template_dir
16
- yield template_dir
17
27
  else
18
- yield default_template_dir(template_dir)
28
+ yield validated_paths
19
29
  end
20
30
  end
21
31
 
22
32
  private
23
33
 
24
- def default_template_dir(template_dir)
25
- if ENV.key?("ENVIRONMENT")
26
- template_dir = File.join("config", "deploy", ENV['ENVIRONMENT'])
34
+ def default_template_dir
35
+ template_dir = if ENV.key?("ENVIRONMENT")
36
+ File.join("config", "deploy", ENV['ENVIRONMENT'])
27
37
  end
28
38
 
29
- if !template_dir || template_dir.empty?
39
+ unless template_dir
30
40
  raise OptionsError, "Template directory is unknown. " \
31
- "Either specify --template-dir argument or set $ENVIRONMENT to use config/deploy/$ENVIRONMENT " \
32
- "as a default path."
41
+ "Either specify --template-dir argument or set $ENVIRONMENT to use config/deploy/$ENVIRONMENT " \
42
+ "as a default path."
43
+ end
44
+ unless Dir.exist?(template_dir)
45
+ raise OptionsError, "Template directory #{template_dir} does not exist."
33
46
  end
34
47
 
35
48
  template_dir
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
  require 'tempfile'
3
3
 
4
+ require 'kubernetes-deploy/common'
4
5
  require 'kubernetes-deploy/renderer'
5
- require 'kubernetes-deploy/template_discovery'
6
6
 
7
7
  module KubernetesDeploy
8
8
  class RenderTask
9
- def initialize(logger:, current_sha:, template_dir:, bindings:)
10
- @logger = logger
9
+ def initialize(logger: nil, current_sha:, template_dir:, bindings:)
10
+ @logger = logger || KubernetesDeploy::FormattedLogger.build
11
11
  @template_dir = template_dir
12
12
  @renderer = KubernetesDeploy::Renderer.new(
13
13
  current_sha: current_sha,
@@ -29,7 +29,7 @@ module KubernetesDeploy
29
29
  @logger.phase_heading("Initializing render task")
30
30
 
31
31
  filenames = if only_filenames.empty?
32
- TemplateDiscovery.new(@template_dir).templates
32
+ Dir.foreach(@template_dir).select { |filename| filename.end_with?(".yml.erb", ".yml", ".yaml", ".yaml.erb") }
33
33
  else
34
34
  only_filenames
35
35
  end
@@ -1,4 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require 'kubernetes-deploy/concurrency'
4
+ require 'kubernetes-deploy/resource_cache'
5
+
2
6
  module KubernetesDeploy
3
7
  class ResourceWatcher
4
8
  extend KubernetesDeploy::StatsD::MeasureMethods
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
+ require 'kubernetes-deploy/common'
3
+ require 'kubernetes-deploy/kubernetes_resource'
4
+ require 'kubernetes-deploy/kubernetes_resource/deployment'
2
5
  require 'kubernetes-deploy/kubeclient_builder'
3
6
  require 'kubernetes-deploy/resource_watcher'
4
7
  require 'kubernetes-deploy/kubectl'
@@ -18,10 +21,11 @@ module KubernetesDeploy
18
21
  HTTP_OK_RANGE = 200..299
19
22
  ANNOTATION = "shipit.shopify.io/restart"
20
23
 
21
- def initialize(context:, namespace:, logger:, max_watch_seconds: nil)
24
+ def initialize(context:, namespace:, logger: nil, max_watch_seconds: nil)
25
+ @logger = logger || KubernetesDeploy::FormattedLogger.build(namespace, context)
26
+ @task_config = KubernetesDeploy::TaskConfig.new(context, namespace, @logger)
22
27
  @context = context
23
28
  @namespace = namespace
24
- @logger = logger
25
29
  @max_watch_seconds = max_watch_seconds
26
30
  end
27
31
 
@@ -37,11 +41,9 @@ module KubernetesDeploy
37
41
  @logger.reset
38
42
 
39
43
  @logger.phase_heading("Initializing restart")
40
- verify_namespace
44
+ verify_config!
41
45
  deployments = identify_target_deployments(deployments_names, selector: selector)
42
- if kubectl.server_version < Gem::Version.new(MIN_KUBE_VERSION)
43
- @logger.warn(KubernetesDeploy::Errors.server_version_warning(kubectl.server_version))
44
- end
46
+
45
47
  @logger.phase_heading("Triggering restart by touching ENV[RESTARTED_AT]")
46
48
  patch_kubeclient_deployments(deployments)
47
49
 
@@ -117,13 +119,6 @@ module KubernetesDeploy
117
119
  end
118
120
  end
119
121
 
120
- def verify_namespace
121
- kubeclient.get_namespace(@namespace)
122
- @logger.info("Namespace #{@namespace} found in context #{@context}")
123
- rescue Kubeclient::ResourceNotFoundError
124
- raise NamespaceNotFoundError.new(@namespace, @context)
125
- end
126
-
127
122
  def patch_deployment_with_restart(record)
128
123
  v1beta1_kubeclient.patch_deployment(
129
124
  record.metadata.name,
@@ -173,6 +168,15 @@ module KubernetesDeploy
173
168
  }
174
169
  end
175
170
 
171
+ def verify_config!
172
+ task_config_validator = TaskConfigValidator.new(@task_config, kubectl, kubeclient_builder)
173
+ unless task_config_validator.valid?
174
+ @logger.summary.add_action("Configuration invalid")
175
+ @logger.summary.add_paragraph(task_config_validator.errors.map { |err| "- #{err}" }.join("\n"))
176
+ raise KubernetesDeploy::TaskConfigurationError
177
+ end
178
+ end
179
+
176
180
  def kubeclient
177
181
  @kubeclient ||= kubeclient_builder.build_v1_kubeclient(@context)
178
182
  end
@@ -1,8 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
  require 'tempfile'
3
3
 
4
+ require 'kubernetes-deploy/common'
4
5
  require 'kubernetes-deploy/kubeclient_builder'
5
6
  require 'kubernetes-deploy/kubectl'
7
+ require 'kubernetes-deploy/resource_cache'
8
+ require 'kubernetes-deploy/resource_watcher'
9
+ require 'kubernetes-deploy/kubernetes_resource'
10
+ require 'kubernetes-deploy/kubernetes_resource/pod'
6
11
 
7
12
  module KubernetesDeploy
8
13
  class RunnerTask
@@ -10,8 +15,9 @@ module KubernetesDeploy
10
15
 
11
16
  attr_reader :pod_name
12
17
 
13
- def initialize(namespace:, context:, logger:, max_watch_seconds: nil)
14
- @logger = logger
18
+ def initialize(namespace:, context:, logger: nil, max_watch_seconds: nil)
19
+ @logger = logger || KubernetesDeploy::FormattedLogger.build(namespace, context)
20
+ @task_config = KubernetesDeploy::TaskConfig.new(context, namespace, @logger)
15
21
  @namespace = namespace
16
22
  @context = context
17
23
  @max_watch_seconds = max_watch_seconds
@@ -135,9 +141,7 @@ module KubernetesDeploy
135
141
  raise TaskConfigurationError, "Configuration invalid: #{errors.join(', ')}"
136
142
  end
137
143
 
138
- if kubectl.server_version < Gem::Version.new(MIN_KUBE_VERSION)
139
- @logger.warn(KubernetesDeploy::Errors.server_version_warning(kubectl.server_version))
140
- end
144
+ TaskConfigValidator.new(@task_config, kubectl, kubeclient_builder, only: [:validate_server_version]).valid?
141
145
  end
142
146
 
143
147
  def get_template(template_name)
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module KubernetesDeploy
3
+ class TaskConfig
4
+ attr_reader :context, :namespace
5
+
6
+ def initialize(context, namespace, logger = nil)
7
+ @context = context
8
+ @namespace = namespace
9
+ @logger = logger
10
+ end
11
+
12
+ def logger
13
+ @logger ||= KubernetesDeploy::FormattedLogger.build(@namespace, @context)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+ module KubernetesDeploy
3
+ class TaskConfigValidator
4
+ DEFAULT_VALIDATIONS = %i(
5
+ validate_kubeconfig
6
+ validate_context_exists_in_kubeconfig
7
+ validate_context_reachable
8
+ validate_server_version
9
+ validate_namespace_exists
10
+ ).freeze
11
+
12
+ delegate :context, :namespace, :logger, to: :@task_config
13
+
14
+ def initialize(task_config, kubectl, kubeclient_builder, only: nil)
15
+ @task_config = task_config
16
+ @kubectl = kubectl
17
+ @kubeclient_builder = kubeclient_builder
18
+ @errors = nil
19
+ @validations = only || DEFAULT_VALIDATIONS
20
+ end
21
+
22
+ def valid?
23
+ @errors = []
24
+ @validations.each do |validator_name|
25
+ break if @errors.present?
26
+ send(validator_name)
27
+ end
28
+ @errors.empty?
29
+ end
30
+
31
+ def errors
32
+ valid?
33
+ @errors
34
+ end
35
+
36
+ private
37
+
38
+ def validate_kubeconfig
39
+ @errors += @kubeclient_builder.validate_config_files
40
+ end
41
+
42
+ def validate_context_exists_in_kubeconfig
43
+ unless context.present?
44
+ return @errors << "Context can not be blank"
45
+ end
46
+
47
+ _, err, st = @kubectl.run("config", "get-contexts", context, "-o", "name",
48
+ use_namespace: false, use_context: false, log_failure: false)
49
+
50
+ unless st.success?
51
+ @errors << if err.match("error: context #{context} not found")
52
+ "Context #{context} missing from your kubeconfig file(s)"
53
+ else
54
+ "Something went wrong. #{err} "
55
+ end
56
+ end
57
+ end
58
+
59
+ def validate_context_reachable
60
+ _, err, st = @kubectl.run("get", "namespaces", "-o", "name",
61
+ use_namespace: false, log_failure: false)
62
+
63
+ unless st.success?
64
+ @errors << "Something went wrong connecting to #{context}. #{err} "
65
+ end
66
+ end
67
+
68
+ def validate_namespace_exists
69
+ unless namespace.present?
70
+ return @errors << "Namespace can not be blank"
71
+ end
72
+
73
+ _, err, st = @kubectl.run("get", "namespace", "-o", "name", namespace,
74
+ use_namespace: false, log_failure: false)
75
+
76
+ unless st.success?
77
+ @errors << if err.match("Error from server [(]NotFound[)]: namespace")
78
+ "Could not find Namespace: #{namespace} in Context: #{context}"
79
+ else
80
+ "Could not connect to kubernetes cluster. #{err}"
81
+ end
82
+ end
83
+ end
84
+
85
+ def validate_server_version
86
+ if @kubectl.server_version < Gem::Version.new(MIN_KUBE_VERSION)
87
+ logger.warn(server_version_warning(@kubectl.server_version))
88
+ end
89
+ end
90
+
91
+ def server_version_warning(server_version)
92
+ "Minimum cluster version requirement of #{MIN_KUBE_VERSION} not met. "\
93
+ "Using #{server_version} could result in unexpected behavior as it is no longer tested against"
94
+ end
95
+ end
96
+ end