sshkit-backend-docker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 77ced5c48d6681caf49f45a90b2318c9c15429d2
4
+ data.tar.gz: 6f905759e8df50cfed6717080e0778bea612aaf9
5
+ SHA512:
6
+ metadata.gz: 0f809f383355a4623b37903160b4e8e6e979f0d43cd9e3676a8e70ffddbbe143f4e635f7d67fda0032d82cae6d328b4b463b940ddf232c9368c687ee9504cf92
7
+ data.tar.gz: 6840d0e8e3e07d526a2f214d70b2662fbc9ad935141b0965ffec3923e7b60a07949f878868f37562708a1052d268ecb030d39b883ba5883adf36b772841ac81f
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2016 Tatsuki Sugiura
2
+
3
+ permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # SSHKit::Backend::Docker
2
+
3
+ Docker connector backend for SSHKit.
4
+ You can execute commands inside docker container without ssh connection.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'sshkit-backend-docker'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install sshkit-backend-docker
21
+
22
+ ## Usage
23
+
24
+ **If you use capistrano, please refer [capistrano-deploy_into_docker](https://github.com/sugi/capistrano-deploy_into_docker).**
25
+
26
+ Currently, you need to set `SSHKit.config.backend` to run command with docker backend.
27
+
28
+ ```ruby
29
+ SSHKit.config.backend = SSHKit::Backend::Docker
30
+ ```
31
+
32
+ ### Host definitions
33
+
34
+ `SSHKit::Backend::Docker` extends `SSHKit::Host`.
35
+ You can specify docker environemnt as Hash. The hash requires `:image` (or :container) key to run.
36
+
37
+ ```ruby
38
+ host = SSHKit::Host.new(docker: {image: 'ruby:slim'})
39
+ ```
40
+
41
+ In addtion, you can add any options for "docker run" via docker_run_image. for example;
42
+
43
+ ```ruby
44
+ host = SSHKit::Host.new(docker: {
45
+ image: 'sugi/rails-base:latest',
46
+ commit: 'new-image-name:tag',
47
+ volume: ['/storage/tmp:/tmp', '/storage/home:/home'],
48
+ network: 'my-net',
49
+ dns: '8.8.8.8',
50
+ dns_search: 'example.com',
51
+ cap_add: ['SYS_NICE', 'SYS_RESOURCE'],
52
+ }, user: 'nobody:nogroup')
53
+ ```
54
+
55
+ ### DSL
56
+
57
+ * `docker_commit` - Commit running container as image. In default `:commit` key of host will be used. You can pass argument to override. Currently this DSL method can commit container *which is ran by SSHKit::Backend::Docker from image*.
58
+ * `docker_run_image` - In useal case, Docker container will start automatically when execute command. However you can run any image intentionally by `docker_run_iamge`.
59
+
60
+ ### Examples
61
+
62
+ ```ruby
63
+ require 'sshkit'
64
+ require 'sshkit-backend-docker'
65
+ include SSHKit::DSL
66
+
67
+ SSHKit.config.backend = SSHKit::Backend::Docker
68
+
69
+ host = {docker: {image: 'ruby:slim'}}
70
+
71
+ on [host] do
72
+ execute :ls
73
+ upload! '/etc/passwd', '/etc/passwd'
74
+ docker_commit 'my-new-image'
75
+ end
76
+ ```
77
+
78
+ ## Contributing
79
+
80
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sugi/sshkit-backend-docker.
81
+
82
+ ## Copyright
83
+
84
+ 2016 Tatsuki Sugiura, this library is distributed [MIT License](https://opensource.org/licenses/MIT).
85
+
@@ -0,0 +1,7 @@
1
+ module Sshkit
2
+ module Backend
3
+ module Docker
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,281 @@
1
+ require 'open3'
2
+ require 'fileutils'
3
+ require 'json'
4
+ require 'sshkit'
5
+ require 'sshkit/backend/docker/version'
6
+ require 'sshkit/docker_host_ext'
7
+
8
+ module SSHKit
9
+ module Backend
10
+ class Docker < Abstract
11
+ class Configuration
12
+ attr_accessor :pty, :use_sudo
13
+ end
14
+
15
+ CONTAINER_MAP = {}
16
+ CONTAINER_WAIT_IO = {}
17
+ attr_accessor :docker_open_stdin
18
+
19
+ def self.host_container_map_key(host)
20
+ key = host.docker_options.dup || {}
21
+ key.delete :container
22
+ key.delete :commit
23
+ key
24
+ end
25
+
26
+ def self.find_cntainer_by_host(host)
27
+ host.docker_options[:container] ||
28
+ CONTAINER_MAP[host_container_map_key(host)]
29
+ end
30
+
31
+ def initialize(host, &block)
32
+ super
33
+ @container = nil
34
+ end
35
+
36
+ def container
37
+ @container and return @container
38
+ if host.docker_options[:container]
39
+ @container = host.docker_options[:container]
40
+ else
41
+ @container = docker_run_image
42
+ host.hostname.chop! << ", container: #{@container})"
43
+ end
44
+ @container
45
+ end
46
+
47
+ def upload!(local, remote, _options = {})
48
+ local_io = local
49
+ local_io.is_a?(String) and
50
+ local_io = File.open(local_io, 'rb')
51
+ @docker_open_stdin = true
52
+
53
+ with_pty(false) do
54
+ IO.popen(to_docker_cmd('sh', '-c', "cat > '#{remote}'"), 'wb') do |f|
55
+ IO.copy_stream(local_io, f)
56
+ end
57
+ end
58
+
59
+ @docker_open_stdin = false
60
+ local.is_a?(String) and
61
+ local_io.close
62
+ end
63
+
64
+ def download!(remote, local=nil, _options = {})
65
+ local_io = local
66
+ local_io.nil? and
67
+ local_io = File.basename(remote)
68
+ local_io.is_a?(String) and
69
+ local_io = File.open(local_io, 'wb')
70
+
71
+ with_pty(false) do
72
+ IO.popen(to_docker_cmd('cat', remote), 'rb') do |f|
73
+ IO.copy_stream(f, local_io)
74
+ end
75
+ end
76
+
77
+ local.nil? || local.is_a?(String) and
78
+ local_io.close
79
+ end
80
+
81
+ def merged_env
82
+ menv = (SSHKit.config.default_env || {}).dup.symbolize_keys
83
+ [*host.docker_options[:env_file]].each do |ef|
84
+ File.foreach(ef) do |line|
85
+ line.include? "=" or next
86
+ key, val = line.chomp.split('=', 2)
87
+ menv[key.strip.to_sym] = val.strip
88
+ end
89
+ end
90
+ if host.docker_options[:env].is_a?(Hash)
91
+ host.docker_options[:env].each do |key, val|
92
+ menv[key.to_sym] = val
93
+ end
94
+ else
95
+ [*host.docker_options[:env]].each do |e|
96
+ key, val = e.split('=', 2)
97
+ menv[key.strip.to_sym] = val.strip
98
+ end
99
+ end
100
+ if menv[:rails_env]
101
+ menv[:RAILS_ENV] = menv.delete(:rails_env)
102
+ end
103
+ menv
104
+ end
105
+
106
+ def docker_run_image(host = nil)
107
+ host ||= self.host
108
+
109
+ if host.is_a?(String)
110
+ image_name = host
111
+ host = _deep_dup(self.host)
112
+ host.docker_options[:image] = image_name
113
+ elsif host.is_a?(Hash)
114
+ d_opts = host
115
+ host = _deep_dup(self.host)
116
+ host.docker_options = d_opts.symbolize_keys
117
+ end
118
+
119
+ map_key = self.class.host_container_map_key(host)
120
+ CONTAINER_MAP[map_key] and
121
+ return CONTAINER_MAP[map_key]
122
+
123
+ image_name = host.docker_options[:image]
124
+ cmd = %w(docker run -i)
125
+ host.docker_options.each do |key, val|
126
+ %w(container image env env_file commit).member?(key.to_s) and next
127
+ if %w(t tty h hostname attach d detach entrypoint rm).member?(key.to_s)
128
+ output.warn "Docker: run option '#{key}' is filtered."
129
+ next
130
+ end
131
+ [*val].each do |v|
132
+ cmd << "--#{key.to_s.tr('_', '-')}" << v
133
+ end
134
+ end
135
+ merged_env.each do |key, val|
136
+ cmd << "-e" << "#{key}=#{val}"
137
+ end
138
+ cmd += [image_name, 'sh', '-c', "# SSHkit \n hostname; read _"]
139
+ cmd.unshift('sudo') if Docker.config.use_sudo
140
+ io = IO.popen cmd, 'r+b'
141
+ cid = io.gets
142
+ if cid.nil?
143
+ output.fatal "Docker: Failed to run image #{image_name}"
144
+ raise "Failed to get container ID! (cmd: #{cmd.inspect})"
145
+ end
146
+ at_exit { io.close }
147
+ CONTAINER_WAIT_IO[map_key] = io
148
+ CONTAINER_MAP[map_key] = cid.strip
149
+ output.info "Docker: run new container #{CONTAINER_MAP[map_key]} from image #{image_name}"
150
+ CONTAINER_MAP[map_key]
151
+ end
152
+
153
+ def docker_commit(host = nil)
154
+ host ||= self.host
155
+
156
+ if host.is_a?(String)
157
+ commit_id = host
158
+ host = _deep_dup(self.host)
159
+ host.docker_options[:commit] = commit_id
160
+ elsif host.is_a?(Hash)
161
+ commit_info = host
162
+ host = _deep_dup(self.host)
163
+ host.docker_options[:commit] ||= {}
164
+ host.docker_options[:commit].update commit_info.symbolize_keys
165
+ end
166
+
167
+ host.docker_options[:commit] or return
168
+
169
+ container = self.class.find_cntainer_by_host(host) or
170
+ raise "Cannot find container for host #{host.inspect}"
171
+
172
+ cmd = %w(docker commit)
173
+
174
+ if host.docker_options[:image]
175
+ # if container is delivered from image, recover USER and CMD.
176
+ image_config = {}
177
+ IO.popen ['docker', 'inspect', '-f', '{{json .Config}}', host.docker_options[:image]], 'rb' do |f|
178
+ image_config = JSON.parse(f.read.chomp)
179
+ end
180
+
181
+ if image_config["User"].to_s.length > 1
182
+ cmd << '-c' << "USER #{image_config["User"]}"
183
+ end
184
+
185
+ c = image_config["Cmd"]
186
+ if c[0] == "/bin/sh" && c[1] == "-c"
187
+ c.shift; c.shift;
188
+ end
189
+ unless c.empty?
190
+ cmd << '-c' << "CMD #{c.join(' ')}"
191
+ end
192
+ end
193
+
194
+ image_name = host.docker_options[:commit]
195
+ if image_name.is_a?(Hash)
196
+ image_name.symbolize_keys.each do |key, val|
197
+ if key == :name
198
+ image_name = val
199
+ else
200
+ [*val].each do |v|
201
+ cmd << "--#{key.to_s.tr('_', '-')}" << v
202
+ end
203
+ end
204
+ end
205
+ end
206
+ cmd << container
207
+ image_name == true or
208
+ cmd << image_name
209
+
210
+ image_hash = nil
211
+ pid = nil
212
+ IO.popen cmd, 'rb' do |f|
213
+ pid = f.pid
214
+ image_hash = f.gets
215
+ end
216
+ image_hash.nil? and
217
+ output.error "Docker: Failed to get image hash. Commit may be failed!"
218
+ image_hash.chomp!
219
+ ret = image_name == true ? image_hash : image_name
220
+ output.info "Docker: commit #{container} as #{ret}"
221
+ ret
222
+ end
223
+
224
+ def to_docker_cmd(*args)
225
+ cmd = %w(docker exec)
226
+ cmd << '-it' if Docker.config.pty
227
+ cmd << '-i' if docker_open_stdin
228
+ cmd << '-u' << host.username
229
+ cmd << container
230
+ cmd += args
231
+ cmd.unshift('sudo') if Docker.config.use_sudo
232
+ cmd
233
+ end
234
+
235
+ private
236
+ def with_pty(flag)
237
+ orig = Docker.config.pty
238
+ Docker.config.pty = flag
239
+ begin
240
+ yield
241
+ ensure
242
+ Docker.config.pty = orig
243
+ end
244
+ end
245
+
246
+ def execute_command(cmd)
247
+ output.log_command_start(cmd)
248
+
249
+ cmd.started = Time.now
250
+
251
+ Open3.popen3(*to_docker_cmd('sh', '-c', cmd.to_command)) do |stdin, stdout, stderr, wait_thr|
252
+ stdout_thread = Thread.new do
253
+ while (line = stdout.gets) do
254
+ cmd.on_stdout(stdin, line)
255
+ output.log_command_data(cmd, :stdout, line)
256
+ end
257
+ end
258
+
259
+ stderr_thread = Thread.new do
260
+ while (line = stderr.gets) do
261
+ cmd.on_stderr(stdin, line)
262
+ output.log_command_data(cmd, :stderr, line)
263
+ end
264
+ end
265
+
266
+ stdout_thread.join
267
+ stderr_thread.join
268
+
269
+ cmd.exit_status = wait_thr.value.to_i
270
+
271
+ output.log_command_exit(cmd)
272
+ end
273
+ end
274
+
275
+ def _deep_dup(obj)
276
+ Marshal.load Marshal.dump(obj)
277
+ end
278
+
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,27 @@
1
+ require 'sshkit'
2
+ module SSHKit
3
+ class Host
4
+ attr_writer :docker_options
5
+
6
+ def docker?
7
+ !docker_options.empty?
8
+ end
9
+
10
+ def docker_options
11
+ @docker_options ||= {}
12
+ end
13
+
14
+ def docker=(hash)
15
+ @hostname = "(docker "
16
+ @user ||= 'root'
17
+ docker_options.update hash.symbolize_keys
18
+ if docker_options.has_key?(:image)
19
+ @hostname << "image: #{@docker_options[:image]})"
20
+ elsif @docker_options.has_key?(:container)
21
+ @hostname << "container: #{@docker_options[:container]})"
22
+ else
23
+ raise ArgumentError, "Please specify image or container for docker! (ex; docker: {image: 'ruby:2.2'})"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1 @@
1
+ require 'sshkit/backend/docker'
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sshkit-backend-docker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tatsuki Sugiura
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-09-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sshkit
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.9.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.9.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.12'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '11.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '11.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest-reporters
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: sshkit-backend-docker provides Docker connection and some utilities.
84
+ You can execute commands inside docker container without ssh connection.
85
+ email:
86
+ - sugi@nemui.org
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - LICENSE
92
+ - README.md
93
+ - lib/sshkit-backend-docker.rb
94
+ - lib/sshkit/backend/docker.rb
95
+ - lib/sshkit/backend/docker/version.rb
96
+ - lib/sshkit/docker_host_ext.rb
97
+ homepage: http://github.com/sugi/sshki-backend-docker
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.5.1
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Docker connector backend for SSHKit
121
+ test_files: []