kubernetes-deploy 0.26.4 → 0.26.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a537ded5641b9cb4b9813594e6da9d7ea06876721b26ce4785e48f0b933536cf
4
- data.tar.gz: 8ee185bc26198298d5ed8c18aba4736a67bc46bd2526a72d71b74c988ac9e657
3
+ metadata.gz: c11bffff21178fb845d50aff17aa56f3044cb95b235ff6db2463c94def2d2589
4
+ data.tar.gz: 54ee8d3c14e3d7ca013d6e9a822aa0772dde1b4738bcce67efb3f96c490ddb08
5
5
  SHA512:
6
- metadata.gz: 33f35406225136f90fa99219f5545d9052e2d323055f87d0d42c81640549cda6f18c0748d661ae4ba4c4ab7a1bcb0e7c717e6c8081ded664849befd2bf571d3c
7
- data.tar.gz: 3d3e78256ad83ea456f9a92106c81b03c0212fc3d0940cdfbb990a45214f7250af1110c4f3231dde8b828ab4113b68c3ce5dfc28d675a001d19eea686b2bb9ca
6
+ metadata.gz: e79311a4c0c7af04db41ee384d5ed1da79afdb1f2676c02595ced432ddc6da38dc08179bc5791bbde34e6e0e8da01bd2ca102e0e4ed5af275ee0542b55a68ea8
7
+ data.tar.gz: b4db7e51c731bb1bac8d4eeea6bd0df469fe17cf82ee3fc171211d56fad146f3a60321f6a38d93ecb081e27ca723205fd59e5b475dec130ba695205cd5a31dbf
@@ -1,4 +1,11 @@
1
1
  steps:
2
+ - name: 'Run Test Suite (:kubernetes: 1.14-latest)'
3
+ command: bin/ci
4
+ agents:
5
+ queue: k8s-ci
6
+ env:
7
+ LOGGING_LEVEL: 4
8
+ KUBERNETES_VERSION: v1.14-latest
2
9
  - name: 'Run Test Suite (:kubernetes: 1.13-latest)'
3
10
  command: bin/ci
4
11
  agents:
@@ -0,0 +1,2 @@
1
+ enabled:
2
+ - cla
@@ -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
@@ -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*
@@ -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-gem pipeline](https://buildkite.com/shopify/kubernetes-deploy-gem) 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.
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 [![Build status](https://badge.buildkite.com/d1aab6d17b010f418e43f740063fe5343c5d65df654e635a8b.svg?branch=master)](https://buildkite.com/shopify/kubernetes-deploy-gem) [![codecov](https://codecov.io/gh/Shopify/kubernetes-deploy/branch/master/graph/badge.svg)](https://codecov.io/gh/Shopify/kubernetes-deploy)
1
+ # kubernetes-deploy [![Build status](https://badge.buildkite.com/61937e40a1fc69754d9d198be120543d6de310de2ba8d3cb0e.svg?branch=master)](https://buildkite.com/shopify/kubernetes-deploy) [![codecov](https://codecov.io/gh/Shopify/kubernetes-deploy/branch/master/graph/badge.svg)](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
- offical compatibility chart below.
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=-`.
@@ -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.0")
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")
@@ -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.load(File.read(file_path))
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
- st, err = nil
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
- namespace_info = nil
578
- with_retries(2) do
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
- unless resource.validate_definition(@kubectl)
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| KubeConfig.read(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
- NOT_FOUND_ERROR_TEXT = 'NotFound'
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 |attempt|
36
- @logger.debug("Running command (attempt #{attempt}): #{args.join(' ')}")
37
- out, err, st = Open3.capture3(*args)
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
- @logger.warn("The following command failed (attempt #{attempt}/#{attempts}): #{Shellwords.join(args)}")
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}") unless output_is_sensitive
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
- sleep(retry_delay(attempt)) unless attempt == attempts
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
- attempt
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
- command = ["create", "-f", file_path, "--dry-run", "--output=name"]
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
- return STANDARD_TIMEOUT_MESSAGE unless 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")
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
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module KubernetesDeploy
3
- VERSION = "0.26.4"
3
+ VERSION = "0.26.5"
4
4
  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
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-04-29 00:00:00.000000000 Z
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.0'
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.0'
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
- - ".buildkite/pipeline.yml"
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
@@ -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