Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docker transport #330

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .kitchen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ suites:
cap_drop:
- NET_ADMIN
- name: inspec
excludes: [opensuse-42.2]
driver:
provision_command: true
verifier:
name: inspec
- name: transport
transport:
name: docker
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,20 @@ Examples:
use_internal_docker_network: true
```

## Docker transport

This plugin also provides a docker transport that uses docker native api instead
of ssh to run commands and upload files to the containers.

Examples:

```yaml
transport:
name: docker
shell: /bin/bash
socket: tcp://docker.example.com:4242
```

## Development

* Source hosted at [GitHub][repo]
Expand Down
87 changes: 87 additions & 0 deletions lib/kitchen/transport/docker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# -*- encoding: utf-8 -*-
#
# Author:: Rene Martin (<rene_martin@intuit.com>)
#
# Copyright (C) 2019, Rene Martin
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'kitchen'
require 'docker'

module Kitchen
module Transport

# Wrapped exception for any internally raised errors.
class DockerExecFailed < TransportFailed; end

# Docker transport for Kitchen. This transport uses the docker api to
# copy and run commands in the running container.
class Docker < Kitchen::Transport::Base
kitchen_transport_api_version 1

plugin_version Kitchen::VERSION

default_config :socket, ENV['DOCKER_HOST'] || 'unix:///var/run/docker.sock'
default_config :shell , '/bin/sh'

def connection(state, &block)
options = config.to_hash.merge(state)
Kitchen::Transport::Docker::Connection.new(options, &block)
end

class Connection < Kitchen::Transport::Base::Connection
def initialize(opts)
@opts = opts
super
end

def docker_connection
@docker_connection ||= ::Docker::Connection.new(@opts[:socket], {})
end

def execute(command)
return if command.nil?

@runner = ::Docker::Container.get(@opts[:container_id], {}, docker_connection)
o = @runner.exec([@opts[:shell], '-c', command], wait: 600, 'e' => { 'TERM' => 'xterm' }) { |_stream, chunk| print chunk.to_s }
@exit_code = o[2]

raise Transport::DockerExecFailed.new("Docker Exec (#{@exit_code}) for command: [#{command}]", @exit_code) if @exit_code != 0
end

def upload(locals, remote)
@runner = ::Docker::Container.get(@opts[:container_id], {}, docker_connection)
Array(locals).each do |local|
full_remote = File.join(remote, File.basename(local))
# Workarround for archive_in bug https://github.com/swipely/docker-api/issues/359
if File.directory? local
tarball = ::Docker::Util.create_dir_tar(local)
@runner.exec(['mkdir', full_remote])
@runner.archive_in_stream(full_remote, overwrite: true) { tarball.read(Excon.defaults[:chunk_size]).to_s }
else
@runner.archive_in([local], File.dirname(full_remote))
end
end
end

def login_command
cols = `tput cols`
lines = `tput lines`
args = ['exec', '-e', "COLUMNS=#{cols}", '-e', "LINES=#{lines}", '-it', @opts[:container_id], @opts[:shell], '-login', '-i']
LoginCommand.new('docker', args)
end
end
end
end
end
1 change: 1 addition & 0 deletions test/spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
end

require 'kitchen/driver/docker'
require 'kitchen/transport/docker'

RSpec.configure do |config|
# Basic configuraiton
Expand Down
122 changes: 122 additions & 0 deletions test/spec/transport_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#
# Copyright 2019, Rene Martin
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'spec_helper'

describe Kitchen::Transport::Docker do
let(:config) { {} }
let(:state) { {} }
let(:transport) { ::Kitchen::Transport::Docker.new(config) }
let(:connection) { transport.connection(state) }
let(:container) { double }

describe '#connection' do
context 'Default context' do
it 'returns an instance of Kitchen::Transport::Docker::Connection' do
conn = transport.connection({})
expect(conn).to be_an_instance_of(Kitchen::Transport::Docker::Connection)
end
end

context '#execute' do
it 'Creates a connection to the docker service' do
docker_con = double
allow(::Docker::Connection).to receive(:new).and_return docker_con
expect(connection.docker_connection).to eq docker_con
end

it 'Returns same connection when called the second time' do
docker_con = double
allow(::Docker::Connection).to receive(:new).and_return docker_con
expect(connection.docker_connection).to eq docker_con
allow(::Docker::Connection).to receive(:new).and_raise('Error!!!!')
expect(connection.docker_connection).to eq docker_con
end

it 'Applies the right configuration' do
expect(true).to eq true
end
end

context '#execute' do
let(:state) { {:container_id => 'container_sha' } }

it 'does nothing when there is no command' do
result = connection.execute(nil)
expect(result).to be nil
expect(::Docker::Container).not_to receive(:get)
end

it 'Runs a sh shell in the container to run the command' do
allow(::Docker::Container).to receive(:get).with('container_sha', {}, anything).and_return container
expect(container).to receive(:exec).with(['/bin/sh', '-c', 'ls -l /'], wait: 600, 'e' => { 'TERM' => 'xterm' }).and_return([nil, nil, 0])
connection.execute('ls -l /')
end


it 'Raise a TransportFailed execption if the command errors out' do
allow(::Docker::Container).to receive(:get).with('container_sha', {}, anything).and_return container
expect(container).to receive(:exec).with(['/bin/sh', '-c', 'ls -l /non_existing_folder'], wait: 600, 'e' => { 'TERM' => 'xterm' }).and_return([nil, nil, 255])
expect { connection.execute('ls -l /non_existing_folder') }.to raise_error(::Kitchen::Transport::TransportFailed)
end
end

context '#execute shell override' do
let(:state) { {:container_id => 'container_sha' } }
let(:config) { {:shell => '/bin/bash'} }
it 'Runs a bash shell when the default value is overriten' do
allow(::Docker::Container).to receive(:get).with('container_sha', {}, anything).and_return container
allow(container).to receive(:exec).with(['/bin/bash', '-c', 'ls -l /'], wait: 600, 'e' => { 'TERM' => 'xterm' }).and_return([nil, nil, 0])
connection.execute('ls -l /')
end
end

context '#upload' do
let(:state) { {:container_id => 'container_sha' } }

before do
allow(::Docker::Container).to receive(:get).with('container_sha', {}, anything).and_return container
allow(::File).to receive(:directory?).and_call_original
end

it 'Uploads a file' do
allow(::File).to receive(:directory?).with('file.txt').and_return false
expect(container).to receive(:archive_in).with(['file.txt'], '/tmp/kitchen/file.txt')
connection.upload(['file.txt'], '/tmp/kitchen/file.txt')
end

it 'Uploads a directory' do
allow(::File).to receive(:directory?).with('dir1').and_return true
tarball = double
expect(::Docker::Util).to receive(:create_dir_tar).with('dir1').and_return tarball
expect(container).to receive(:exec).with(['mkdir', '/tmp/kitchen/dir1'])
expect(container).to receive(:archive_in_stream).with('/tmp/kitchen/dir1', overwrite: true)
connection.upload(['dir1'], '/tmp/kitchen/')
end
end

context '#login_command' do
let(:state) { {:container_id => 'container_sha' } }
it 'Returns a nice docker login command' do
expect(Kitchen::LoginCommand).to receive(:new)
.with('docker', ['exec', '-e', "COLUMNS=#{`tput cols`}", '-e', "LINES=#{`tput lines`}", '-it', 'container_sha', '/bin/sh', '-login', '-i'])
.and_call_original
cmd = connection.login_command
expect(cmd).to be_an_instance_of(::Kitchen::LoginCommand)
end
end
end
end