centurion 1.8.10 → 1.9.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.
@@ -1,34 +1,47 @@
1
1
  require 'pty'
2
2
  require_relative 'logging'
3
3
  require_relative 'shell'
4
+ require 'centurion/ssh'
4
5
 
5
6
  module Centurion; end
6
7
 
7
8
  class Centurion::DockerViaCli
8
9
  include Centurion::Logging
9
10
 
10
- def initialize(hostname, port, docker_path, tls_args = {})
11
- @docker_host = "tcp://#{hostname}:#{port}"
11
+ def initialize(hostname, port, docker_path, connection_opts = {})
12
+ if connection_opts[:ssh]
13
+ @docker_host = hostname
14
+ else
15
+ @docker_host = "tcp://#{hostname}:#{port}"
16
+ end
12
17
  @docker_path = docker_path
13
- @tls_args = tls_args
18
+ @connection_opts = connection_opts
14
19
  end
15
20
 
16
21
  def pull(image, tag='latest')
17
22
  info 'Using CLI to pull'
18
- Centurion::Shell.echo(build_command(:pull, "#{image}:#{tag}"))
23
+ connect do
24
+ Centurion::Shell.echo(build_command(:pull, "#{image}:#{tag}"))
25
+ end
19
26
  end
20
27
 
21
28
  def tail(container_id)
22
29
  info "Tailing the logs on #{container_id}"
23
- Centurion::Shell.echo(build_command(:logs, container_id))
30
+ connect do
31
+ Centurion::Shell.echo(build_command(:logs, container_id))
32
+ end
24
33
  end
25
34
 
26
35
  def attach(container_id)
27
- Centurion::Shell.echo(build_command(:attach, container_id))
36
+ connect do
37
+ Centurion::Shell.echo(build_command(:attach, container_id))
38
+ end
28
39
  end
29
40
 
30
41
  def exec(container_id, commandline)
31
- Centurion::Shell.echo(build_command(:exec, "#{container_id} #{commandline}"))
42
+ connect do
43
+ Centurion::Shell.echo(build_command(:exec, "#{container_id} #{commandline}"))
44
+ end
32
45
  end
33
46
 
34
47
  def exec_it(container_id, commandline)
@@ -36,7 +49,9 @@ class Centurion::DockerViaCli
36
49
  # because docker exec returns the same exit code as the latest command executed on
37
50
  # the shell, which causes an exception to be raised if the latest comand executed
38
51
  # was unsuccessful when you exit the shell.
39
- Centurion::Shell.echo(build_command(:exec, "-it #{container_id} #{commandline} || true"))
52
+ connect do
53
+ Centurion::Shell.echo(build_command(:exec, "-it #{container_id} #{commandline} || true"))
54
+ end
40
55
  end
41
56
 
42
57
  private
@@ -46,28 +61,29 @@ class Centurion::DockerViaCli
46
61
  end
47
62
 
48
63
  def all_tls_path_available?
49
- self.class.tls_keys.all? { |key| @tls_args.key?(key) }
64
+ self.class.tls_keys.all? { |key| @connection_opts.key?(key) }
50
65
  end
51
66
 
52
67
  def tls_parameters
53
- return '' if @tls_args.nil? || @tls_args.empty?
68
+ return '' if @connection_opts.nil? || @connection_opts.empty?
54
69
 
55
70
  tls_flags = ''
56
71
 
57
72
  # --tlsverify can be set without passing the cacert, cert and key flags
58
- if @tls_args[:tls] == true || all_tls_path_available?
73
+ if @connection_opts[:tls] == true || all_tls_path_available?
59
74
  tls_flags << ' --tlsverify'
60
75
  end
61
76
 
62
77
  self.class.tls_keys.each do |key|
63
- tls_flags << " --#{key}=#{@tls_args[key]}" if @tls_args[key]
78
+ tls_flags << " --#{key}=#{@connection_opts[key]}" if @connection_opts[key]
64
79
  end
65
80
 
66
81
  tls_flags
67
82
  end
68
83
 
69
84
  def build_command(action, destination)
70
- command = "#{@docker_path} -H=#{@docker_host}"
85
+ host = @socket ? "unix://#{@socket}" : @docker_host
86
+ command = "#{@docker_path} -H=#{host}"
71
87
  command << tls_parameters || ''
72
88
  command << case action
73
89
  when :pull then ' pull '
@@ -78,4 +94,17 @@ class Centurion::DockerViaCli
78
94
  command << destination
79
95
  command
80
96
  end
97
+
98
+ def connect
99
+ if @connection_opts[:ssh]
100
+ Centurion::SSH.with_docker_socket(@docker_host, @connection_opts[:ssh_user], @connection_opts[:ssh_log_level]) do |socket|
101
+ @socket = socket
102
+ ret = yield
103
+ @socket = nil
104
+ ret
105
+ end
106
+ else
107
+ yield
108
+ end
109
+ end
81
110
  end
@@ -5,7 +5,7 @@ module Centurion
5
5
  class Service
6
6
  extend ::Capistrano::DSL
7
7
 
8
- attr_accessor :command, :dns, :extra_hosts, :image, :name, :volumes, :port_bindings, :network_mode, :cap_adds, :cap_drops, :ipc_mode
8
+ attr_accessor :command, :dns, :extra_hosts, :image, :name, :volumes, :port_bindings, :network_mode, :cap_adds, :cap_drops, :ipc_mode, :security_opt
9
9
  attr_reader :memory, :cpu_shares, :env_vars, :labels
10
10
 
11
11
  def initialize(name)
@@ -16,6 +16,7 @@ module Centurion
16
16
  @cap_adds = []
17
17
  @cap_drops = []
18
18
  @labels = {}
19
+ @security_opt = []
19
20
  @network_mode = 'bridge'
20
21
  end
21
22
 
@@ -38,6 +39,7 @@ module Centurion
38
39
  s.memory = fetch(:memory, 0)
39
40
  s.cpu_shares = fetch(:cpu_shares, 0)
40
41
  s.ipc_mode = fetch(:ipc_mode, nil)
42
+ s.security_opt = fetch(:security_opt, [])
41
43
 
42
44
  s.add_labels(fetch(:labels, {}))
43
45
  s.add_env_vars(fetch(:env_vars, {}))
@@ -100,6 +102,10 @@ module Centurion
100
102
  @ipc_mode = mode
101
103
  end
102
104
 
105
+ def add_security_opt(seccomp)
106
+ @security_opt << seccomp
107
+ end
108
+
103
109
  def build_config(server_hostname, &block)
104
110
  container_config = {}.tap do |c|
105
111
  c['Image'] = image
@@ -164,6 +170,9 @@ module Centurion
164
170
  # Set ipc mode
165
171
  host_config['IpcMode'] = ipc_mode if ipc_mode
166
172
 
173
+ # Set seccomp profile
174
+ host_config['SecurityOpt'] = security_opt unless security_opt.nil? || security_opt.empty?
175
+
167
176
  # Restart Policy
168
177
  if restart_policy
169
178
  host_config['RestartPolicy'] = {}
@@ -0,0 +1,40 @@
1
+ require 'net/ssh'
2
+ require 'sshkit'
3
+
4
+ module Centurion; end
5
+
6
+ module Centurion::SSH
7
+ extend self
8
+
9
+ def with_docker_socket(hostname, user, log_level = nil)
10
+ log_level ||= Logger::WARN
11
+
12
+ with_sshkit(hostname, user) do
13
+ with_ssh do |ssh|
14
+ ssh.logger = Logger.new STDERR
15
+ ssh.logger.level = log_level
16
+
17
+ # Tempfile ensures permissions are 0600
18
+ local_socket_path_file = Tempfile.new('docker_forward')
19
+ local_socket_path = local_socket_path_file.path
20
+ ssh.forward.local_socket(local_socket_path, '/var/run/docker.sock')
21
+
22
+ t = Thread.new do
23
+ yield local_socket_path
24
+ end
25
+
26
+ ssh.loop { t.alive? }
27
+ ssh.forward.cancel_local_socket local_socket_path
28
+ local_socket_path_file.delete
29
+ t.value
30
+ end
31
+ end
32
+ end
33
+
34
+ def with_sshkit(hostname, user, &block)
35
+ uri = hostname
36
+ uri = "#{user}@#{uri}" if user
37
+ host = SSHKit::Host.new uri
38
+ SSHKit::Backend::Netssh.new(host, &block).run
39
+ end
40
+ end
@@ -1,3 +1,3 @@
1
1
  module Centurion
2
- VERSION = '1.8.10'
2
+ VERSION = '1.9.0'
3
3
  end
@@ -210,4 +210,19 @@ describe Centurion::DeployDSL do
210
210
  DeployDSLTest.set(:image, 'charlemagne')
211
211
  expect(DeployDSLTest.defined_service.image).to eq('charlemagne:roland')
212
212
  end
213
+
214
+ it 'configures ssh connections with no user' do
215
+ DeployDSLTest.set(:ssh, true)
216
+ DeployDSLTest.set(:hosts, %w{ host1 })
217
+
218
+ DeployDSLTest.on_each_docker_host { |h| expect(h.describe).to eq("host1 via SSH") }
219
+ end
220
+
221
+ it 'configures ssh connections with a user' do
222
+ DeployDSLTest.set(:ssh, true)
223
+ DeployDSLTest.set(:ssh_user, 'myuser')
224
+ DeployDSLTest.set(:hosts, %w{ host1 })
225
+
226
+ DeployDSLTest.on_each_docker_host { |h| expect(h.describe).to eq("host1 via SSH user myuser") }
227
+ end
213
228
  end
@@ -3,249 +3,174 @@ require 'centurion/docker_via_api'
3
3
 
4
4
  describe Centurion::DockerViaApi do
5
5
  let(:hostname) { 'example.com' }
6
- let(:port) { '2375' }
6
+ let(:port) { 2375 }
7
7
  let(:api_version) { '1.12' }
8
8
  let(:json_string) { '[{ "Hello": "World" }]' }
9
9
  let(:json_value) { JSON.load(json_string) }
10
10
 
11
- context 'without TLS certificates' do
12
- let(:excon_uri) { "http://#{hostname}:#{port}/" }
13
- let(:api) { Centurion::DockerViaApi.new(hostname, port) }
14
-
11
+ shared_examples "docker API" do
15
12
  it 'lists processes' do
16
- expect(Excon).to receive(:get).
17
- with(excon_uri + "v1.12" + "/containers/json", {}).
18
- and_return(double(body: json_string, status: 200))
13
+ Excon.stub(base_req.merge(method: :get, path: '/v1.12/containers/json'), {body: json_string, status: 200})
19
14
  expect(api.ps).to eq(json_value)
20
15
  end
21
16
 
22
17
  it 'lists all processes' do
23
- expect(Excon).to receive(:get).
24
- with(excon_uri + "v1.12" + "/containers/json?all=1", {}).
25
- and_return(double(body: json_string, status: 200))
18
+ Excon.stub(base_req.merge(method: :get, path: '/v1.12/containers/json?all=1'), {body: json_string, status: 200})
26
19
  expect(api.ps(all: true)).to eq(json_value)
27
20
  end
28
21
 
29
22
  it 'creates a container' do
30
- configuration_as_json = double
23
+ configuration_as_json = 'body'
31
24
  configuration = double(to_json: configuration_as_json)
32
- expect(Excon).to receive(:post).
33
- with(excon_uri + "v1.12" + "/containers/create",
34
- query: nil,
35
- body: configuration_as_json,
36
- headers: {'Content-Type' => 'application/json'}).
37
- and_return(double(body: json_string, status: 201))
25
+ Excon.stub(base_req.merge(
26
+ method: :post,
27
+ path: '/v1.12/containers/create',
28
+ body: configuration_as_json,
29
+ headers: {'Content-Type' => 'application/json'}
30
+ ),
31
+ {body: json_string, status: 201})
38
32
  api.create_container(configuration)
39
33
  end
40
34
 
41
35
  it 'creates a container with a name' do
42
- configuration_as_json = double
36
+ configuration_as_json = 'body'
43
37
  configuration = double(to_json: configuration_as_json)
44
- expect(Excon).to receive(:post).
45
- with(excon_uri + "v1.12" + "/containers/create",
46
- query: { name: match(/^app1-[a-f0-9]+$/) },
47
- body: configuration_as_json,
48
- headers: {'Content-Type' => 'application/json'}).
49
- and_return(double(body: json_string, status: 201))
38
+ Excon.stub(base_req.merge(
39
+ method: :post,
40
+ path: '/v1.12/containers/create',
41
+ query: /^name=app1-[a-f0-9]+$/,
42
+ body: configuration_as_json,
43
+ headers: {'Content-Type' => 'application/json'}
44
+ ),
45
+ {body: json_string, status: 201})
50
46
  api.create_container(configuration, 'app1')
51
47
  end
52
48
 
53
49
  it 'starts a container' do
54
- configuration_as_json = double
50
+ configuration_as_json = 'body'
55
51
  configuration = double(to_json: configuration_as_json)
56
- expect(Excon).to receive(:post).
57
- with(excon_uri + "v1.12" + "/containers/12345/start",
58
- body: configuration_as_json,
59
- headers: {'Content-Type' => 'application/json'}).
60
- and_return(double(body: json_string, status: 204))
52
+ Excon.stub(base_req.merge(
53
+ method: :post,
54
+ path: '/v1.12/containers/12345/start',
55
+ body: configuration_as_json,
56
+ headers: {'Content-Type' => 'application/json'}
57
+ ),
58
+ {body: json_string, status: 204})
61
59
  api.start_container('12345', configuration)
62
60
  end
63
61
 
64
62
  it 'stops a container' do
65
- expect(Excon).to receive(:post).
66
- with(excon_uri + "v1.12" + "/containers/12345/stop?t=300", {read_timeout: 420}).
67
- and_return(double(status: 204))
63
+ Excon.stub(base_req.merge(method: :post, path: '/v1.12/containers/12345/stop?t=300', read_timeout: 420), {status: 204})
68
64
  api.stop_container('12345', 300)
69
65
  end
70
66
 
71
67
  it 'stops a container with a custom timeout' do
72
- expect(Excon).to receive(:post).
73
- with(excon_uri + "v1.12" + "/containers/12345/stop?t=30", {read_timeout: 150}).
74
- and_return(double(status: 204))
68
+ Excon.stub(base_req.merge(method: :post, path: '/v1.12/containers/12345/stop?t=30', read_timeout: 150), {status: 204})
75
69
  api.stop_container('12345')
76
70
  end
77
71
 
78
72
  it 'restarts a container' do
79
- expect(Excon).to receive(:post).
80
- with(excon_uri + "v1.12" + "/containers/12345/restart?t=30",
81
- {read_timeout: 150}).
82
- and_return(double(body: json_string, status: 204))
73
+ Excon.stub(base_req.merge(method: :post, path: '/v1.12/containers/12345/restart?t=30', read_timeout: 150), {status: 204})
83
74
  api.restart_container('12345')
84
75
  end
85
76
 
86
77
  it 'restarts a container with a custom timeout' do
87
- expect(Excon).to receive(:post).
88
- with(excon_uri + "v1.12" + "/containers/12345/restart?t=300", {:read_timeout=>420}).
89
- and_return(double(body: json_string, status: 204))
78
+ Excon.stub(base_req.merge(method: :post, path: '/v1.12/containers/12345/restart?t=300', read_timeout: 420), {status: 204})
90
79
  api.restart_container('12345', 300)
91
80
  end
92
81
 
93
82
  it 'inspects a container' do
94
- expect(Excon).to receive(:get).
95
- with(excon_uri + "v1.12" + "/containers/12345/json", {}).
96
- and_return(double(body: json_string, status: 200))
83
+ Excon.stub(base_req.merge(method: :get, path: '/v1.12/containers/12345/json'), {body: json_string, status: 200})
97
84
  expect(api.inspect_container('12345')).to eq(json_value)
98
85
  end
99
86
 
100
87
  it 'removes a container' do
101
- expect(Excon).to receive(:delete).
102
- with(excon_uri + "v1.12" + "/containers/12345", {}).
103
- and_return(double(status: 204))
88
+ Excon.stub(base_req.merge(method: :delete, path: '/v1.12/containers/12345'), {body: json_string, status: 204})
104
89
  expect(api.remove_container('12345')).to eq(true)
105
90
  end
106
91
 
107
92
  it 'inspects an image' do
108
- expect(Excon).to receive(:get).
109
- with(excon_uri + "v1.12" + "/images/foo:bar/json",
110
- headers: {'Accept' => 'application/json'}).
111
- and_return(double(body: json_string, status: 200))
93
+ Excon.stub(base_req.merge(method: :get, path: '/v1.12/images/foo:bar/json', headers: {'Accept' => 'application/json'}), {body: json_string, status: 200})
112
94
  expect(api.inspect_image('foo', 'bar')).to eq(json_value)
113
95
  end
96
+ end
97
+
98
+ context 'without TLS certificates' do
99
+ let(:api) { Centurion::DockerViaApi.new(hostname, port) }
100
+ let(:base_req) { {hostname: hostname, port: port} }
114
101
 
102
+ it_behaves_like 'docker API'
115
103
  end
116
104
 
117
105
  context 'with TLS certificates' do
118
- let(:excon_uri) { "https://#{hostname}:#{port}/" }
119
106
  let(:tls_args) { { tls: true, tlscacert: '/certs/ca.pem',
120
107
  tlscert: '/certs/cert.pem', tlskey: '/certs/key.pem' } }
108
+ let(:base_req) { {
109
+ hostname: hostname,
110
+ port: port,
111
+ client_cert: '/certs/cert.pem',
112
+ client_key: '/certs/key.pem',
113
+ } }
121
114
  let(:api) { Centurion::DockerViaApi.new(hostname, port, tls_args) }
122
115
 
123
- it 'lists processes' do
124
- expect(Excon).to receive(:get).
125
- with(excon_uri + "v1.12" + "/containers/json",
126
- client_cert: '/certs/cert.pem',
127
- client_key: '/certs/key.pem').
128
- and_return(double(body: json_string, status: 200))
129
- expect(api.ps).to eq(json_value)
130
- end
131
-
132
- it 'lists all processes' do
133
- expect(Excon).to receive(:get).
134
- with(excon_uri + "v1.12" + "/containers/json?all=1",
135
- client_cert: '/certs/cert.pem',
136
- client_key: '/certs/key.pem').
137
- and_return(double(body: json_string, status: 200))
138
- expect(api.ps(all: true)).to eq(json_value)
139
- end
116
+ it_behaves_like 'docker API'
117
+ end
140
118
 
141
- it 'inspects an image' do
142
- expect(Excon).to receive(:get).
143
- with(excon_uri + "v1.12" + "/images/foo:bar/json",
144
- client_cert: '/certs/cert.pem',
145
- client_key: '/certs/key.pem',
146
- headers: {'Accept' => 'application/json'}).
147
- and_return(double(body: json_string, status: 200))
148
- expect(api.inspect_image('foo', 'bar')).to eq(json_value)
149
- end
119
+ context 'with default TLS certificates' do
120
+ let(:tls_args) { { tls: true } }
121
+ let(:base_req) { {
122
+ hostname: hostname,
123
+ port: port,
124
+ client_cert: File.expand_path('~/.docker/cert.pem'),
125
+ client_key: File.expand_path('~/.docker/key.pem'),
126
+ } }
127
+ let(:api) { Centurion::DockerViaApi.new(hostname, port, tls_args) }
150
128
 
151
- it 'creates a container' do
152
- configuration_as_json = double
153
- configuration = double(to_json: configuration_as_json)
154
- expect(Excon).to receive(:post).
155
- with(excon_uri + "v1.12" + "/containers/create",
156
- client_cert: '/certs/cert.pem',
157
- client_key: '/certs/key.pem',
158
- query: nil,
159
- body: configuration_as_json,
160
- headers: {'Content-Type' => 'application/json'}).
161
- and_return(double(body: json_string, status: 201))
162
- api.create_container(configuration)
163
- end
129
+ it_behaves_like 'docker API'
130
+ end
164
131
 
165
- it 'starts a container' do
166
- configuration_as_json = double
167
- configuration = double(to_json: configuration_as_json)
168
- expect(Excon).to receive(:post).
169
- with(excon_uri + "v1.12" + "/containers/12345/start",
170
- client_cert: '/certs/cert.pem',
171
- client_key: '/certs/key.pem',
172
- body: configuration_as_json,
173
- headers: {'Content-Type' => 'application/json'}).
174
- and_return(double(body: json_string, status: 204))
175
- api.start_container('12345', configuration)
132
+ context 'with a SSH connection' do
133
+ let(:hostname) { 'hostname' }
134
+ let(:port) { nil }
135
+ let(:ssh_user) { 'myuser' }
136
+ let(:ssh_log_level) { nil }
137
+ let(:base_req) { {
138
+ socket: '/tmp/socket/path'
139
+ } }
140
+ let(:api) { Centurion::DockerViaApi.new(hostname, port, params) }
141
+ let(:params) do
142
+ p = { ssh: true}
143
+ p[:ssh_user] = ssh_user if ssh_user
144
+ p[:ssh_log_level] = ssh_log_level if ssh_log_level
145
+ p
176
146
  end
177
147
 
178
- it 'stops a container' do
179
- expect(Excon).to receive(:post).
180
- with(excon_uri + "v1.12" + "/containers/12345/stop?t=300",
181
- client_cert: '/certs/cert.pem',
182
- client_key: '/certs/key.pem',
183
- read_timeout: 420).
184
- and_return(double(status: 204))
185
- api.stop_container('12345', 300)
186
- end
148
+ context 'with no log level' do
149
+ before do
150
+ expect(Centurion::SSH).to receive(:with_docker_socket).with(hostname, ssh_user, nil).and_yield('/tmp/socket/path')
151
+ end
187
152
 
188
- it 'stops a container with a custom timeout' do
189
- expect(Excon).to receive(:post).
190
- with(excon_uri + "v1.12" + "/containers/12345/stop?t=30",
191
- client_cert: '/certs/cert.pem',
192
- client_key: '/certs/key.pem',
193
- read_timeout: 150).
194
- and_return(double(status: 204))
195
- api.stop_container('12345')
153
+ it_behaves_like 'docker API'
196
154
  end
197
155
 
198
- it 'restarts a container' do
199
- expect(Excon).to receive(:post).
200
- with(excon_uri + "v1.12" + "/containers/12345/restart?t=30",
201
- client_cert: '/certs/cert.pem',
202
- client_key: '/certs/key.pem',
203
- read_timeout: 150).
204
- and_return(double(body: json_string, status: 204))
205
- api.restart_container('12345')
206
- end
156
+ context 'with no user' do
157
+ let(:ssh_user) { nil }
207
158
 
208
- it 'restarts a container with a custom timeout' do
209
- expect(Excon).to receive(:post).
210
- with(excon_uri + "v1.12" + "/containers/12345/restart?t=300",
211
- client_cert: '/certs/cert.pem',
212
- client_key: '/certs/key.pem',
213
- read_timeout: 420).
214
- and_return(double(body: json_string, status: 204))
215
- api.restart_container('12345', 300)
216
- end
159
+ before do
160
+ expect(Centurion::SSH).to receive(:with_docker_socket).with(hostname, nil, nil).and_yield('/tmp/socket/path')
161
+ end
217
162
 
218
- it 'inspects a container' do
219
- expect(Excon).to receive(:get).
220
- with(excon_uri + "v1.12" + "/containers/12345/json",
221
- client_cert: '/certs/cert.pem',
222
- client_key: '/certs/key.pem').
223
- and_return(double(body: json_string, status: 200))
224
- expect(api.inspect_container('12345')).to eq(json_value)
163
+ it_behaves_like 'docker API'
225
164
  end
226
165
 
227
- it 'removes a container' do
228
- expect(Excon).to receive(:delete).
229
- with(excon_uri + "v1.12" + "/containers/12345",
230
- client_cert: '/certs/cert.pem',
231
- client_key: '/certs/key.pem').
232
- and_return(double(status: 204))
233
- expect(api.remove_container('12345')).to eq(true)
234
- end
235
- end
166
+ context 'with a log level set' do
167
+ let(:ssh_log_level) { Logger::DEBUG }
236
168
 
237
- context 'with default TLS certificates' do
238
- let(:excon_uri) { "https://#{hostname}:#{port}/" }
239
- let(:tls_args) { { tls: true } }
240
- let(:api) { Centurion::DockerViaApi.new(hostname, port, tls_args) }
169
+ before do
170
+ expect(Centurion::SSH).to receive(:with_docker_socket).with(hostname, ssh_user, Logger::DEBUG).and_yield('/tmp/socket/path')
171
+ end
241
172
 
242
- it 'lists processes' do
243
- expect(Excon).to receive(:get).
244
- with(excon_uri + "v1.12" + "/containers/json",
245
- client_cert: File.expand_path('~/.docker/cert.pem'),
246
- client_key: File.expand_path('~/.docker/key.pem')).
247
- and_return(double(body: json_string, status: 200))
248
- expect(api.ps).to eq(json_value)
173
+ it_behaves_like 'docker API'
249
174
  end
250
175
  end
251
176
  end