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.
- 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
|