kubernetes-deploy 0.23.0 → 0.24.0

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: 01ddead06a28a399d7364d5789c2bcb1cee0e0aa4ae471a4af1a90c32973050b
4
- data.tar.gz: ebcedef932871bd542db69bc0b48fb6f47f2482c712aa9380a79cb872b6c3e82
3
+ metadata.gz: e8e6aa3c405f95c085c79e274915275e1d2a225803c4abf21813c11543bdfa8f
4
+ data.tar.gz: ae315a1ce265d03205bd39972303d54d015a32210b43151c8f5d541377946143
5
5
  SHA512:
6
- metadata.gz: 837dcaf96f89beae9f9a57ceb207595d602f1e039217007750112828a8076d0337c17bb6a75bcc2d5a5a8c531f8b0db363a19293d5dc6ce011cdd70440c888a7
7
- data.tar.gz: e8e6484dbfae1b867542a02c39f7c96ed4434ab3f900f49fea72523466ea9be23ce2d1f2209d9fb609b7e71af63d1c047ab6f98f756ec28eef8bb47245fb9180
6
+ metadata.gz: 0111a5ab0e4959ff3e135311ed879f284cb9db70b5a510f0fa0e667385f35ab2c0c0323167f3ac30378d032e2daddeeab2f3afeacf6c36b5f0d3e7a5f5a21754
7
+ data.tar.gz: 182840922280570f41aa6bff6918b24f8c0a34da631d01fe97c3b078a3ef376346118c0026ef3d4f6f1c96cc1d96e9789f483b00fb0ea35d19044129ea0830e2
@@ -4,6 +4,14 @@ shared: &shared
4
4
  - exit_status: "*"
5
5
  limit: 1
6
6
  steps:
7
+ - name: 'Run Test Suite (:kubernetes: 1.13-latest)'
8
+ <<: *shared
9
+ command: bin/ci
10
+ agents:
11
+ queue: k8s-ci
12
+ env:
13
+ LOGGING_LEVEL: 4
14
+ KUBERNETES_VERSION: v1.13-latest
7
15
  - name: 'Run Test Suite (:kubernetes: 1.12-latest)'
8
16
  <<: *shared
9
17
  command: bin/ci
@@ -28,11 +36,3 @@ steps:
28
36
  env:
29
37
  LOGGING_LEVEL: 4
30
38
  KUBERNETES_VERSION: v1.10-latest
31
- - name: 'Run Test Suite (:kubernetes: 1.9-latest)'
32
- <<: *shared
33
- command: bin/ci
34
- agents:
35
- queue: minikube-ci
36
- env:
37
- LOGGING_LEVEL: 4
38
- KUBERNETES_VERSION: v1.9-latest
@@ -4,6 +4,14 @@ shared: &shared
4
4
  - exit_status: "*"
5
5
  limit: 1
6
6
  steps:
7
+ - name: 'Run Test Suite (:kubernetes: 1.13-latest)'
8
+ <<: *shared
9
+ command: bin/ci
10
+ agents:
11
+ queue: k8s-ci
12
+ env:
13
+ LOGGING_LEVEL: 4
14
+ KUBERNETES_VERSION: v1.13-latest
7
15
  - name: 'Run Test Suite (:kubernetes: 1.12-latest)'
8
16
  <<: *shared
9
17
  command: bin/ci
@@ -28,11 +36,3 @@ steps:
28
36
  env:
29
37
  LOGGING_LEVEL: 4
30
38
  KUBERNETES_VERSION: v1.10-latest
31
- - name: 'Run Test Suite (:kubernetes: 1.9-latest)'
32
- <<: *shared
33
- command: bin/ci
34
- agents:
35
- queue: minikube-ci
36
- env:
37
- LOGGING_LEVEL: 4
38
- KUBERNETES_VERSION: v1.9-latest
@@ -4,15 +4,6 @@ inherit_from:
4
4
  AllCops:
5
5
  TargetRubyVersion: 2.3
6
6
 
7
- Style/TrailingCommaInArrayLiteral:
8
- Enabled: false
9
-
10
- Style/TrailingCommaInHashLiteral:
11
- Enabled: false
12
-
13
- Style/MethodCallWithArgsParentheses:
14
- Enabled: false
15
-
16
7
  Naming/FileName:
17
8
  Enabled: true
18
9
  Exclude:
@@ -1,3 +1,20 @@
1
+ ## next
2
+
3
+ ## 0.24.0
4
+
5
+ *Features*
6
+ - Add support for specifying pass/fail conditions of Custom Resources ([#376](https://github.com/Shopify/kubernetes-deploy/pull/376)).
7
+ - Add support for custom timeouts for Custom Resources([#376](https://github.com/Shopify/kubernetes-deploy/pull/376))
8
+
9
+ *Enhancements*
10
+ - Officially support Kubernetes 1.13 ([#409](https://github.com/Shopify/kubernetes-deploy/pull/409))
11
+
12
+ *Bug fixes*
13
+ - Fixed bug that caused `NameError: wrong constant name` if custom resources had kind with a lowercase first letter. ([#413](https://github.com/Shopify/kubernetes-deploy/pull/413))
14
+
15
+ *Other*
16
+ - Kubernetes 1.9 is no longer officially supported as of this version
17
+
1
18
  ## 0.23.0
2
19
 
3
20
  *Features*
data/README.md CHANGED
@@ -42,6 +42,7 @@ This repo also includes related tools for [running tasks](#kubernetes-run) and [
42
42
  * [Customizing behaviour with annotations](#customizing-behaviour-with-annotations)
43
43
  * [Running tasks at the beginning of a deploy](#running-tasks-at-the-beginning-of-a-deploy)
44
44
  * [Deploying Kubernetes secrets (from EJSON)](#deploying-kubernetes-secrets-from-ejson)
45
+ * [Deploying custom resources](#deploying-custom-resources)
45
46
 
46
47
  **KUBERNETES-RESTART**
47
48
  * [Usage](#usage-1)
@@ -73,7 +74,7 @@ This repo also includes related tools for [running tasks](#kubernetes-run) and [
73
74
  ## Prerequisites
74
75
 
75
76
  * Ruby 2.3+
76
- * Your cluster must be running Kubernetes v1.9.0 or higher<sup>1</sup>
77
+ * Your cluster must be running Kubernetes v1.10.0 or higher<sup>1</sup>
77
78
  * Each app must have a deploy directory containing its Kubernetes templates (see [Templates](#using-templates-and-variables))
78
79
  * You must remove the` kubectl.kubernetes.io/last-applied-configuration` annotation from any resources in the namespace that are not included in your deploy directory. This annotation is added automatically when you create resources with `kubectl apply`. `kubernetes-deploy` will prune any resources that have this annotation and are not in the deploy directory.<sup>2</sup>
79
80
  * Each app managed by `kubernetes-deploy` must have its own exclusive Kubernetes namespace.
@@ -92,7 +93,7 @@ offical compatibility chart below.
92
93
 
93
94
  ## Installation
94
95
 
95
- 1. [Install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-via-curl) (requires v1.9.0 or higher) and make sure it is available in your $PATH
96
+ 1. [Install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-via-curl) (requires v1.10.0 or higher) and make sure it is available in your $PATH
96
97
  2. Set up your [kubeconfig file](https://kubernetes.io/docs/tasks/access-application-cluster/authenticate-across-clusters-kubeconfig/) for access to your cluster(s).
97
98
  3. `gem install kubernetes-deploy`
98
99
 
@@ -312,7 +313,95 @@ Since their data is only base64 encoded, Kubernetes secrets should not be commit
312
313
  }
313
314
  ```
314
315
 
316
+ ### Deploying custom resources
315
317
 
318
+ By default, kubernetes-deploy does not check the status of custom resources; it simply assumes that they deployed successfully. In order to meaningfully monitor the rollout of custom resources, kubernetes-deploy supports configuring pass/fail conditions using annotations on CustomResourceDefinitions (CRDs).
319
+
320
+ >Note:
321
+ This feature is only available on clusters running Kubernetes 1.11+ since it relies on the `metadata.generation` field being updated when custom resource specs are changed.
322
+
323
+ *Requirements:*
324
+
325
+ * The custom resource must expose a `status` subresource with an `observedGeneration` field.
326
+ * The `kubernetes-deploy.shopify.io/instance-rollout-conditions` annotation must be present on the CRD that defines the custom resource.
327
+ * (optional) The `kubernetes-deploy.shopify.io/instance-timeout` annotation can be added to the CRD that defines the custom resource to override the global default timeout for all instances of that resource. This annotation can use ISO8601 format or unprefixed ISO8601 time components (e.g. '1H', '60S').
328
+
329
+ #### Specifying pass/fail conditions
330
+
331
+ The presence of a valid `kubernetes-deploy.shopify.io/instance-rollout-conditions` annotation on a CRD will cause kubernetes-deploy to monitor the rollout of all instances of that custom resource. Its value can either be `"true"` (giving you the defaults described in the next section) or a valid JSON string with the following format:
332
+ ```
333
+ '{
334
+ "success_conditions": [
335
+ { "path": <JsonPath expression>, "value": <target value> }
336
+ ... more success conditions
337
+ ],
338
+ "failure_conditions": [
339
+ { "path": <JsonPath expression>, "value": <target value> }
340
+ ... more failure conditions
341
+ ]
342
+ }'
343
+ ```
344
+
345
+ For all conditions, `path` must be a valid JsonPath expression that points to a field in the custom resource's status. `value` is the value that must be present at `path` in order to fulfill a condition. For a deployment to be successful, _all_ `success_conditions` must be fulfilled. Conversely, the deploy will be marked as failed if _any one of_ `failure_conditions` is fulfilled. `success_conditions` are mandatory, but `failure_conditions` can be omitted (the resource will simply time out if it never reaches a successful state).
346
+
347
+ In addition to `path` and `value`, a failure condition can also contain `error_msg_path` or `custom_error_msg`. `error_msg_path` is a JsonPath expression that points to a field you want to surface when a failure condition is fulfilled. For example, a status condition may expose a `message` field that contains a description of the problem it encountered. `custom_error_msg` is a string that can be used if your custom resource doesn't contain sufficient information to warrant using `error_msg_path`. Note that `custom_error_msg` has higher precedence than `error_msg_path` so it will be used in favor of `error_msg_path` when both fields are present.
348
+
349
+ **Warning:**
350
+
351
+ You **must** ensure that your custom resource controller sets `.status.observedGeneration` to match the observed `.metadata.generation` of the monitored resource once its sync is complete. If this does not happen, kubernetes-deploy will not check success or failure conditions and the deploy will time out.
352
+
353
+ #### Example
354
+
355
+ As an example, the following is the default configuration that will be used if you set `kubernetes-deploy.shopify.io/instance-rollout-conditions: "true"` on the CRD that defines the custom resources you wish to monitor:
356
+
357
+ ```
358
+ '{
359
+ "success_conditions": [
360
+ {
361
+ "path": "$.status.conditions[?(@.type == \"Ready\")].status",
362
+ "value": "True",
363
+ },
364
+ ],
365
+ "failure_conditions": [
366
+ {
367
+ "path": '$.status.conditions[?(@.type == \"Failed\")].status',
368
+ "value": "True",
369
+ "error_msg_path": '$.status.conditions[?(@.type == \"Failed\")].message',
370
+ },
371
+ ],
372
+ }'
373
+ ```
374
+
375
+ The paths defined here are based on the [typical status properties](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#typical-status-properties) as defined by the Kubernetes community. It expects the `status` subresource to contain a `conditions` array whose entries minimally specify `type`, `status`, and `message` fields.
376
+
377
+ You can see how these conditions relate to the following resource:
378
+
379
+ ```
380
+ apiVersion: stable.shopify.io/v1
381
+ kind: Example
382
+ metadata:
383
+ generation: 2
384
+ name: example
385
+ namespace: namespace
386
+ spec:
387
+ ...
388
+ status:
389
+ observedGeneration: 2
390
+ conditions:
391
+ - type: "Ready"
392
+ status: "False"
393
+ reason: "exampleNotReady"
394
+ message: "resource is not ready"
395
+ - type: "Failed"
396
+ status: "True"
397
+ reason: "exampleFailed"
398
+ message: "resource is failed"
399
+ ```
400
+
401
+ - `observedGeneration == metadata.generation`, so kubernetes-deploy will check this resource's success and failure conditions.
402
+ - Since `$.status.conditions[?(@.type == "Ready")].status == "False"`, the resource is not considered successful yet.
403
+ - `$.status.conditions[?(@.type == "Failed")].status == "True"` means that a failure condition has been fulfilled and the resource is considered failed.
404
+ - Since `error_msg_path` is specified, kubernetes-deploy will log the contents of `$.status.conditions[?(@.type == "Failed")].message`, which in this case is: `resource is failed`.
316
405
 
317
406
  # kubernetes-restart
318
407
 
@@ -354,7 +443,7 @@ With this done, you can use the following command to restart all of them:
354
443
 
355
444
  ## Prerequisites
356
445
 
357
- * You've already deployed a [`PodTemplate`](https://kubernetes.io/docs/api-reference/v1.9/#podtemplate-v1-core) object with field `template` containing a `Pod` specification that does not include the `apiVersion` or `kind` parameters. An example is provided in this repo in `test/fixtures/hello-cloud/template-runner.yml`.
446
+ * You've already deployed a [`PodTemplate`](https://v1-10.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/#podtemplate-v1-core) object with field `template` containing a `Pod` specification that does not include the `apiVersion` or `kind` parameters. An example is provided in this repo in `test/fixtures/hello-cloud/template-runner.yml`.
358
447
  * The `Pod` specification in that template has a container named `task-runner`.
359
448
 
360
449
  Based on this specification `kubernetes-run` will create a new pod with the entrypoint of the `task-runner ` container overridden with the supplied arguments.
@@ -411,7 +500,7 @@ kubernetes-render --template-dir=./path/to/template/dir this-template.yaml.erb t
411
500
 
412
501
  If you work for Shopify, just run `dev up`, but otherwise:
413
502
 
414
- 1. [Install kubectl version 1.9.0 or higher](https://kubernetes.io/docs/user-guide/prereqs/) and make sure it is in your path
503
+ 1. [Install kubectl version 1.10.0 or higher](https://kubernetes.io/docs/user-guide/prereqs/) and make sure it is in your path
415
504
  2. [Install minikube](https://kubernetes.io/docs/getting-started-guides/minikube/#installation) (required to run the test suite)
416
505
  3. Check out the repo
417
506
  4. Run `bin/setup` to install dependencies
data/Rakefile CHANGED
@@ -2,28 +2,28 @@
2
2
  require "bundler/gem_tasks"
3
3
  require "rake/testtask"
4
4
 
5
- desc "Run integration tests that can be run in parallel"
5
+ desc("Run integration tests that can be run in parallel")
6
6
  Rake::TestTask.new(:integration_test) do |t|
7
7
  t.libs << "test"
8
8
  t.libs << "lib"
9
9
  t.test_files = FileList['test/integration/**/*_test.rb']
10
10
  end
11
11
 
12
- desc "Run integration tests that CANNOT be run in parallel"
12
+ desc("Run integration tests that CANNOT be run in parallel")
13
13
  Rake::TestTask.new(:serial_integration_test) do |t|
14
14
  t.libs << "test"
15
15
  t.libs << "lib"
16
16
  t.test_files = FileList['test/integration-serial/**/*_test.rb']
17
17
  end
18
18
 
19
- desc "Run unit tests"
19
+ desc("Run unit tests")
20
20
  Rake::TestTask.new(:unit_test) do |t|
21
21
  t.libs << "test"
22
22
  t.libs << "lib"
23
23
  t.test_files = FileList['test/unit/**/*_test.rb']
24
24
  end
25
25
 
26
- desc "Run all tests"
27
- task test: %w(unit_test serial_integration_test integration_test)
26
+ desc("Run all tests")
27
+ task(test: %w(unit_test serial_integration_test integration_test))
28
28
 
29
- task default: :test
29
+ task(default: :test)
data/bin/setup CHANGED
@@ -9,8 +9,8 @@ if [ ! -x "$(which minikube)" ]; then
9
9
  fi
10
10
 
11
11
  if [ ! -x "$(which kubectl)" ]; then
12
- echo -e "\n\033[0;33mPlease install kubectl version 1.9.0 or higher:\nhttps://kubernetes.io/docs/user-guide/prereqs/\033[0m"
12
+ echo -e "\n\033[0;33mPlease install kubectl version 1.10.0 or higher:\nhttps://kubernetes.io/docs/user-guide/prereqs/\033[0m"
13
13
  else
14
14
  KUBECTL_VERSION=$(kubectl version --short --client | grep -oe "v[[:digit:]\.]\+")
15
- echo -e "\n\033[0;32mKubectl version $KUBECTL_VERSION is already installed. This gem requires version v1.9.0 or greater.\033[0m"
15
+ echo -e "\n\033[0;32mKubectl version $KUBECTL_VERSION is already installed. This gem requires version v1.10.0 or greater.\033[0m"
16
16
  fi
@@ -63,7 +63,7 @@ begin
63
63
  prune: prune
64
64
  )
65
65
  rescue KubernetesDeploy::DeploymentTimeoutError
66
- exit 70
66
+ exit(70)
67
67
  rescue KubernetesDeploy::FatalDeploymentError
68
- exit 1
68
+ exit(1)
69
69
  end
@@ -29,4 +29,4 @@ runner = KubernetesDeploy::RenderTask.new(
29
29
  )
30
30
 
31
31
  success = runner.run(STDOUT, templates)
32
- exit 1 unless success
32
+ exit(1) unless success
@@ -23,7 +23,7 @@ restart = KubernetesDeploy::RestartTask.new(namespace: namespace, context: conte
23
23
  begin
24
24
  restart.perform!(raw_deployments)
25
25
  rescue KubernetesDeploy::DeploymentTimeoutError
26
- exit 70
26
+ exit(70)
27
27
  rescue KubernetesDeploy::FatalDeploymentError
28
- exit 1
28
+ exit(1)
29
29
  end
@@ -41,4 +41,4 @@ success = runner.run(
41
41
  args: ARGV[2..-1],
42
42
  env_vars: env_vars
43
43
  )
44
- exit 1 unless success
44
+ exit(1) unless success
@@ -23,19 +23,20 @@ Gem::Specification.new do |spec|
23
23
  spec.require_paths = %w(lib)
24
24
 
25
25
  spec.required_ruby_version = '>= 2.3.0'
26
- spec.add_dependency "activesupport", ">= 5.0"
27
- spec.add_dependency "kubeclient", "~> 3.0"
28
- spec.add_dependency "googleauth", "~> 0.6.6" # https://github.com/google/google-auth-library-ruby/issues/153
29
- spec.add_dependency "ejson", "~> 1.0"
30
- spec.add_dependency "colorize", "~> 0.8"
31
- spec.add_dependency "statsd-instrument", '~> 2.3', '>= 2.3.2'
32
- spec.add_dependency "oj", "~> 3.7"
33
- spec.add_dependency "concurrent-ruby", "~> 1.1"
26
+ spec.add_dependency("activesupport", ">= 5.0")
27
+ spec.add_dependency("kubeclient", "~> 3.0")
28
+ spec.add_dependency("googleauth", "~> 0.6.6") # https://github.com/google/google-auth-library-ruby/issues/153
29
+ spec.add_dependency("ejson", "~> 1.0")
30
+ spec.add_dependency("colorize", "~> 0.8")
31
+ spec.add_dependency("statsd-instrument", '~> 2.3', '>= 2.3.2')
32
+ spec.add_dependency("oj", "~> 3.7")
33
+ spec.add_dependency("concurrent-ruby", "~> 1.1")
34
+ spec.add_dependency("jsonpath", "~> 0.9.6")
34
35
 
35
- spec.add_development_dependency "bundler"
36
- spec.add_development_dependency "rake", "~> 10.0"
37
- spec.add_development_dependency "minitest", "~> 5.0"
38
- spec.add_development_dependency "minitest-stub-const", "~> 0.6"
39
- spec.add_development_dependency "webmock", "~> 3.0"
40
- spec.add_development_dependency "mocha", "~> 1.5"
36
+ spec.add_development_dependency("bundler")
37
+ spec.add_development_dependency("rake", "~> 10.0")
38
+ spec.add_development_dependency("minitest", "~> 5.0")
39
+ spec.add_development_dependency("minitest-stub-const", "~> 0.6")
40
+ spec.add_development_dependency("webmock", "~> 3.0")
41
+ spec.add_development_dependency("mocha", "~> 1.5")
41
42
  end
@@ -23,6 +23,6 @@ require 'kubernetes-deploy/duration_parser'
23
23
  require 'kubernetes-deploy/resource_cache'
24
24
 
25
25
  module KubernetesDeploy
26
- MIN_KUBE_VERSION = '1.9.0'
26
+ MIN_KUBE_VERSION = '1.10.0'
27
27
  StatsD.build
28
28
  end
@@ -29,7 +29,7 @@ module KubernetesDeploy
29
29
  prefix_str = "[#{container_name}] " if prefix
30
30
 
31
31
  lines[@next_print_index..-1].each do |msg|
32
- @logger.info "#{prefix_str}#{msg}"
32
+ @logger.info("#{prefix_str}#{msg}")
33
33
  end
34
34
 
35
35
  @next_print_index = lines.length
@@ -6,6 +6,7 @@ require 'tempfile'
6
6
  require 'fileutils'
7
7
  require 'kubernetes-deploy/kubernetes_resource'
8
8
  %w(
9
+ custom_resource
9
10
  cloudsql
10
11
  config_map
11
12
  deployment
@@ -24,7 +25,6 @@ require 'kubernetes-deploy/kubernetes_resource'
24
25
  elasticsearch
25
26
  statefulservice
26
27
  topic
27
- bucket
28
28
  stateful_set
29
29
  cron_job
30
30
  job
@@ -46,19 +46,6 @@ module KubernetesDeploy
46
46
  include KubeclientBuilder
47
47
  extend KubernetesDeploy::StatsD::MeasureMethods
48
48
 
49
- PREDEPLOY_SEQUENCE = %w(
50
- ResourceQuota
51
- Cloudsql
52
- Redis
53
- Memcached
54
- ConfigMap
55
- PersistentVolumeClaim
56
- ServiceAccount
57
- Role
58
- RoleBinding
59
- Pod
60
- )
61
-
62
49
  PROTECTED_NAMESPACES = %w(
63
50
  default
64
51
  kube-system
@@ -73,6 +60,22 @@ module KubernetesDeploy
73
60
  # extensions/v1beta1/ReplicaSet -- managed by deployments
74
61
  # core/v1/Secret -- should not committed / managed by shipit
75
62
 
63
+ def predeploy_sequence
64
+ before_crs = %w(
65
+ ResourceQuota
66
+ )
67
+ after_crs = %w(
68
+ ConfigMap
69
+ PersistentVolumeClaim
70
+ ServiceAccount
71
+ Role
72
+ RoleBinding
73
+ Pod
74
+ )
75
+
76
+ before_crs + cluster_resource_discoverer.crds.map(&:kind) + after_crs
77
+ end
78
+
76
79
  def prune_whitelist
77
80
  wl = %w(
78
81
  core/v1/ConfigMap
@@ -194,11 +197,11 @@ module KubernetesDeploy
194
197
  end
195
198
 
196
199
  def deploy_has_priority_resources?(resources)
197
- resources.any? { |r| PREDEPLOY_SEQUENCE.include?(r.type) }
200
+ resources.any? { |r| predeploy_sequence.include?(r.type) }
198
201
  end
199
202
 
200
203
  def predeploy_priority_resources(resource_list)
201
- PREDEPLOY_SEQUENCE.each do |resource_type|
204
+ predeploy_sequence.each do |resource_type|
202
205
  matching_resources = resource_list.select { |r| r.type == resource_type }
203
206
  next if matching_resources.empty?
204
207
  deploy_resources(matching_resources, verify: true, record_summary: false)
@@ -254,14 +257,16 @@ module KubernetesDeploy
254
257
 
255
258
  def discover_resources
256
259
  resources = []
260
+ crds = cluster_resource_discoverer.crds.group_by(&:kind)
257
261
  @logger.info("Discovering templates:")
258
262
 
259
263
  TemplateDiscovery.new(@template_dir).templates.each do |filename|
260
264
  split_templates(filename) do |r_def|
261
- r = KubernetesResource.build(namespace: @namespace, context: @context, logger: @logger,
262
- definition: r_def, statsd_tags: @namespace_tags)
265
+ crd = crds[r_def["kind"]]&.first
266
+ r = KubernetesResource.build(namespace: @namespace, context: @context, logger: @logger, definition: r_def,
267
+ statsd_tags: @namespace_tags, crd: crd)
263
268
  resources << r
264
- @logger.info " - #{r.id}"
269
+ @logger.info(" - #{r.id}")
265
270
  end
266
271
  end
267
272
  if (global = resources.select(&:global?).presence)
@@ -145,9 +145,9 @@ module KubernetesDeploy
145
145
  "name" => secret_name,
146
146
  "labels" => { "name" => secret_name },
147
147
  "namespace" => @namespace,
148
- "annotations" => { MANAGEMENT_ANNOTATION => "true" }
148
+ "annotations" => { MANAGEMENT_ANNOTATION => "true" },
149
149
  },
150
- "data" => encoded_data
150
+ "data" => encoded_data,
151
151
  }
152
152
  secret.to_yaml
153
153
  end
@@ -100,7 +100,7 @@ module KubernetesDeploy
100
100
  auth_options: kube_context.auth_options,
101
101
  timeouts: {
102
102
  open: KubernetesDeploy::Kubectl::DEFAULT_TIMEOUT,
103
- read: KubernetesDeploy::Kubectl::DEFAULT_TIMEOUT
103
+ read: KubernetesDeploy::Kubectl::DEFAULT_TIMEOUT,
104
104
  }
105
105
  )
106
106
  client.discover
@@ -30,7 +30,7 @@ module KubernetesDeploy
30
30
  out, err, st = nil
31
31
 
32
32
  (1..attempts).to_a.each do |attempt|
33
- @logger.debug "Running command (attempt #{attempt}): #{args.join(' ')}"
33
+ @logger.debug("Running command (attempt #{attempt}): #{args.join(' ')}")
34
34
  out, err, st = Open3.capture3(*args)
35
35
  @logger.debug("Kubectl out: " + out.gsub(/\s+/, ' ')) unless output_is_sensitive?
36
36
 
@@ -47,7 +47,7 @@ module KubernetesDeploy
47
47
  @logger.debug("Kubectl err: #{err}") unless output_is_sensitive?
48
48
  StatsD.increment('kubectl.error', 1, tags: { context: @context, namespace: @namespace, cmd: args[1] })
49
49
  end
50
- sleep retry_delay(attempt) unless attempt == attempts
50
+ sleep(retry_delay(attempt)) unless attempt == attempts
51
51
  end
52
52
 
53
53
  [out.chomp, err.chomp, st]
@@ -31,14 +31,21 @@ module KubernetesDeploy
31
31
  TIMEOUT_OVERRIDE_ANNOTATION = "kubernetes-deploy.shopify.io/timeout-override"
32
32
 
33
33
  class << self
34
- def build(namespace:, context:, definition:, logger:, statsd_tags:)
34
+ def build(namespace:, context:, definition:, logger:, statsd_tags:, crd: nil)
35
35
  opts = { namespace: namespace, context: context, definition: definition, logger: logger,
36
36
  statsd_tags: statsd_tags }
37
37
  if definition["kind"].blank?
38
38
  raise InvalidTemplateError.new("Template missing 'Kind'", content: definition.to_yaml)
39
- elsif KubernetesDeploy.const_defined?(definition["kind"])
40
- klass = KubernetesDeploy.const_get(definition["kind"])
41
- klass.new(**opts)
39
+ end
40
+ begin
41
+ if KubernetesDeploy.const_defined?(definition["kind"])
42
+ klass = KubernetesDeploy.const_get(definition["kind"])
43
+ return klass.new(**opts)
44
+ end
45
+ rescue NameError
46
+ end
47
+ if crd
48
+ CustomResource.new(crd: crd, **opts)
42
49
  else
43
50
  inst = new(**opts)
44
51
  inst.type = definition["kind"]
@@ -162,13 +169,13 @@ module KubernetesDeploy
162
169
 
163
170
  def current_generation
164
171
  return -1 unless exists? # must be different default than observed_generation
165
- @instance_data["metadata"]["generation"]
172
+ @instance_data.dig("metadata", "generation")
166
173
  end
167
174
 
168
175
  def observed_generation
169
176
  return -2 unless exists?
170
177
  # populating this is a best practice, but not all controllers actually do it
171
- @instance_data["status"]["observedGeneration"]
178
+ @instance_data.dig('status', 'observedGeneration')
172
179
  end
173
180
 
174
181
  def status
@@ -319,7 +326,7 @@ module KubernetesDeploy
319
326
  '(ne .reason "SuccessfulCreate")',
320
327
  '(ne .reason "Scheduled")',
321
328
  '(ne .reason "Pulling")',
322
- '(ne .reason "Pulled")'
329
+ '(ne .reason "Pulled")',
323
330
  ]
324
331
  condition_start = "{{if and #{and_conditions.join(' ')}}}"
325
332
  field_part = FIELDS.map { |f| "{{#{f}}}" }.join(%({{print "#{FIELD_SEPARATOR}"}}))
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+ require 'jsonpath'
3
+
4
+ module KubernetesDeploy
5
+ class CustomResource < KubernetesResource
6
+ TIMEOUT_MESSAGE_DIFFERENT_GENERATIONS = <<~MSG
7
+ This resource's status could not be used to determine rollout success because it is not up-to-date
8
+ (.metadata.generation != .status.observedGeneration).
9
+ MSG
10
+
11
+ def initialize(namespace:, context:, definition:, logger:, statsd_tags: [], crd:)
12
+ super(namespace: namespace, context: context, definition: definition,
13
+ logger: logger, statsd_tags: statsd_tags)
14
+ @crd = crd
15
+ end
16
+
17
+ def timeout
18
+ timeout_override || @crd.timeout_for_instance || TIMEOUT
19
+ end
20
+
21
+ def deploy_succeeded?
22
+ return super unless rollout_conditions
23
+ return false unless observed_generation == current_generation
24
+
25
+ rollout_conditions.rollout_successful?(@instance_data)
26
+ end
27
+
28
+ def deploy_failed?
29
+ return super unless rollout_conditions
30
+ return false unless observed_generation == current_generation
31
+
32
+ rollout_conditions.rollout_failed?(@instance_data)
33
+ end
34
+
35
+ def failure_message
36
+ return super unless rollout_conditions
37
+ messages = rollout_conditions.failure_messages(@instance_data)
38
+ messages.join("\n") if messages.present?
39
+ end
40
+
41
+ def timeout_message
42
+ if rollout_conditions && current_generation != observed_generation
43
+ TIMEOUT_MESSAGE_DIFFERENT_GENERATIONS
44
+ else
45
+ super
46
+ end
47
+ end
48
+
49
+ def status
50
+ if !exists? || rollout_conditions.nil?
51
+ super
52
+ elsif deploy_succeeded?
53
+ "Healthy"
54
+ elsif deploy_failed?
55
+ "Unhealthy"
56
+ else
57
+ "Unknown"
58
+ end
59
+ end
60
+
61
+ def type
62
+ kind
63
+ end
64
+
65
+ def validate_definition(kubectl)
66
+ super
67
+
68
+ @crd.validate_rollout_conditions
69
+ rescue RolloutConditionsError => e
70
+ @validation_errors << "The CRD that specifies this resource is using invalid rollout conditions. " \
71
+ "Kubernetes-deploy will not be able to continue until those rollout conditions are fixed.\n" \
72
+ "Rollout conditions can be found on the CRD that defines this resource (#{@crd.name}), " \
73
+ "under the annotation #{CustomResourceDefinition::ROLLOUT_CONDITIONS_ANNOTATION}.\n" \
74
+ "Validation failed with: #{e}"
75
+ end
76
+
77
+ private
78
+
79
+ def kind
80
+ @definition["kind"]
81
+ end
82
+
83
+ def rollout_conditions
84
+ @crd.rollout_conditions
85
+ end
86
+ end
87
+ end
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
+ require 'kubernetes-deploy/rollout_conditions'
3
+
2
4
  module KubernetesDeploy
3
5
  class CustomResourceDefinition < KubernetesResource
4
6
  TIMEOUT = 2.minutes
7
+ ROLLOUT_CONDITIONS_ANNOTATION = "kubernetes-deploy.shopify.io/instance-rollout-conditions"
8
+ TIMEOUT_FOR_INSTANCE_ANNOTATION = "kubernetes-deploy.shopify.io/instance-timeout"
5
9
  GLOBAL = true
6
10
 
7
11
  def deploy_succeeded?
@@ -16,6 +20,13 @@ module KubernetesDeploy
16
20
  "The names this CRD is attempting to register were neither accepted nor rejected in time"
17
21
  end
18
22
 
23
+ def timeout_for_instance
24
+ timeout = @definition.dig("metadata", "annotations", TIMEOUT_FOR_INSTANCE_ANNOTATION)
25
+ DurationParser.new(timeout).parse!.to_i
26
+ rescue DurationParser::ParsingError
27
+ nil
28
+ end
29
+
19
30
  def status
20
31
  if !exists?
21
32
  super
@@ -36,11 +47,42 @@ module KubernetesDeploy
36
47
  @definition.dig("spec", "names", "kind")
37
48
  end
38
49
 
50
+ def name
51
+ @definition.dig("metadata", "name")
52
+ end
53
+
39
54
  def prunable?
40
55
  prunable = @definition.dig("metadata", "annotations", "kubernetes-deploy.shopify.io/prunable")
41
56
  prunable == "true"
42
57
  end
43
58
 
59
+ def rollout_conditions
60
+ return @rollout_conditions if defined?(@rollout_conditions)
61
+
62
+ @rollout_conditions = if rollout_conditions_annotation
63
+ RolloutConditions.from_annotation(rollout_conditions_annotation)
64
+ end
65
+ rescue RolloutConditionsError
66
+ @rollout_conditions = nil
67
+ end
68
+
69
+ def validate_definition(_)
70
+ super
71
+
72
+ validate_rollout_conditions
73
+ rescue RolloutConditionsError => e
74
+ @validation_errors << "Annotation #{ROLLOUT_CONDITIONS_ANNOTATION} on #{name} is invalid: #{e}"
75
+ end
76
+
77
+ def validate_rollout_conditions
78
+ if rollout_conditions_annotation && @rollout_conditions_validated.nil?
79
+ conditions = RolloutConditions.from_annotation(rollout_conditions_annotation)
80
+ conditions.validate!
81
+ end
82
+
83
+ @rollout_conditions_validated = true
84
+ end
85
+
44
86
  private
45
87
 
46
88
  def names_accepted_condition
@@ -51,5 +93,9 @@ module KubernetesDeploy
51
93
  def names_accepted_status
52
94
  names_accepted_condition["status"]
53
95
  end
96
+
97
+ def rollout_conditions_annotation
98
+ @definition.dig("metadata", "annotations", ROLLOUT_CONDITIONS_ANNOTATION)
99
+ end
54
100
  end
55
101
  end
@@ -11,7 +11,7 @@ module KubernetesDeploy
11
11
  puts "Template directory is unknown. " \
12
12
  "Either specify --template-dir argument or set $ENVIRONMENT to use config/deploy/$ENVIRONMENT " \
13
13
  + "as a default path."
14
- exit 1
14
+ exit(1)
15
15
  end
16
16
 
17
17
  template_dir
@@ -59,7 +59,7 @@ module KubernetesDeploy
59
59
  {
60
60
  namespace: @namespace,
61
61
  context: @context,
62
- sha: @sha
62
+ sha: @sha,
63
63
  }
64
64
  end
65
65
 
@@ -132,7 +132,7 @@ module KubernetesDeploy
132
132
  deployments.each do |record|
133
133
  begin
134
134
  patch_deployment_with_restart(record)
135
- @logger.info "Triggered `#{record.metadata.name}` restart"
135
+ @logger.info("Triggered `#{record.metadata.name}` restart")
136
136
  rescue Kubeclient::ResourceNotFoundError, Kubeclient::HttpError => e
137
137
  raise RestartAPIError.new(record.metadata.name, e.message)
138
138
  end
@@ -164,12 +164,12 @@ module KubernetesDeploy
164
164
  containers: containers.map do |container|
165
165
  {
166
166
  name: container.name,
167
- env: [{ name: "RESTARTED_AT", value: Time.now.to_i.to_s }]
167
+ env: [{ name: "RESTARTED_AT", value: Time.now.to_i.to_s }],
168
168
  }
169
- end
170
- }
171
- }
172
- }
169
+ end,
170
+ },
171
+ },
172
+ },
173
173
  }
174
174
  end
175
175
 
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+ module KubernetesDeploy
3
+ class RolloutConditionsError < StandardError
4
+ end
5
+
6
+ class RolloutConditions
7
+ VALID_FAILURE_CONDITION_KEYS = [:path, :value, :error_msg_path, :custom_error_msg]
8
+ VALID_SUCCESS_CONDITION_KEYS = [:path, :value]
9
+
10
+ class << self
11
+ def from_annotation(conditions_string)
12
+ return new(default_conditions) if conditions_string.downcase.strip == "true"
13
+
14
+ conditions = JSON.parse(conditions_string).slice('success_conditions', 'failure_conditions')
15
+ conditions.deep_symbolize_keys!
16
+
17
+ # Create JsonPath objects
18
+ conditions[:success_conditions]&.each do |query|
19
+ query.slice!(*VALID_SUCCESS_CONDITION_KEYS)
20
+ query[:path] = JsonPath.new(query[:path]) if query.key?(:path)
21
+ end
22
+ conditions[:failure_conditions]&.each do |query|
23
+ query.slice!(*VALID_FAILURE_CONDITION_KEYS)
24
+ query[:path] = JsonPath.new(query[:path]) if query.key?(:path)
25
+ query[:error_msg_path] = JsonPath.new(query[:error_msg_path]) if query.key?(:error_msg_path)
26
+ end
27
+
28
+ new(conditions)
29
+ rescue JSON::ParserError => e
30
+ raise RolloutConditionsError, "Rollout conditions are not valid JSON: #{e}"
31
+ rescue StandardError => e
32
+ raise RolloutConditionsError,
33
+ "Error parsing rollout conditions. " \
34
+ "This is most likely caused by an invalid JsonPath expression. Failed with: #{e}"
35
+ end
36
+
37
+ def default_conditions
38
+ {
39
+ success_conditions: [
40
+ {
41
+ path: JsonPath.new('$.status.conditions[?(@.type == "Ready")].status'),
42
+ value: "True",
43
+ },
44
+ ],
45
+ failure_conditions: [
46
+ {
47
+ path: JsonPath.new('$.status.conditions[?(@.type == "Failed")].status'),
48
+ value: "True",
49
+ error_msg_path: JsonPath.new('$.status.conditions[?(@.type == "Failed")].message'),
50
+ },
51
+ ],
52
+ }
53
+ end
54
+ end
55
+
56
+ def initialize(conditions)
57
+ @success_conditions = conditions.fetch(:success_conditions, [])
58
+ @failure_conditions = conditions.fetch(:failure_conditions, [])
59
+ end
60
+
61
+ def rollout_successful?(instance_data)
62
+ @success_conditions.all? do |query|
63
+ query[:path].first(instance_data) == query[:value]
64
+ end
65
+ end
66
+
67
+ def rollout_failed?(instance_data)
68
+ @failure_conditions.any? do |query|
69
+ query[:path].first(instance_data) == query[:value]
70
+ end
71
+ end
72
+
73
+ def failure_messages(instance_data)
74
+ @failure_conditions.map do |query|
75
+ next unless query[:path].first(instance_data) == query[:value]
76
+ query[:custom_error_msg].presence || query[:error_msg_path]&.first(instance_data)
77
+ end.compact
78
+ end
79
+
80
+ def validate!
81
+ errors = validate_conditions(@success_conditions, 'success_conditions')
82
+ errors += validate_conditions(@failure_conditions, 'failure_conditions', required: false)
83
+ raise RolloutConditionsError, errors.join(", ") unless errors.empty?
84
+ end
85
+
86
+ private
87
+
88
+ def validate_conditions(conditions, source_key, required: true)
89
+ return [] unless conditions.present? || required
90
+ errors = []
91
+ errors << "#{source_key} should be Array but found #{conditions.class}" unless conditions.is_a?(Array)
92
+ return errors if errors.present?
93
+ errors << "#{source_key} must contain at least one entry" if conditions.empty?
94
+ return errors if errors.present?
95
+
96
+ conditions.each do |query|
97
+ missing = [:path, :value].reject { |k| query.key?(k) }
98
+ errors << "Missing required key(s) for #{source_key.singularize}: #{missing}" if missing.present?
99
+ end
100
+ errors
101
+ end
102
+ end
103
+ end
@@ -59,7 +59,7 @@ module KubernetesDeploy
59
59
  private
60
60
 
61
61
  def create_pod(pod)
62
- @logger.info "Creating pod '#{pod.name}'"
62
+ @logger.info("Creating pod '#{pod.name}'")
63
63
  pod.deploy_started_at = Time.now.utc
64
64
  kubeclient.create_pod(pod.to_kubeclient_resource)
65
65
  @pod_name = pod.name
@@ -121,7 +121,7 @@ module KubernetesDeploy
121
121
 
122
122
  begin
123
123
  kubeclient.get_namespace(@namespace) if @namespace.present?
124
- @logger.info "Using namespace '#{@namespace}' in context '#{@context}'"
124
+ @logger.info("Using namespace '#{@namespace}' in context '#{@context}'")
125
125
  rescue KubeException => e
126
126
  msg = e.error_code == 404 ? "Namespace was not found" : "Could not connect to kubernetes cluster"
127
127
  errors << msg
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module KubernetesDeploy
3
- VERSION = "0.23.0"
3
+ VERSION = "0.24.0"
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.23.0
4
+ version: 0.24.0
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: 2018-12-14 00:00:00.000000000 Z
12
+ date: 2019-01-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -129,6 +129,20 @@ dependencies:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
131
  version: '1.1'
132
+ - !ruby/object:Gem::Dependency
133
+ name: jsonpath
134
+ requirement: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.9.6
139
+ type: :runtime
140
+ prerelease: false
141
+ version_requirements: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.9.6
132
146
  - !ruby/object:Gem::Dependency
133
147
  name: bundler
134
148
  requirement: !ruby/object:Gem::Requirement
@@ -261,10 +275,10 @@ files:
261
275
  - lib/kubernetes-deploy/kubeclient_builder/google_friendly_config.rb
262
276
  - lib/kubernetes-deploy/kubectl.rb
263
277
  - lib/kubernetes-deploy/kubernetes_resource.rb
264
- - lib/kubernetes-deploy/kubernetes_resource/bucket.rb
265
278
  - lib/kubernetes-deploy/kubernetes_resource/cloudsql.rb
266
279
  - lib/kubernetes-deploy/kubernetes_resource/config_map.rb
267
280
  - lib/kubernetes-deploy/kubernetes_resource/cron_job.rb
281
+ - lib/kubernetes-deploy/kubernetes_resource/custom_resource.rb
268
282
  - lib/kubernetes-deploy/kubernetes_resource/custom_resource_definition.rb
269
283
  - lib/kubernetes-deploy/kubernetes_resource/daemon_set.rb
270
284
  - lib/kubernetes-deploy/kubernetes_resource/deployment.rb
@@ -296,6 +310,7 @@ files:
296
310
  - lib/kubernetes-deploy/resource_cache.rb
297
311
  - lib/kubernetes-deploy/resource_watcher.rb
298
312
  - lib/kubernetes-deploy/restart_task.rb
313
+ - lib/kubernetes-deploy/rollout_conditions.rb
299
314
  - lib/kubernetes-deploy/runner_task.rb
300
315
  - lib/kubernetes-deploy/statsd.rb
301
316
  - lib/kubernetes-deploy/template_discovery.rb
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
- module KubernetesDeploy
3
- class Bucket < KubernetesResource
4
- def deploy_succeeded?
5
- return false unless deploy_started?
6
-
7
- unless @success_assumption_warning_shown
8
- @logger.warn("Don't know how to monitor resources of type #{type}. Assuming #{id} deployed successfully.")
9
- @success_assumption_warning_shown = true
10
- end
11
- true
12
- end
13
-
14
- def status
15
- exists? ? "Available" : "Unknown"
16
- end
17
-
18
- def deploy_failed?
19
- false
20
- end
21
- end
22
- end