-
-
Notifications
You must be signed in to change notification settings - Fork 285
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Remove dependency on docker-compose gem; support Ruby 3.x - reroll of #…
…795 (#803) This is just the reroll of all the work of #795 since i cannot reach the initial author but would love to incorporate his work. Co-authored-by: takahashim <takahashimm@gmail.com>
- Loading branch information
1 parent
02a633b
commit 60be788
Showing
9 changed files
with
306 additions
and
9 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
begin | ||
require 'pty' | ||
rescue LoadError | ||
# for Windows support, tolerate a missing PTY module | ||
end | ||
|
||
module DockerSync | ||
# based on `Backticks::Command` from `Backticks` gem | ||
class Command | ||
FOREVER = 86_400 * 365 | ||
CHUNK = 1_024 | ||
|
||
# @return [Integer] child process ID | ||
attr_reader :pid | ||
|
||
# @return [nil,Process::Status] result of command if it has ended; nil if still running | ||
attr_reader :status | ||
|
||
# @return [String] all input that has been captured so far | ||
attr_reader :captured_input | ||
|
||
# @return [String] all output that has been captured so far | ||
attr_reader :captured_output | ||
|
||
# @return [String] all output to stderr that has been captured so far | ||
attr_reader :captured_error | ||
|
||
# Run a command. The parameters are same as `Kernel#spawn`. | ||
# | ||
# Usage: | ||
# run('docker-compose', '--file=joe.yml', 'up', '-d', 'mysvc') | ||
def self.run(*argv, dir: nil) | ||
nopty = !defined?(PTY) | ||
|
||
stdin_r, stdin = nopty ? IO.pipe : PTY.open | ||
stdout, stdout_w = nopty ? IO.pipe : PTY.open | ||
stderr, stderr_w = IO.pipe | ||
|
||
chdir = dir || Dir.pwd | ||
pid = spawn(*argv, in: stdin_r, out: stdout_w, err: stderr_w, chdir: chdir) | ||
|
||
stdin_r.close | ||
stdout_w.close | ||
stderr_w.close | ||
|
||
self.new(pid, stdin, stdout, stderr) | ||
end | ||
|
||
def initialize(pid, stdin, stdout, stderr) | ||
@pid = pid | ||
@stdin = stdin | ||
@stdout = stdout | ||
@stderr = stderr | ||
@status = nil | ||
|
||
@captured_input = String.new(encoding: Encoding::BINARY) | ||
@captured_output = String.new(encoding: Encoding::BINARY) | ||
@captured_error = String.new(encoding: Encoding::BINARY) | ||
end | ||
|
||
def success? | ||
status.success? | ||
end | ||
|
||
def join(limit = FOREVER) | ||
return self if @status | ||
|
||
tf = Time.now + limit | ||
until (t = Time.now) >= tf | ||
capture(tf - t) | ||
res = Process.waitpid(@pid, Process::WNOHANG) | ||
if res | ||
@status = $? | ||
return self | ||
end | ||
end | ||
|
||
nil | ||
end | ||
|
||
private | ||
|
||
def capture(limit) | ||
streams = [@stdout, @stderr] | ||
streams << STDIN if STDIN.tty? | ||
|
||
ready, = IO.select(streams, [], [], limit) | ||
|
||
# proxy STDIN to child's stdin | ||
if ready && ready.include?(STDIN) | ||
data = STDIN.readpartial(CHUNK) rescue nil | ||
if data | ||
@captured_input << data | ||
@stdin.write(data) | ||
else | ||
# our own STDIN got closed; proxy this fact to the child | ||
@stdin.close unless @stdin.closed? | ||
end | ||
end | ||
|
||
# capture child's stdout and maybe proxy to STDOUT | ||
if ready && ready.include?(@stdout) | ||
data = @stdout.readpartial(CHUNK) rescue nil | ||
if data | ||
@captured_output << data | ||
STDOUT.write(data) | ||
fresh_output = data | ||
end | ||
end | ||
|
||
# capture child's stderr and maybe proxy to STDERR | ||
if ready && ready.include?(@stderr) | ||
data = @stderr.readpartial(CHUNK) rescue nil | ||
if data | ||
@captured_error << data | ||
STDERR.write(data) | ||
end | ||
end | ||
fresh_output | ||
rescue Interrupt | ||
# Proxy Ctrl+C to the child | ||
Process.kill('INT', @pid) rescue nil | ||
raise | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
module DockerSync | ||
# based on `Docker::Compose::Compose` from `docker-compose` gem | ||
class DockerComposeSession | ||
def initialize(dir: nil, files: nil) | ||
@dir = dir | ||
@files = files || [] # Array[String] | ||
@last_command = nil | ||
end | ||
|
||
def up(build: false) | ||
args = [] | ||
args << '--build' if build | ||
|
||
run!('up', *args) | ||
end | ||
|
||
def stop | ||
run!('stop') | ||
end | ||
|
||
def down | ||
run!('down') | ||
end | ||
|
||
private | ||
|
||
def run!(*args) | ||
# file_args and args should be Array of String | ||
file_args = @files.map { |file| "--file=#{file}" } | ||
|
||
@last_command = Command.run('docker-compose', *file_args, *args, dir: @dir).join | ||
status = @last_command.status | ||
out = @last_command.captured_output | ||
err = @last_command.captured_error | ||
unless status.success? | ||
desc = (out + err).strip.lines.first || '(no output)' | ||
message = format("'%s' failed with status %s: %s", args.first, status.to_s, desc) | ||
raise message | ||
end | ||
|
||
out | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
require 'spec_helper' | ||
require 'docker-sync/command' | ||
|
||
RSpec.describe DockerSync::Command do | ||
# actual subprocess invocations are mocked out. | ||
describe '.run' do | ||
let(:pid) { 123 } | ||
%i[master slave reader writer].each do |fd| | ||
let(fd) { double(fd, close: true) } | ||
end | ||
|
||
before do | ||
# Use fake PTY to avoid MacOS resource exhaustion | ||
allow(PTY).to receive(:open).and_return([master, slave]) | ||
allow(IO).to receive(:pipe).and_return([reader, writer]) | ||
allow(described_class).to receive(:spawn).and_return(pid) | ||
end | ||
|
||
it 'spawns with new pwd with :dir option' do | ||
expect(described_class).to receive(:spawn).with('ls', hash_including(chdir: '/tmp/banana')) | ||
described_class.run('ls', dir: '/tmp/banana') | ||
end | ||
|
||
it 'spawns with PWD without :dir option' do | ||
expect(described_class).to receive(:spawn).with('ls', hash_including(chdir: Dir.pwd)) | ||
described_class.run('ls') | ||
end | ||
|
||
it 'works when interactive' do | ||
expect(PTY).to receive(:open).twice | ||
expect(IO).to receive(:pipe).once | ||
expect(described_class).to receive(:spawn) | ||
cmd = described_class.run('ls') | ||
expect(cmd.pid).to eq pid | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
require 'spec_helper' | ||
require 'docker-sync/docker_compose_session' | ||
require 'docker-sync/command' | ||
|
||
RSpec.describe DockerSync::DockerComposeSession do | ||
let(:exitstatus) { 0 } | ||
let(:status) { double('exit status', to_s: "pid 12345 exit #{exitstatus}", to_i: exitstatus) } | ||
let(:output) { 'exit output' } | ||
let(:command) do | ||
double('command', | ||
status: status, | ||
captured_output: output, | ||
captured_error: '') | ||
end | ||
|
||
before do | ||
allow(status).to receive(:success?).and_return(exitstatus == 0) | ||
allow(DockerSync::Command).to receive(:run).and_return(command) | ||
allow(command).to receive(:join).and_return(command) | ||
end | ||
|
||
describe '.new' do | ||
it 'allows file override' do | ||
session = DockerSync::DockerComposeSession.new(files: ['foo.yml']) | ||
expect(DockerSync::Command).to receive(:run).with('docker-compose', '--file=foo.yml', 'up', dir: nil) | ||
session.up | ||
end | ||
|
||
it 'allows more files override' do | ||
session = DockerSync::DockerComposeSession.new(files: ['foo.yml', 'bar.yml', 'buz.yml']) | ||
expect(DockerSync::Command).to receive(:run).with('docker-compose', | ||
'--file=foo.yml', | ||
'--file=bar.yml', | ||
'--file=buz.yml', | ||
'up', | ||
dir: nil) | ||
session.up | ||
end | ||
|
||
it 'allows file and directory override' do | ||
session = DockerSync::DockerComposeSession.new(files: ['foo.yml'], dir: './tmp') | ||
expect(DockerSync::Command).to receive(:run).with('docker-compose', '--file=foo.yml', 'up', dir: './tmp') | ||
session.up | ||
end | ||
end | ||
|
||
describe '#up' do | ||
it 'runs containers without build option' do | ||
session = DockerSync::DockerComposeSession.new | ||
expect(DockerSync::Command).to receive(:run).with('docker-compose', 'up', dir: nil) | ||
session.up | ||
end | ||
|
||
it 'runs containers with build option' do | ||
session = DockerSync::DockerComposeSession.new | ||
expect(DockerSync::Command).to receive(:run).with('docker-compose', 'up', '--build', dir: nil) | ||
session.up(build: true) | ||
end | ||
|
||
it 'returns captured output' do | ||
session = DockerSync::DockerComposeSession.new | ||
result = session.up | ||
expect(result).to eq 'exit output' | ||
end | ||
end | ||
|
||
describe '#down' do | ||
it 'brings down containers' do | ||
session = DockerSync::DockerComposeSession.new | ||
expect(DockerSync::Command).to receive(:run).with('docker-compose', 'down', dir: nil) | ||
session.down | ||
end | ||
|
||
it 'brings down containers with files: and dir: options' do | ||
session = DockerSync::DockerComposeSession.new(files: ['foo.yml'], dir: './tmp') | ||
expect(DockerSync::Command).to receive(:run).with('docker-compose', '--file=foo.yml', 'down', dir: './tmp') | ||
session.down | ||
end | ||
end | ||
|
||
describe '#stop' do | ||
it 'stops containers' do | ||
session = DockerSync::DockerComposeSession.new | ||
expect(DockerSync::Command).to receive(:run).with('docker-compose', 'stop', dir: nil) | ||
session.stop | ||
end | ||
|
||
it 'stops containers with files: and dir: options' do | ||
session = DockerSync::DockerComposeSession.new(files: ['foo.yml'], dir: './tmp') | ||
expect(DockerSync::Command).to receive(:run).with('docker-compose', '--file=foo.yml', 'stop', dir: './tmp') | ||
session.stop | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters