Skip to content

Commit

Permalink
Prototype ps command. Closes #20
Browse files Browse the repository at this point in the history
  • Loading branch information
xeger committed May 25, 2016
1 parent 0f12228 commit 7c1d89b
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 10 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ gemspec

group :development do
gem 'pry'
gem 'pry-byebug'
end
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ compose = Docker::Compose.new
compose.version

compose.up(detached:true)

exited = compose.ps.where { |c| !c.up? }
puts "We have some exited containers: " + exited.join(', ')

sum = compose.ps.inject(0) { |a,c| a + c.size }
puts format("Composition is using %.1f MiB disk space", sum/1024/1024)
```

### Invoking from Rake
Expand Down
19 changes: 11 additions & 8 deletions bin/console
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
require "bundler/setup"
require "docker/compose"

# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
@session = Docker::Compose::Session.new

# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start

require "irb"
IRB.start
begin
require 'pry'
Pry.start(@session)
rescue LoadError
require 'irb'
IRB.setup nil
IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
require 'irb/ext/multi-irb'
IRB.irb nil, @session
end
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apple:
image: busybox
command: /bin/sh -c 'sleep 3600'
orange:
image: busybox
command: /bin/sh -c 'sleep 3600'
4 changes: 4 additions & 0 deletions lib/docker/compose.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
require 'net/http'

require_relative 'compose/version'
require_relative 'compose/error'
require_relative 'compose/container'
require_relative 'compose/collection'
require_relative 'compose/session'
require_relative 'compose/net_info'
require_relative 'compose/mapper'
Expand Down
13 changes: 13 additions & 0 deletions lib/docker/compose/collection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Docker::Compose
class Collection < Array
# @example find containers that are up
# who_is_up = coll.where { |c| c.up? }
# @example count space taken by all containers
# coll.map { |c| c.size }.inject(0) { |a, x| a + x }
def where
hits = Collection.new
self.each { |c| hits << c if yield(c) }
hits
end
end
end
63 changes: 63 additions & 0 deletions lib/docker/compose/container.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
module Docker::Compose
class Container
PS_STATUS = /^([A-Za-z]+) ?\(?([0-9]*)\)? ?(.*)$/i

attr_reader :id, :image, :size, :status, :exitstatus
attr_reader :names, :labels, :ports, :mounts

# @param [String] id
# @param [String] image
# @param [String,Numeric] size
# @param [String,#map] status e.g. ['Exited', '0', '3 minutes ago']
# @param [String,#map] names
# @param [String,#map] labels
# @param [String,#map] ports
# @param [String,#map] mounts
def initialize(id, image, size, status, names, labels, ports, mounts)
if size.is_a?(String)
scalar, units = size.split(' ')
scalar = size[0].to_i # lazy: invalid --> 0
mult = case units.downcase
when 'b' then 1
when 'kb' then 1_024
when 'mb' then 1_024^2
when 'gb' then 1_024^3
when 'tb' then 1_024^4
else
raise Error.new('Service#new', units, 'Impossibly large unit')
end
size = scalar * mult
end

if status.is_a?(String)
status = PS_STATUS.match(status)
raise Error.new('Service#new', status, 'Unrecognized status') unless status
end

names = names.split(',') if names.is_a?(String)
labels = labels.split(',') if labels.is_a?(String)
ports = ports.split(',') if ports.is_a?(String)
mounts = ports.split(',') if mounts.is_a?(String)

@id = id
@image = image
@size = size
@status = status[1].downcase.to_sym
@exitstatus = !status[2].empty? && status[2].to_i # sloppy!
@names = names
@labels = labels
@ports = ports
@mounts = mounts
end

# @return [String]
def name
names.first
end

# @return [Boolean]
def up?
self.status == :up
end
end
end
11 changes: 10 additions & 1 deletion lib/docker/compose/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@ class Error < RuntimeError
def initialize(cmd, status, detail)
@status = status
@detail = detail

brief = detail.split("\n").first || '(no output)'
message = format("'%s' failed with status %d: %s", cmd, status, brief)

case status
when Numeric
status = status.to_s
else
status = "'#{status}'"
end

message = format("'%s' failed with status %s: %s", cmd, status, brief)
super(message)
end
end
Expand Down
59 changes: 58 additions & 1 deletion lib/docker/compose/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ def logs(*services)
true
end

def ps()
lines = run!('ps', q:true).split(/[\r\n]+/)
containers = Collection.new

lines.each do |id|
containers << docker_ps(id)
end

containers
end

# Idempotently up the given services in the project.
# @param [Array] services list of String service names to run
# @param [Boolean] detached if true, to start services in the background;
Expand All @@ -63,7 +74,7 @@ def rm(*services, force:false, volumes:false, all:true)
run!('rm', {f:force, v:volumes, a:all}, services)
end

# Idempotently run a service in the project.
# Idempotently run an arbitrary command with a service container.
# @param [String] service name to run
# @param [String] cmd command statement to run
# @param [Boolean] detached if true, to start services in the background;
Expand Down Expand Up @@ -138,5 +149,51 @@ def run!(*args)
out
end
end

private

PS_FORMAT = '({{.ID}}) ({{.Image}}) ({{.Size}}) ({{.Status}}) ({{.Names}}) ({{.Labels}}) ({{.Ports}}) ({{.Mounts}})'
PS_REGEXP = /\(([^)])\) \(([^)])\) \(([^)])\) \(([^)])\) \(([^)])\) \(([^)])\) \(([^)])\) \(([^)])\)/

def docker_ps(id)
# docker ps -f id=c9e116fe1ce9732f7f715386078a317d8e322adaf98fa41507d1077d3af9ba02
cmd = @shell.run('docker', 'ps', a:true, f:"id=#{id}", format:PS_FORMAT).join
status, out, err = cmd.status, cmd.captured_output, cmd.captured_error
raise Error, "Unexpected output from docker ps" unless status == 0
lines = out.split(/[\r\n]+/)
return nil if lines.empty?
l = lines.shift
m = parse(l)
raise Error, "Cannot parse docker ps output: #{l}" unless m.respond_to?(:size) && m.size == 6
return Container.new(m[0],m[1],m[2],m[3],m[4],m[5],m[6],m[7])
end

def parse(str)
fields = []
nest = 0
field = ''
str.each_char do |ch|
if nest == 0
nest += 1 if ch == '('
else
if ch == '('
nest += 1
elsif ch == ')'
nest -= 1
field << ch unless nest == 0
else
field << ch
end
end

# nest just became 0
if nest == 0 && !field.empty?
fields << field
field = ''
end
end

fields
end
end
end
6 changes: 6 additions & 0 deletions spec/docker/compose/session_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
end
end

describe '#ps' do
it 'lists containers' do
session.ps
end
end

describe '#up' do
it 'runs containers' do
expect(shell).to receive(:run).with('docker-compose', anything, 'up', anything, anything)
Expand Down
6 changes: 6 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'docker/compose'

begin
require 'pry'
rescue LoadError
# bah!
end

0 comments on commit 7c1d89b

Please sign in to comment.