kubernetes-deploy 0.26.7 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
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