kitchen-kubernetes 1.0.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.
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ #
2
+ # Copyright 2017, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'bundler/gem_tasks'
@@ -0,0 +1,50 @@
1
+ #
2
+ # Copyright 2017, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ lib = File.expand_path('../lib', __FILE__)
18
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
19
+ require 'kitchen-kubernetes/version'
20
+
21
+ Gem::Specification.new do |spec|
22
+ spec.name = 'kitchen-kubernetes'
23
+ spec.version = KitchenKubernetes::VERSION
24
+ spec.authors = ['Noah Kantrowitz']
25
+ spec.email = ['noah@coderanger.net']
26
+ spec.description = %q{A Kubernetes Driver for Test Kitchen}
27
+ spec.summary = spec.description
28
+ spec.homepage = 'https://github.com/coderanger/kitchen-kubernetes'
29
+ spec.license = 'Apache 2.0'
30
+
31
+ spec.files = `git ls-files`.split($/)
32
+ spec.executables = []
33
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
34
+ spec.require_paths = ['lib']
35
+
36
+ spec.add_dependency 'test-kitchen', '~> 1.18'
37
+
38
+ spec.add_development_dependency 'bundler'
39
+ spec.add_development_dependency 'rake'
40
+
41
+ # Unit testing gems.
42
+ spec.add_development_dependency 'rspec', '~> 3.2'
43
+ spec.add_development_dependency 'rspec-its', '~> 1.2'
44
+ spec.add_development_dependency 'fuubar', '~> 2.0'
45
+ spec.add_development_dependency 'simplecov', '~> 0.9'
46
+ spec.add_development_dependency 'codecov', '~> 0.0', '>= 0.0.2'
47
+
48
+ # Integration testing.
49
+ spec.add_development_dependency 'kitchen-inspec', '~> 0.20'
50
+ end
@@ -0,0 +1,45 @@
1
+ #
2
+ # Copyright 2017, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+
18
+ module KitchenKubernetes
19
+ # Utility mixin for other classes in this plugin.
20
+ #
21
+ # @since 1.0
22
+ # @api private
23
+ module Helper
24
+ # Because plugins and connections have different APIs.
25
+ def kube_options
26
+ if defined?(config)
27
+ config
28
+ elsif defined?(options)
29
+ options
30
+ else
31
+ raise "Something went wrong, please file a bug"
32
+ end
33
+ end
34
+
35
+ def kubectl_command(*cmd)
36
+ out = [kube_options[:kubectl_command]]
37
+ if kube_options[:context]
38
+ out << '--context'
39
+ out << kube_options[:context]
40
+ end
41
+ out.concat(cmd)
42
+ out
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ #
2
+ # Copyright 2017, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+
18
+ module KitchenKubernetes
19
+ VERSION = "1.0.0"
20
+ end
@@ -0,0 +1,216 @@
1
+ #
2
+ # Copyright 2017, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'erb'
18
+ require 'json'
19
+
20
+ require 'kitchen/driver/base'
21
+ require 'kitchen/provisioner/chef_base'
22
+ require 'kitchen/shell_out'
23
+ require 'kitchen/verifier/busser'
24
+
25
+ require 'kitchen/transport/kubernetes'
26
+ require 'kitchen-kubernetes/helper'
27
+
28
+
29
+ module Kitchen
30
+ module Driver
31
+
32
+ # Kubernetes driver for Kitchen.
33
+ #
34
+ # @author Noah Kantrowitz <noah@coderanger>
35
+ # @since 1.0
36
+ # @see Kitchen::Transport::Kubernetes
37
+ class Kubernetes < Kitchen::Driver::Base
38
+ include ShellOut
39
+ include KitchenKubernetes::Helper
40
+
41
+ default_config :cache_path, '/data/chef/%{chef_version}'
42
+ default_config :chef_image, 'chef/chef'
43
+ default_config :chef_version, 'latest'
44
+ default_config :context, nil
45
+ default_config :image_pull_policy, nil
46
+ default_config :image_pull_secrets, nil
47
+ default_config :init_system, nil
48
+ default_config :kubectl_command, 'kubectl'
49
+ default_config :pod_template, File.expand_path('../pod.yaml.erb', __FILE__)
50
+ default_config :rsync_command, 'rsync'
51
+ default_config :rsync_image, 'kitchenkubernetes/rsync:3.1.2-r5'
52
+ default_config :rsync_rsh, "#{RbConfig.ruby} -e \"exec('kubectl', 'exec', '--stdin', '--container=rsync', ARGV[0], '--', *ARGV[1..-1])\""
53
+
54
+ default_config :cache_volume do |driver|
55
+ if driver[:cache_path]
56
+ path = driver[:cache_path] % {chef_version: driver[:chef_version]}
57
+ {hostPath: {path: path, type: 'DirectoryOrCreate'}}
58
+ else
59
+ {emptyDir: {}}
60
+ end
61
+ end
62
+
63
+ default_config :image do |driver|
64
+ driver.default_image
65
+ end
66
+
67
+ default_config :pod_name do |driver|
68
+ # Borrowed from kitchen-rackspace.
69
+ [
70
+ driver.instance.name.gsub(/\W/, ''),
71
+ (Etc.getlogin || 'nologin').gsub(/\W/, ''),
72
+ Socket.gethostname.gsub(/\W/, '')[0..20],
73
+ Array.new(8) { rand(36).to_s(36) }.join
74
+ ].join('-')
75
+ end
76
+
77
+ # Don't expand path on commands that don't look like a path, otherwise
78
+ # it turns kubectl in to /path/to/cookbook/kubectl.
79
+ expand_path_for :kubectl_command do |driver|
80
+ driver[:kubectl_command] =~ %r{/|\\}
81
+ end
82
+ expand_path_for :pod_template
83
+ expand_path_for :rsync_command do |driver|
84
+ driver[:rsync_command] =~ %r{/|\\}
85
+ end
86
+
87
+ # Work out the default primary container image to use for this instance.
88
+ # Can be overridden by subclasses. Must return a string compatible with
89
+ # a Kubernetes container image specification.
90
+ #
91
+ # @return [String]
92
+ def default_image
93
+ if instance.platform.name =~ /^(.*)-([^-]*)$/
94
+ "#{$1}:#{$2}"
95
+ else
96
+ instance.platform.name
97
+ end
98
+ end
99
+
100
+ # Muck with some other plugins to make the UX easier. Haxxxx.
101
+ #
102
+ # @api private
103
+ def finalize_config!(instance)
104
+ super.tap do
105
+ # Force the use of the Kubernetes transport since it isn't much use
106
+ # without that.
107
+ instance.transport = Kitchen::Transport::Kubernetes.new(config)
108
+ # Leave room for the possibility of other provisioners in the future,
109
+ # but force some options we need.
110
+ if instance.provisioner.is_a?(Kitchen::Provisioner::ChefBase)
111
+ instance.provisioner.send(:config).update(
112
+ require_chef_omnibus: false,
113
+ product_name: nil,
114
+ chef_omnibus_root: '/opt/chef',
115
+ sudo: false,
116
+ )
117
+ end
118
+ # Ditto to the above, other verifiers will need their own hacks, but
119
+ # this is a start at least.
120
+ if instance.verifier.is_a?(Kitchen::Verifier::Busser)
121
+ instance.verifier.send(:config).update(
122
+ root_path: '/tmp/kitchen/verifier',
123
+ sudo: false,
124
+ )
125
+ elsif defined?(Kitchen::Verifier::Inspec) && instance.verifier.is_a?(Kitchen::Verifier::Inspec)
126
+ # Monkeypatch kitchen-inspec to use my copy of the kubernetes train transport.
127
+ # Pending https://github.com/chef/train/pull/205 and https://github.com/chef/kitchen-inspec/pull/148
128
+ # or https://github.com/chef/kitchen-inspec/pull/149.
129
+ require 'kitchen/verifier/train_kubernetes_hack'
130
+ _config = config # Because closure madness.
131
+ old_runner_options = instance.verifier.method(:runner_options)
132
+ instance.verifier.send(:define_singleton_method, :runner_options) do |transport, state = {}, platform = nil, suite = nil|
133
+ if transport.is_a?(Kitchen::Transport::Kubernetes)
134
+ # Initiate 1337 ha><0rz.
135
+ {
136
+ "backend" => "kubernetes_hack",
137
+ "logger" => logger,
138
+ "pod" => state[:pod_id],
139
+ "container" => "default",
140
+ "kubectl_path" => _config[:kubectl_path],
141
+ "context" => _config[:context],
142
+ }.tap do |runner_options|
143
+ # Copied directly from kitchen-inspec because there is no way not to. Sigh.
144
+ runner_options["color"] = (config[:color].nil? ? true : config[:color])
145
+ runner_options["format"] = config[:format] unless config[:format].nil?
146
+ runner_options["output"] = config[:output] % { platform: platform, suite: suite } unless config[:output].nil?
147
+ runner_options["profiles_path"] = config[:profiles_path] unless config[:profiles_path].nil?
148
+ runner_options[:controls] = config[:controls]
149
+ end
150
+ else
151
+ old_runner_options.call(transport, state, platform, suite)
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ # (see Base#create)
159
+ def create(state)
160
+ # Already created, we're good.
161
+ return if state[:pod_id]
162
+ # Lock in our name with randomness and whatever.
163
+ pod_id = config[:pod_name]
164
+ # Render the pod YAML and feed it to kubectl.
165
+ tpl = ERB.new(IO.read(config[:pod_template]), 0, '-')
166
+ tpl.filename = config[:pod_template]
167
+ pod_yaml = tpl.result(binding)
168
+ debug("Creating pod with YAML:\n#{pod_yaml}\n")
169
+ run_command(kubectl_command('create', '--filename', '-'), input: pod_yaml)
170
+ # Wait until the pod reaches Running status.
171
+ status = nil
172
+ start_time = Time.now
173
+ while status != 'Running'
174
+ if Time.now - start_time > 20
175
+ # More than 20 seconds, start giving user feedback. 20 second threshold
176
+ # was 100% pulled from my ass based on how long it takes to launch
177
+ # on my local minikube, may need changing for reality.
178
+ info("Waiting for pod #{pod_id} to be running, currently #{status}")
179
+ end
180
+ sleep(1)
181
+ # Can't use run_command here because error! is unwanted and logging is a bit much.
182
+ status_cmd = Mixlib::ShellOut.new(kubectl_command('get', 'pod', pod_id, '--output=json'))
183
+ status_cmd.run_command
184
+ unless status_cmd.error? || status_cmd.stdout.empty?
185
+ status = JSON.parse(status_cmd.stdout)['status']['phase']
186
+ end
187
+ end
188
+ # Save the pod ID.
189
+ state[:pod_id] = pod_id
190
+ rescue Exception => ex
191
+ # If something goes wrong, try to clean up.
192
+ if pod_id
193
+ begin
194
+ debug("Failure during create, trying to clean up pod #{pod_id}")
195
+ run_command(kubectl_command('delete', 'pod', pod_id, '--now'))
196
+ rescue ShellCommandFailed => cleanup_ex
197
+ # Welp, we tried.
198
+ debug("Cleanup failed, continuing anyway: #{cleanup_ex}")
199
+ end
200
+ end
201
+ raise ex
202
+ end
203
+
204
+ # (see Base#destroy)
205
+ def destroy(state)
206
+ return unless state[:pod_id]
207
+ run_command(kubectl_command('delete', 'pod', state[:pod_id], '--now'))
208
+ # Explicitly not waiting for the delete to finish, if k8s has problems
209
+ # with deletes in the future, I can add a wait here.
210
+ rescue ShellCommandFailed => ex
211
+ raise unless ex.to_s.include?('(NotFound)')
212
+ end
213
+
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,96 @@
1
+ #
2
+ # Copyright 2017, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ apiVersion: v1
18
+ kind: Pod
19
+ metadata:
20
+ name: <%= pod_id %>
21
+ labels:
22
+ heritage: kitchen-kubernetes
23
+ annotations:
24
+ coderanger.net/kitchen-instance: <%= instance.name %>
25
+ coderanger.net/kitchen-platform: <%= instance.platform.name %>
26
+ coderanger.net/kitchen-created-by: <%= Etc.getlogin || 'unknown' %>
27
+ coderanger.net/kitchen-created-on: <%= Socket.gethostname %>
28
+ spec:
29
+ initContainers:
30
+ - name: chef
31
+ image: <%= config[:chef_image] %>:<%= config[:chef_version] %>
32
+ <%- if config[:chef_version] == 'latest' -%>
33
+ command: ["/bin/cp", "-a", "-u", "/opt/chef", "/mnt"]
34
+ <%- else -%>
35
+ command: ["/bin/sh", "-c", "if [ ! -d /mnt/chef/bin ]; then cp -a /opt/chef /mnt; fi"]
36
+ <%- end -%>
37
+ volumeMounts:
38
+ - mountPath: /mnt/chef
39
+ name: chef
40
+ containers:
41
+ - name: default
42
+ image: <%= config[:image] %>
43
+ <%- if config[:image_pull_policy] %>
44
+ imagePullPolicy: <%= config[:image_pull_policy] %>
45
+ <%- end -%>
46
+ <%- if config[:init_system] == 'systemd' -%>
47
+ env:
48
+ - name: container
49
+ value: docker
50
+ command: ["/sbin/init"]
51
+ <%- else -%>
52
+ command: ["/bin/sh", "-c", "trap 'exit 0' TERM; sleep 2147483647 & wait"]
53
+ <%- end -%>
54
+ volumeMounts:
55
+ - mountPath: /opt/chef
56
+ name: chef
57
+ - mountPath: /tmp/kitchen
58
+ name: kitchen
59
+ <%- if config[:init_system] == 'systemd' -%>
60
+ - mountPath: /tmp
61
+ name: systemd-tmp
62
+ - mountPath: /run
63
+ name: systemd-run
64
+ - mountPath: /run/lock
65
+ name: systemd-lock
66
+ - mountPath: /sys/fs/cgroup
67
+ name: systemd-cgroup
68
+ readOnly: true
69
+ <%- end -%>
70
+ - name: rsync
71
+ image: <%= config[:rsync_image] %>
72
+ volumeMounts:
73
+ - mountPath: /tmp/kitchen
74
+ name: kitchen
75
+ volumes:
76
+ - <%= {name: 'chef'}.merge(config[:cache_volume]).to_json %>
77
+ - name: kitchen
78
+ emptyDir: {}
79
+ <%- if config[:init_system] == 'systemd' -%>
80
+ - name: systemd-tmp
81
+ emptyDir:
82
+ medium: Memory
83
+ - name: systemd-run
84
+ emptyDir:
85
+ medium:
86
+ - name: systemd-lock
87
+ emptyDir:
88
+ medium: Memory
89
+ - name: systemd-cgroup
90
+ hostPath:
91
+ path: /sys/fs/cgroup
92
+ type: Directory
93
+ <%- end -%>
94
+ <%- if config[:image_pull_secrets] -%>
95
+ imagePullSecrets: <%= Array(config[:image_pull_secrets]).map {|n| {name: n} }.to_json %>
96
+ <%- end -%>