docker-tools/dup.rb

402 lines
11 KiB
Ruby
Raw Normal View History

2017-02-08 15:45:53 +00:00
#!/usr/bin/ruby
require 'yaml'
require 'pp'
require 'shellwords'
require 'getoptlong'
2018-11-07 12:21:23 +00:00
require 'ostruct'
require 'json'
MAPPINGS = [
2018-11-07 12:21:23 +00:00
['stdin_open', '--interactive', lambda{|c,i| c.Config.OpenStdin}, {:type=>:switch}],
['tty', '--tty', lambda{|c,i| c.Config.Tty}, {:type=>:switch}],
#['detach', '--detach', {:type=>:switch}],
['remove', '--rm', lambda{|c,i| c.HostConfig.AutoRemove}, {:type=>:switch}],
['container_name', '--name', nil, {:type=>:name}],
['ports', '--publish', lambda{|c,i| c.HostConfig.PortBindings.to_h.collect{|k,v| "#{v.first.HostPort}:#{k.to_s.gsub("/tcp", "")}" }}],
['restart', '--restart', lambda{|c,i| c.HostConfig.RestartPolicy.Name}],
['environment', '--env', lambda{|c,i| e=c.Config.Env.delete_if{|v| i.Config.Env.include?(v)}; e}],
['volumes', '--volume', lambda{|c,i| c.HostConfig.Binds}],
['mem_limit', '--memory', lambda{|c,i| (mem=c.HostConfig.Memory)==0 ? nil : mem}],
['links', '--link'],
2018-11-07 12:21:23 +00:00
['stop_signal', '--stop-signal', lambda{|c,i| c.Config.StopSignal!="SIGTERM" ? c.Config.StopSignal : nil}],
['stop_grace_period', '--stop-timeout', lambda{|c,i| c.Config.StopTimeout}],
['devices', '--device'],
['net', '--net'],
2019-03-21 14:48:53 +00:00
['ipv6', '--ipv6'],
2018-11-07 12:21:23 +00:00
['networks', '--network', lambda{|c,i| c.NetworkSettings.Networks.to_h.keys.map(&:to_s)}, {:type=>:hidden}],
['entrypoint', '--entrypoint', lambda{|c,i| (ep=c.Config.Entrypoint) == i.Config.Entrypoint ? nil : ep}, {:allow_empty=>true}],
['labels', '--label', lambda{|c,i| l=Hash[c.Config.Labels.to_h.delete_if{|k,v| i.Config.Labels[k]==v rescue false}.map{|k,v| [k.to_s, v]}] ; l}],
['hostname', '--hostname'],
['working_dir', '--workdir', lambda{|c,i| (wd=c.Config.WorkingDir) == i.Config.WorkingDir ? nil : wd}],
['image', nil, lambda{|c,i| c.Config.Image}],
['command', nil, lambda{|c,i| (cmd=c.Config.Cmd.join(" ") rescue nil) == (i.Config.Cmd.join(" ") rescue nil)? nil : cmd}, {:escape=>false}]
]
2017-02-08 15:45:53 +00:00
def get_sample(name="container")
return <<~HEREDOC
#{name}:
image: "my/image:1.2.3"
restart: always
2018-11-07 12:21:23 +00:00
detach: true
test: true # Having this set to true will prevent dup from creating a label de.fabianonline.dup
2017-02-08 15:45:53 +00:00
ports:
- "1234:1234"
- "8080"
- "1001:1001/udp"
environment:
VERSION: 1.7.0
TZ:
volumes:
2018-11-07 12:21:23 +00:00
- "/etc/localtime:/etc/localtime:ro"
2017-02-08 15:45:53 +00:00
links:
- "another_container"
2018-11-07 12:21:23 +00:00
labels:
com.centurylinklabs.watchtower.enable: true
nginx_virtual_host: container.home.fabianonline.de
nginx_port: 80
nginx_allow: fabian # "user" or "@group" or "user, user, @group" or "all"
nginx_no_robots: true
nginx_public_paths: "/public, /api"
nginx_type: # "http" (default), "fastcgi", "skip" (doesn't create any entries)
2017-02-08 15:45:53 +00:00
2018-11-07 12:21:23 +00:00
networks:
- "nginx"
- "mosquitto"
- "bridge"
test: true
command: "/bin/bash"
entrypoint: "/script.sh"
build: "/data/dir"
pull: false
2017-02-08 15:45:53 +00:00
container_name: "Something"
2018-11-07 12:21:23 +00:00
hostname: "foo"
2017-02-08 15:45:53 +00:00
mem_limit: 125M
stdin_open: false
net: host
tty: false
2017-02-08 15:45:53 +00:00
remove: false
stop_signal: SIGUSR1
stop_grace_period: 5
2017-02-08 15:45:53 +00:00
before_build:
- "echo 'Starting build'"
after_build:
- "echo 'After build'"
before_run:
- "echo 'Starting run'"
after_run:
- "echo 'Run has finished.'"
- "docker restart container"
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0"
HEREDOC
end
def action_sample
2017-02-08 15:45:53 +00:00
puts get_sample
exit 1
end
def action_help
puts "-h, --help Show this help."
puts "-n, --dry-run Don't execute any commands."
puts "-s, --sample Outputs a sample yml file."
puts "-c, --create Create a new container yml file."
puts "-p, --pull (Try to) Pull the image(s) before starting the container."
2018-11-07 12:21:23 +00:00
puts "-l, --list Lists all available containers."
puts " --host Starts a container with net: host - which implies not using ports, networks and/or links."
exit 1
end
2018-11-07 12:21:23 +00:00
def run(cmd, ignore_returnvalue=false, catch_interrupt=false)
puts "+ #{cmd}" if $dry_run
returnvalue=false
begin
returnvalue = $dry_run ? true : system("bash -c #{cmd.shellescape}")
rescue Interrupt
raise if not catch_interrupt
end
2017-02-08 15:45:53 +00:00
raise "Command returned a non-zero exit value." if returnvalue!=true && !ignore_returnvalue
end
def esc(obj, escape=true)
return obj unless escape
return obj.to_s.shellescape
end
def action_create(container, file)
2017-02-08 15:45:53 +00:00
raise "File #{file} already exists" if File.exists?(file)
File.open(file, "w") {|f| f.write(get_sample(container))}
2017-02-08 15:45:53 +00:00
2019-02-26 08:55:40 +00:00
exec("/usr/bin/editor #{file.shellescape}")
2017-02-08 15:45:53 +00:00
exit 1 # will never be reached because exec replaces this process. But just in case... ;-)
end
def action_run(container, file)
2018-11-07 12:21:23 +00:00
# $net_host
raise "File #{file} not found." unless File.exists?(file)
2017-02-08 15:45:53 +00:00
data = File.open(file, "r") {|f| YAML.load(f.read)}
2017-02-08 15:45:53 +00:00
raise "Expected #{file} to define (at least) a container named #{container}." unless data.has_key?(container)
2017-02-08 15:45:53 +00:00
data.each do |key, data|
if data["build"]
(data["before_build"] || []).each{|cmd| run(cmd)}
cmd = ["docker", "build", "-t", data["image"].shellescape, data["build"].shellescape]
run(cmd.join(" "))
(data["after_build"] || []).each{|cmd| run(cmd)}
end
2018-11-07 12:21:23 +00:00
if !data["test"]
if data["labels"]==nil
data["labels"] = ["de.fabianonline.dup=true"]
elsif data["labels"].is_a? Array
data["labels"] << "de.fabianonline.dup=true"
elsif data["labels"].is_a? Hash
data["labels"]["de.fabianonline.dup"] = "true"
else
raise "data['labels'] is of an unexpected type: #{data["labels"].class}"
end
end
(data["before_run"] || []).each{|cmd| run(cmd)}
2018-11-07 12:21:23 +00:00
cmd = ["docker", "create"]
cmd << "--net" << data["networks"][0] if data["networks"] && !$net_host
cmd << "--net" << "host" if $net_host
MAPPINGS.each do |mapping|
2018-11-07 12:21:23 +00:00
yml_name, cmd_name, reverse_lambda, options = *mapping
next if $net_host && %w(links net ports).include?(yml_name)
next if options && options[:type]==:hidden
options ||= {}
if !data[yml_name]
if options[:type]==:name
cmd << cmd_name << esc(key)
end
next
end
2018-11-07 12:21:23 +00:00
if options[:allow_empty] && data[yml_name]==""
cmd << cmd_name << '""'
next
end
if options[:type]==:switch
cmd << cmd_name
next
end
if data[yml_name].is_a?(Array)
data[yml_name].each {|val| cmd << cmd_name << esc(val, options[:escape])}
elsif data[yml_name].is_a?(Hash)
data[yml_name].each {|key, val| cmd << cmd_name << "#{esc(key, options[:escape])}=\"#{esc(val, options[:escape])}\""}
else
cmd << cmd_name << esc(data[yml_name], options[:escape])
end
end
if data["pull"] || $pull
run("docker pull #{data["image"].shellescape}", true)
end
2018-11-07 12:21:23 +00:00
puts "Stopping and removing old container..."
run("docker rm -f #{(data["container_name"] || key).shellescape} >/dev/null 2>&1", true)
if !$net_host && (data["networks"]||=[]).count>0
networks = `docker network ls --format '{{.Name}}'`.split
data["networks"].each do |network|
if networks.include?(network)
puts "Network #{network} exists."
else
puts "Creating network #{network}..."
run("docker network create #{network.shellescape}", true)
end
end
end
puts "Creating container..."
run(cmd.compact.join(" ") + " >/dev/null")
if !$net_host && data["networks"].count>0
data["networks"].each do |network|
puts "Connecting container to network #{network}..."
run("docker network connect #{network.shellescape} #{(data["container_name"] || key).shellescape} >/dev/null", true)
end
end
puts "Starting container..."
run("docker start #{(data["container_name"] || key).shellescape} >/dev/null")
(data["after_run"] || []).each{|cmd| run(cmd)}
2018-11-07 12:21:23 +00:00
if ! data["detach"]
puts "Attaching container..."
run("docker attach #{(data["container_name"] || key).shellescape}")
else
puts "Opening log... (press Ctrl-C to exit)"
puts
run("docker logs -f #{(data["container_name"] || key).shellescape}", true, true)
end
2017-02-08 15:45:53 +00:00
end
end
2018-11-07 12:21:23 +00:00
def list(base_dir)
result = {:containers=>{}, :errors=>[]}
Dir[File.join(base_dir, "*.yml")].sort.each do |file|
basename = File.basename(file)[0..-5]
yaml = File.open(file, "r") {|f| YAML.load(f.read)}
keys = yaml.keys
if keys.include? basename
2018-11-07 12:21:23 +00:00
additional = keys.reject{|k| k==basename}
result[:containers][basename] = additional
else
2018-11-07 12:21:23 +00:00
result[:errors] << "#{file}:\n Missing a key called #{basename}\n Has keys:\n#{keys.map{|k| " #{k}"}.join("\n")}"
end
end
2018-11-07 12:21:23 +00:00
return result
end
def action_list(base_dir)
list = list(base_dir)
list[:containers].each do |container, additional|
puts container
additional.each{|c| puts " + #{c}"}
end
2018-11-07 12:21:23 +00:00
list[:errors].each do |e|
puts "ERROR: #{e}"
puts
end
2018-11-07 12:21:23 +00:00
return list[:errors].empty? ? 0 : 1
end
def action_completion(base_dir, complete_str)
#puts "in completion"
return unless base_dir
#puts "str is #{complete_str}"
containers = list(base_dir)[:containers].keys.select{|c| c.start_with?(complete_str)}
puts containers.join(" ")
end
def action_test(container)
c_data = `docker inspect #{container}`
c = JSON.parse(c_data, object_class: OpenStruct).first
i_data = `docker inspect #{c.Image}`
i = JSON.parse(i_data, object_class: OpenStruct).first
data = {}
MAPPINGS.each do |m|
name, cmd, lmbd, opts = *m
next unless lmbd
result = lmbd.call(c, i)
next if opts && opts[:type]==:switch && result==false
next if result==nil || result==[]
data[name] = result
end
puts ({container=>data}.to_yaml.lines[1..-1].join())
end
action = :run
container = nil
$dry_run = false
$pull = false
needs_container = true
needs_basedir = true
2018-11-07 12:21:23 +00:00
completion_str = ""
silent_basedir = false
$net_host = false
opts = GetoptLong.new(
[ '--sample', '-s', GetoptLong::NO_ARGUMENT ],
[ '--create', '-c', GetoptLong::NO_ARGUMENT ],
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
[ '--dry-run', '-n', GetoptLong::NO_ARGUMENT ],
[ '--list', '-l', GetoptLong::NO_ARGUMENT ],
[ '--pull', '-p', GetoptLong::NO_ARGUMENT ],
2018-11-07 12:21:23 +00:00
[ '--update', '-u', GetoptLong::NO_ARGUMENT ],
[ '--_completion', GetoptLong::OPTIONAL_ARGUMENT ],
[ '--host', GetoptLong::NO_ARGUMENT ],
[ '--test', GetoptLong::NO_ARGUMENT ]
)
opts.each do |opt, arg|
case opt
when '--sample'
action_sample
when '--create'
action = :create
needs_basedir = true
when '--help'
action_help
when '--dry-run'
puts "Dry-run. Not going to execute any command."
$dry_run = true
when '--list'
action = :list
needs_container = false
when '--pull'
$pull = true
2018-11-07 12:21:23 +00:00
when '--update'
action = :update
needs_container = false
needs_basedir = true
when '--_completion'
action = :_completion
completion_str = arg
needs_container = false
needs_basedir = true
silent_basedir = true
when '--host'
$net_host = true
when '--test'
action = :test
needs_basedir = false
end
end
container = ARGV.shift
if needs_container && (container==nil || container=="")
raise "No container given."
end
if needs_basedir
if ENV['DUP_DIR']
base_dir = ENV['DUP_DIR']
else
base_dir = File.join(Dir.home, ".dup")
2018-11-07 12:21:23 +00:00
puts "Environment variable DUP_DIR is not set. Looking for .yml files in #{base_dir}" unless silent_basedir
end
end
file = "%s/%s.yml" % [ base_dir, container ]
if action == :create
action_create(container, file)
elsif action == :run
action_run(container, file)
elsif action == :list
action_list(base_dir)
2018-11-07 12:21:23 +00:00
elsif action == :update
action_update(base_dir)
elsif action == :test
action_test(container)
elsif action == :_completion
action_completion(base_dir, completion_str)
end