From 86e5f189e5545e8c1bf6289209602712e99e3fb6 Mon Sep 17 00:00:00 2001 From: Andrew Bloomgarden Date: Wed, 27 Sep 2017 14:48:51 -0400 Subject: [PATCH 1/2] SSH support Set :ssh to true to connect via SSH, :ssh_user with which user. --- README.md | 21 +++ centurion.gemspec | 2 + lib/centurion/deploy_dsl.rb | 28 ++-- lib/centurion/docker_server.rb | 28 ++-- lib/centurion/docker_via_api.rb | 93 ++++++++---- lib/centurion/docker_via_cli.rb | 55 +++++-- lib/centurion/ssh.rb | 40 +++++ spec/deploy_dsl_spec.rb | 15 ++ spec/docker_via_api_spec.rb | 259 ++++++++++++-------------------- spec/docker_via_cli_spec.rb | 96 +++++++----- spec/spec_helper.rb | 13 ++ 11 files changed, 380 insertions(+), 270 deletions(-) create mode 100644 lib/centurion/ssh.rb diff --git a/README.md b/README.md index 56cdaa29..26d50253 100644 --- a/README.md +++ b/README.md @@ -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 --------- diff --git a/centurion.gemspec b/centurion.gemspec index 87ca92bb..4d321a60 100644 --- a/centurion.gemspec +++ b/centurion.gemspec @@ -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' diff --git a/lib/centurion/deploy_dsl.rb b/lib/centurion/deploy_dsl.rb index 726778cf..807f921b 100644 --- a/lib/centurion/deploy_dsl.rb +++ b/lib/centurion/deploy_dsl.rb @@ -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) @@ -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 diff --git a/lib/centurion/docker_server.rb b/lib/centurion/docker_server.rb index 05cb36a7..b609d2d0 100644 --- a/lib/centurion/docker_server.rb +++ b/lib/centurion/docker_server.rb @@ -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) @@ -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) diff --git a/lib/centurion/docker_via_api.rb b/lib/centurion/docker_via_api.rb index 0111294b..eee9e603 100644 --- a/lib/centurion/docker_via_api.rb +++ b/lib/centurion/docker_via_api.rb @@ -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 @@ -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) @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/lib/centurion/docker_via_cli.rb b/lib/centurion/docker_via_cli.rb index 6a286736..636d84e9 100644 --- a/lib/centurion/docker_via_cli.rb +++ b/lib/centurion/docker_via_cli.rb @@ -1,34 +1,47 @@ require 'pty' require_relative 'logging' require_relative 'shell' +require 'centurion/ssh' module Centurion; end class Centurion::DockerViaCli include Centurion::Logging - def initialize(hostname, port, docker_path, tls_args = {}) - @docker_host = "tcp://#{hostname}:#{port}" + def initialize(hostname, port, docker_path, connection_opts = {}) + if connection_opts[:ssh] + @docker_host = hostname + else + @docker_host = "tcp://#{hostname}:#{port}" + end @docker_path = docker_path - @tls_args = tls_args + @connection_opts = connection_opts end def pull(image, tag='latest') info 'Using CLI to pull' - Centurion::Shell.echo(build_command(:pull, "#{image}:#{tag}")) + connect do + Centurion::Shell.echo(build_command(:pull, "#{image}:#{tag}")) + end end def tail(container_id) info "Tailing the logs on #{container_id}" - Centurion::Shell.echo(build_command(:logs, container_id)) + connect do + Centurion::Shell.echo(build_command(:logs, container_id)) + end end def attach(container_id) - Centurion::Shell.echo(build_command(:attach, container_id)) + connect do + Centurion::Shell.echo(build_command(:attach, container_id)) + end end def exec(container_id, commandline) - Centurion::Shell.echo(build_command(:exec, "#{container_id} #{commandline}")) + connect do + Centurion::Shell.echo(build_command(:exec, "#{container_id} #{commandline}")) + end end def exec_it(container_id, commandline) @@ -36,7 +49,9 @@ def exec_it(container_id, commandline) # because docker exec returns the same exit code as the latest command executed on # the shell, which causes an exception to be raised if the latest comand executed # was unsuccessful when you exit the shell. - Centurion::Shell.echo(build_command(:exec, "-it #{container_id} #{commandline} || true")) + connect do + Centurion::Shell.echo(build_command(:exec, "-it #{container_id} #{commandline} || true")) + end end private @@ -46,28 +61,29 @@ def self.tls_keys end def all_tls_path_available? - self.class.tls_keys.all? { |key| @tls_args.key?(key) } + self.class.tls_keys.all? { |key| @connection_opts.key?(key) } end def tls_parameters - return '' if @tls_args.nil? || @tls_args.empty? + return '' if @connection_opts.nil? || @connection_opts.empty? tls_flags = '' # --tlsverify can be set without passing the cacert, cert and key flags - if @tls_args[:tls] == true || all_tls_path_available? + if @connection_opts[:tls] == true || all_tls_path_available? tls_flags << ' --tlsverify' end self.class.tls_keys.each do |key| - tls_flags << " --#{key}=#{@tls_args[key]}" if @tls_args[key] + tls_flags << " --#{key}=#{@connection_opts[key]}" if @connection_opts[key] end tls_flags end def build_command(action, destination) - command = "#{@docker_path} -H=#{@docker_host}" + host = @socket ? "unix://#{@socket}" : @docker_host + command = "#{@docker_path} -H=#{host}" command << tls_parameters || '' command << case action when :pull then ' pull ' @@ -78,4 +94,17 @@ def build_command(action, destination) command << destination command end + + def connect + if @connection_opts[:ssh] + Centurion::SSH.with_docker_socket(@docker_host, @connection_opts[:ssh_user], @connection_opts[:ssh_log_level]) do |socket| + @socket = socket + ret = yield + @socket = nil + ret + end + else + yield + end + end end diff --git a/lib/centurion/ssh.rb b/lib/centurion/ssh.rb new file mode 100644 index 00000000..652cc9bc --- /dev/null +++ b/lib/centurion/ssh.rb @@ -0,0 +1,40 @@ +require 'net/ssh' +require 'sshkit' + +module Centurion; end + +module Centurion::SSH + extend self + + def with_docker_socket(hostname, user, log_level = nil) + log_level ||= Logger::WARN + + with_sshkit(hostname, user) do + with_ssh do |ssh| + ssh.logger = Logger.new STDERR + ssh.logger.level = log_level + + # Tempfile ensures permissions are 0600 + local_socket_path_file = Tempfile.new('docker_forward') + local_socket_path = local_socket_path_file.path + ssh.forward.local_socket(local_socket_path, '/var/run/docker.sock') + + t = Thread.new do + yield local_socket_path + end + + ssh.loop { t.alive? } + ssh.forward.cancel_local_socket local_socket_path + local_socket_path_file.delete + t.value + end + end + end + + def with_sshkit(hostname, user, &block) + uri = hostname + uri = "#{user}@#{uri}" if user + host = SSHKit::Host.new uri + SSHKit::Backend::Netssh.new(host, &block).run + end +end diff --git a/spec/deploy_dsl_spec.rb b/spec/deploy_dsl_spec.rb index cc6c3cbb..f9653684 100644 --- a/spec/deploy_dsl_spec.rb +++ b/spec/deploy_dsl_spec.rb @@ -210,4 +210,19 @@ class DeployDSLTest DeployDSLTest.set(:image, 'charlemagne') expect(DeployDSLTest.defined_service.image).to eq('charlemagne:roland') end + + it 'configures ssh connections with no user' do + DeployDSLTest.set(:ssh, true) + DeployDSLTest.set(:hosts, %w{ host1 }) + + DeployDSLTest.on_each_docker_host { |h| expect(h.describe).to eq("host1 via SSH") } + end + + it 'configures ssh connections with a user' do + DeployDSLTest.set(:ssh, true) + DeployDSLTest.set(:ssh_user, 'myuser') + DeployDSLTest.set(:hosts, %w{ host1 }) + + DeployDSLTest.on_each_docker_host { |h| expect(h.describe).to eq("host1 via SSH user myuser") } + end end diff --git a/spec/docker_via_api_spec.rb b/spec/docker_via_api_spec.rb index 42fdc2fe..5399e1e2 100644 --- a/spec/docker_via_api_spec.rb +++ b/spec/docker_via_api_spec.rb @@ -3,249 +3,174 @@ describe Centurion::DockerViaApi do let(:hostname) { 'example.com' } - let(:port) { '2375' } + let(:port) { 2375 } let(:api_version) { '1.12' } let(:json_string) { '[{ "Hello": "World" }]' } let(:json_value) { JSON.load(json_string) } - context 'without TLS certificates' do - let(:excon_uri) { "http://#{hostname}:#{port}/" } - let(:api) { Centurion::DockerViaApi.new(hostname, port) } - + shared_examples "docker API" do it 'lists processes' do - expect(Excon).to receive(:get). - with(excon_uri + "v1.12" + "/containers/json", {}). - and_return(double(body: json_string, status: 200)) + Excon.stub(base_req.merge(method: :get, path: '/v1.12/containers/json'), {body: json_string, status: 200}) expect(api.ps).to eq(json_value) end it 'lists all processes' do - expect(Excon).to receive(:get). - with(excon_uri + "v1.12" + "/containers/json?all=1", {}). - and_return(double(body: json_string, status: 200)) + Excon.stub(base_req.merge(method: :get, path: '/v1.12/containers/json?all=1'), {body: json_string, status: 200}) expect(api.ps(all: true)).to eq(json_value) end it 'creates a container' do - configuration_as_json = double + configuration_as_json = 'body' configuration = double(to_json: configuration_as_json) - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/create", - query: nil, - body: configuration_as_json, - headers: {'Content-Type' => 'application/json'}). - and_return(double(body: json_string, status: 201)) + Excon.stub(base_req.merge( + method: :post, + path: '/v1.12/containers/create', + body: configuration_as_json, + headers: {'Content-Type' => 'application/json'} + ), + {body: json_string, status: 201}) api.create_container(configuration) end it 'creates a container with a name' do - configuration_as_json = double + configuration_as_json = 'body' configuration = double(to_json: configuration_as_json) - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/create", - query: { name: match(/^app1-[a-f0-9]+$/) }, - body: configuration_as_json, - headers: {'Content-Type' => 'application/json'}). - and_return(double(body: json_string, status: 201)) + Excon.stub(base_req.merge( + method: :post, + path: '/v1.12/containers/create', + query: /^name=app1-[a-f0-9]+$/, + body: configuration_as_json, + headers: {'Content-Type' => 'application/json'} + ), + {body: json_string, status: 201}) api.create_container(configuration, 'app1') end it 'starts a container' do - configuration_as_json = double + configuration_as_json = 'body' configuration = double(to_json: configuration_as_json) - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/12345/start", - body: configuration_as_json, - headers: {'Content-Type' => 'application/json'}). - and_return(double(body: json_string, status: 204)) + Excon.stub(base_req.merge( + method: :post, + path: '/v1.12/containers/12345/start', + body: configuration_as_json, + headers: {'Content-Type' => 'application/json'} + ), + {body: json_string, status: 204}) api.start_container('12345', configuration) end it 'stops a container' do - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/12345/stop?t=300", {read_timeout: 420}). - and_return(double(status: 204)) + Excon.stub(base_req.merge(method: :post, path: '/v1.12/containers/12345/stop?t=300', read_timeout: 420), {status: 204}) api.stop_container('12345', 300) end it 'stops a container with a custom timeout' do - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/12345/stop?t=30", {read_timeout: 150}). - and_return(double(status: 204)) + Excon.stub(base_req.merge(method: :post, path: '/v1.12/containers/12345/stop?t=30', read_timeout: 150), {status: 204}) api.stop_container('12345') end it 'restarts a container' do - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/12345/restart?t=30", - {read_timeout: 150}). - and_return(double(body: json_string, status: 204)) + Excon.stub(base_req.merge(method: :post, path: '/v1.12/containers/12345/restart?t=30', read_timeout: 150), {status: 204}) api.restart_container('12345') end it 'restarts a container with a custom timeout' do - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/12345/restart?t=300", {:read_timeout=>420}). - and_return(double(body: json_string, status: 204)) + Excon.stub(base_req.merge(method: :post, path: '/v1.12/containers/12345/restart?t=300', read_timeout: 420), {status: 204}) api.restart_container('12345', 300) end it 'inspects a container' do - expect(Excon).to receive(:get). - with(excon_uri + "v1.12" + "/containers/12345/json", {}). - and_return(double(body: json_string, status: 200)) + Excon.stub(base_req.merge(method: :get, path: '/v1.12/containers/12345/json'), {body: json_string, status: 200}) expect(api.inspect_container('12345')).to eq(json_value) end it 'removes a container' do - expect(Excon).to receive(:delete). - with(excon_uri + "v1.12" + "/containers/12345", {}). - and_return(double(status: 204)) + Excon.stub(base_req.merge(method: :delete, path: '/v1.12/containers/12345'), {body: json_string, status: 204}) expect(api.remove_container('12345')).to eq(true) end it 'inspects an image' do - expect(Excon).to receive(:get). - with(excon_uri + "v1.12" + "/images/foo:bar/json", - headers: {'Accept' => 'application/json'}). - and_return(double(body: json_string, status: 200)) + Excon.stub(base_req.merge(method: :get, path: '/v1.12/images/foo:bar/json', headers: {'Accept' => 'application/json'}), {body: json_string, status: 200}) expect(api.inspect_image('foo', 'bar')).to eq(json_value) end + end + + context 'without TLS certificates' do + let(:api) { Centurion::DockerViaApi.new(hostname, port) } + let(:base_req) { {hostname: hostname, port: port} } + it_behaves_like 'docker API' end context 'with TLS certificates' do - let(:excon_uri) { "https://#{hostname}:#{port}/" } let(:tls_args) { { tls: true, tlscacert: '/certs/ca.pem', tlscert: '/certs/cert.pem', tlskey: '/certs/key.pem' } } + let(:base_req) { { + hostname: hostname, + port: port, + client_cert: '/certs/cert.pem', + client_key: '/certs/key.pem', + } } let(:api) { Centurion::DockerViaApi.new(hostname, port, tls_args) } - it 'lists processes' do - expect(Excon).to receive(:get). - with(excon_uri + "v1.12" + "/containers/json", - client_cert: '/certs/cert.pem', - client_key: '/certs/key.pem'). - and_return(double(body: json_string, status: 200)) - expect(api.ps).to eq(json_value) - end - - it 'lists all processes' do - expect(Excon).to receive(:get). - with(excon_uri + "v1.12" + "/containers/json?all=1", - client_cert: '/certs/cert.pem', - client_key: '/certs/key.pem'). - and_return(double(body: json_string, status: 200)) - expect(api.ps(all: true)).to eq(json_value) - end + it_behaves_like 'docker API' + end - it 'inspects an image' do - expect(Excon).to receive(:get). - with(excon_uri + "v1.12" + "/images/foo:bar/json", - client_cert: '/certs/cert.pem', - client_key: '/certs/key.pem', - headers: {'Accept' => 'application/json'}). - and_return(double(body: json_string, status: 200)) - expect(api.inspect_image('foo', 'bar')).to eq(json_value) - end + context 'with default TLS certificates' do + let(:tls_args) { { tls: true } } + let(:base_req) { { + hostname: hostname, + port: port, + client_cert: File.expand_path('~/.docker/cert.pem'), + client_key: File.expand_path('~/.docker/key.pem'), + } } + let(:api) { Centurion::DockerViaApi.new(hostname, port, tls_args) } - it 'creates a container' do - configuration_as_json = double - configuration = double(to_json: configuration_as_json) - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/create", - client_cert: '/certs/cert.pem', - client_key: '/certs/key.pem', - query: nil, - body: configuration_as_json, - headers: {'Content-Type' => 'application/json'}). - and_return(double(body: json_string, status: 201)) - api.create_container(configuration) - end + it_behaves_like 'docker API' + end - it 'starts a container' do - configuration_as_json = double - configuration = double(to_json: configuration_as_json) - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/12345/start", - client_cert: '/certs/cert.pem', - client_key: '/certs/key.pem', - body: configuration_as_json, - headers: {'Content-Type' => 'application/json'}). - and_return(double(body: json_string, status: 204)) - api.start_container('12345', configuration) + context 'with a SSH connection' do + let(:hostname) { 'hostname' } + let(:port) { nil } + let(:ssh_user) { 'myuser' } + let(:ssh_log_level) { nil } + let(:base_req) { { + socket: '/tmp/socket/path' + } } + let(:api) { Centurion::DockerViaApi.new(hostname, port, params) } + let(:params) do + p = { ssh: true} + p[:ssh_user] = ssh_user if ssh_user + p[:ssh_log_level] = ssh_log_level if ssh_log_level + p end - it 'stops a container' do - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/12345/stop?t=300", - client_cert: '/certs/cert.pem', - client_key: '/certs/key.pem', - read_timeout: 420). - and_return(double(status: 204)) - api.stop_container('12345', 300) - end + context 'with no log level' do + before do + expect(Centurion::SSH).to receive(:with_docker_socket).with(hostname, ssh_user, nil).and_yield('/tmp/socket/path') + end - it 'stops a container with a custom timeout' do - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/12345/stop?t=30", - client_cert: '/certs/cert.pem', - client_key: '/certs/key.pem', - read_timeout: 150). - and_return(double(status: 204)) - api.stop_container('12345') + it_behaves_like 'docker API' end - it 'restarts a container' do - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/12345/restart?t=30", - client_cert: '/certs/cert.pem', - client_key: '/certs/key.pem', - read_timeout: 150). - and_return(double(body: json_string, status: 204)) - api.restart_container('12345') - end + context 'with no user' do + let(:ssh_user) { nil } - it 'restarts a container with a custom timeout' do - expect(Excon).to receive(:post). - with(excon_uri + "v1.12" + "/containers/12345/restart?t=300", - client_cert: '/certs/cert.pem', - client_key: '/certs/key.pem', - read_timeout: 420). - and_return(double(body: json_string, status: 204)) - api.restart_container('12345', 300) - end + before do + expect(Centurion::SSH).to receive(:with_docker_socket).with(hostname, nil, nil).and_yield('/tmp/socket/path') + end - it 'inspects a container' do - expect(Excon).to receive(:get). - with(excon_uri + "v1.12" + "/containers/12345/json", - client_cert: '/certs/cert.pem', - client_key: '/certs/key.pem'). - and_return(double(body: json_string, status: 200)) - expect(api.inspect_container('12345')).to eq(json_value) + it_behaves_like 'docker API' end - it 'removes a container' do - expect(Excon).to receive(:delete). - with(excon_uri + "v1.12" + "/containers/12345", - client_cert: '/certs/cert.pem', - client_key: '/certs/key.pem'). - and_return(double(status: 204)) - expect(api.remove_container('12345')).to eq(true) - end - end + context 'with a log level set' do + let(:ssh_log_level) { Logger::DEBUG } - context 'with default TLS certificates' do - let(:excon_uri) { "https://#{hostname}:#{port}/" } - let(:tls_args) { { tls: true } } - let(:api) { Centurion::DockerViaApi.new(hostname, port, tls_args) } + before do + expect(Centurion::SSH).to receive(:with_docker_socket).with(hostname, ssh_user, Logger::DEBUG).and_yield('/tmp/socket/path') + end - it 'lists processes' do - expect(Excon).to receive(:get). - with(excon_uri + "v1.12" + "/containers/json", - client_cert: File.expand_path('~/.docker/cert.pem'), - client_key: File.expand_path('~/.docker/key.pem')). - and_return(double(body: json_string, status: 200)) - expect(api.ps).to eq(json_value) + it_behaves_like 'docker API' end end end diff --git a/spec/docker_via_cli_spec.rb b/spec/docker_via_cli_spec.rb index 93f5a71a..1a5632cd 100644 --- a/spec/docker_via_cli_spec.rb +++ b/spec/docker_via_cli_spec.rb @@ -4,27 +4,33 @@ describe Centurion::DockerViaCli do let(:docker_path) { 'docker' } - context 'without TLS certificates' do - let(:docker_via_cli) { Centurion::DockerViaCli.new('host1', 2375, docker_path) } + shared_examples 'docker CLI' do it 'pulls the latest image given its name' do expect(Centurion::Shell).to receive(:echo). - with("docker -H=tcp://host1:2375 pull foo:latest") + with("docker #{prefix} pull foo:latest") docker_via_cli.pull('foo') end it 'pulls an image given its name & tag' do expect(Centurion::Shell).to receive(:echo). - with("docker -H=tcp://host1:2375 pull foo:bar") + with("docker #{prefix} pull foo:bar") docker_via_cli.pull('foo', 'bar') end it 'tails logs on a container' do id = '12345abcdef' expect(Centurion::Shell).to receive(:echo). - with("docker -H=tcp://host1:2375 logs -f #{id}") + with("docker #{prefix} logs -f #{id}") docker_via_cli.tail(id) end + it 'attach to a container' do + id = '12345abcdef' + expect(Centurion::Shell).to receive(:echo). + with("docker #{prefix} attach #{id}") + docker_via_cli.attach(id) + end + it 'should print all chars when one thread is running' do expect(Centurion::Shell).to receive(:run_with_echo) @@ -41,51 +47,63 @@ docker_via_cli.pull('foo') end end + + context 'without TLS certificates' do + let(:docker_via_cli) { Centurion::DockerViaCli.new('host1', 2375, docker_path) } + let(:prefix) { "-H=tcp://host1:2375" } + + it_behaves_like 'docker CLI' + end + context 'with TLS certificates' do let(:tls_args) { { tls: true, tlscacert: '/certs/ca.pem', tlscert: '/certs/cert.pem', tlskey: '/certs/key.pem' } } let(:docker_via_cli) { Centurion::DockerViaCli.new('host1', 2375, docker_path, tls_args) } - it 'pulls the latest image given its name' do - expect(Centurion::Shell).to receive(:echo). - with('docker -H=tcp://host1:2375 ' \ - '--tlsverify ' \ - '--tlscacert=/certs/ca.pem ' \ - '--tlscert=/certs/cert.pem ' \ - '--tlskey=/certs/key.pem pull foo:latest') - docker_via_cli.pull('foo') + let(:prefix) { "-H=tcp://host1:2375 --tlsverify --tlscacert=/certs/ca.pem --tlscert=/certs/cert.pem --tlskey=/certs/key.pem" } + + it_behaves_like 'docker CLI' + end + + context 'with a SSH connection' do + let(:hostname) { 'host1' } + let(:ssh_user) { 'myuser' } + let(:ssh_log_level) { nil } + let(:docker_via_cli) { Centurion::DockerViaCli.new(hostname, nil, docker_path, params) } + let(:prefix) { "-H=unix:///tmp/socket/path" } + let(:params) do + p = { ssh: true} + p[:ssh_user] = ssh_user if ssh_user + p[:ssh_log_level] = ssh_log_level if ssh_log_level + p end - it 'pulls an image given its name & tag' do - expect(Centurion::Shell).to receive(:echo). - with('docker -H=tcp://host1:2375 ' \ - '--tlsverify ' \ - '--tlscacert=/certs/ca.pem ' \ - '--tlscert=/certs/cert.pem ' \ - '--tlskey=/certs/key.pem pull foo:bar') - docker_via_cli.pull('foo', 'bar') + context 'with no log level' do + before do + expect(Centurion::SSH).to receive(:with_docker_socket).with(hostname, ssh_user, nil).and_yield('/tmp/socket/path') + end + + it_behaves_like 'docker CLI' end - it 'tails logs on a container' do - id = '12345abcdef' - expect(Centurion::Shell).to receive(:echo). - with('docker -H=tcp://host1:2375 ' \ - '--tlsverify ' \ - '--tlscacert=/certs/ca.pem ' \ - '--tlscert=/certs/cert.pem ' \ - "--tlskey=/certs/key.pem logs -f #{id}") - docker_via_cli.tail(id) + context 'with no user' do + let(:ssh_user) { nil } + + before do + expect(Centurion::SSH).to receive(:with_docker_socket).with(hostname, nil, nil).and_yield('/tmp/socket/path') + end + + it_behaves_like 'docker CLI' end - it 'attach to a container' do - id = '12345abcdef' - expect(Centurion::Shell).to receive(:echo). - with('docker -H=tcp://host1:2375 ' \ - '--tlsverify ' \ - '--tlscacert=/certs/ca.pem ' \ - '--tlscert=/certs/cert.pem ' \ - "--tlskey=/certs/key.pem attach #{id}") - docker_via_cli.attach(id) + context 'with a log level set' do + let(:ssh_log_level) { Logger::DEBUG } + + before do + expect(Centurion::SSH).to receive(:with_docker_socket).with(hostname, ssh_user, Logger::DEBUG).and_yield('/tmp/socket/path') + end + + it_behaves_like 'docker CLI' end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 50b499bd..0bbccfcd 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,5 +3,18 @@ add_filter '/spec' end +require 'excon' + +RSpec.configure do |config| + # Mock by default + config.before(:all) do + Excon.defaults[:mock] = true + end + + config.after(:each) do + Excon.stubs.clear + end +end + $: << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) Dir[File.dirname(__FILE__) + '/support/**/*.rb'].each {|f| require f} From e8fd5081c4c72139223a0b50a147fd5c6ec8da8d Mon Sep 17 00:00:00 2001 From: Andrew Bloomgarden Date: Thu, 26 Oct 2017 09:42:55 -0400 Subject: [PATCH 2/2] Bump version to 1.9.0 --- lib/centurion/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/centurion/version.rb b/lib/centurion/version.rb index 4d0d2cfa..9f179c00 100644 --- a/lib/centurion/version.rb +++ b/lib/centurion/version.rb @@ -1,3 +1,3 @@ module Centurion - VERSION = '1.8.10' + VERSION = '1.9.0' end