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.
@@ -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