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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +16 -0
- data/CONTRIBUTING.md +1 -1
- data/README.md +1 -0
- data/exe/kubernetes-deploy +22 -6
- data/exe/kubernetes-render +7 -6
- data/exe/kubernetes-restart +3 -3
- data/exe/kubernetes-run +1 -3
- data/lib/kubernetes-deploy.rb +3 -27
- data/lib/kubernetes-deploy/common.rb +24 -0
- data/lib/kubernetes-deploy/deferred_summary_logging.rb +2 -0
- data/lib/kubernetes-deploy/deploy_task.rb +58 -65
- data/lib/kubernetes-deploy/duration_parser.rb +2 -0
- data/lib/kubernetes-deploy/ejson_secret_provisioner.rb +7 -14
- data/lib/kubernetes-deploy/errors.rb +0 -8
- data/lib/kubernetes-deploy/formatted_logger.rb +1 -0
- data/lib/kubernetes-deploy/kubeclient_builder.rb +2 -2
- data/lib/kubernetes-deploy/kubectl.rb +1 -0
- data/lib/kubernetes-deploy/kubernetes_resource.rb +3 -1
- data/lib/kubernetes-deploy/kubernetes_resource/custom_resource_definition.rb +0 -2
- data/lib/kubernetes-deploy/kubernetes_resource/deployment.rb +2 -0
- data/lib/kubernetes-deploy/kubernetes_resource/pod_set_base.rb +2 -0
- data/lib/kubernetes-deploy/kubernetes_resource/replica_set.rb +1 -0
- data/lib/kubernetes-deploy/kubernetes_resource/service.rb +21 -8
- data/lib/kubernetes-deploy/options_helper.rb +25 -12
- data/lib/kubernetes-deploy/render_task.rb +4 -4
- data/lib/kubernetes-deploy/resource_watcher.rb +4 -0
- data/lib/kubernetes-deploy/restart_task.rb +17 -13
- data/lib/kubernetes-deploy/runner_task.rb +9 -5
- data/lib/kubernetes-deploy/task_config.rb +16 -0
- data/lib/kubernetes-deploy/task_config_validator.rb +96 -0
- data/lib/kubernetes-deploy/template_sets.rb +135 -0
- data/lib/kubernetes-deploy/version.rb +1 -1
- metadata +7 -7
- data/lib/kubernetes-deploy/kubernetes_resource/memcached.rb +0 -43
- data/lib/kubernetes-deploy/kubernetes_resource/redis.rb +0 -56
- data/lib/kubernetes-deploy/template_discovery.rb +0 -15
@@ -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:,
|
20
|
+
def initialize(namespace:, context:, ejson_keys_secret:, ejson_file:, logger:, statsd_tags:, selector: nil)
|
21
21
|
@namespace = namespace
|
22
22
|
@context = context
|
23
|
-
@
|
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
|
@@ -106,11 +106,11 @@ module KubernetesDeploy
|
|
106
106
|
def validate_config_files
|
107
107
|
errors = []
|
108
108
|
if @kubeconfig_files.empty?
|
109
|
-
errors << "
|
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 << "
|
113
|
+
errors << "Kubeconfig not found at #{f}" unless File.file?(f)
|
114
114
|
end
|
115
115
|
end
|
116
116
|
errors
|
@@ -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,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
|
-
|
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
|
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
|
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
|
-
#
|
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
|
-
|
73
|
-
@
|
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
|
10
|
-
|
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
|
-
|
24
|
+
validated_paths << dir
|
25
|
+
yield validated_paths
|
14
26
|
end
|
15
|
-
elsif template_dir
|
16
|
-
yield template_dir
|
17
27
|
else
|
18
|
-
yield
|
28
|
+
yield validated_paths
|
19
29
|
end
|
20
30
|
end
|
21
31
|
|
22
32
|
private
|
23
33
|
|
24
|
-
def default_template_dir
|
25
|
-
if ENV.key?("ENVIRONMENT")
|
26
|
-
|
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
|
-
|
39
|
+
unless template_dir
|
30
40
|
raise OptionsError, "Template directory is unknown. " \
|
31
|
-
|
32
|
-
|
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
|
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
|
-
|
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,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
|
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
|
-
|
44
|
+
verify_config!
|
41
45
|
deployments = identify_target_deployments(deployments_names, selector: selector)
|
42
|
-
|
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
|
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
|
-
|
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
|