jarl 0.2.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 +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +6 -0
- data/bin/jarl +5 -0
- data/jarl.gemspec +25 -0
- data/lib/jarl/application.rb +101 -0
- data/lib/jarl/base.rb +48 -0
- data/lib/jarl/cli.rb +150 -0
- data/lib/jarl/console.rb +88 -0
- data/lib/jarl/docker.rb +229 -0
- data/lib/jarl/jarl_file.rb +21 -0
- data/lib/jarl/version.rb +3 -0
- data/lib/jarl.rb +15 -0
- data/spec/spec_helper.rb +11 -0
- metadata +118 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d5f658f6a2817ddf675c13d89d831c744dd27fcf
|
4
|
+
data.tar.gz: 087abc1978c7461efc0e2b2154b321ad4c0c1285
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: abfa4bd3f766e2d50352076b32ab0d1e56151fddd19ea8c3c769e44d971e11eecc0148c6187c6077a0fff02b77befaa1e7567507467a1f40cf1a2ed2e2513735
|
7
|
+
data.tar.gz: 1f397a352b7a36204e4ad6888235b7bf0b34d0d267b1084fbbd0d4bb6fc562bc22dd0078e92365d6308f9c2832216c655bd9431317694315317b06be772d5256
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Alex Kukushkin
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Jarl
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'jarl'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install jarl
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
24
|
+
|
25
|
+
## Contributing
|
26
|
+
|
27
|
+
1. Fork it ( https://github.com/[my-github-username]/jarl/fork )
|
28
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
29
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
30
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
31
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/jarl
ADDED
data/jarl.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'jarl/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'jarl'
|
8
|
+
spec.version = Jarl::VERSION
|
9
|
+
spec.authors = ['Alex Kukushkin']
|
10
|
+
spec.email = ['alex.kukushkin@cubits.com']
|
11
|
+
spec.summary = %q{Jarl, docker container management tool}
|
12
|
+
spec.description = %q{Jarl, docker container management tool}
|
13
|
+
spec.homepage = ''
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'thor', '~> 0.19'
|
22
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
23
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
24
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
25
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Jarl
|
2
|
+
class Application
|
3
|
+
attr_reader :group, :name, :image, :volumes, :ports, :environment, :entrypoint, :command
|
4
|
+
|
5
|
+
# Create Application object from application definition:
|
6
|
+
#
|
7
|
+
# app_name:
|
8
|
+
# image: blabla
|
9
|
+
# volumes:
|
10
|
+
# - /host/path:/container/path
|
11
|
+
# ports:
|
12
|
+
# - 1234:5678
|
13
|
+
# command: blabla
|
14
|
+
#
|
15
|
+
def initialize(group, name, params)
|
16
|
+
@group = group
|
17
|
+
@name = name
|
18
|
+
@image = params['image'] || fail("Application '#{self}' has no 'image' defined")
|
19
|
+
unless @image.is_a?(String)
|
20
|
+
fail "Application '#{self}' has invalid 'image' definition, String is expected"
|
21
|
+
end
|
22
|
+
@volumes = params['volumes'] || []
|
23
|
+
unless @volumes.is_a?(Array)
|
24
|
+
fail "Application '#{self}' has invalid 'volumes' definition, Array is expected"
|
25
|
+
end
|
26
|
+
@ports = params['ports'] || []
|
27
|
+
unless @ports.is_a?(Array)
|
28
|
+
fail "Application '#{self}' has invalid 'ports' definition, Array is expected"
|
29
|
+
end
|
30
|
+
@environment = params['environment'] || {}
|
31
|
+
unless @environment.is_a?(Hash)
|
32
|
+
fail "Application '#{self}' has invalid 'environment' definition, Hash is expected"
|
33
|
+
end
|
34
|
+
@entrypoint = params['entrypoint']
|
35
|
+
if @entrypoint && !@entrypoint.is_a?(String)
|
36
|
+
fail "Application '#{self}' has invalid 'entrypoint' definition, String is expected"
|
37
|
+
end
|
38
|
+
@command = params['command']
|
39
|
+
if @command && !@command.is_a?(String)
|
40
|
+
fail "Application '#{self}' has invalid 'command' definition, String is expected"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def full_name
|
45
|
+
"#{group}.#{name}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def image_is_a_path?
|
49
|
+
image =~ /^[~\.\/]/
|
50
|
+
end
|
51
|
+
|
52
|
+
def running?
|
53
|
+
running_container
|
54
|
+
end
|
55
|
+
|
56
|
+
def running_container
|
57
|
+
Docker.containers_running.find { |c| c.name == full_name }
|
58
|
+
end
|
59
|
+
|
60
|
+
def start
|
61
|
+
running_container.stop! if running?
|
62
|
+
Docker.start(
|
63
|
+
name: full_name,
|
64
|
+
image: image_name,
|
65
|
+
volumes: volumes,
|
66
|
+
ports: ports,
|
67
|
+
environment: environment,
|
68
|
+
command: command
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def build
|
73
|
+
fail 'Not a file based image' unless image_is_a_path?
|
74
|
+
Docker::Image.new(image_name, image_path).build!
|
75
|
+
end
|
76
|
+
|
77
|
+
def image_name
|
78
|
+
image_is_a_path? ? name : image
|
79
|
+
end
|
80
|
+
|
81
|
+
def image_path
|
82
|
+
image_is_a_path? ? image : nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def show_log(*_args)
|
86
|
+
return unless running?
|
87
|
+
begin
|
88
|
+
IO.popen "docker logs -f #{running_container.id}", 'r' do |p|
|
89
|
+
str = '<<< STARTED >>>'
|
90
|
+
puts "#{esc_yellow full_name}: #{str}" while str = p.gets
|
91
|
+
end
|
92
|
+
rescue Interrupt
|
93
|
+
#
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_s
|
98
|
+
full_name
|
99
|
+
end
|
100
|
+
end # class Application
|
101
|
+
end # module Jarl
|
data/lib/jarl/base.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Jarl
|
2
|
+
#
|
3
|
+
# Load config and application definitions
|
4
|
+
#
|
5
|
+
def self.load
|
6
|
+
abort 'Docker is not installed' unless Docker.installed?
|
7
|
+
if Jarl.jarl_files.empty?
|
8
|
+
abort 'No *.jarl files found'
|
9
|
+
elsif Jarl.applications.empty?
|
10
|
+
abort 'No application definitions found'
|
11
|
+
end
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# Returns list of found *.jarl files
|
17
|
+
#
|
18
|
+
def self.jarl_files
|
19
|
+
@jarl_files ||= load_jarl_files
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns list of registered applications
|
23
|
+
#
|
24
|
+
def self.applications
|
25
|
+
@applications ||= jarl_files.map do |jf|
|
26
|
+
begin
|
27
|
+
jf.applications.map { |app_name, params| Application.new(jf.name, app_name, params) }
|
28
|
+
rescue StandardError => e
|
29
|
+
abort "Failed to parse application definition in file '#{jf.path}': #{e}"
|
30
|
+
end
|
31
|
+
end.flatten
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns list of applications matching the pattern
|
35
|
+
#
|
36
|
+
def self.find_applications_by(pattern)
|
37
|
+
applications.select { |a| a.full_name.index(pattern) }
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def self.load_jarl_files
|
43
|
+
Pathname.glob(Pathname.pwd + '**' + '*.jarl').map do |p|
|
44
|
+
JarlFile.new(p)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end # module Jarl
|
data/lib/jarl/cli.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Jarl
|
4
|
+
class CLI < Thor
|
5
|
+
# global options
|
6
|
+
#
|
7
|
+
class_option :config,
|
8
|
+
aliases: '-c',
|
9
|
+
type: :string,
|
10
|
+
desc: 'Use specified config file (defaul: ~/.jarl)'
|
11
|
+
|
12
|
+
#
|
13
|
+
desc 'status [NAME]', 'Show applications statuses'
|
14
|
+
def status(name = nil)
|
15
|
+
Jarl.load
|
16
|
+
apps = name ? Jarl.find_applications_by(name) : Jarl.applications
|
17
|
+
show_jarl_applications_status(apps)
|
18
|
+
end
|
19
|
+
default_task :status
|
20
|
+
|
21
|
+
#
|
22
|
+
desc 'up [NAME]', '(Re)start applications'
|
23
|
+
def up(name = nil)
|
24
|
+
Jarl.load
|
25
|
+
apps = name ? Jarl.find_applications_by(name) : Jarl.applications
|
26
|
+
apps.each do |app|
|
27
|
+
puts esc_green(app.running? ? "Restarting '#{app}'..." : "Starting '#{app}'...")
|
28
|
+
app.start
|
29
|
+
end
|
30
|
+
# show_jarl_applications_status(apps)
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
desc 'down [NAME]', 'Stop applications'
|
35
|
+
def down(name = nil)
|
36
|
+
Jarl.load
|
37
|
+
apps = name ? Jarl.find_applications_by(name) : Jarl.applications
|
38
|
+
apps.each do |app|
|
39
|
+
next unless app.running?
|
40
|
+
puts esc_yellow "Stopping '#{app}'..."
|
41
|
+
app.running_container.stop!
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
option :ip, type: :boolean, desc: 'Show only IPs'
|
47
|
+
desc 'inspect [NAME]', 'Inspect applications'
|
48
|
+
def inspect(name = nil)
|
49
|
+
Jarl.load
|
50
|
+
apps = name ? Jarl.find_applications_by(name) : Jarl.applications
|
51
|
+
apps.each do |app|
|
52
|
+
if options[:ip]
|
53
|
+
puts app.running_container.ip if app.running?
|
54
|
+
else
|
55
|
+
show_jarl_application_inspect(app)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
desc 'build [NAME]', '(Re)build docker images'
|
61
|
+
def build(name = nil)
|
62
|
+
Jarl.load
|
63
|
+
apps = name ? Jarl.find_applications_by(name) : Jarl.applications
|
64
|
+
apps.select(&:image_is_a_path?).each do |app|
|
65
|
+
puts esc_yellow "Building image for '#{app}'..."
|
66
|
+
app.build
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
desc 'logs [NAME]', 'Show log output for applications'
|
71
|
+
def logs(name = nil)
|
72
|
+
Jarl.load
|
73
|
+
apps = name ? Jarl.find_applications_by(name) : Jarl.applications
|
74
|
+
threads = []
|
75
|
+
apps.select(&:running?).each do |app|
|
76
|
+
threads << Thread.new { app.show_log }
|
77
|
+
end
|
78
|
+
begin
|
79
|
+
threads.each(&:join)
|
80
|
+
rescue Interrupt
|
81
|
+
#
|
82
|
+
end
|
83
|
+
puts
|
84
|
+
end
|
85
|
+
|
86
|
+
desc 'version', 'Show Jarl version'
|
87
|
+
def version
|
88
|
+
puts app_name_version
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def app_name_version
|
94
|
+
"Jarl v#{Jarl::VERSION}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def show_jarl_applications_status(apps)
|
98
|
+
if apps.size == 0
|
99
|
+
puts esc_yellow 'No applications found'
|
100
|
+
return
|
101
|
+
end
|
102
|
+
max_full_name_len = [apps.map(&:full_name).map(&:size).max, 12].max
|
103
|
+
puts esc_format(
|
104
|
+
['APPLICATION', max_full_name_len],
|
105
|
+
['RUNNING', 12],
|
106
|
+
['UPTIME', 12],
|
107
|
+
['IP', 12],
|
108
|
+
['PORTS']
|
109
|
+
# ['IMAGE']
|
110
|
+
)
|
111
|
+
apps.each do |app|
|
112
|
+
container = app.running_container
|
113
|
+
puts esc_format(
|
114
|
+
[app.full_name, max_full_name_len, :yellow],
|
115
|
+
(app.running? ? [container.id, 12, :green] : ['no', 12]),
|
116
|
+
(app.running? ? [seconds_to_human(container.uptime), 12, :yellow] : ['', 12]),
|
117
|
+
(app.running? ? [container.ip, 12, :yellow] : ['', 12]),
|
118
|
+
(app.running? ? [container.ports.map { |p| "#{p[:from]}"}.join(', ')] : [''])
|
119
|
+
|
120
|
+
# (app.image_is_a_path? ? ["path: #{app.image}"] : [app.image])
|
121
|
+
)
|
122
|
+
end
|
123
|
+
# puts "#{app.full_name} (#{app.image})"
|
124
|
+
# puts " image: #{app.image}"
|
125
|
+
# puts " volumes: #{app.volumes}"
|
126
|
+
# puts " ports: #{app.ports}"
|
127
|
+
# puts " environment: #{app.environment}"
|
128
|
+
# puts " entrypoint: #{app.entrypoint}"
|
129
|
+
# puts " command: #{app.command}"
|
130
|
+
end
|
131
|
+
|
132
|
+
def show_jarl_application_inspect(app)
|
133
|
+
unless app.running?
|
134
|
+
puts esc_yellow("#{app.full_name}: ") + 'down'
|
135
|
+
return
|
136
|
+
end
|
137
|
+
container = app.running_container
|
138
|
+
puts esc_yellow "#{app.full_name}: " + esc_green(container.id)
|
139
|
+
puts " name: #{esc_yellow container.name}"
|
140
|
+
puts " image: #{esc_yellow container.image}"
|
141
|
+
puts " full ID: #{esc_yellow container.long_id}"
|
142
|
+
puts " uptime: #{esc_yellow seconds_to_human container.uptime}"
|
143
|
+
puts " IP: #{esc_yellow container.ip}"
|
144
|
+
puts " ports: #{esc_yellow container.ports.map { |p| "#{p[:from]}->#{p[:to]}"}.join(', ')}"
|
145
|
+
puts " volumes: #{esc_yellow container.params['Volumes']}"
|
146
|
+
# puts " ps: #{esc_yellow container.ps}"
|
147
|
+
# puts " params: #{esc_yellow container.params}"
|
148
|
+
end
|
149
|
+
end # class CLI
|
150
|
+
end # module Jarl
|
data/lib/jarl/console.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
module Console
|
2
|
+
module Shell
|
3
|
+
def sh(*args)
|
4
|
+
puts "> #{args.join(' ')}"
|
5
|
+
system(*args)
|
6
|
+
end
|
7
|
+
end # module Shell
|
8
|
+
|
9
|
+
module Utils
|
10
|
+
def seconds_to_human(s)
|
11
|
+
s = s.round
|
12
|
+
m = (s / 60).floor
|
13
|
+
s = s % 60
|
14
|
+
h = (m / 60).floor
|
15
|
+
m = m % 60
|
16
|
+
d = (h / 24).floor
|
17
|
+
h = h % 24
|
18
|
+
|
19
|
+
output = "#{s}s"
|
20
|
+
output = "#{m}m " + output if m > 0 || h > 0 || d > 0
|
21
|
+
output = "#{h}h " + output if h > 0 || d > 0
|
22
|
+
output = "#{d}d " + output if d > 0
|
23
|
+
output
|
24
|
+
end
|
25
|
+
end # module Utils
|
26
|
+
|
27
|
+
module Colors
|
28
|
+
# Returns ANSI escaped string.
|
29
|
+
#
|
30
|
+
def esc_string(esc, text)
|
31
|
+
esc + text.to_s + "\e[0m"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns ANSI escaped string for the red colored text.
|
35
|
+
#
|
36
|
+
def esc_red(text)
|
37
|
+
esc_string "\e[31m", text
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns ANSI escaped string for the green colored text.
|
41
|
+
#
|
42
|
+
def esc_green(text)
|
43
|
+
esc_string "\e[32m", text
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns ANSI escaped string for the yellow colored text.
|
47
|
+
#
|
48
|
+
def esc_yellow(text)
|
49
|
+
esc_string "\e[33m", text
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns ANSI escaped string for the blue colored text.
|
53
|
+
#
|
54
|
+
def esc_blue(text)
|
55
|
+
esc_string "\e[34m", text
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns ANSI escaped string for the bold text.
|
59
|
+
#
|
60
|
+
def esc_bold(text)
|
61
|
+
esc_string "\e[01;37m", text
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns string padded to given number of characters, text can be escaped.
|
65
|
+
#
|
66
|
+
def esc_pad(text, num, color = nil)
|
67
|
+
esc_text = text
|
68
|
+
case color
|
69
|
+
when :red, :yellow, :green
|
70
|
+
esc_text = send :"esc_#{color}", text
|
71
|
+
end
|
72
|
+
pad = esc_text.size - text.size
|
73
|
+
num > 0 ? ("%-#{num + pad}s" % esc_text) : esc_text
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns string with formatted and padded fields
|
77
|
+
# Each arg is an Array:
|
78
|
+
# [<text>, [padding_num], [color]]
|
79
|
+
#
|
80
|
+
def esc_format(*args)
|
81
|
+
out = []
|
82
|
+
args.each do |arg|
|
83
|
+
out << esc_pad(arg[0], arg[1] || 0, arg[2])
|
84
|
+
end
|
85
|
+
out.join(' ')
|
86
|
+
end
|
87
|
+
end # module Colors
|
88
|
+
end # module Console
|
data/lib/jarl/docker.rb
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Jarl
|
4
|
+
module Docker
|
5
|
+
INSECURE_KEY_PATH = '/home/vagrant/.ssh/insecure_key'
|
6
|
+
|
7
|
+
# Returns true if Docker is installed
|
8
|
+
#
|
9
|
+
def self.installed?
|
10
|
+
!`which docker`.empty?
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns available docker images
|
14
|
+
#
|
15
|
+
def self.images
|
16
|
+
@docker_images ||= Pathname.glob(Pathname.new(DOCKER_IMAGES_PATH).join('*/')).sort.map do |path|
|
17
|
+
Image.new(path.basename, path)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a list of used images
|
22
|
+
#
|
23
|
+
def self.images_used
|
24
|
+
used_images_list = File.readlines(USED_IMAGES_FILE) rescue ['']
|
25
|
+
used_images_list.map!(&:chomp)
|
26
|
+
@docker_images_used ||= images.select { |di| used_images_list.include?(di.name) }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns a list of unused images
|
30
|
+
#
|
31
|
+
def self.images_unused
|
32
|
+
images - images_used
|
33
|
+
end
|
34
|
+
|
35
|
+
# Adds image to a list of used images
|
36
|
+
#
|
37
|
+
def self.images_used_add(name)
|
38
|
+
list = images_used.map(&:name)
|
39
|
+
list << name
|
40
|
+
File.open(USED_IMAGES_FILE, 'w') do |f|
|
41
|
+
f.puts list.join("\n")
|
42
|
+
end
|
43
|
+
reload!
|
44
|
+
end
|
45
|
+
|
46
|
+
# Removes image from a list of used images
|
47
|
+
#
|
48
|
+
def self.images_used_remove(name)
|
49
|
+
list = images_used.map(&:name).select { |n| n != name }
|
50
|
+
File.open(USED_IMAGES_FILE, 'w') do |f|
|
51
|
+
f.puts list.join("\n")
|
52
|
+
end
|
53
|
+
reload!
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a list of running docker containers
|
57
|
+
#
|
58
|
+
def self.containers_running
|
59
|
+
return @docker_containers_running if @docker_containers_running
|
60
|
+
container_ids = `docker ps -q`.split("\n")
|
61
|
+
@docker_containers_running = container_ids.map do |id|
|
62
|
+
Container.new id
|
63
|
+
end
|
64
|
+
@docker_containers_running
|
65
|
+
end
|
66
|
+
|
67
|
+
# Reloads lists of images and containers
|
68
|
+
#
|
69
|
+
def self.reload!
|
70
|
+
@docker_images = nil
|
71
|
+
@docker_images_used = nil
|
72
|
+
@docker_containers_running = nil
|
73
|
+
end
|
74
|
+
|
75
|
+
# Cleans unused images
|
76
|
+
#
|
77
|
+
def self.clean_images
|
78
|
+
sh "docker rmi $(docker images -a | grep '^<none>' | awk '{print $3}') >/dev/null 2>&1"
|
79
|
+
rescue RuntimeError => e
|
80
|
+
# omit conflicting images error reports
|
81
|
+
raise unless e.to_s =~ /Command failed/
|
82
|
+
end
|
83
|
+
|
84
|
+
# Cleans stopped containers
|
85
|
+
#
|
86
|
+
def self.clean_containers
|
87
|
+
sh "docker rm $(docker ps -a | grep 'Exited' | awk '{print $1}')"
|
88
|
+
rescue RuntimeError => e
|
89
|
+
# omit conflicting images error reports
|
90
|
+
raise unless e.to_s =~ /Command failed/
|
91
|
+
end
|
92
|
+
|
93
|
+
# Start container defined by params
|
94
|
+
#
|
95
|
+
def self.start(params)
|
96
|
+
opts = []
|
97
|
+
opts << params[:volumes].map do |v|
|
98
|
+
"-v #{v}"
|
99
|
+
end.join(' ')
|
100
|
+
opts << params[:ports].map do |p|
|
101
|
+
"-p #{p}"
|
102
|
+
end.join(' ')
|
103
|
+
opts << params[:environment].map do |k, v|
|
104
|
+
"-e #{k}=#{v}"
|
105
|
+
end.join(' ')
|
106
|
+
docker_cmd = "docker run -d #{opts.join(' ')} " \
|
107
|
+
" --hostname #{params[:name]} " \
|
108
|
+
" --name #{params[:name]} #{params[:image]} #{params[:command]}"
|
109
|
+
# puts docker_cmd
|
110
|
+
sh docker_cmd
|
111
|
+
end
|
112
|
+
|
113
|
+
# Image
|
114
|
+
#
|
115
|
+
class Image
|
116
|
+
attr_accessor :name, :path
|
117
|
+
|
118
|
+
def initialize(name, path)
|
119
|
+
@name = name.to_s
|
120
|
+
@path = path
|
121
|
+
end
|
122
|
+
|
123
|
+
def prebuilt?
|
124
|
+
self.class.prebuilt_image_names.include?(name)
|
125
|
+
end
|
126
|
+
|
127
|
+
def running?
|
128
|
+
!!running_container
|
129
|
+
end
|
130
|
+
|
131
|
+
def running_container
|
132
|
+
Docker.containers_running.select { |c| c.name == name }.first
|
133
|
+
end
|
134
|
+
|
135
|
+
def build!
|
136
|
+
sh "docker build -t #{name} #{path}"
|
137
|
+
end
|
138
|
+
|
139
|
+
def start!
|
140
|
+
Container.clean_containers(name)
|
141
|
+
run_flags = "--name=#{name} --hostname=#{name} -d"
|
142
|
+
sh "docker run #{run_flags} #{name}"
|
143
|
+
end
|
144
|
+
|
145
|
+
def stop!
|
146
|
+
running_container.stop!
|
147
|
+
end
|
148
|
+
|
149
|
+
def open_ssh_session!
|
150
|
+
running_container.open_ssh_session!
|
151
|
+
end
|
152
|
+
|
153
|
+
def use!
|
154
|
+
build! unless prebuilt?
|
155
|
+
Docker.images_used_add(name)
|
156
|
+
end
|
157
|
+
|
158
|
+
def unuse!
|
159
|
+
Docker.images_used_remove(name)
|
160
|
+
end
|
161
|
+
|
162
|
+
def used?
|
163
|
+
Docker.images_used.map(&:name).include?(name)
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.prebuilt_image_names
|
167
|
+
return @prebuilt_image_names if @prebuilt_image_names
|
168
|
+
di = `docker images`
|
169
|
+
@prebuilt_image_names = di.split("\n").map do |line|
|
170
|
+
m = line.match(/^(\S*)\s*/)
|
171
|
+
m[1]
|
172
|
+
end
|
173
|
+
@prebuilt_image_names
|
174
|
+
end
|
175
|
+
end # class Image
|
176
|
+
|
177
|
+
# Container
|
178
|
+
#
|
179
|
+
class Container
|
180
|
+
attr_accessor :id, :long_id, :name, :image, :image_id, :ip, :ps, :ports, :params
|
181
|
+
|
182
|
+
def initialize(id)
|
183
|
+
@id = id
|
184
|
+
@ps = `docker ps | grep #{id}`
|
185
|
+
container_inspect = `docker inspect #{id}`
|
186
|
+
@params = JSON.parse(container_inspect).first
|
187
|
+
@long_id = params['Id']
|
188
|
+
@name = params['Name'].gsub('/', '')
|
189
|
+
@image = params['Config']['Image']
|
190
|
+
@image_id = params['Image']
|
191
|
+
@ip = params['NetworkSettings']['IPAddress']
|
192
|
+
port_maps = params['NetworkSettings']['Ports']
|
193
|
+
@ports = port_maps.keys.map do |port|
|
194
|
+
{ from: port, to: port_maps[port] }
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def open_ssh_session!
|
199
|
+
ssh_flags = "-oStrictHostKeyChecking=no -i #{INSECURE_KEY_PATH}"
|
200
|
+
sh "ssh #{ssh_flags} root@#{ip}"
|
201
|
+
end
|
202
|
+
|
203
|
+
def stop!
|
204
|
+
sh "docker stop #{name}"
|
205
|
+
clean!
|
206
|
+
end
|
207
|
+
|
208
|
+
def clean!
|
209
|
+
self.class.clean_containers(name)
|
210
|
+
end
|
211
|
+
|
212
|
+
def uptime
|
213
|
+
params['State'] && params['State']['StartedAt'] &&
|
214
|
+
Time.now.utc - DateTime.parse(params['State']['StartedAt']).to_time.utc
|
215
|
+
end
|
216
|
+
|
217
|
+
def self.clean_containers(*names)
|
218
|
+
names.each do |name|
|
219
|
+
begin
|
220
|
+
sh "docker rm #{name} >/dev/null 2>&1"
|
221
|
+
rescue RuntimeError => e
|
222
|
+
# omit reports on missing container
|
223
|
+
raise unless e.to_s =~ /Command failed/
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end # class ContainerInfo
|
228
|
+
end # module Docker
|
229
|
+
end # module Jarl
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Jarl
|
5
|
+
class JarlFile
|
6
|
+
attr_reader :path, :name, :applications
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
@path = Pathname.new(path)
|
10
|
+
@name = @path.basename('.jarl')
|
11
|
+
load
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def load
|
17
|
+
@applications = YAML.load File.read(path)
|
18
|
+
end
|
19
|
+
|
20
|
+
end # class JarlFile
|
21
|
+
end # module Jarl
|
data/lib/jarl/version.rb
ADDED
data/lib/jarl.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# require 'rubygems'
|
2
|
+
# require 'bundler/setup'
|
3
|
+
# Bundler.setup
|
4
|
+
|
5
|
+
require 'jarl/base'
|
6
|
+
require 'jarl/version'
|
7
|
+
require 'jarl/cli'
|
8
|
+
require 'jarl/application'
|
9
|
+
require 'jarl/jarl_file'
|
10
|
+
require 'jarl/docker'
|
11
|
+
require 'jarl/console'
|
12
|
+
|
13
|
+
include Console::Colors
|
14
|
+
include Console::Shell
|
15
|
+
include Console::Utils
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jarl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Kukushkin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-01-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.19'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.19'
|
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.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
description: Jarl, docker container management tool
|
70
|
+
email:
|
71
|
+
- alex.kukushkin@cubits.com
|
72
|
+
executables:
|
73
|
+
- jarl
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- Gemfile
|
79
|
+
- LICENSE.txt
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- bin/jarl
|
83
|
+
- jarl.gemspec
|
84
|
+
- lib/jarl.rb
|
85
|
+
- lib/jarl/application.rb
|
86
|
+
- lib/jarl/base.rb
|
87
|
+
- lib/jarl/cli.rb
|
88
|
+
- lib/jarl/console.rb
|
89
|
+
- lib/jarl/docker.rb
|
90
|
+
- lib/jarl/jarl_file.rb
|
91
|
+
- lib/jarl/version.rb
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
homepage: ''
|
94
|
+
licenses:
|
95
|
+
- MIT
|
96
|
+
metadata: {}
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubyforge_project:
|
113
|
+
rubygems_version: 2.2.2
|
114
|
+
signing_key:
|
115
|
+
specification_version: 4
|
116
|
+
summary: Jarl, docker container management tool
|
117
|
+
test_files:
|
118
|
+
- spec/spec_helper.rb
|