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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.travis.yml +50 -0
- data/Gemfile +21 -0
- data/LICENSE +201 -0
- data/README.md +191 -0
- data/Rakefile +17 -0
- data/kitchen-kubernetes.gemspec +50 -0
- data/lib/kitchen-kubernetes/helper.rb +45 -0
- data/lib/kitchen-kubernetes/version.rb +20 -0
- data/lib/kitchen/driver/kubernetes.rb +216 -0
- data/lib/kitchen/driver/pod.yaml.erb +96 -0
- data/lib/kitchen/transport/kubernetes.rb +84 -0
- data/lib/kitchen/verifier/train_kubernetes_hack.rb +90 -0
- data/rsync/Dockerfile +21 -0
- data/test/.kitchen.yml +57 -0
- data/test/cookbooks/test/metadata.rb +17 -0
- data/test/cookbooks/test/recipes/default.rb +30 -0
- data/test/cookbooks/test/recipes/service.rb +28 -0
- data/test/cookbooks/test/templates/template.erb +1 -0
- data/test/gemfiles/master.gemfile +22 -0
- data/test/gemfiles/train-new.gemfile +19 -0
- data/test/gemfiles/train-old.gemfile +19 -0
- data/test/integration/default/serverspec/default_spec.rb +32 -0
- data/test/integration/inspec/default_spec.rb +29 -0
- data/test/integration/service/serverspec/service_spec.rb +23 -0
- metadata +212 -0
@@ -0,0 +1,84 @@
|
|
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 'shellwords'
|
18
|
+
|
19
|
+
require 'kitchen/login_command'
|
20
|
+
require 'kitchen/shell_out'
|
21
|
+
require 'kitchen/transport/base'
|
22
|
+
|
23
|
+
require 'kitchen-kubernetes/helper'
|
24
|
+
|
25
|
+
|
26
|
+
module Kitchen
|
27
|
+
module Transport
|
28
|
+
|
29
|
+
# Kubernetes transport for Kitchen. Uses kubectl exec.
|
30
|
+
#
|
31
|
+
# @author Noah Kantrowitz <noah@coderanger>
|
32
|
+
# @since 1.0.0
|
33
|
+
# @see Kitchen::Driver::Kubernetes
|
34
|
+
class Kubernetes < Kitchen::Transport::Base
|
35
|
+
# All configuration options can be found in the Driver class.
|
36
|
+
|
37
|
+
# (see Base#connection)
|
38
|
+
def connection(state, &block)
|
39
|
+
# No persistent anything so no need to reuse connections.
|
40
|
+
Connection.new(
|
41
|
+
pod_id: state[:pod_id],
|
42
|
+
kubectl_command: config[:kubectl_command],
|
43
|
+
rsync_command: config[:rsync_command],
|
44
|
+
rsync_rsh: config[:rsync_rsh],
|
45
|
+
log_level: config[:log_level],
|
46
|
+
logger: logger
|
47
|
+
).tap do |conn|
|
48
|
+
block.call(conn) if block
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Connection < Kitchen::Transport::Base::Connection
|
53
|
+
include ShellOut
|
54
|
+
include KitchenKubernetes::Helper
|
55
|
+
|
56
|
+
# (see Base::Connection#execute)
|
57
|
+
def execute(command)
|
58
|
+
return if command.nil?
|
59
|
+
# Run via kubectl exec.
|
60
|
+
run_command(kubectl_command('exec', '--tty', '--container=default', options[:pod_id], '--', *Shellwords.split(command)))
|
61
|
+
end
|
62
|
+
|
63
|
+
# (see Base::Connection#upload)
|
64
|
+
def upload(locals, remote)
|
65
|
+
return if locals.empty?
|
66
|
+
# Use rsync over kubectl exec to send files.
|
67
|
+
run_command([options[:rsync_command], '--archive', '--progress', '--blocking-io', '--rsh', options[:rsync_rsh]] + (options[:log_level] == :debug ? %w{--verbose --verbose --verbose} : []) + locals + ["#{options[:pod_id]}:#{remote}"])
|
68
|
+
end
|
69
|
+
|
70
|
+
# (see Base::Connection#login_command)
|
71
|
+
def login_command
|
72
|
+
# Find a valid login shell and exec it. This is so weridly complex
|
73
|
+
# because it has to work with a /bin/sh that might be bash, dash, or
|
74
|
+
# busybox. Also CentOS images doesn't have `which` for some reason.
|
75
|
+
# Dash's `type` is super weird so use `which` first in case of dash but
|
76
|
+
# fall back to `type` for basically just CentOS.
|
77
|
+
login_cmd = "IFS=$'\n'; for f in `which bash zsh sh 2>/dev/null || type -P bash zsh sh`; do exec \"$f\" -l; done"
|
78
|
+
cmd = kubectl_command('exec', '--stdin', '--tty', '--container=default', options[:pod_id], '--', '/bin/sh', '-c', login_cmd)
|
79
|
+
LoginCommand.new(cmd[0], cmd.drop(1))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,90 @@
|
|
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
|
+
# This is copied from https://github.com/chef/train/pull/205 until InSpec can
|
17
|
+
# support it internally. The only changes are to rename with _hack and
|
18
|
+
# back-compat support for the changes in the files API (LinuxFile vs File::Remote::Linux).
|
19
|
+
|
20
|
+
require 'mixlib/shellout'
|
21
|
+
|
22
|
+
require 'train'
|
23
|
+
|
24
|
+
module Train::Transports
|
25
|
+
class KubernetesHack < Train.plugin(1)
|
26
|
+
name 'kubernetes_hack'
|
27
|
+
|
28
|
+
include_options Train::Extras::CommandWrapper
|
29
|
+
option :pod, required: true
|
30
|
+
option :container, default: nil
|
31
|
+
option :kubectl_path, default: 'kubectl'
|
32
|
+
option :context, default: nil
|
33
|
+
|
34
|
+
def connection(state = {})
|
35
|
+
opts = merge_options(options, state || {})
|
36
|
+
validate_options(opts)
|
37
|
+
opts[:logger] ||= logger
|
38
|
+
unless @connection && @connection_opts == opts
|
39
|
+
@connection ||= Connection.new(opts)
|
40
|
+
@connection_opts = opts.dup
|
41
|
+
end
|
42
|
+
@connection
|
43
|
+
end
|
44
|
+
|
45
|
+
class Connection < BaseConnection
|
46
|
+
if Gem::Requirement.create('< 0.30').satisfied_by?(Gem::Version.create(Train::VERSION))
|
47
|
+
# The API for a connection changed a lot in 0.30, this is a compat shim.
|
48
|
+
def os
|
49
|
+
@os ||= OSCommon.new(self, family: 'unix')
|
50
|
+
end
|
51
|
+
|
52
|
+
def file(path)
|
53
|
+
@files[path] ||= file_via_connection(path)
|
54
|
+
end
|
55
|
+
|
56
|
+
def run_command(cmd)
|
57
|
+
run_command_via_connection(cmd)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def file_via_connection(path)
|
62
|
+
defined?(Train::File::Remote::Linux) ? Train::File::Remote::Linux.new(self, path) : LinuxFile.new(self, path)
|
63
|
+
end
|
64
|
+
|
65
|
+
def run_command_via_connection(cmd)
|
66
|
+
kubectl_cmd = [options[:kubectl_path], 'exec']
|
67
|
+
kubectl_cmd.concat(['--context', options[:context]]) if options[:context]
|
68
|
+
kubectl_cmd.concat(['--container', options[:container]]) if options[:container]
|
69
|
+
kubectl_cmd.concat([options[:pod], '--', '/bin/sh', '-c', cmd])
|
70
|
+
|
71
|
+
so = Mixlib::ShellOut.new(kubectl_cmd, logger: logger)
|
72
|
+
so.run_command
|
73
|
+
if so.error?
|
74
|
+
# Trim the "command terminated with exit code N" line from the end
|
75
|
+
# of the stderr content.
|
76
|
+
so.stderr.gsub!(/command terminated with exit code #{so.exitstatus}\n\Z/, '')
|
77
|
+
end
|
78
|
+
CommandResult.new(so.stdout, so.stderr, so.exitstatus)
|
79
|
+
end
|
80
|
+
|
81
|
+
def uri
|
82
|
+
if options[:container]
|
83
|
+
"kubernetes://#{options[:pod]}/#{options[:container]}"
|
84
|
+
else
|
85
|
+
"kubernetes://#{options[:pod]}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/rsync/Dockerfile
ADDED
@@ -0,0 +1,21 @@
|
|
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
|
+
# This is used to build the kitchenkubernetes/rsync image.
|
18
|
+
|
19
|
+
FROM alpine
|
20
|
+
RUN apk add --update-cache rsync
|
21
|
+
ENTRYPOINT ["/bin/sh", "-c", "trap 'exit 0' TERM; sleep 2147483647 & wait"]
|
data/test/.kitchen.yml
ADDED
@@ -0,0 +1,57 @@
|
|
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
|
+
driver:
|
18
|
+
name: kubernetes
|
19
|
+
|
20
|
+
provisioner:
|
21
|
+
cookbooks_path: test/cookbooks/
|
22
|
+
|
23
|
+
kitchen:
|
24
|
+
test_base_path: <%= File.expand_path('../integration', __FILE__) %>
|
25
|
+
|
26
|
+
platforms:
|
27
|
+
- name: centos-6
|
28
|
+
- name: centos-7
|
29
|
+
- name: ubuntu-14.04
|
30
|
+
- name: ubuntu-16.04
|
31
|
+
- name: debian-7
|
32
|
+
- name: debian-8
|
33
|
+
- name: debian-9
|
34
|
+
- name: amazonlinux-2016.09
|
35
|
+
- name: amazonlinux-2017.03
|
36
|
+
- name: amazonlinux-2017.09
|
37
|
+
- name: fedora-25
|
38
|
+
- name: fedora-26
|
39
|
+
|
40
|
+
suites:
|
41
|
+
- name: default
|
42
|
+
run_list:
|
43
|
+
- recipe[test]
|
44
|
+
- name: service
|
45
|
+
run_list:
|
46
|
+
- recipe[test::service]
|
47
|
+
driver:
|
48
|
+
init_system: systemd
|
49
|
+
includes:
|
50
|
+
- centos-7
|
51
|
+
- ubuntu-16.04
|
52
|
+
- fedora-26
|
53
|
+
- name: inspec
|
54
|
+
run_list:
|
55
|
+
- recipe[test]
|
56
|
+
verifier:
|
57
|
+
name: inspec
|
@@ -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
|
+
name 'test'
|
@@ -0,0 +1,30 @@
|
|
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
|
+
file '/testfile' do
|
18
|
+
owner 'root'
|
19
|
+
group 'root'
|
20
|
+
mode '741'
|
21
|
+
content "I am a teapot\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
template '/testtemplate' do
|
25
|
+
owner 'root'
|
26
|
+
group 'root'
|
27
|
+
mode '444'
|
28
|
+
source 'template.erb'
|
29
|
+
variables chef_version: Chef::VERSION
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
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
|
+
apt_update if platform_family?('debian')
|
18
|
+
|
19
|
+
package_name = platform_family?('rhel', 'amazon', 'fedora') ? 'httpd' : 'apache2'
|
20
|
+
|
21
|
+
package package_name
|
22
|
+
|
23
|
+
service package_name do
|
24
|
+
action [:enable, :start]
|
25
|
+
end
|
26
|
+
|
27
|
+
# For tests.
|
28
|
+
package 'curl'
|
@@ -0,0 +1 @@
|
|
1
|
+
ver=<%= @chef_version %>
|
@@ -0,0 +1,22 @@
|
|
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
|
+
eval_gemfile File.expand_path('../../../Gemfile', __FILE__)
|
18
|
+
|
19
|
+
gem 'train', github: 'chef/train'
|
20
|
+
gem 'inspec', github: 'chef/inspec'
|
21
|
+
gem 'kitchen-inspec', github: 'chef/kitchen-inspec'
|
22
|
+
gem 'test-kitchen', github: 'test-kitchen/test-kitchen'
|
@@ -0,0 +1,19 @@
|
|
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
|
+
eval_gemfile File.expand_path('../../../Gemfile', __FILE__)
|
18
|
+
|
19
|
+
gem 'train', '>= 0.30'
|
@@ -0,0 +1,19 @@
|
|
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
|
+
eval_gemfile File.expand_path('../../../Gemfile', __FILE__)
|
18
|
+
|
19
|
+
gem 'train', '< 0.30'
|
@@ -0,0 +1,32 @@
|
|
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 'serverspec'
|
18
|
+
set :backend, :exec
|
19
|
+
|
20
|
+
describe file('/testfile') do
|
21
|
+
it { is_expected.to be_a_file }
|
22
|
+
it { is_expected.to be_owned_by 'root' }
|
23
|
+
it { is_expected.to be_mode 741 }
|
24
|
+
its(:content) { is_expected.to eq "I am a teapot\n" }
|
25
|
+
end
|
26
|
+
|
27
|
+
describe file('/testtemplate') do
|
28
|
+
it { is_expected.to be_a_file }
|
29
|
+
it { is_expected.to be_owned_by 'root' }
|
30
|
+
it { is_expected.to be_mode 444 }
|
31
|
+
its(:content) { is_expected.to match /^ver=13/ }
|
32
|
+
end
|
@@ -0,0 +1,29 @@
|
|
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
|
+
describe file('/testfile') do
|
18
|
+
it { is_expected.to be_a_file }
|
19
|
+
it { is_expected.to be_owned_by 'root' }
|
20
|
+
it { is_expected.to be_mode 0741 }
|
21
|
+
its(:content) { is_expected.to eq "I am a teapot\n" }
|
22
|
+
end
|
23
|
+
|
24
|
+
describe file('/testtemplate') do
|
25
|
+
it { is_expected.to be_a_file }
|
26
|
+
it { is_expected.to be_owned_by 'root' }
|
27
|
+
it { is_expected.to be_mode 0444 }
|
28
|
+
its(:content) { is_expected.to match /^ver=13/ }
|
29
|
+
end
|