dockit 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Dockerfile +17 -0
- data/Gemfile +4 -0
- data/README.md +124 -0
- data/Rakefile +28 -0
- data/VERSION +1 -0
- data/bin/console +10 -0
- data/bin/dockit +8 -0
- data/bin/setup +7 -0
- data/dockit.gemspec +32 -0
- data/lib/dockit.rb +84 -0
- data/lib/dockit/cli.rb +249 -0
- data/lib/dockit/config.rb +63 -0
- data/lib/dockit/container.rb +81 -0
- data/lib/dockit/digitalocean.rb +148 -0
- data/lib/dockit/image.rb +47 -0
- data/lib/dockit/service.rb +90 -0
- data/spec/deploy/Dockit.rb +7 -0
- data/spec/deploy/mod/Dockit.rb +6 -0
- data/spec/deploy/mod/Dockit.yaml +0 -0
- data/spec/deploy/svc/Dockit.yaml +2 -0
- data/spec/dockit/bad.yaml +2 -0
- data/spec/dockit/config_spec.rb +52 -0
- data/spec/dockit/locals.yaml +3 -0
- data/spec/dockit/simple.yaml +2 -0
- data/spec/dockit_spec.rb +80 -0
- data/spec/spec_helper.rb +8 -0
- metadata +209 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
# Parse a dockit config file.
|
2
|
+
# The dockit configuration file should contain the necessary options for
|
3
|
+
# passing to the appropriate Docker api methods for image creation (build) or
|
4
|
+
# container creation (running.)
|
5
|
+
# The file is passed through ERB before YAML loading.
|
6
|
+
require 'erb'
|
7
|
+
require 'dotenv'
|
8
|
+
|
9
|
+
module Dockit
|
10
|
+
class Config
|
11
|
+
ENVFILE='.env'
|
12
|
+
##
|
13
|
+
# Instantiate and parse the file.
|
14
|
+
#
|
15
|
+
# file [String] :: dockit yaml file
|
16
|
+
# locals [Hash] :: local variables to bind in the ERB context
|
17
|
+
def initialize(file, locals={})
|
18
|
+
root = Dockit::Env.new.root
|
19
|
+
Dotenv.load(File.join(root, ENVFILE))
|
20
|
+
locals['root'] ||= root
|
21
|
+
|
22
|
+
begin
|
23
|
+
@config = YAML::load(ERB.new(File.read(file)).result(bindings(locals)))
|
24
|
+
rescue NameError => e
|
25
|
+
error(e)
|
26
|
+
rescue ArgumentError => e
|
27
|
+
error(e)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# The +Dockit.yaml+ file should have top-level entries for (at least)
|
32
|
+
# +build+ and/or +create+
|
33
|
+
# name :: Top-level key
|
34
|
+
# key :: key in +name+ hash
|
35
|
+
# ==== Returns
|
36
|
+
# - Hash for +name+ if +key+ is nil.
|
37
|
+
# - Value of +key+ in +name+ hash.
|
38
|
+
def get(name, key=nil)
|
39
|
+
phase = @config[name.to_s]
|
40
|
+
return phase unless key && phase
|
41
|
+
|
42
|
+
phase[key.to_s]
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
# Generate bindings object for locals to pass to erb
|
47
|
+
# locals :: hash converted to local variables
|
48
|
+
# Returns :: binding object
|
49
|
+
def bindings(locals)
|
50
|
+
b = binding
|
51
|
+
locals.each do |k,v|
|
52
|
+
b.local_variable_set(k, v)
|
53
|
+
end
|
54
|
+
|
55
|
+
return b
|
56
|
+
end
|
57
|
+
|
58
|
+
def error(e)
|
59
|
+
abort [e.message.capitalize,
|
60
|
+
"Did you forget '--locals key:value'?"].join("\n")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Dockit
|
2
|
+
class Container
|
3
|
+
attr_reader :container
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def list(all: false, filters: nil)
|
7
|
+
Docker::Container.all(all: all, filters: JSON.dump(filters))
|
8
|
+
end
|
9
|
+
|
10
|
+
def clean(force: false)
|
11
|
+
list(
|
12
|
+
all: force,
|
13
|
+
filters: force ? nil : {status: [:exited]}
|
14
|
+
).each do |container|
|
15
|
+
puts " #{container.id}"
|
16
|
+
container.delete(force: true, v: force)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def find(name: nil, id: nil)
|
21
|
+
unless name || id
|
22
|
+
STDERR.puts "Must specify name or id"
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
list().find do |container|
|
26
|
+
name && container.info['Names'].include?(name) ||
|
27
|
+
id && container.id == id
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(options)
|
34
|
+
@tty = options['Tty']
|
35
|
+
@container = Docker::Container.create(options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def start(options={}, verbose: true, transient: false)
|
39
|
+
container.start!(options)
|
40
|
+
if transient
|
41
|
+
if @tty
|
42
|
+
trap("INT") {}
|
43
|
+
STDIN.raw!
|
44
|
+
end
|
45
|
+
container.attach(tty: @tty, stdin: @tty ? STDIN : nil) do |*args|
|
46
|
+
if @tty then
|
47
|
+
print args[0]
|
48
|
+
else
|
49
|
+
msg(*args)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
STDIN.cooked!
|
53
|
+
destroy
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def destroy
|
58
|
+
puts "Deleting container #{container.id}"
|
59
|
+
container.delete(force: true, v: true)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def msg(stream, chunk)
|
64
|
+
pfx = stream.to_s == 'stdout' ? 'INFO: ' : 'ERROR: '
|
65
|
+
puts pfx +
|
66
|
+
[chunk.sub(/^\n/,'').split("\n")]
|
67
|
+
.flatten
|
68
|
+
.collect(&:rstrip)
|
69
|
+
.reject(&:empty?)
|
70
|
+
.join("\n#{pfx}")
|
71
|
+
end
|
72
|
+
|
73
|
+
def binds(options)
|
74
|
+
return unless options['Volumes']
|
75
|
+
|
76
|
+
options['Volumes'].collect do |k, v|
|
77
|
+
"#{Dir.pwd}/#{v}:#{k}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# This class allows for basic deployment to a digitalocean docker droplet
|
2
|
+
# via ssh (without exposing tcp access to the docker service.)
|
3
|
+
require 'droplet_kit'
|
4
|
+
|
5
|
+
class DO < Thor
|
6
|
+
USERNAME = 'root'.freeze
|
7
|
+
REMOTE_CMDS = %w[start push create].freeze
|
8
|
+
|
9
|
+
def self.remote_required?(extra_cmds=[])
|
10
|
+
ARGV[0] == 'do' && (
|
11
|
+
REMOTE_CMDS.include?(ARGV[1]) || extra_cmds.include?(ARGV[1]))
|
12
|
+
end
|
13
|
+
|
14
|
+
class_option :remote, type: :string, desc: 'remote droplet address',
|
15
|
+
required: remote_required?, aliases: ['r']
|
16
|
+
class_option :user , type: :string, desc: 'remote user',
|
17
|
+
default: USERNAME, aliases: ['u']
|
18
|
+
class_option :token , type: :string,
|
19
|
+
desc: 'token filename relative to "~/.digitalocean"',
|
20
|
+
default: 'token'
|
21
|
+
|
22
|
+
desc 'create', 'create droplet REMOTE'
|
23
|
+
option :size, type: :string, desc: 'size for droplet', default: '512mb'
|
24
|
+
option :region, type: :string, desc: 'region for droplet', default: 'nyc3'
|
25
|
+
def create
|
26
|
+
if find(options.remote)
|
27
|
+
say "Droplet #{options.remote} exists. Please destroy it first.", :red
|
28
|
+
exit 1
|
29
|
+
end
|
30
|
+
say "creating droplet: #{options.remote}"
|
31
|
+
d = client.droplets.create(DropletKit::Droplet.new(
|
32
|
+
name: options.remote,
|
33
|
+
region: options.region,
|
34
|
+
size: options[:size],
|
35
|
+
image: 'docker',
|
36
|
+
ssh_keys: client.ssh_keys.all.collect(&:id)))
|
37
|
+
say [d.id, d.status, d.name].join(' ')
|
38
|
+
end
|
39
|
+
|
40
|
+
desc 'list', 'list droplets'
|
41
|
+
def list
|
42
|
+
l = client.droplets.all.collect do |d|
|
43
|
+
[d.id, d.name, d.status, d.networks[:v4].first.ip_address]
|
44
|
+
end
|
45
|
+
l.unshift %w[id name status ip]
|
46
|
+
print_table l
|
47
|
+
end
|
48
|
+
|
49
|
+
desc 'destroy', 'destroy REMOTE droplet'
|
50
|
+
option :force, type: :boolean, desc: "don't prompt"
|
51
|
+
def destroy
|
52
|
+
force = options[:force]
|
53
|
+
say "Destroying droplet: #{options.remote}", force ? :red : nil
|
54
|
+
if force || yes?("Are you sure?", :red)
|
55
|
+
client.droplets.delete(id: find(options.remote).id)
|
56
|
+
else
|
57
|
+
say "#{options.remote} not destroyed", :red
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
desc 'push [SERVICES]', 'push service(s) to digitalocean (default all)'
|
62
|
+
option :backup, type: :boolean, desc: "Backup (tag) current version before push",
|
63
|
+
aliases: ['b']
|
64
|
+
option :tag, type: :string, desc: "tag name for backup", default: 'last'
|
65
|
+
def push(*args)
|
66
|
+
args = dockit.services.keys if args.empty?
|
67
|
+
say "Processing images for #{args}"
|
68
|
+
args.each do |k|
|
69
|
+
s = service(k)
|
70
|
+
unless s.image
|
71
|
+
say ". No image for #{k}!", :red
|
72
|
+
next
|
73
|
+
end
|
74
|
+
name = s.config.get(:build, :t)
|
75
|
+
id = s.image.id
|
76
|
+
msg = "#{k}: #{id[0..11]}(#{name})"
|
77
|
+
if ssh(options.remote, options.user,
|
78
|
+
"docker images --no-trunc | grep #{id} > /dev/null")
|
79
|
+
say ". Exists #{msg}"
|
80
|
+
else
|
81
|
+
if options.backup
|
82
|
+
tag = "#{k}:#{options.tag}"
|
83
|
+
say "Tagging #{name} as #{tag}"
|
84
|
+
ssh(options.remote, options.user, "docker tag #{name} #{tag}")
|
85
|
+
end
|
86
|
+
say ". Pushing #{msg}"
|
87
|
+
ssh(options.remote, options.user, 'docker load', "docker save #{name}")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
desc 'start [SERVICE]', 'start a container for SERVICE on remote server'
|
93
|
+
option :vars, type: :hash,
|
94
|
+
desc: 'extra environment variables not defined in Dockit.yaml'
|
95
|
+
def start(name)
|
96
|
+
s = service(name)
|
97
|
+
name = s.config.get(:create, :name) || s.config.get(:build, :t)
|
98
|
+
links = config(s, :run, :Links, 'l')
|
99
|
+
binds = config(s, :run, :Binds, 'v')
|
100
|
+
|
101
|
+
env = config(s, :create, :Env, 'e')
|
102
|
+
env << (options[:vars]||{}).collect { |k,v| ['-e', "#{k}='#{v}'"]}
|
103
|
+
env << ['-e', "ENV='#{options.env}'"]
|
104
|
+
|
105
|
+
cmd = ['docker', 'run', env, links, binds].join(' ')
|
106
|
+
ssh(options.remote, options.user, cmd)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
def ssh(host, user, cmd, src=nil)
|
111
|
+
src << '|' if src
|
112
|
+
system("#{src}ssh #{user}@#{host} #{cmd}")
|
113
|
+
end
|
114
|
+
|
115
|
+
def client
|
116
|
+
@client ||= DropletKit::Client.new(
|
117
|
+
access_token: File.read(File.join(Dir.home, '.digitalocean', options.token)))
|
118
|
+
end
|
119
|
+
|
120
|
+
def find(hostname)
|
121
|
+
client.droplets.all.find do |d|
|
122
|
+
d.name == hostname
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def dockit
|
127
|
+
@dockit ||= Dockit::Env.new
|
128
|
+
end
|
129
|
+
|
130
|
+
def service(name)
|
131
|
+
Dockit::Service.new(
|
132
|
+
service_file(name),
|
133
|
+
locals: {env: options.env ? "-#{options.env}" : ''}.merge(options[:locals]||{}))
|
134
|
+
end
|
135
|
+
|
136
|
+
def service_file(name)
|
137
|
+
file = dockit.services[name]
|
138
|
+
unless file
|
139
|
+
say "Service '#{name}' does not exist!", :red
|
140
|
+
exit 1
|
141
|
+
end
|
142
|
+
file
|
143
|
+
end
|
144
|
+
|
145
|
+
def config(service, phase, key, flag)
|
146
|
+
(service.config.get(phase, key)||[]).collect { |v| ["-#{flag}", "'#{v}'"] }
|
147
|
+
end
|
148
|
+
end
|
data/lib/dockit/image.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Dockit
|
2
|
+
class Image
|
3
|
+
attr_reader :image
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def list(all: false, filters: nil)
|
7
|
+
Docker::Image.all(all: all, filters: JSON.dump(filters))
|
8
|
+
end
|
9
|
+
|
10
|
+
def create(config)
|
11
|
+
unless config
|
12
|
+
STDERR.puts "No build target configured"
|
13
|
+
return
|
14
|
+
end
|
15
|
+
repos = config['t']
|
16
|
+
puts "Building #{repos}"
|
17
|
+
image = Docker::Image.build_from_dir('.', config) do |chunk|
|
18
|
+
begin
|
19
|
+
chunk = JSON.parse(chunk)
|
20
|
+
progress = chunk['progress']
|
21
|
+
id = progress ? '' : chunk['id']
|
22
|
+
print chunk['stream'] ? chunk['stream'] :
|
23
|
+
[chunk['status'], id, progress, progress ? "\r" : "\n"].join(' ')
|
24
|
+
rescue
|
25
|
+
puts chunk
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
image
|
30
|
+
end
|
31
|
+
|
32
|
+
def get(name)
|
33
|
+
Docker::Image.get(name)
|
34
|
+
end
|
35
|
+
|
36
|
+
def clean(force: false)
|
37
|
+
list(
|
38
|
+
all: force,
|
39
|
+
filters: force ? nil : {dangling: ['true']}
|
40
|
+
).each do |image|
|
41
|
+
puts " #{image.id}"
|
42
|
+
image.remove(force: true)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Dockit
|
2
|
+
class Service
|
3
|
+
attr_reader :config
|
4
|
+
attr_reader :image
|
5
|
+
|
6
|
+
def initialize(file="./Dockit.yaml", locals: {})
|
7
|
+
@config = Dockit::Config.new(file, locals)
|
8
|
+
|
9
|
+
# get the image if it is specified and already exists
|
10
|
+
if name = config.get(:create, :Image) || config.get(:build, :t)
|
11
|
+
begin
|
12
|
+
@image = Dockit::Image.get(name)
|
13
|
+
rescue Docker::Error::NotFoundError
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def build
|
19
|
+
@image = Dockit::Image.create(config.get(:build))
|
20
|
+
end
|
21
|
+
|
22
|
+
def start(options)
|
23
|
+
opts = merge_config(:create, stringify(options[:create]))
|
24
|
+
unless image || opts['Image']
|
25
|
+
raise "No runnable image found or specified!"
|
26
|
+
end
|
27
|
+
|
28
|
+
opts['Image'] ||= image.id if image
|
29
|
+
opts['name'] ||= config.get(:build, :t)
|
30
|
+
|
31
|
+
run = merge_config(:run, stringify(options[:run]))
|
32
|
+
|
33
|
+
if options[:verbose]
|
34
|
+
cmd = [(opts['Entrypoint']||[]), ((opts['Cmd'] || %w[default]))].flatten
|
35
|
+
puts " * %s (%s)" % [ opts['name'] || 'unnamed', cmd.join(' ') ]
|
36
|
+
|
37
|
+
puts " * #{run}" if run.length > 0
|
38
|
+
end
|
39
|
+
|
40
|
+
Dockit::Container.new(opts).start(
|
41
|
+
run, verbose: options[:verbose], transient: options[:transient])
|
42
|
+
end
|
43
|
+
|
44
|
+
def push(registry, tag=nil, force=false)
|
45
|
+
raise "No image found!" unless image
|
46
|
+
|
47
|
+
image.tag(repo: "#{registry}/#{config.get(:build, 't')}", force: force)
|
48
|
+
STDOUT.sync = true
|
49
|
+
image.push(tag: tag) do |chunk|
|
50
|
+
chunk = JSON.parse(chunk)
|
51
|
+
progress = chunk['progress']
|
52
|
+
id = progress ? '' : "#{chunk['id']} "
|
53
|
+
print chunk['status'], ' ', id, progress, progress ? "\r" : "\n"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def pull(registry, tag=nil, force=false)
|
58
|
+
unless repo = config.get(:build, 't')
|
59
|
+
STDERR.puts "No such locally built image"
|
60
|
+
exit 1
|
61
|
+
end
|
62
|
+
|
63
|
+
name = "#{registry}/#{repo}"
|
64
|
+
image = Docker::Image.create(
|
65
|
+
fromImage: name) do |chunk|
|
66
|
+
chunk = JSON.parse(chunk)
|
67
|
+
progress = chunk['progress']
|
68
|
+
id = progress ? '' : chunk['id']
|
69
|
+
print chunk['stream'] ? chunk['stream'] :
|
70
|
+
[chunk['status'], id, progress, progress ? "\r" : "\n"].join(' ')
|
71
|
+
end
|
72
|
+
puts "Tagging #{name} as #{repo}:#{tag||'latest'}"
|
73
|
+
image.tag(repo: repo, tag: tag, force: force)
|
74
|
+
end
|
75
|
+
|
76
|
+
def id
|
77
|
+
image.id
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
def merge_config(key, opts)
|
82
|
+
(config.get(key) || {}).merge(opts||{})
|
83
|
+
end
|
84
|
+
|
85
|
+
def stringify(hash)
|
86
|
+
return nil unless hash
|
87
|
+
Hash[hash.map {|k,v| [k.to_s, v]}]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|