centurion 1.8.10 → 1.9.0

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