Skip to content

Commit

Permalink
Merge pull request #184 from newrelic/andrew/ssh
Browse files Browse the repository at this point in the history
BETA SSH support
  • Loading branch information
intjonathan authored Oct 26, 2017
2 parents 9b67411 + e8fd508 commit b0e14ce
Show file tree
Hide file tree
Showing 12 changed files with 381 additions and 271 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,27 @@ You have to set the following keys:

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

### Use SSH to connect *beta*

If your Docker server does not expose its HTTP service over TCP, you can
instead talk to it via SSH.

This functions by creating a local Unix socket that forwards to the remote
Docker Unix socket, so it requires that the user you connect as has access to
the Docker socket without any `sudo`. Currently it also assumes that you
authenticate via public key, so be sure that you have `ssh-add`ed your key to
your SSH agent if it has a passcode.

You can configure it with a few options:

```ruby
task :common do
set :ssh, true # enable ssh connections
set :ssh_user, "myuser" # if you want to specify the user to connect as, otherwise your current user
set :ssh_log_level, Logger::DEBUG # passed on to net/ssh, can be noisy; defaults to Logger::WARN
end
```

Deploying
---------

Expand Down
2 changes: 2 additions & 0 deletions centurion.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Gem::Specification.new do |spec|
spec.add_dependency 'trollop'
spec.add_dependency 'excon', '~> 0.33'
spec.add_dependency 'logger-colors'
spec.add_dependency 'net-ssh'
spec.add_dependency 'sshkit'

spec.add_development_dependency 'bundler'
spec.add_development_dependency 'rake', '~> 10.5'
Expand Down
28 changes: 19 additions & 9 deletions lib/centurion/deploy_dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def defined_restart_policy

def build_server_group
hosts, docker_path = fetch(:hosts, []), fetch(:docker_path)
Centurion::DockerServerGroup.new(hosts, docker_path, build_tls_params)
Centurion::DockerServerGroup.new(hosts, docker_path, build_server_params)
end

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

def build_tls_params
return {} unless fetch(:tlsverify)
{
tls: fetch(:tlsverify || tls_paths_available?),
tlscacert: fetch(:tlscacert),
tlscert: fetch(:tlscert),
tlskey: fetch(:tlskey)
}
def build_server_params
opts = {}
if fetch(:tlsverify)
opts[:tls] = fetch(:tlsverify || tls_paths_available?)
opts[:tlscacert] = fetch(:tlscacert)
opts[:tlscert] = fetch(:tlscert)
opts[:tlskey] = fetch(:tlskey)
end

if fetch(:ssh, false) == true
opts[:ssh] = true

# nil is OK for both of these, defaults applied internally
opts[:ssh_user] = fetch(:ssh_user)
opts[:ssh_log_level] = fetch(:ssh_log_level)
end

opts
end
end
28 changes: 19 additions & 9 deletions lib/centurion/docker_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ class Centurion::DockerServer
:remove_container, :restart_container
def_delegators :docker_via_cli, :pull, :tail, :attach, :exec, :exec_it

def initialize(host, docker_path, tls_params = {})
def initialize(host, docker_path, connection_opts = {})
@docker_path = docker_path
@hostname, @port = host.split(':')
@port ||= if tls_params.empty?
'2375'
else
'2376'
end
@tls_params = tls_params
@port ||= if connection_opts[:tls]
'2376'
else
'2375'
end
@connection_opts = connection_opts
end

def current_tags_for(image)
Expand Down Expand Up @@ -64,16 +64,26 @@ def old_containers_for_name(wanted_name)
end
end

def describe
desc = hostname
desc += " via TLS" if @connection_opts[:tls]
if @connection_opts[:ssh]
desc += " via SSH"
desc += " user #{@connection_opts[:ssh_user]}" if @connection_opts[:ssh_user]
end
desc
end

private

def docker_via_api
@docker_via_api ||= Centurion::DockerViaApi.new(@hostname, @port,
@tls_params, nil)
@connection_opts, nil)
end

def docker_via_cli
@docker_via_cli ||= Centurion::DockerViaCli.new(@hostname, @port,
@docker_path, @tls_params)
@docker_path, @connection_opts)
end

def parse_image_tags_for(running_containers)
Expand Down
93 changes: 60 additions & 33 deletions lib/centurion/docker_via_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
require 'json'
require 'uri'
require 'securerandom'
require 'centurion/ssh'

module Centurion; end

class Centurion::DockerViaApi
def initialize(hostname, port, tls_args = {}, api_version = nil)
@tls_args = default_tls_args(tls_args[:tls]).merge(tls_args.reject { |k, v| v.nil? }) # Required by tls_enable?
@base_uri = "http#{'s' if tls_enable?}://#{hostname}:#{port}"
def initialize(hostname, port, connection_opts = {}, api_version = nil)
@tls_args = default_tls_args(connection_opts[:tls]).merge(connection_opts.reject { |k, v| v.nil? }) # Required by tls_enable?
if connection_opts[:ssh]
@base_uri = hostname
@ssh = true
@ssh_user = connection_opts[:ssh_user]
@ssh_log_level = connection_opts[:ssh_log_level]
else
@base_uri = "http#{'s' if tls_enable?}://#{hostname}:#{port}"
end
api_version ||= "/v1.12"
@docker_api_version = api_version
configure_excon_globally
Expand All @@ -17,7 +25,7 @@ def initialize(hostname, port, tls_args = {}, api_version = nil)
def ps(options={})
path = @docker_api_version + "/containers/json"
path += "?all=1" if options[:all]
response = Excon.get(@base_uri + path, tls_excon_arguments)
response = with_excon {|e| e.get(path: path)}

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

response = Excon.get(
@base_uri + path,
tls_excon_arguments.merge(headers: {'Accept' => 'application/json'})
)
response = with_excon do |e|
e.get(
path: path,
headers: {'Accept' => 'application/json'}
)
end
raise response.inspect unless response.status == 200
JSON.load(response.body)
end

def remove_container(container_id)
path = @docker_api_version + "/containers/#{container_id}"
response = Excon.delete(
@base_uri + path,
tls_excon_arguments
)
response = with_excon do |e|
e.delete(
path: path,
)
end
raise response.inspect unless response.status == 204
true
end

def stop_container(container_id, timeout = 30)
path = @docker_api_version + "/containers/#{container_id}/stop?t=#{timeout}"
response = Excon.post(
@base_uri + path,
tls_excon_arguments.merge(
response = with_excon do |e|
e.post(
path: path,
# Wait for both the docker stop timeout AND the kill AND
# potentially a very slow HTTP server.
read_timeout: timeout + 120
)
)
end
raise response.inspect unless response.status == 204
true
end

def create_container(configuration, name = nil)
path = @docker_api_version + "/containers/create"
response = Excon.post(
@base_uri + path,
tls_excon_arguments.merge(
query: name ? {name: "#{name}-#{SecureRandom.hex(7)}"} : nil,
response = with_excon do |e|
e.post(
path: path,
query: name ? "name=#{name}-#{SecureRandom.hex(7)}" : nil,
body: configuration.to_json,
headers: { "Content-Type" => "application/json" }
)
)
end
raise response.inspect unless response.status == 201
JSON.load(response.body)
end

def start_container(container_id, configuration)
path = @docker_api_version + "/containers/#{container_id}/start"
response = Excon.post(
@base_uri + path,
tls_excon_arguments.merge(
response = with_excon do |e|
e.post(
path: path,
body: configuration.to_json,
headers: { "Content-Type" => "application/json" }
)
)
end
case response.status
when 204
true
Expand All @@ -94,14 +105,14 @@ def start_container(container_id, configuration)

def restart_container(container_id, timeout = 30)
path = @docker_api_version + "/containers/#{container_id}/restart?t=#{timeout}"
response = Excon.post(
@base_uri + path,
tls_excon_arguments.merge(
response = with_excon do |e|
e.post(
path: path,
# Wait for both the docker stop timeout AND the kill AND
# potentially a very slow HTTP server.
read_timeout: timeout + 120
)
)
end
case response.status
when 204
true
Expand All @@ -116,10 +127,11 @@ def restart_container(container_id, timeout = 30)

def inspect_container(container_id)
path = @docker_api_version + "/containers/#{container_id}/json"
response = Excon.get(
@base_uri + path,
tls_excon_arguments
)
response = with_excon do |e|
e.get(
path: path,
)
end
raise response.inspect unless response.status == 200
JSON.load(response.body)
end
Expand Down Expand Up @@ -172,4 +184,19 @@ def default_tls_args(tls_enabled)
{}
end
end

def with_excon(&block)
if @ssh
with_excon_via_ssh(&block)
else
yield Excon.new(@base_uri, tls_excon_arguments)
end
end

def with_excon_via_ssh
Centurion::SSH.with_docker_socket(@base_uri, @ssh_user, @ssh_log_level) do |socket|
conn = Excon.new('unix:///', socket: socket)
yield conn
end
end
end
Loading

0 comments on commit b0e14ce

Please sign in to comment.