kubernetes-deploy 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d1e66d28cabf4ab39da9f1418817e5466ea2e73f
4
- data.tar.gz: 69967786f96afae72c074a49c44763e1e0377d1e
3
+ metadata.gz: d6c0635aa2dd4ca87b4cea59f90b783251f93418
4
+ data.tar.gz: 9859f840e6b082188e50e8545546b756b399e95c
5
5
  SHA512:
6
- metadata.gz: 1aba4d518ff8cb0afad70fd9d5a2710ee6696f02c06fe497907a1ac593fc94191e970b1afabe119032dbf5ad361dd9fc06255d1ca817748e36d9c66b581037e9
7
- data.tar.gz: d4213a7380ac484572837a8723713ebfb8cc93448cab122e89a80c55d36bd0399397f8d93e8a27b858fae34dcaa6a4783a65d220e1b561079f5c323eb890105d
6
+ metadata.gz: f637386dec36fb619f319d22109f113b621587c0e1b6f8774d76801582a3c349355ed2f2ac3de57e7d2c1f30792e9c796abd27fde46c58b1bd558ec849290209
7
+ data.tar.gz: 57c73ae638bab026343598ae9f503f9a1af48d11a77ceed8e4c73238d3795314ff1eacf775101dcc19cdf76c2fd3ef584c13c53ca9910db8f69b61bf6a8f7cd4
data/.gitignore CHANGED
@@ -12,3 +12,4 @@
12
12
  .rubocop-http---shopify-github-io-ruby-style-guide-rubocop-yml
13
13
 
14
14
  .byebug_history
15
+ .ruby-version
data/README.md CHANGED
@@ -40,6 +40,27 @@ 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
+ ### Running one off tasks
44
+
45
+ To trigger a one-off job such as a rake task _outside_ of a deploy, use the following command:
46
+
47
+ `kubernetes-run <kube namespace> <kube context> <arguments> --entrypoint=/bin/bash`
48
+
49
+ This command assumes that you've already deployed a `PodTemplate` named `task-runner-template`, which contains a full pod specification in its `data`. The pod specification in turn must have a container named `task-runner`. Based on this specification `kubernetes-run` will create a new pod with the entrypoint of the task-runner container overriden with the supplied arguments.
50
+
51
+ #### Creating a PodTemplate
52
+
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`.
54
+
55
+ #### Providing multiple different task-runner configurations
56
+
57
+ If your application requires task runner templates you can specify which template to use by using the `--template` option. All templates are expected to provide a container called `task-runner`.
58
+
59
+ #### Specifying environment variables for the container
60
+
61
+ If you also need to specify environment variables on top of the arguments, you can specify the `--env-vars` flag which accepts a comma separated list of environment variables like so: `--env-vars="ENV=VAL,ENV2=VAL"`
62
+
63
+
43
64
  ## Development
44
65
 
45
66
  After checking out the repo, run `bin/setup` to install dependencies. You currently need to [manually install kubectl version 1.6.0 or higher](https://kubernetes.io/docs/user-guide/prereqs/) as well if you don't already have it.
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'kubernetes-deploy'
5
+ require 'kubernetes-deploy/runner_task'
6
+ require 'optparse'
7
+
8
+ template = "task-runner-template"
9
+ entrypoint = nil
10
+ env_vars = []
11
+
12
+ ARGV.options do |opts|
13
+ opts.on("--template=TEMPLATE") { |n| template = n }
14
+ opts.on("--env-vars=ENV_VARS") { |vars| env_vars = n.split(",")}
15
+ opts.on("--entrypoint=ENTRYPOINT") { |c| entrypoint = [c] }
16
+ opts.parse!
17
+ end
18
+
19
+ runner = KubernetesDeploy::RunnerTask.new(
20
+ namespace: ARGV[0],
21
+ context: ARGV[1],
22
+ )
23
+
24
+ KubernetesDeploy::Runner.with_friendly_errors do
25
+ runner.run(
26
+ task_template: template,
27
+ entrypoint: entrypoint,
28
+ args: ARGV[2..-1],
29
+ env_vars: env_vars
30
+ )
31
+ end
@@ -10,5 +10,12 @@ require 'kubernetes-deploy/runner'
10
10
 
11
11
  module KubernetesDeploy
12
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
+
13
20
  include Logger
14
21
  end
@@ -35,6 +35,7 @@ module KubernetesDeploy
35
35
  when 'ingress' then Ingress.new(name, namespace, context, file)
36
36
  when 'persistentvolumeclaim' then PersistentVolumeClaim.new(name, namespace, context, file)
37
37
  when 'service' then Service.new(name, namespace, context, file)
38
+ when 'podtemplate' then PodTemplate.new(name, namespace, context, file)
38
39
  else self.new(name, namespace, context, file).tap { |r| r.type = type }
39
40
  end
40
41
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ module KubernetesDeploy
3
+ class PodTemplate < KubernetesResource
4
+ def initialize(name, namespace, context, file)
5
+ @name = name
6
+ @namespace = namespace
7
+ @context = context
8
+ @file = file
9
+ end
10
+
11
+ def sync
12
+ _, _err, st = run_kubectl("get", type, @name)
13
+ @status = st.success? ? "Available" : "Unknown"
14
+ @found = st.success?
15
+ log_status
16
+ end
17
+
18
+ def deploy_succeeded?
19
+ exists?
20
+ end
21
+
22
+ def deploy_failed?
23
+ false
24
+ end
25
+
26
+ def exists?
27
+ @found
28
+ end
29
+ end
30
+ end
@@ -14,12 +14,6 @@ module KubernetesDeploy
14
14
  end
15
15
  end
16
16
 
17
- class NamespaceNotFoundError < FatalDeploymentError
18
- def initialize(name, context)
19
- super("Namespace `#{name}` not found in context `#{context}`. Aborting the task.")
20
- end
21
- end
22
-
23
17
  class RestartError < FatalDeploymentError
24
18
  def initialize(deployment_name, response)
25
19
  super("Failed to restart #{deployment_name}. " \
@@ -5,7 +5,6 @@ require 'erb'
5
5
  require 'yaml'
6
6
  require 'shellwords'
7
7
  require 'tempfile'
8
-
9
8
  require 'kubernetes-deploy/kubernetes_resource'
10
9
  %w(
11
10
  cloudsql
@@ -16,6 +15,7 @@ require 'kubernetes-deploy/kubernetes_resource'
16
15
  pod
17
16
  redis
18
17
  service
18
+ pod_template
19
19
  bugsnag
20
20
  ).each do |subresource|
21
21
  require "kubernetes-deploy/kubernetes_resource/#{subresource}"
@@ -116,6 +116,7 @@ MSG
116
116
  if PROTECTED_NAMESPACES.include?(@namespace) && @prune
117
117
  raise FatalDeploymentError, "Refusing to deploy to protected namespace '#{@namespace}' with pruning enabled"
118
118
  end
119
+
119
120
  deploy_resources(resources, prune: @prune)
120
121
 
121
122
  return unless wait_for_completion?
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+ require 'tempfile'
3
+
4
+ require 'kubernetes-deploy/kubeclient_builder'
5
+ require 'kubernetes-deploy/ui_helpers'
6
+ require 'kubernetes-deploy/kubectl'
7
+
8
+ module KubernetesDeploy
9
+ class RunnerTask
10
+ include KubeclientBuilder
11
+ include Kubectl
12
+ include UIHelpers
13
+
14
+ class FatalTaskRunError < FatalDeploymentError; end
15
+ class TaskTemplateMissingError < FatalDeploymentError
16
+ def initialize(task_template, namespace, context)
17
+ super("Pod template `#{task_template}` cannot be found in namespace: `#{namespace}`, context: `#{context}`")
18
+ end
19
+ end
20
+
21
+ def initialize(namespace:, context:, logger: KubernetesDeploy.logger)
22
+ @logger = logger
23
+ @namespace = namespace
24
+ @kubeclient = build_v1_kubeclient(context)
25
+ @context = context
26
+ end
27
+
28
+ def run(task_template:, entrypoint:, args:, env_vars: [])
29
+ phase_heading("Validating configuration")
30
+ validate_configuration(task_template, args)
31
+
32
+ phase_heading("Fetching task template")
33
+ raw_template = get_template(task_template)
34
+
35
+ phase_heading("Constructing final pod specification")
36
+ rendered_template = build_pod_template(raw_template, entrypoint, args, env_vars)
37
+
38
+ validate_pod_spec(rendered_template)
39
+
40
+ phase_heading("Creating pod")
41
+ KubernetesDeploy.logger.info("Starting task runner pod: '#{rendered_template.metadata.name}'")
42
+ @kubeclient.create_pod(rendered_template)
43
+ end
44
+
45
+ private
46
+
47
+ def validate_configuration(task_template, args)
48
+ errors = []
49
+
50
+ if task_template.blank?
51
+ errors << "Task template name can't be nil"
52
+ end
53
+
54
+ if @namespace.blank?
55
+ errors << "Namespace can't be empty"
56
+ end
57
+
58
+ if args.blank?
59
+ errors << "Args can't be nil"
60
+ end
61
+
62
+ begin
63
+ @kubeclient.get_namespace(@namespace)
64
+ rescue KubeException => e
65
+ errors << if e.error_code == 404
66
+ "Namespace was not found"
67
+ else
68
+ "Could not connect to kubernetes cluster"
69
+ end
70
+ end
71
+
72
+ raise FatalTaskRunError, "Configuration invalid: #{errors.join(', ')}" unless errors.empty?
73
+ end
74
+
75
+ def get_template(template_name)
76
+ KubernetesDeploy.logger.info(
77
+ "Fetching task runner pod template: '#{template_name}' in namespace: '#{@namespace}'"
78
+ )
79
+
80
+ pod_template = @kubeclient.get_pod_template(template_name, @namespace)
81
+
82
+ pod_template.template
83
+ rescue KubeException => error
84
+ if error.error_code == 404
85
+ raise TaskTemplateMissingError.new(template_name, @namespace, @context)
86
+ else
87
+ raise
88
+ end
89
+ end
90
+
91
+ def build_pod_template(base_template, entrypoint, args, env_vars)
92
+ KubernetesDeploy.logger.info("Rendering template for task runner pod")
93
+
94
+ rendered_template = base_template.dup
95
+ rendered_template.kind = 'Pod'
96
+ rendered_template.apiVersion = 'v1'
97
+
98
+ container = rendered_template.spec.containers.find { |cont| cont.name == 'task-runner' }
99
+
100
+ raise FatalTaskRunError, "Pod spec does not contain a template container called 'task-runner'" if container.nil?
101
+
102
+ container.command = entrypoint
103
+ container.args = args
104
+ container.env ||= []
105
+
106
+ env_args = env_vars.map do |key, value|
107
+ key, value = env.split('=', 2)
108
+ { name: key, value: value }
109
+ end
110
+
111
+ container.env = container.env.map(&:to_h) + env_args
112
+
113
+ unique_name = rendered_template.metadata.name + "-" + SecureRandom.hex(8)
114
+
115
+ KubernetesDeploy.logger.warn("Name is too long, using '#{unique_name[0..62]}'") if unique_name.length > 63
116
+ rendered_template.metadata.name = unique_name[0..62]
117
+ rendered_template.metadata.namespace = @namespace
118
+
119
+ rendered_template
120
+ end
121
+
122
+ def validate_pod_spec(pod)
123
+ f = Tempfile.new(pod.metadata.name)
124
+ f.write recursive_to_h(pod).to_json
125
+ f.close
126
+
127
+ _out, err, status = Kubectl.run_kubectl(
128
+ "apply", "--dry-run", "-f", f.path,
129
+ namespace: @namespace,
130
+ context: @context
131
+ )
132
+
133
+ unless status.success?
134
+ raise FatalTaskRunError, "Invalid pod spec: #{err}"
135
+ end
136
+ end
137
+
138
+ def recursive_to_h(struct)
139
+ if struct.is_a?(Array)
140
+ return struct.map { |v| v.is_a?(OpenStruct) || v.is_a?(Array) || v.is_a?(Hash) ? recursive_to_h(v) : v }
141
+ end
142
+
143
+ hash = {}
144
+
145
+ struct.each_pair do |k, v|
146
+ recursive_val = v.is_a?(OpenStruct) || v.is_a?(Array) || v.is_a?(Hash)
147
+ hash[k] = recursive_val ? recursive_to_h(v) : v
148
+ end
149
+ hash
150
+ end
151
+ end
152
+ end
@@ -1,3 +1,3 @@
1
1
  module KubernetesDeploy
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
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.5.0
4
+ version: 0.6.0
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-04-21 00:00:00.000000000 Z
13
+ date: 2017-04-27 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -102,6 +102,7 @@ email:
102
102
  executables:
103
103
  - kubernetes-deploy
104
104
  - kubernetes-restart
105
+ - kubernetes-run
105
106
  extensions: []
106
107
  extra_rdoc_files: []
107
108
  files:
@@ -115,6 +116,7 @@ files:
115
116
  - bin/setup
116
117
  - exe/kubernetes-deploy
117
118
  - exe/kubernetes-restart
119
+ - exe/kubernetes-run
118
120
  - kubernetes-deploy.gemspec
119
121
  - lib/kubernetes-deploy.rb
120
122
  - lib/kubernetes-deploy/kubeclient_builder.rb
@@ -127,12 +129,14 @@ files:
127
129
  - lib/kubernetes-deploy/kubernetes_resource/ingress.rb
128
130
  - lib/kubernetes-deploy/kubernetes_resource/persistent_volume_claim.rb
129
131
  - lib/kubernetes-deploy/kubernetes_resource/pod.rb
132
+ - lib/kubernetes-deploy/kubernetes_resource/pod_template.rb
130
133
  - lib/kubernetes-deploy/kubernetes_resource/redis.rb
131
134
  - lib/kubernetes-deploy/kubernetes_resource/service.rb
132
135
  - lib/kubernetes-deploy/logger.rb
133
136
  - lib/kubernetes-deploy/resource_watcher.rb
134
137
  - lib/kubernetes-deploy/restart_task.rb
135
138
  - lib/kubernetes-deploy/runner.rb
139
+ - lib/kubernetes-deploy/runner_task.rb
136
140
  - lib/kubernetes-deploy/ui_helpers.rb
137
141
  - lib/kubernetes-deploy/version.rb
138
142
  homepage: https://github.com/Shopify/kubernetes-deploy
@@ -155,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
159
  version: '0'
156
160
  requirements: []
157
161
  rubyforge_project:
158
- rubygems_version: 2.5.1
162
+ rubygems_version: 2.6.10
159
163
  signing_key:
160
164
  specification_version: 4
161
165
  summary: Kubernetes deploy scripts