kitchen-kubernetes 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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 -%>