kubernetes-deploy 0.6.1 → 0.6.2

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
  SHA1:
3
- metadata.gz: 8d4d12495a2c45feea06818619b789fe435c9458
4
- data.tar.gz: be4b5c1fd86dd89e61ff4c209325dd355a9c4ca6
3
+ metadata.gz: 55540c9fa5a002f87eba1c06883e0b237d194803
4
+ data.tar.gz: 5d2e45261bb51d18d34f5337eb7006892885b99e
5
5
  SHA512:
6
- metadata.gz: dbc9a37c932a11084c881b5333bb32d2d86500095c704f0a17ad2e0d53331f91105b4f0afbe5134a30f244fdce6b53b48cc9e43f8005d6c6bcfc4ed6ff59c29a
7
- data.tar.gz: 3910718f3c377f15a88e478211c1ebc25a9ba365af017405f7999f1c0a2fcdf14f43b0b348d900666640200a623baaef2b3c37149d32ccee83f75246e6130cd5
6
+ metadata.gz: 0fb77ac54ebf8f56eb7c116a29ef373da76d694c4977a5c704f78cafacf9c97c2cad25e5137e9991eaf689661323ab889b205da8db96a87c02c0ce94d641f023
7
+ data.tar.gz: 2e00d11e27cd3965e30592552adcf77ea7b599cb120a376d2c32b226b7bd07f4c0a1040da24534304b9910ed8c95cf4ad447937a3773d01cc11d5e1b5eac3e2a
data/Gemfile CHANGED
@@ -4,6 +4,7 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  gem 'pry'
7
+ gem 'pry-byebug'
7
8
  gem 'rubocop'
8
9
  gem 'timecop'
9
10
  gem 'byebug'
data/README.md CHANGED
@@ -40,6 +40,42 @@ The following command will restart all pods in the `web` and `jobs` deployments:
40
40
 
41
41
  `kubernetes-restart <kube namespace> <kube context> --deployments=web,jobs`
42
42
 
43
+ ### Deploying Kubernetes secrets
44
+
45
+ **Note: If you're a Shopify employee using our cloud platform, this setup has already been done for you. Please consult the CloudPlatform User Guide for usage instructions.**
46
+
47
+ Since their data is only base64 encoded, Kubernetes secrets should not be committed to your repository. Instead, `kubernetes-deploy` supports generating secrets from an encrypted [ejson](https://github.com/Shopify/ejson) file in your template directory. Here's how to use this feature:
48
+
49
+ 1. Install the ejson gem: `gem install ejson`
50
+ 2. Generate a new keypair: `ejson keygen` (prints the keypair to stdout)
51
+ 3. Create a Kubernetes secret in your target namespace with the new keypair: `kubectl create secret generic ejson-keys --from-literal=YOUR_PUBLIC_KEY=YOUR_PRIVATE_KEY --namespace=TARGET_NAMESPACE`
52
+ 4. (optional but highly recommended) Back up the keypair somewhere secure, such as a password manager, for disaster recovery purposes.
53
+ 5. In your template directory (alongside your Kubernetes templates), create `secrets.ejson` with the format shown below. The `_type` key should have the value “kubernetes.io/tls” for TLS secrets and “Opaque” for all others. The `data` key must be a json object, but its keys and values can be whatever you need.
54
+
55
+ ```json
56
+ {
57
+ "_public_key": "YOUR_PUBLIC_KEY",
58
+ "kubernetes_secrets": {
59
+ "catphotoscom": {
60
+ "_type": "kubernetes.io/tls",
61
+ "data": {
62
+ "tls.crt": "cert-data-here",
63
+ "tls.key": "key-data-here"
64
+ }
65
+ },
66
+ "monitoring-token": {
67
+ "_type": "Opaque",
68
+ "data": {
69
+ "api-token": "token-value-here"
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ 6. Encrypt the file: `ejson encrypt /PATH/TO/secrets.ejson`
77
+ 7. Commit the encrypted file and deploy as usual. The deploy will create secrets from the data in the `kubernetes_secrets` key.
78
+
43
79
  ### Running one off tasks
44
80
 
45
81
  To trigger a one-off job such as a rake task _outside_ of a deploy, use the following command:
@@ -50,7 +86,7 @@ This command assumes that you've already deployed a `PodTemplate` named `task-ru
50
86
 
51
87
  #### Creating a PodTemplate
52
88
 
53
- The [`PodTemplate`](https://kubernetes.io/docs/api-reference/v1.6/#podtemplate-v1-core) object should have a field `template` containing a `Pod` specification which does not include the `apiVersion` or `kind` parameters. An example is provided in this repo in `test/fixtures/hello-cloud/template-runner.yml`.
89
+ The [`PodTemplate`](https://kubernetes.io/docs/api-reference/v1.6/#podtemplate-v1-core) object should have a field `template` containing a `Pod` specification which does not include the `apiVersion` or `kind` parameters. An example is provided in this repo in `test/fixtures/hello-cloud/template-runner.yml`.
54
90
 
55
91
  #### Providing multiple different task-runner configurations
56
92
 
@@ -8,16 +8,11 @@ require 'kubernetes-deploy/restart_task'
8
8
 
9
9
  raw_deployments = nil
10
10
  ARGV.options do |opts|
11
- opts.on("--deployments=LIST") { |v| raw_deployments = v }
11
+ opts.on("--deployments=LIST") { |v| raw_deployments = v.split(",") }
12
12
  opts.parse!
13
13
  end
14
14
 
15
- if raw_deployments.nil?
16
- puts "Failed: specify at least one deployment to restart with --deployments flag"
17
- exit 1
18
- end
19
-
20
15
  KubernetesDeploy::Runner.with_friendly_errors do
21
16
  restart = KubernetesDeploy::RestartTask.new(namespace: ARGV[0], context: ARGV[1])
22
- restart.perform(raw_deployments.split(","))
17
+ restart.perform(raw_deployments)
23
18
  end
@@ -23,10 +23,12 @@ Gem::Specification.new do |spec|
23
23
  spec.add_dependency "activesupport", ">= 4.2"
24
24
  spec.add_dependency "kubeclient", "~> 2.3"
25
25
  spec.add_dependency "googleauth", ">= 0.5"
26
+ spec.add_dependency "ejson", "1.0.1"
26
27
 
27
28
  spec.add_development_dependency "bundler", "~> 1.13"
28
29
  spec.add_development_dependency "rake", "~> 10.0"
29
30
  spec.add_development_dependency "minitest", "~> 5.0"
30
31
  spec.add_development_dependency "minitest-stub-const", "~> 0.6"
31
32
  spec.add_development_dependency "webmock", "~> 3.0"
33
+ spec.add_development_dependency "mocha", "~> 1.1"
32
34
  end
@@ -4,18 +4,12 @@ require 'active_support/core_ext/hash/slice'
4
4
  require 'active_support/core_ext/numeric/time'
5
5
  require 'active_support/core_ext/string/inflections'
6
6
  require 'active_support/core_ext/string/strip'
7
+ require 'active_support/core_ext/hash/keys'
7
8
 
9
+ require 'kubernetes-deploy/errors'
8
10
  require 'kubernetes-deploy/logger'
9
11
  require 'kubernetes-deploy/runner'
10
12
 
11
13
  module KubernetesDeploy
12
- class FatalDeploymentError < StandardError; end
13
-
14
- class NamespaceNotFoundError < FatalDeploymentError
15
- def initialize(name, context)
16
- super("Namespace `#{name}` not found in context `#{context}`. Aborting the task.")
17
- end
18
- end
19
-
20
14
  include Logger
21
15
  end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+ require 'base64'
4
+ require 'open3'
5
+ require 'kubernetes-deploy/logger'
6
+
7
+ module KubernetesDeploy
8
+ class EjsonSecretError < FatalDeploymentError
9
+ def initialize(msg)
10
+ super("Creation of Kubernetes secrets from ejson failed: #{msg}")
11
+ end
12
+ end
13
+
14
+ class EjsonSecretProvisioner
15
+ MANAGEMENT_ANNOTATION = "kubernetes-deploy.shopify.io/ejson-secret"
16
+ MANAGED_SECRET_EJSON_KEY = "kubernetes_secrets"
17
+ EJSON_SECRETS_FILE = "secrets.ejson"
18
+ EJSON_KEYS_SECRET = "ejson-keys"
19
+
20
+ def initialize(namespace:, template_dir:, client:)
21
+ @namespace = namespace
22
+ @ejson_file = "#{template_dir}/#{EJSON_SECRETS_FILE}"
23
+ @kubeclient = client
24
+ end
25
+
26
+ def secret_changes_required?
27
+ File.exist?(@ejson_file) || managed_secrets_exist?
28
+ end
29
+
30
+ def run
31
+ create_secrets
32
+ prune_managed_secrets
33
+ end
34
+
35
+ private
36
+
37
+ def create_secrets
38
+ with_decrypted_ejson do |decrypted|
39
+ secrets = decrypted[MANAGED_SECRET_EJSON_KEY]
40
+ unless secrets.present?
41
+ KubernetesDeploy.logger.warn("#{EJSON_SECRETS_FILE} does not have key #{MANAGED_SECRET_EJSON_KEY}."\
42
+ "No secrets will be created.")
43
+ return
44
+ end
45
+
46
+ secrets.each do |secret_name, secret_spec|
47
+ validate_secret_spec(secret_name, secret_spec)
48
+ create_or_update_secret(secret_name, secret_spec["_type"], secret_spec["data"])
49
+ end
50
+ end
51
+ end
52
+
53
+ def prune_managed_secrets
54
+ ejson_secret_names = encrypted_ejson.fetch(MANAGED_SECRET_EJSON_KEY, {}).keys
55
+ live_secrets = @kubeclient.get_secrets(namespace: @namespace)
56
+
57
+ live_secrets.each do |secret|
58
+ secret_name = secret.metadata.name
59
+ next unless secret_managed?(secret)
60
+ next if ejson_secret_names.include?(secret_name)
61
+
62
+ KubernetesDeploy.logger.info("Pruning secret #{secret_name}")
63
+ @kubeclient.delete_secret(secret_name, @namespace)
64
+ end
65
+ end
66
+
67
+ def managed_secrets_exist?
68
+ all_secrets = @kubeclient.get_secrets(namespace: @namespace)
69
+ all_secrets.any? { |secret| secret_managed?(secret) }
70
+ end
71
+
72
+ def secret_managed?(secret)
73
+ secret.metadata.annotations.to_h.stringify_keys.key?(MANAGEMENT_ANNOTATION)
74
+ end
75
+
76
+ def encrypted_ejson
77
+ @encrypted_ejson ||= load_ejson_from_file
78
+ end
79
+
80
+ def public_key
81
+ encrypted_ejson["_public_key"]
82
+ end
83
+
84
+ def private_key
85
+ @private_key ||= fetch_private_key_from_secret
86
+ end
87
+
88
+ def validate_secret_spec(secret_name, spec)
89
+ errors = []
90
+ errors << "secret type unspecified" if spec["_type"].blank?
91
+ errors << "no data provided" if spec["data"].blank?
92
+
93
+ unless errors.empty?
94
+ raise EjsonSecretError, "Ejson incomplete for secret #{secret_name}: #{errors.join(', ')}"
95
+ end
96
+ end
97
+
98
+ def create_or_update_secret(secret_name, secret_type, data)
99
+ metadata = {
100
+ name: secret_name,
101
+ labels: { "name" => secret_name },
102
+ namespace: @namespace,
103
+ annotations: { MANAGEMENT_ANNOTATION => "true" }
104
+ }
105
+ secret = Kubeclient::Secret.new(type: secret_type, stringData: data, metadata: metadata)
106
+ if secret_exists?(secret)
107
+ KubernetesDeploy.logger.info("Updating secret #{secret_name}")
108
+ @kubeclient.update_secret(secret)
109
+ else
110
+ KubernetesDeploy.logger.info("Creating secret #{secret_name}")
111
+ @kubeclient.create_secret(secret)
112
+ end
113
+ rescue KubeException => e
114
+ raise unless e.error_code == 400
115
+ raise EjsonSecretError, "Data for secret #{secret_name} was invalid: #{e}"
116
+ end
117
+
118
+ def secret_exists?(secret)
119
+ @kubeclient.get_secret(secret.metadata.name, @namespace)
120
+ true
121
+ rescue KubeException => error
122
+ raise unless error.error_code == 404
123
+ false
124
+ end
125
+
126
+ def load_ejson_from_file
127
+ return {} unless File.exist?(@ejson_file)
128
+ JSON.parse(File.read(@ejson_file))
129
+ rescue JSON::ParserError => e
130
+ raise EjsonSecretError, "Failed to parse encrypted ejson:\n #{e}"
131
+ end
132
+
133
+ def with_decrypted_ejson
134
+ return unless File.exist?(@ejson_file)
135
+
136
+ Dir.mktmpdir("ejson_keydir") do |key_dir|
137
+ File.write(File.join(key_dir, public_key), private_key)
138
+ decrypted = decrypt_ejson(key_dir)
139
+ yield decrypted
140
+ end
141
+ end
142
+
143
+ def decrypt_ejson(key_dir)
144
+ KubernetesDeploy.logger.info("Decrypting #{EJSON_SECRETS_FILE}")
145
+ # ejson seems to dump both errors and output to STDOUT
146
+ out_err, st = Open3.capture2e("EJSON_KEYDIR=#{key_dir} ejson decrypt #{@ejson_file}")
147
+ raise EjsonSecretError, out_err unless st.success?
148
+ JSON.parse(out_err)
149
+ rescue JSON::ParserError => e
150
+ raise EjsonSecretError, "Failed to parse decrypted ejson:\n #{e}"
151
+ end
152
+
153
+ def fetch_private_key_from_secret
154
+ KubernetesDeploy.logger.info("Fetching ejson private key from secret #{EJSON_KEYS_SECRET}")
155
+ secret = @kubeclient.get_secret(EJSON_KEYS_SECRET, @namespace)
156
+ encoded_private_key = secret["data"][public_key]
157
+ unless encoded_private_key
158
+ raise EjsonSecretError, "Private key for #{public_key} not found in #{EJSON_KEYS_SECRET} secret"
159
+ end
160
+
161
+ Base64.decode64(encoded_private_key)
162
+ rescue KubeException => error
163
+ raise unless error.error_code == 404
164
+ secret_missing_err = "Failed to decrypt ejson: secret #{EJSON_KEYS_SECRET} not found in namespace #{@namespace}."
165
+ raise EjsonSecretError, secret_missing_err
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ module KubernetesDeploy
3
+ class FatalDeploymentError < StandardError; end
4
+
5
+ class NamespaceNotFoundError < FatalDeploymentError
6
+ def initialize(name, context)
7
+ super("Namespace `#{name}` not found in context `#{context}`. Aborting the task.")
8
+ end
9
+ end
10
+ end
@@ -23,6 +23,7 @@ module KubernetesDeploy
23
23
  end
24
24
 
25
25
  HTTP_OK_RANGE = 200..299
26
+ ANNOTATION = "shipit.shopify.io/restart"
26
27
 
27
28
  def initialize(context:, namespace:, logger: KubernetesDeploy.logger)
28
29
  @context = context
@@ -32,22 +33,33 @@ module KubernetesDeploy
32
33
  @v1beta1_kubeclient = build_v1beta1_kubeclient(context)
33
34
  end
34
35
 
35
- def perform(deployments_names)
36
+ def perform(deployments_names = nil)
36
37
  verify_namespace
37
38
 
38
- if deployments_names.empty?
39
- raise ArgumentError, "#perform takes at least one deployment to restart"
39
+ if deployments_names
40
+ deployments = fetch_deployments(deployments_names.uniq)
41
+
42
+ if deployments.none?
43
+ raise ArgumentError, "no deployments with names #{deployments_names} found in namespace #{@namespace}"
44
+ end
45
+ else
46
+ deployments = @v1beta1_kubeclient
47
+ .get_deployments(namespace: @namespace)
48
+ .select { |d| d.metadata.annotations[ANNOTATION] }
49
+
50
+ if deployments.none?
51
+ raise ArgumentError, "no deployments found in namespace #{@namespace} with #{ANNOTATION} annotation available"
52
+ end
40
53
  end
41
54
 
42
55
  phase_heading("Triggering restart by touching ENV[RESTARTED_AT]")
43
- deployments = fetch_deployments(deployments_names.uniq)
44
56
  patch_kubeclient_deployments(deployments)
45
57
 
46
58
  phase_heading("Waiting for rollout")
47
59
  wait_for_rollout(deployments)
48
60
 
49
61
  names = deployments.map { |d| "`#{d.metadata.name}`" }
50
- @logger.info "Restart of #{names.join(', ')} deployments succeeded"
62
+ @logger.info "Restart of #{names.sort.join(', ')} deployments succeeded"
51
63
  end
52
64
 
53
65
  private
@@ -23,10 +23,13 @@ end
23
23
  require 'kubernetes-deploy/resource_watcher'
24
24
  require "kubernetes-deploy/ui_helpers"
25
25
  require 'kubernetes-deploy/kubectl'
26
+ require 'kubernetes-deploy/kubeclient_builder'
27
+ require 'kubernetes-deploy/ejson_secret_provisioner'
26
28
 
27
29
  module KubernetesDeploy
28
30
  class Runner
29
31
  include UIHelpers
32
+ include KubeclientBuilder
30
33
 
31
34
  PREDEPLOY_SEQUENCE = %w(
32
35
  Cloudsql
@@ -39,6 +42,7 @@ module KubernetesDeploy
39
42
  PROTECTED_NAMESPACES = %w(
40
43
  default
41
44
  kube-system
45
+ kube-public
42
46
  )
43
47
 
44
48
  # Things removed from default prune whitelist:
@@ -109,6 +113,16 @@ MSG
109
113
  phase_heading("Checking initial resource statuses")
110
114
  resources.each(&:sync)
111
115
 
116
+ ejson = EjsonSecretProvisioner.new(
117
+ namespace: @namespace,
118
+ template_dir: @template_dir,
119
+ client: build_v1_kubeclient(@context)
120
+ )
121
+ if ejson.secret_changes_required?
122
+ phase_heading("Deploying kubernetes secrets from #{EjsonSecretProvisioner::EJSON_SECRETS_FILE}")
123
+ ejson.run
124
+ end
125
+
112
126
  phase_heading("Predeploying priority resources")
113
127
  predeploy_priority_resources(resources)
114
128
 
@@ -1,3 +1,3 @@
1
1
  module KubernetesDeploy
2
- VERSION = "0.6.1"
2
+ VERSION = "0.6.2"
3
3
  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.6.1
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kir Shatrov
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2017-05-01 00:00:00.000000000 Z
13
+ date: 2017-05-08 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -54,6 +54,20 @@ dependencies:
54
54
  - - ">="
55
55
  - !ruby/object:Gem::Version
56
56
  version: '0.5'
57
+ - !ruby/object:Gem::Dependency
58
+ name: ejson
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - '='
62
+ - !ruby/object:Gem::Version
63
+ version: 1.0.1
64
+ type: :runtime
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - '='
69
+ - !ruby/object:Gem::Version
70
+ version: 1.0.1
57
71
  - !ruby/object:Gem::Dependency
58
72
  name: bundler
59
73
  requirement: !ruby/object:Gem::Requirement
@@ -124,6 +138,20 @@ dependencies:
124
138
  - - "~>"
125
139
  - !ruby/object:Gem::Version
126
140
  version: '3.0'
141
+ - !ruby/object:Gem::Dependency
142
+ name: mocha
143
+ requirement: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - "~>"
146
+ - !ruby/object:Gem::Version
147
+ version: '1.1'
148
+ type: :development
149
+ prerelease: false
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - "~>"
153
+ - !ruby/object:Gem::Version
154
+ version: '1.1'
127
155
  description: Kubernetes deploy scripts
128
156
  email:
129
157
  - ops-accounts+shipit@shopify.com
@@ -147,6 +175,8 @@ files:
147
175
  - exe/kubernetes-run
148
176
  - kubernetes-deploy.gemspec
149
177
  - lib/kubernetes-deploy.rb
178
+ - lib/kubernetes-deploy/ejson_secret_provisioner.rb
179
+ - lib/kubernetes-deploy/errors.rb
150
180
  - lib/kubernetes-deploy/kubeclient_builder.rb
151
181
  - lib/kubernetes-deploy/kubeclient_builder/google_friendly_config.rb
152
182
  - lib/kubernetes-deploy/kubectl.rb
@@ -188,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
218
  version: '0'
189
219
  requirements: []
190
220
  rubyforge_project:
191
- rubygems_version: 2.4.5.1
221
+ rubygems_version: 2.5.1
192
222
  signing_key:
193
223
  specification_version: 4
194
224
  summary: Kubernetes deploy scripts