Skip to content

Commit b0e14ce

Browse files
authored
Merge pull request #184 from newrelic/andrew/ssh
BETA SSH support
2 parents 9b67411 + e8fd508 commit b0e14ce

12 files changed

+381
-271
lines changed

README.md

+21
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,27 @@ You have to set the following keys:
352352

353353
Modify the paths as appropriate for your cert, ca, and key files.
354354

355+
### Use SSH to connect *beta*
356+
357+
If your Docker server does not expose its HTTP service over TCP, you can
358+
instead talk to it via SSH.
359+
360+
This functions by creating a local Unix socket that forwards to the remote
361+
Docker Unix socket, so it requires that the user you connect as has access to
362+
the Docker socket without any `sudo`. Currently it also assumes that you
363+
authenticate via public key, so be sure that you have `ssh-add`ed your key to
364+
your SSH agent if it has a passcode.
365+
366+
You can configure it with a few options:
367+
368+
```ruby
369+
task :common do
370+
set :ssh, true # enable ssh connections
371+
set :ssh_user, "myuser" # if you want to specify the user to connect as, otherwise your current user
372+
set :ssh_log_level, Logger::DEBUG # passed on to net/ssh, can be noisy; defaults to Logger::WARN
373+
end
374+
```
375+
355376
Deploying
356377
---------
357378

centurion.gemspec

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Gem::Specification.new do |spec|
3636
spec.add_dependency 'trollop'
3737
spec.add_dependency 'excon', '~> 0.33'
3838
spec.add_dependency 'logger-colors'
39+
spec.add_dependency 'net-ssh'
40+
spec.add_dependency 'sshkit'
3941

4042
spec.add_development_dependency 'bundler'
4143
spec.add_development_dependency 'rake', '~> 10.5'

lib/centurion/deploy_dsl.rb

+19-9
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def defined_restart_policy
150150

151151
def build_server_group
152152
hosts, docker_path = fetch(:hosts, []), fetch(:docker_path)
153-
Centurion::DockerServerGroup.new(hosts, docker_path, build_tls_params)
153+
Centurion::DockerServerGroup.new(hosts, docker_path, build_server_params)
154154
end
155155

156156
def validate_options_keys(options, valid_keys)
@@ -180,13 +180,23 @@ def tls_paths_available?
180180
Centurion::DockerViaCli.tls_keys.all? { |key| fetch(key).present? }
181181
end
182182

183-
def build_tls_params
184-
return {} unless fetch(:tlsverify)
185-
{
186-
tls: fetch(:tlsverify || tls_paths_available?),
187-
tlscacert: fetch(:tlscacert),
188-
tlscert: fetch(:tlscert),
189-
tlskey: fetch(:tlskey)
190-
}
183+
def build_server_params
184+
opts = {}
185+
if fetch(:tlsverify)
186+
opts[:tls] = fetch(:tlsverify || tls_paths_available?)
187+
opts[:tlscacert] = fetch(:tlscacert)
188+
opts[:tlscert] = fetch(:tlscert)
189+
opts[:tlskey] = fetch(:tlskey)
190+
end
191+
192+
if fetch(:ssh, false) == true
193+
opts[:ssh] = true
194+
195+
# nil is OK for both of these, defaults applied internally
196+
opts[:ssh_user] = fetch(:ssh_user)
197+
opts[:ssh_log_level] = fetch(:ssh_log_level)
198+
end
199+
200+
opts
191201
end
192202
end

lib/centurion/docker_server.rb

+19-9
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ class Centurion::DockerServer
1818
:remove_container, :restart_container
1919
def_delegators :docker_via_cli, :pull, :tail, :attach, :exec, :exec_it
2020

21-
def initialize(host, docker_path, tls_params = {})
21+
def initialize(host, docker_path, connection_opts = {})
2222
@docker_path = docker_path
2323
@hostname, @port = host.split(':')
24-
@port ||= if tls_params.empty?
25-
'2375'
26-
else
27-
'2376'
28-
end
29-
@tls_params = tls_params
24+
@port ||= if connection_opts[:tls]
25+
'2376'
26+
else
27+
'2375'
28+
end
29+
@connection_opts = connection_opts
3030
end
3131

3232
def current_tags_for(image)
@@ -64,16 +64,26 @@ def old_containers_for_name(wanted_name)
6464
end
6565
end
6666

67+
def describe
68+
desc = hostname
69+
desc += " via TLS" if @connection_opts[:tls]
70+
if @connection_opts[:ssh]
71+
desc += " via SSH"
72+
desc += " user #{@connection_opts[:ssh_user]}" if @connection_opts[:ssh_user]
73+
end
74+
desc
75+
end
76+
6777
private
6878

6979
def docker_via_api
7080
@docker_via_api ||= Centurion::DockerViaApi.new(@hostname, @port,
71-
@tls_params, nil)
81+
@connection_opts, nil)
7282
end
7383

7484
def docker_via_cli
7585
@docker_via_cli ||= Centurion::DockerViaCli.new(@hostname, @port,
76-
@docker_path, @tls_params)
86+
@docker_path, @connection_opts)
7787
end
7888

7989
def parse_image_tags_for(running_containers)

lib/centurion/docker_via_api.rb

+60-33
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@
22
require 'json'
33
require 'uri'
44
require 'securerandom'
5+
require 'centurion/ssh'
56

67
module Centurion; end
78

89
class Centurion::DockerViaApi
9-
def initialize(hostname, port, tls_args = {}, api_version = nil)
10-
@tls_args = default_tls_args(tls_args[:tls]).merge(tls_args.reject { |k, v| v.nil? }) # Required by tls_enable?
11-
@base_uri = "http#{'s' if tls_enable?}://#{hostname}:#{port}"
10+
def initialize(hostname, port, connection_opts = {}, api_version = nil)
11+
@tls_args = default_tls_args(connection_opts[:tls]).merge(connection_opts.reject { |k, v| v.nil? }) # Required by tls_enable?
12+
if connection_opts[:ssh]
13+
@base_uri = hostname
14+
@ssh = true
15+
@ssh_user = connection_opts[:ssh_user]
16+
@ssh_log_level = connection_opts[:ssh_log_level]
17+
else
18+
@base_uri = "http#{'s' if tls_enable?}://#{hostname}:#{port}"
19+
end
1220
api_version ||= "/v1.12"
1321
@docker_api_version = api_version
1422
configure_excon_globally
@@ -17,7 +25,7 @@ def initialize(hostname, port, tls_args = {}, api_version = nil)
1725
def ps(options={})
1826
path = @docker_api_version + "/containers/json"
1927
path += "?all=1" if options[:all]
20-
response = Excon.get(@base_uri + path, tls_excon_arguments)
28+
response = with_excon {|e| e.get(path: path)}
2129

2230
raise unless response.status == 200
2331
JSON.load(response.body)
@@ -27,61 +35,64 @@ def inspect_image(image, tag = "latest")
2735
repository = "#{image}:#{tag}"
2836
path = @docker_api_version + "/images/#{repository}/json"
2937

30-
response = Excon.get(
31-
@base_uri + path,
32-
tls_excon_arguments.merge(headers: {'Accept' => 'application/json'})
33-
)
38+
response = with_excon do |e|
39+
e.get(
40+
path: path,
41+
headers: {'Accept' => 'application/json'}
42+
)
43+
end
3444
raise response.inspect unless response.status == 200
3545
JSON.load(response.body)
3646
end
3747

3848
def remove_container(container_id)
3949
path = @docker_api_version + "/containers/#{container_id}"
40-
response = Excon.delete(
41-
@base_uri + path,
42-
tls_excon_arguments
43-
)
50+
response = with_excon do |e|
51+
e.delete(
52+
path: path,
53+
)
54+
end
4455
raise response.inspect unless response.status == 204
4556
true
4657
end
4758

4859
def stop_container(container_id, timeout = 30)
4960
path = @docker_api_version + "/containers/#{container_id}/stop?t=#{timeout}"
50-
response = Excon.post(
51-
@base_uri + path,
52-
tls_excon_arguments.merge(
61+
response = with_excon do |e|
62+
e.post(
63+
path: path,
5364
# Wait for both the docker stop timeout AND the kill AND
5465
# potentially a very slow HTTP server.
5566
read_timeout: timeout + 120
5667
)
57-
)
68+
end
5869
raise response.inspect unless response.status == 204
5970
true
6071
end
6172

6273
def create_container(configuration, name = nil)
6374
path = @docker_api_version + "/containers/create"
64-
response = Excon.post(
65-
@base_uri + path,
66-
tls_excon_arguments.merge(
67-
query: name ? {name: "#{name}-#{SecureRandom.hex(7)}"} : nil,
75+
response = with_excon do |e|
76+
e.post(
77+
path: path,
78+
query: name ? "name=#{name}-#{SecureRandom.hex(7)}" : nil,
6879
body: configuration.to_json,
6980
headers: { "Content-Type" => "application/json" }
7081
)
71-
)
82+
end
7283
raise response.inspect unless response.status == 201
7384
JSON.load(response.body)
7485
end
7586

7687
def start_container(container_id, configuration)
7788
path = @docker_api_version + "/containers/#{container_id}/start"
78-
response = Excon.post(
79-
@base_uri + path,
80-
tls_excon_arguments.merge(
89+
response = with_excon do |e|
90+
e.post(
91+
path: path,
8192
body: configuration.to_json,
8293
headers: { "Content-Type" => "application/json" }
8394
)
84-
)
95+
end
8596
case response.status
8697
when 204
8798
true
@@ -94,14 +105,14 @@ def start_container(container_id, configuration)
94105

95106
def restart_container(container_id, timeout = 30)
96107
path = @docker_api_version + "/containers/#{container_id}/restart?t=#{timeout}"
97-
response = Excon.post(
98-
@base_uri + path,
99-
tls_excon_arguments.merge(
108+
response = with_excon do |e|
109+
e.post(
110+
path: path,
100111
# Wait for both the docker stop timeout AND the kill AND
101112
# potentially a very slow HTTP server.
102113
read_timeout: timeout + 120
103114
)
104-
)
115+
end
105116
case response.status
106117
when 204
107118
true
@@ -116,10 +127,11 @@ def restart_container(container_id, timeout = 30)
116127

117128
def inspect_container(container_id)
118129
path = @docker_api_version + "/containers/#{container_id}/json"
119-
response = Excon.get(
120-
@base_uri + path,
121-
tls_excon_arguments
122-
)
130+
response = with_excon do |e|
131+
e.get(
132+
path: path,
133+
)
134+
end
123135
raise response.inspect unless response.status == 200
124136
JSON.load(response.body)
125137
end
@@ -172,4 +184,19 @@ def default_tls_args(tls_enabled)
172184
{}
173185
end
174186
end
187+
188+
def with_excon(&block)
189+
if @ssh
190+
with_excon_via_ssh(&block)
191+
else
192+
yield Excon.new(@base_uri, tls_excon_arguments)
193+
end
194+
end
195+
196+
def with_excon_via_ssh
197+
Centurion::SSH.with_docker_socket(@base_uri, @ssh_user, @ssh_log_level) do |socket|
198+
conn = Excon.new('unix:///', socket: socket)
199+
yield conn
200+
end
201+
end
175202
end

0 commit comments

Comments
 (0)