Skip to content

Commit

Permalink
Remove dependency on docker-compose gem; support Ruby 3.x - reroll of #…
Browse files Browse the repository at this point in the history
…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
EugenMayer and takahashim authored Aug 25, 2022
1 parent 02a633b commit 60be788
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 9 deletions.
5 changes: 4 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ PATH
specs:
docker-sync (0.7.2)
daemons (~> 1.2, >= 1.2.3)
docker-compose (~> 1.1, >= 1.1.7)
dotenv (~> 2.1, >= 2.1.1)
gem_update_checker (~> 0.2.0, >= 0.2.0)
os
Expand All @@ -18,13 +17,10 @@ GEM
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
backticks (1.0.3)
coderay (1.1.3)
concurrent-ruby (1.1.10)
daemons (1.4.1)
diff-lcs (1.5.0)
docker-compose (1.1.13)
backticks (~> 1.0)
dotenv (2.7.6)
gem_update_checker (0.2.0)
i18n (1.10.0)
Expand Down
1 change: 0 additions & 1 deletion docker-sync.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ Gem::Specification.new do |s|

s.add_runtime_dependency 'thor', '~> 1.0', '>= 1.0.0'
s.add_runtime_dependency 'gem_update_checker', '~> 0.2.0', '>= 0.2.0'
s.add_runtime_dependency 'docker-compose', '~> 1.1', '>= 1.1.7'
s.add_runtime_dependency 'terminal-notifier', '2.0.0'
s.add_runtime_dependency 'dotenv', '~> 2.1', '>= 2.1.1'
s.add_runtime_dependency 'daemons', '~> 1.2', '>= 1.2.3'
Expand Down
126 changes: 126 additions & 0 deletions lib/docker-sync/command.rb
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
3 changes: 1 addition & 2 deletions lib/docker-sync/compose.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
require 'docker/compose'
require 'pp'
class ComposeManager
include Thor::Shell
Expand Down Expand Up @@ -30,7 +29,7 @@ def initialize(global_options)
compose_files.push compose_dev_path
end
end
@compose_session = Docker::Compose::Session.new(dir:'./', :file => compose_files)
@compose_session = DockerSync::DockerComposeSession.new(dir: './', files: compose_files)
end

def run
Expand Down
44 changes: 44 additions & 0 deletions lib/docker-sync/docker_compose_session.rb
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
37 changes: 37 additions & 0 deletions spec/lib/docker-sync/command_spec.rb
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
94 changes: 94 additions & 0 deletions spec/lib/docker-sync/docker_compose_session_spec.rb
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
1 change: 0 additions & 1 deletion tasks/stack/stack.thor
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ require 'docker-sync'
require 'docker-sync/sync_manager'
require 'docker-sync/update_check'
require 'docker-sync/upgrade_check'
require 'docker/compose'
require 'docker-sync/compose'
require 'docker-sync/config/project_config'

Expand Down

0 comments on commit 60be788

Please sign in to comment.