dockit 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ class All < SubCommand
2
+ desc 'build', 'build modules'
3
+ def build
4
+ invoke_service 'mod'
5
+ invoke_default 'svc'
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ class Mod < SubCommand
2
+ desc 'build', 'prep and build this service'
3
+ def build
4
+ invoke_default
5
+ end
6
+ end