kubernetes-deploy 0.26.4 → 0.26.5
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/.buildkite/pipeline.nightly.yml +7 -0
- data/.github/probots.yml +2 -0
- data/.shopify-build/VERSION +1 -0
- data/.shopify-build/kubernetes-deploy.yml +46 -0
- data/CHANGELOG.md +13 -0
- data/CONTRIBUTING.md +7 -1
- data/README.md +14 -2
- data/kubernetes-deploy.gemspec +1 -1
- data/lib/kubernetes-deploy.rb +1 -0
- data/lib/kubernetes-deploy/bindings_parser.rb +1 -1
- data/lib/kubernetes-deploy/deploy_task.rb +14 -15
- data/lib/kubernetes-deploy/ejson_secret_provisioner.rb +3 -2
- data/lib/kubernetes-deploy/kubeclient_builder.rb +9 -2
- data/lib/kubernetes-deploy/kubectl.rb +43 -20
- data/lib/kubernetes-deploy/kubernetes_resource.rb +15 -12
- data/lib/kubernetes-deploy/kubernetes_resource/custom_resource_definition.rb +5 -0
- data/lib/kubernetes-deploy/kubernetes_resource/persistent_volume_claim.rb +64 -2
- data/lib/kubernetes-deploy/kubernetes_resource/pod.rb +19 -4
- data/lib/kubernetes-deploy/version.rb +1 -1
- metadata +7 -6
- data/.buildkite/pipeline.yml +0 -29
- data/lib/kubernetes-deploy/kubeclient_builder/kube_config.rb +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c11bffff21178fb845d50aff17aa56f3044cb95b235ff6db2463c94def2d2589
|
|
4
|
+
data.tar.gz: 54ee8d3c14e3d7ca013d6e9a822aa0772dde1b4738bcce67efb3f96c490ddb08
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e79311a4c0c7af04db41ee384d5ed1da79afdb1f2676c02595ced432ddc6da38dc08179bc5791bbde34e6e0e8da01bd2ca102e0e4ed5af275ee0542b55a68ea8
|
|
7
|
+
data.tar.gz: b4db7e51c731bb1bac8d4eeea6bd0df469fe17cf82ee3fc171211d56fad146f3a60321f6a38d93ecb081e27ca723205fd59e5b475dec130ba695205cd5a31dbf
|
data/.github/probots.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v1
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
containers:
|
|
2
|
+
default:
|
|
3
|
+
docker: circleci/ruby:2.5.1-node-browsers
|
|
4
|
+
|
|
5
|
+
steps:
|
|
6
|
+
- label: Lint
|
|
7
|
+
timeout: 5m
|
|
8
|
+
run:
|
|
9
|
+
- bundle exec rubocop
|
|
10
|
+
dependencies:
|
|
11
|
+
- bundler
|
|
12
|
+
- label: 'Run Test Suite (:kubernetes: 1.14-latest)'
|
|
13
|
+
command: bin/ci
|
|
14
|
+
agents:
|
|
15
|
+
queue: k8s-ci
|
|
16
|
+
env:
|
|
17
|
+
LOGGING_LEVEL: "4"
|
|
18
|
+
KUBERNETES_VERSION: v1.14-latest
|
|
19
|
+
- label: 'Run Test Suite (:kubernetes: 1.13-latest)'
|
|
20
|
+
command: bin/ci
|
|
21
|
+
agents:
|
|
22
|
+
queue: k8s-ci
|
|
23
|
+
env:
|
|
24
|
+
LOGGING_LEVEL: "4"
|
|
25
|
+
KUBERNETES_VERSION: v1.13-latest
|
|
26
|
+
- label: 'Run Test Suite (:kubernetes: 1.12-latest)'
|
|
27
|
+
command: bin/ci
|
|
28
|
+
agents:
|
|
29
|
+
queue: k8s-ci
|
|
30
|
+
env:
|
|
31
|
+
LOGGING_LEVEL: "4"
|
|
32
|
+
KUBERNETES_VERSION: v1.12-latest
|
|
33
|
+
- label: 'Run Test Suite (:kubernetes: 1.11-latest)'
|
|
34
|
+
command: bin/ci
|
|
35
|
+
agents:
|
|
36
|
+
queue: k8s-ci
|
|
37
|
+
env:
|
|
38
|
+
LOGGING_LEVEL: "4"
|
|
39
|
+
KUBERNETES_VERSION: v1.11-latest
|
|
40
|
+
- label: 'Run Test Suite (:kubernetes: 1.10-latest)'
|
|
41
|
+
command: bin/ci
|
|
42
|
+
agents:
|
|
43
|
+
queue: minikube-ci
|
|
44
|
+
env:
|
|
45
|
+
LOGGING_LEVEL: "4"
|
|
46
|
+
KUBERNETES_VERSION: v1.10-latest
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
## next
|
|
2
2
|
|
|
3
|
+
## 0.26.5
|
|
4
|
+
|
|
5
|
+
*Bug Fixes*
|
|
6
|
+
- Support 'volumeBindingMode: WaitForFirstConsumer' condition in StorageClass. [#479](https://github.com/Shopify/kubernetes-deploy/pull/479)
|
|
7
|
+
- Fix: Undefined method "merge" on LabelSelector. [#488](https://github.com/Shopify/kubernetes-deploy/pull/488)
|
|
8
|
+
|
|
9
|
+
*Enhancements*
|
|
10
|
+
- Officially support Kubernetes 1.14. [#461](https://github.com/Shopify/kubernetes-deploy/pull/461)
|
|
11
|
+
- Allow customising which custom resources are deployed in the pre-deploy phase. [#505](https://github.com/Shopify/kubernetes-deploy/pull/505)
|
|
12
|
+
|
|
13
|
+
*Other*
|
|
14
|
+
- Removes special treatment of GCP authentication by upgrading to `kubeclient` 4.3. [#465](https://github.com/Shopify/kubernetes-deploy/pull/465)
|
|
15
|
+
|
|
3
16
|
## 0.26.4
|
|
4
17
|
|
|
5
18
|
*Bug fixes*
|
data/CONTRIBUTING.md
CHANGED
|
@@ -13,6 +13,7 @@ The following is a set of guidelines for contributing to kubernetes-deploy. Plea
|
|
|
13
13
|
* [High-level design decisions](#high-level-design-decisions)
|
|
14
14
|
* [Feature acceptance policies](#feature-acceptance-policies)
|
|
15
15
|
* [Adding a new resource type](#contributing-a-new-resource-type)
|
|
16
|
+
* [Contributor License Agreement](#contributor-license-agreement)
|
|
16
17
|
|
|
17
18
|
[Development](#development)
|
|
18
19
|
* [Setup](#setup)
|
|
@@ -105,6 +106,11 @@ This gem uses subclasses of `KubernetesResource` to implement custom success/fai
|
|
|
105
106
|
6. Add a basic example of the type to the hello-cloud [fixture set](https://github.com/Shopify/kubernetes-deploy/tree/master/test/fixtures/hello-cloud) and appropriate assertions to `#assert_all_up` in [`hello_cloud.rb`](https://github.com/Shopify/kubernetes-deploy/blob/master/test/helpers/fixture_sets/hello_cloud.rb). This will get you coverage in several existing tests, such as `test_full_hello_cloud_set_deploy_succeeds`.
|
|
106
107
|
7. Add tests for any edge cases you foresee.
|
|
107
108
|
|
|
109
|
+
### Contributor License Agreement
|
|
110
|
+
|
|
111
|
+
New contributors will be required to sign [Shopify's Contributor License Agreement (CLA)](https://cla.shopify.com/).
|
|
112
|
+
There are two versions of the CLA: one for individuals and one for organizations.
|
|
113
|
+
|
|
108
114
|
# Development
|
|
109
115
|
|
|
110
116
|
## Setup
|
|
@@ -157,6 +163,6 @@ Please make sure you run the tests locally before submitting your PR (see [Runni
|
|
|
157
163
|
|
|
158
164
|
#### Employees: Triggering CI for a contributed PR
|
|
159
165
|
|
|
160
|
-
Go to the [kubernetes-deploy
|
|
166
|
+
Go to the [kubernetes-deploy pipeline](https://buildkite.com/shopify/kubernetes-deploy) and click "New Build". Use branch `external_contrib_ci` and the specific sha of the commit you want to build. Add `BUILDKITE_REFSPEC="refs/pull/${PR_NUM}/head"` in the Environment Variables section. Since CI is only visible to Shopify employees, you will need to provide any failing tests and output to the the contributor.
|
|
161
167
|
|
|
162
168
|
<img width="350" alt="build external contrib PR" src="https://screenshot.click/2017-11-07--163728_7ovek-wrpwq.png">
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# kubernetes-deploy [](https://buildkite.com/shopify/kubernetes-deploy) [](https://codecov.io/gh/Shopify/kubernetes-deploy)
|
|
2
2
|
|
|
3
3
|
`kubernetes-deploy` is a command line tool that helps you ship changes to a Kubernetes namespace and understand the result. At Shopify, we use it within our much-beloved, open-source [Shipit](https://github.com/Shopify/shipit-engine#kubernetes) deployment app.
|
|
4
4
|
|
|
@@ -72,7 +72,7 @@ This repo also includes related tools for [running tasks](#kubernetes-run) and [
|
|
|
72
72
|
* Each app must have a deploy directory containing its Kubernetes templates (see [Templates](#using-templates-and-variables))
|
|
73
73
|
|
|
74
74
|
<sup>1</sup> We run integration tests against these Kubernetes versions. You can find our
|
|
75
|
-
|
|
75
|
+
official compatibility chart below.
|
|
76
76
|
|
|
77
77
|
| Kubernetes version | Last officially supported in gem version |
|
|
78
78
|
| :----------------: | :-------------------: |
|
|
@@ -80,6 +80,7 @@ offical compatibility chart below.
|
|
|
80
80
|
| 1.6 | 0.15.2 |
|
|
81
81
|
| 1.7 | 0.20.6 |
|
|
82
82
|
| 1.8 | 0.21.1 |
|
|
83
|
+
| 1.9 | 0.24.0 |
|
|
83
84
|
|
|
84
85
|
## Installation
|
|
85
86
|
|
|
@@ -244,6 +245,11 @@ before the deployment is considered successful.
|
|
|
244
245
|
- _Compatibility_: Custom Resource Definition
|
|
245
246
|
- `true`: The custom resource will be pruned if the resource is not in the deploy directory.
|
|
246
247
|
- All other values: The custom resource will not be pruned.
|
|
248
|
+
- `kubernetes-deploy.shopify.io/predeployed`: Causes a Custom Resource to be deployed in the pre-deploy phase.
|
|
249
|
+
- _Compatibility_: Custom Resource Definition
|
|
250
|
+
- _Default_: `true`
|
|
251
|
+
- `true`: The custom resource will be deployed in the pre-deploy phase.
|
|
252
|
+
- All other values: The custom resource will be deployed in the main deployment phase.
|
|
247
253
|
|
|
248
254
|
### Running tasks at the beginning of a deploy
|
|
249
255
|
|
|
@@ -500,6 +506,12 @@ To render some templates in a template dir, run kubernetes-render with the names
|
|
|
500
506
|
kubernetes-render --template-dir=./path/to/template/dir this-template.yaml.erb that-template.yaml.erb
|
|
501
507
|
```
|
|
502
508
|
|
|
509
|
+
To render a template in a template dir and output it to a file, run kubernetes-render with the name of the template and redirect the output to a file:
|
|
510
|
+
|
|
511
|
+
```
|
|
512
|
+
kubernetes-render --template-dir=./path/to/template/dir template.yaml.erb > template.yaml
|
|
513
|
+
```
|
|
514
|
+
|
|
503
515
|
*Options:*
|
|
504
516
|
|
|
505
517
|
- `--template-dir=DIR`: Used to set the directory to interpret template names relative to. This is often the same directory passed as `--template-dir` when running `kubernetes-deploy` to actually deploy templates. Set `$ENVIRONMENT` instead to use `config/deploy/$ENVIRONMENT`. This flag also supports reading from STDIN. You can do this by using `--template-dir=-`.
|
data/kubernetes-deploy.gemspec
CHANGED
|
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
|
|
|
24
24
|
|
|
25
25
|
spec.required_ruby_version = '>= 2.3.0'
|
|
26
26
|
spec.add_dependency("activesupport", ">= 5.0")
|
|
27
|
-
spec.add_dependency("kubeclient", "~> 4.
|
|
27
|
+
spec.add_dependency("kubeclient", "~> 4.3")
|
|
28
28
|
spec.add_dependency("googleauth", "~> 0.6.6") # https://github.com/google/google-auth-library-ruby/issues/153
|
|
29
29
|
spec.add_dependency("ejson", "~> 1.0")
|
|
30
30
|
spec.add_dependency("colorize", "~> 0.8")
|
data/lib/kubernetes-deploy.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'active_support/core_ext/object/blank'
|
|
4
|
+
require 'active_support/core_ext/hash/reverse_merge'
|
|
4
5
|
require 'active_support/core_ext/hash/slice'
|
|
5
6
|
require 'active_support/core_ext/numeric/time'
|
|
6
7
|
require 'active_support/core_ext/string/inflections'
|
|
@@ -41,7 +41,7 @@ module KubernetesDeploy
|
|
|
41
41
|
when '.json'
|
|
42
42
|
bindings = parse_json(File.read(file_path))
|
|
43
43
|
when '.yaml', '.yml'
|
|
44
|
-
bindings = YAML.
|
|
44
|
+
bindings = YAML.safe_load(File.read(file_path))
|
|
45
45
|
else
|
|
46
46
|
raise ArgumentError, "Supplied file does not appear to be JSON or YAML"
|
|
47
47
|
end
|
|
@@ -75,7 +75,7 @@ module KubernetesDeploy
|
|
|
75
75
|
Pod
|
|
76
76
|
)
|
|
77
77
|
|
|
78
|
-
before_crs + cluster_resource_discoverer.crds.map(&:kind) + after_crs
|
|
78
|
+
before_crs + cluster_resource_discoverer.crds.select(&:predeployed?).map(&:kind) + after_crs
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def prune_whitelist
|
|
@@ -552,15 +552,20 @@ module KubernetesDeploy
|
|
|
552
552
|
end
|
|
553
553
|
|
|
554
554
|
def confirm_namespace_exists
|
|
555
|
-
|
|
556
|
-
with_retries(2) do
|
|
557
|
-
_, err, st = kubectl.run("get", "namespace", @namespace, use_namespace: false, log_failure: true)
|
|
558
|
-
st.success? || err.include?(KubernetesDeploy::Kubectl::NOT_FOUND_ERROR_TEXT)
|
|
559
|
-
end
|
|
560
|
-
raise FatalDeploymentError, "Failed to find namespace. #{err}" unless st.success?
|
|
555
|
+
raise FatalDeploymentError, "Namespace #{@namespace} not found" unless namespace_definition.present?
|
|
561
556
|
@logger.info("Namespace #{@namespace} found")
|
|
562
557
|
end
|
|
563
558
|
|
|
559
|
+
def namespace_definition
|
|
560
|
+
@namespace_definition ||= begin
|
|
561
|
+
definition, _err, st = kubectl.run("get", "namespace", @namespace, use_namespace: false,
|
|
562
|
+
log_failure: true, raise_if_not_found: true, attempts: 3, output: 'json')
|
|
563
|
+
st.success? ? JSON.parse(definition, symbolize_names: true) : nil
|
|
564
|
+
end
|
|
565
|
+
rescue Kubectl::ResourceNotFoundError
|
|
566
|
+
nil
|
|
567
|
+
end
|
|
568
|
+
|
|
564
569
|
# make sure to never prune the ejson-keys secret
|
|
565
570
|
def confirm_ejson_keys_not_prunable
|
|
566
571
|
secret = ejson_provisioner.ejson_keys_secret
|
|
@@ -574,14 +579,8 @@ module KubernetesDeploy
|
|
|
574
579
|
end
|
|
575
580
|
|
|
576
581
|
def tags_from_namespace_labels
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
namespace_info, _, st = kubectl.run("get", "namespace", @namespace, "-o", "json", use_namespace: false,
|
|
580
|
-
log_failure: true)
|
|
581
|
-
st.success?
|
|
582
|
-
end
|
|
583
|
-
return [] if namespace_info.blank?
|
|
584
|
-
namespace_labels = JSON.parse(namespace_info, symbolize_names: true).fetch(:metadata, {}).fetch(:labels, {})
|
|
582
|
+
return [] if namespace_definition.blank?
|
|
583
|
+
namespace_labels = namespace_definition.fetch(:metadata, {}).fetch(:labels, {})
|
|
585
584
|
namespace_labels.map { |key, value| "#{key}:#{value}" }
|
|
586
585
|
end
|
|
587
586
|
|
|
@@ -63,7 +63,8 @@ module KubernetesDeploy
|
|
|
63
63
|
secrets.map do |secret_name, secret_spec|
|
|
64
64
|
validate_secret_spec(secret_name, secret_spec)
|
|
65
65
|
resource = generate_secret_resource(secret_name, secret_spec["_type"], secret_spec["data"])
|
|
66
|
-
|
|
66
|
+
resource.validate_definition(@kubectl)
|
|
67
|
+
if resource.validation_failed?
|
|
67
68
|
raise EjsonSecretError, "Resulting resource Secret/#{secret_name} failed validation"
|
|
68
69
|
end
|
|
69
70
|
resource
|
|
@@ -105,7 +106,7 @@ module KubernetesDeploy
|
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
labels = { "name" => secret_name }
|
|
108
|
-
labels.reverse_merge!(@selector) if @selector
|
|
109
|
+
labels.reverse_merge!(@selector.to_h) if @selector
|
|
109
110
|
|
|
110
111
|
secret = {
|
|
111
112
|
'kind' => 'Secret',
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'kubeclient'
|
|
3
|
-
require 'kubernetes-deploy/kubeclient_builder/kube_config'
|
|
4
3
|
|
|
5
4
|
module KubernetesDeploy
|
|
6
5
|
class KubeclientBuilder
|
|
@@ -96,6 +95,14 @@ module KubernetesDeploy
|
|
|
96
95
|
)
|
|
97
96
|
end
|
|
98
97
|
|
|
98
|
+
def build_storage_v1_kubeclient(context)
|
|
99
|
+
build_kubeclient(
|
|
100
|
+
api_version: "v1",
|
|
101
|
+
context: context,
|
|
102
|
+
endpoint_path: "/apis/storage.k8s.io"
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
99
106
|
def validate_config_files
|
|
100
107
|
errors = []
|
|
101
108
|
if @kubeconfig_files.empty?
|
|
@@ -118,7 +125,7 @@ module KubernetesDeploy
|
|
|
118
125
|
|
|
119
126
|
def build_kubeclient(api_version:, context:, endpoint_path: nil)
|
|
120
127
|
validate_config_files!
|
|
121
|
-
@kubeclient_configs ||= @kubeconfig_files.map { |f|
|
|
128
|
+
@kubeclient_configs ||= @kubeconfig_files.map { |f| Kubeclient::Config.read(f) }
|
|
122
129
|
# Find a context defined in kube conf files that matches the input context by name
|
|
123
130
|
config = @kubeclient_configs.find { |c| c.contexts.include?(context) }
|
|
124
131
|
raise ContextMissingError.new(context, @kubeconfig_files) unless config
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module KubernetesDeploy
|
|
4
4
|
class Kubectl
|
|
5
|
+
ERROR_MATCHERS = {
|
|
6
|
+
not_found: /NotFound/,
|
|
7
|
+
client_timeout: /Client\.Timeout exceeded while awaiting headers/,
|
|
8
|
+
}
|
|
5
9
|
DEFAULT_TIMEOUT = 15
|
|
6
|
-
|
|
10
|
+
MAX_RETRY_DELAY = 16
|
|
7
11
|
|
|
8
12
|
class ResourceNotFoundError < StandardError; end
|
|
9
13
|
|
|
@@ -21,43 +25,45 @@ module KubernetesDeploy
|
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def run(*args, log_failure: nil, use_context: true, use_namespace: true, output: nil,
|
|
24
|
-
raise_if_not_found: false, attempts: 1, output_is_sensitive: nil)
|
|
28
|
+
raise_if_not_found: false, attempts: 1, output_is_sensitive: nil, retry_whitelist: nil)
|
|
25
29
|
log_failure = @log_failure_by_default if log_failure.nil?
|
|
26
30
|
output_is_sensitive = @output_is_sensitive_default if output_is_sensitive.nil?
|
|
27
|
-
|
|
28
|
-
args = args.unshift("kubectl")
|
|
29
|
-
args.push("--namespace=#{@namespace}") if use_namespace
|
|
30
|
-
args.push("--context=#{@context}") if use_context
|
|
31
|
-
args.push("--output=#{output}") if output
|
|
32
|
-
args.push("--request-timeout=#{@default_timeout}") if @default_timeout
|
|
31
|
+
cmd = build_command_from_options(args, use_namespace, use_context, output)
|
|
33
32
|
out, err, st = nil
|
|
34
33
|
|
|
35
|
-
(1..attempts).to_a.each do |
|
|
36
|
-
@logger.debug("Running command (attempt #{
|
|
37
|
-
out, err, st = Open3.capture3(*
|
|
34
|
+
(1..attempts).to_a.each do |current_attempt|
|
|
35
|
+
@logger.debug("Running command (attempt #{current_attempt}): #{cmd.join(' ')}")
|
|
36
|
+
out, err, st = Open3.capture3(*cmd)
|
|
38
37
|
@logger.debug("Kubectl out: " + out.gsub(/\s+/, ' ')) unless output_is_sensitive
|
|
39
38
|
|
|
40
39
|
break if st.success?
|
|
40
|
+
raise(ResourceNotFoundError, err) if err.match(ERROR_MATCHERS[:not_found]) && raise_if_not_found
|
|
41
41
|
|
|
42
42
|
if log_failure
|
|
43
|
-
|
|
43
|
+
warning = if current_attempt == attempts
|
|
44
|
+
"The following command failed (attempt #{current_attempt}/#{attempts})"
|
|
45
|
+
elsif retriable_err?(err, retry_whitelist)
|
|
46
|
+
"The following command failed and will be retried (attempt #{current_attempt}/#{attempts})"
|
|
47
|
+
else
|
|
48
|
+
"The following command failed and cannot be retried"
|
|
49
|
+
end
|
|
50
|
+
@logger.warn("#{warning}: #{Shellwords.join(cmd)}")
|
|
44
51
|
@logger.warn(err) unless output_is_sensitive
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
if err.match(NOT_FOUND_ERROR_TEXT)
|
|
48
|
-
raise(ResourceNotFoundError, err) if raise_if_not_found
|
|
49
52
|
else
|
|
50
|
-
@logger.debug("Kubectl err: #{err}")
|
|
51
|
-
StatsD.increment('kubectl.error', 1, tags: { context: @context, namespace: @namespace, cmd: args[1] })
|
|
53
|
+
@logger.debug("Kubectl err: #{output_is_sensitive ? '<suppressed sensitive output>' : err}")
|
|
52
54
|
end
|
|
53
|
-
|
|
55
|
+
StatsD.increment('kubectl.error', 1, tags: { context: @context, namespace: @namespace, cmd: cmd[1] })
|
|
56
|
+
|
|
57
|
+
break unless retriable_err?(err, retry_whitelist) && current_attempt < attempts
|
|
58
|
+
sleep(retry_delay(current_attempt))
|
|
54
59
|
end
|
|
55
60
|
|
|
56
61
|
[out.chomp, err.chomp, st]
|
|
57
62
|
end
|
|
58
63
|
|
|
59
64
|
def retry_delay(attempt)
|
|
60
|
-
|
|
65
|
+
# exponential backoff starting at 1s with cap at 16s, offset by up to 0.5s
|
|
66
|
+
[2**(attempt - 1), MAX_RETRY_DELAY].min - Random.rand(0.5).round(1)
|
|
61
67
|
end
|
|
62
68
|
|
|
63
69
|
def version_info
|
|
@@ -79,6 +85,23 @@ module KubernetesDeploy
|
|
|
79
85
|
|
|
80
86
|
private
|
|
81
87
|
|
|
88
|
+
def build_command_from_options(args, use_namespace, use_context, output)
|
|
89
|
+
cmd = ["kubectl"] + args
|
|
90
|
+
cmd.push("--namespace=#{@namespace}") if use_namespace
|
|
91
|
+
cmd.push("--context=#{@context}") if use_context
|
|
92
|
+
cmd.push("--output=#{output}") if output
|
|
93
|
+
cmd.push("--request-timeout=#{@default_timeout}") if @default_timeout
|
|
94
|
+
cmd
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def retriable_err?(err, retry_whitelist)
|
|
98
|
+
return !err.match(ERROR_MATCHERS[:not_found]) if retry_whitelist.nil?
|
|
99
|
+
retry_whitelist.any? do |retriable|
|
|
100
|
+
raise NotImplementedError, "No matcher defined for #{retriable.inspect}" unless ERROR_MATCHERS.key?(retriable)
|
|
101
|
+
err.match(ERROR_MATCHERS[retriable])
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
82
105
|
def extract_version_info_from_kubectl_response(response)
|
|
83
106
|
info = {}
|
|
84
107
|
response.each_line do |l|
|
|
@@ -122,18 +122,8 @@ module KubernetesDeploy
|
|
|
122
122
|
@validation_errors = []
|
|
123
123
|
validate_selector(selector) if selector
|
|
124
124
|
validate_timeout_annotation
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
_, err, st = kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?)
|
|
128
|
-
return true if st.success?
|
|
129
|
-
if sensitive_template_content?
|
|
130
|
-
@validation_errors << <<-EOS
|
|
131
|
-
Validation for #{id} failed. Detailed information is unavailable as the raw error may contain sensitive data.
|
|
132
|
-
EOS
|
|
133
|
-
else
|
|
134
|
-
@validation_errors << err
|
|
135
|
-
end
|
|
136
|
-
false
|
|
125
|
+
validate_spec_with_kubectl(kubectl)
|
|
126
|
+
@validation_errors.present?
|
|
137
127
|
end
|
|
138
128
|
|
|
139
129
|
def validation_error_msg
|
|
@@ -433,6 +423,19 @@ module KubernetesDeploy
|
|
|
433
423
|
end
|
|
434
424
|
end
|
|
435
425
|
|
|
426
|
+
def validate_spec_with_kubectl(kubectl)
|
|
427
|
+
command = ["create", "-f", file_path, "--dry-run", "--output=name"]
|
|
428
|
+
_, err, st = kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?,
|
|
429
|
+
retry_whitelist: [:client_timeout], attempts: 3)
|
|
430
|
+
|
|
431
|
+
return true if st.success?
|
|
432
|
+
@validation_errors << if sensitive_template_content?
|
|
433
|
+
"Validation for #{id} failed. Detailed information is unavailable as the raw error may contain sensitive data."
|
|
434
|
+
else
|
|
435
|
+
err
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
436
439
|
def labels
|
|
437
440
|
@definition.dig("metadata", "labels")
|
|
438
441
|
end
|
|
@@ -56,6 +56,11 @@ module KubernetesDeploy
|
|
|
56
56
|
prunable == "true"
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
def predeployed?
|
|
60
|
+
predeployed = @definition.dig("metadata", "annotations", "kubernetes-deploy.shopify.io/predeployed")
|
|
61
|
+
predeployed.nil? || predeployed == "true"
|
|
62
|
+
end
|
|
63
|
+
|
|
59
64
|
def rollout_conditions
|
|
60
65
|
return @rollout_conditions if defined?(@rollout_conditions)
|
|
61
66
|
|
|
@@ -3,16 +3,78 @@ module KubernetesDeploy
|
|
|
3
3
|
class PersistentVolumeClaim < KubernetesResource
|
|
4
4
|
TIMEOUT = 5.minutes
|
|
5
5
|
|
|
6
|
+
def sync(cache)
|
|
7
|
+
super
|
|
8
|
+
@storage_classes = cache.get_all("StorageClass").map { |sc| StorageClass.new(sc) }
|
|
9
|
+
end
|
|
10
|
+
|
|
6
11
|
def status
|
|
7
12
|
exists? ? @instance_data["status"]["phase"] : "Not Found"
|
|
8
13
|
end
|
|
9
14
|
|
|
10
15
|
def deploy_succeeded?
|
|
11
|
-
status == "Bound"
|
|
16
|
+
return true if status == "Bound"
|
|
17
|
+
|
|
18
|
+
# if the StorageClass has volumeBindingMode: WaitForFirstConsumer,
|
|
19
|
+
# it won't bind until after a Pod mounts it. But it must be pre-deployed,
|
|
20
|
+
# as the Pod requires it. So 'Pending' must be treated as a 'Success' state
|
|
21
|
+
if storage_class&.volume_binding_mode == "WaitForFirstConsumer"
|
|
22
|
+
return status == "Pending" || status == "Bound"
|
|
23
|
+
end
|
|
24
|
+
false
|
|
12
25
|
end
|
|
13
26
|
|
|
14
27
|
def deploy_failed?
|
|
15
|
-
status == "Lost"
|
|
28
|
+
status == "Lost" || failure_message.present?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def failure_message
|
|
32
|
+
if storage_class_name.nil? && @storage_classes.count(&:default?) > 1
|
|
33
|
+
"PVC has no StorageClass specified and there are multiple StorageClasses " \
|
|
34
|
+
"annotated as default. This is an invalid cluster configuration."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def timeout_message
|
|
39
|
+
return STANDARD_TIMEOUT_MESSAGE unless storage_class_name.present? && !storage_class
|
|
40
|
+
"PVC specified a StorageClass of #{storage_class_name} but the resource does not exist"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def storage_class_name
|
|
46
|
+
@definition.dig("spec", "storageClassName")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def storage_class
|
|
50
|
+
if storage_class_name.present?
|
|
51
|
+
@storage_classes.detect { |sc| sc.name == storage_class_name }
|
|
52
|
+
# storage_class_name = "" is an explicit request for no storage class
|
|
53
|
+
# storage_class_name = nil is an impplicit request for default storage class
|
|
54
|
+
elsif storage_class_name != ""
|
|
55
|
+
@storage_classes.detect(&:default?)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class StorageClass < KubernetesResource
|
|
60
|
+
DEFAULT_CLASS_ANNOTATION = "storageclass.kubernetes.io/is-default-class"
|
|
61
|
+
DEFAULT_CLASS_BETA_ANNOTATION = "storageclass.beta.kubernetes.io/is-default-class"
|
|
62
|
+
|
|
63
|
+
attr_reader :name
|
|
64
|
+
|
|
65
|
+
def initialize(definition)
|
|
66
|
+
@definition = definition
|
|
67
|
+
@name = definition.dig("metadata", "name").to_s
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def volume_binding_mode
|
|
71
|
+
@definition.dig("volumeBindingMode")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def default?
|
|
75
|
+
@definition.dig("metadata", "annotations", DEFAULT_CLASS_ANNOTATION) == "true" ||
|
|
76
|
+
@definition.dig("metadata", "annotations", DEFAULT_CLASS_BETA_ANNOTATION) == "true"
|
|
77
|
+
end
|
|
16
78
|
end
|
|
17
79
|
end
|
|
18
80
|
end
|
|
@@ -65,10 +65,15 @@ module KubernetesDeploy
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
def timeout_message
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
if readiness_probe_failure?
|
|
69
|
+
probe_failure_msgs = @containers.map(&:readiness_fail_reason).compact
|
|
70
|
+
header = "The following containers have not passed their readiness probes on at least one pod:\n"
|
|
71
|
+
header + probe_failure_msgs.join("\n")
|
|
72
|
+
elsif failed_schedule_reason.present?
|
|
73
|
+
"Pod could not be scheduled because #{failed_schedule_reason}"
|
|
74
|
+
else
|
|
75
|
+
STANDARD_TIMEOUT_MESSAGE
|
|
76
|
+
end
|
|
72
77
|
end
|
|
73
78
|
|
|
74
79
|
def failure_message
|
|
@@ -98,6 +103,16 @@ module KubernetesDeploy
|
|
|
98
103
|
|
|
99
104
|
private
|
|
100
105
|
|
|
106
|
+
def failed_schedule_reason
|
|
107
|
+
if phase == "Pending"
|
|
108
|
+
conditions = @instance_data.dig('status', 'conditions') || []
|
|
109
|
+
unschedulable = conditions.find do |condition|
|
|
110
|
+
condition["type"] == "PodScheduled" && condition["status"] == "False"
|
|
111
|
+
end
|
|
112
|
+
unschedulable&.dig('message')
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
101
116
|
def failed_phase?
|
|
102
117
|
phase == FAILED_PHASE_NAME
|
|
103
118
|
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.26.
|
|
4
|
+
version: 0.26.5
|
|
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-
|
|
12
|
+
date: 2019-07-03 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activesupport
|
|
@@ -31,14 +31,14 @@ dependencies:
|
|
|
31
31
|
requirements:
|
|
32
32
|
- - "~>"
|
|
33
33
|
- !ruby/object:Gem::Version
|
|
34
|
-
version: '4.
|
|
34
|
+
version: '4.3'
|
|
35
35
|
type: :runtime
|
|
36
36
|
prerelease: false
|
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
|
38
38
|
requirements:
|
|
39
39
|
- - "~>"
|
|
40
40
|
- !ruby/object:Gem::Version
|
|
41
|
-
version: '4.
|
|
41
|
+
version: '4.3'
|
|
42
42
|
- !ruby/object:Gem::Dependency
|
|
43
43
|
name: googleauth
|
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -239,9 +239,11 @@ extensions: []
|
|
|
239
239
|
extra_rdoc_files: []
|
|
240
240
|
files:
|
|
241
241
|
- ".buildkite/pipeline.nightly.yml"
|
|
242
|
-
- ".
|
|
242
|
+
- ".github/probots.yml"
|
|
243
243
|
- ".gitignore"
|
|
244
244
|
- ".rubocop.yml"
|
|
245
|
+
- ".shopify-build/VERSION"
|
|
246
|
+
- ".shopify-build/kubernetes-deploy.yml"
|
|
245
247
|
- CHANGELOG.md
|
|
246
248
|
- CODE_OF_CONDUCT.md
|
|
247
249
|
- CONTRIBUTING.md
|
|
@@ -273,7 +275,6 @@ files:
|
|
|
273
275
|
- lib/kubernetes-deploy/errors.rb
|
|
274
276
|
- lib/kubernetes-deploy/formatted_logger.rb
|
|
275
277
|
- lib/kubernetes-deploy/kubeclient_builder.rb
|
|
276
|
-
- lib/kubernetes-deploy/kubeclient_builder/kube_config.rb
|
|
277
278
|
- lib/kubernetes-deploy/kubectl.rb
|
|
278
279
|
- lib/kubernetes-deploy/kubernetes_resource.rb
|
|
279
280
|
- lib/kubernetes-deploy/kubernetes_resource/cloudsql.rb
|
data/.buildkite/pipeline.yml
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
steps:
|
|
2
|
-
- name: 'Run Test Suite (:kubernetes: 1.13-latest)'
|
|
3
|
-
command: bin/ci
|
|
4
|
-
agents:
|
|
5
|
-
queue: k8s-ci
|
|
6
|
-
env:
|
|
7
|
-
LOGGING_LEVEL: 4
|
|
8
|
-
KUBERNETES_VERSION: v1.13-latest
|
|
9
|
-
- name: 'Run Test Suite (:kubernetes: 1.12-latest)'
|
|
10
|
-
command: bin/ci
|
|
11
|
-
agents:
|
|
12
|
-
queue: k8s-ci
|
|
13
|
-
env:
|
|
14
|
-
LOGGING_LEVEL: 4
|
|
15
|
-
KUBERNETES_VERSION: v1.12-latest
|
|
16
|
-
- name: 'Run Test Suite (:kubernetes: 1.11-latest)'
|
|
17
|
-
command: bin/ci
|
|
18
|
-
agents:
|
|
19
|
-
queue: k8s-ci
|
|
20
|
-
env:
|
|
21
|
-
LOGGING_LEVEL: 4
|
|
22
|
-
KUBERNETES_VERSION: v1.11-latest
|
|
23
|
-
- name: 'Run Test Suite (:kubernetes: 1.10-latest)'
|
|
24
|
-
command: bin/ci
|
|
25
|
-
agents:
|
|
26
|
-
queue: minikube-ci
|
|
27
|
-
env:
|
|
28
|
-
LOGGING_LEVEL: 4
|
|
29
|
-
KUBERNETES_VERSION: v1.10-latest
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'googleauth'
|
|
4
|
-
module KubernetesDeploy
|
|
5
|
-
class KubeclientBuilder
|
|
6
|
-
class KubeConfig < Kubeclient::Config
|
|
7
|
-
def self.read(filename)
|
|
8
|
-
parsed = YAML.safe_load(File.read(filename), [Date, Time])
|
|
9
|
-
new(parsed, File.dirname(filename))
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def fetch_user_auth_options(user)
|
|
13
|
-
if user.dig('auth-provider', 'name') == 'gcp'
|
|
14
|
-
{ bearer_token: Kubeclient::GoogleApplicationDefaultCredentials.token }
|
|
15
|
-
else
|
|
16
|
-
super
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|