dockit 1.0.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 +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
|