sshkit-backend-docker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []