Many changes.
This commit is contained in:
parent
3bcaab101b
commit
5a3a1be5a2
229
dup.rb
229
dup.rb
@ -3,26 +3,32 @@ require 'yaml'
|
|||||||
require 'pp'
|
require 'pp'
|
||||||
require 'shellwords'
|
require 'shellwords'
|
||||||
require 'getoptlong'
|
require 'getoptlong'
|
||||||
|
require 'ostruct'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
MAPPINGS = [
|
MAPPINGS = [
|
||||||
['stdin_open', '--interactive', {:type=>:switch}],
|
['stdin_open', '--interactive', lambda{|c,i| c.Config.OpenStdin}, {:type=>:switch}],
|
||||||
['tty', '--tty', {:type=>:switch}],
|
['tty', '--tty', lambda{|c,i| c.Config.Tty}, {:type=>:switch}],
|
||||||
['detach', '--detach', {:type=>:switch}],
|
#['detach', '--detach', {:type=>:switch}],
|
||||||
['remove', '--rm', {:type=>:switch}],
|
['remove', '--rm', lambda{|c,i| c.HostConfig.AutoRemove}, {:type=>:switch}],
|
||||||
['container_name', '--name', {:type=>:name}],
|
['container_name', '--name', nil, {:type=>:name}],
|
||||||
['ports', '--publish'],
|
['ports', '--publish', lambda{|c,i| c.HostConfig.PortBindings.to_h.collect{|k,v| "#{v.first.HostPort}:#{k.to_s.gsub("/tcp", "")}" }}],
|
||||||
['restart', '--restart'],
|
['restart', '--restart', lambda{|c,i| c.HostConfig.RestartPolicy.Name}],
|
||||||
['environment', '--env'],
|
['environment', '--env', lambda{|c,i| e=c.Config.Env.delete_if{|v| i.Config.Env.include?(v)}; e}],
|
||||||
['volumes', '--volume'],
|
['volumes', '--volume', lambda{|c,i| c.HostConfig.Binds}],
|
||||||
['mem_limit', '--memory'],
|
['mem_limit', '--memory', lambda{|c,i| (mem=c.HostConfig.Memory)==0 ? nil : mem}],
|
||||||
['links', '--link'],
|
['links', '--link'],
|
||||||
['stop_signal', '--stop-signal'],
|
['stop_signal', '--stop-signal', lambda{|c,i| c.Config.StopSignal!="SIGTERM" ? c.Config.StopSignal : nil}],
|
||||||
['stop_grace_period', '--stop-timeout'],
|
['stop_grace_period', '--stop-timeout', lambda{|c,i| c.Config.StopTimeout}],
|
||||||
['devices', '--device'],
|
['devices', '--device'],
|
||||||
['net', '--net'],
|
['net', '--net'],
|
||||||
['entrypoint', '--entrypoint'],
|
['networks', '--network', lambda{|c,i| c.NetworkSettings.Networks.to_h.keys.map(&:to_s)}, {:type=>:hidden}],
|
||||||
['image', nil],
|
['entrypoint', '--entrypoint', lambda{|c,i| (ep=c.Config.Entrypoint) == i.Config.Entrypoint ? nil : ep}, {:allow_empty=>true}],
|
||||||
['command', nil, {:escape=>false}]
|
['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}]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -31,10 +37,7 @@ def get_sample(name="container")
|
|||||||
#{name}:
|
#{name}:
|
||||||
image: "my/image:1.2.3"
|
image: "my/image:1.2.3"
|
||||||
restart: always
|
restart: always
|
||||||
command: "/bin/bash"
|
detach: true
|
||||||
entrypoint: "/script.sh"
|
|
||||||
build: "/data/dir"
|
|
||||||
pull: false
|
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "1234:1234"
|
- "1234:1234"
|
||||||
@ -46,17 +49,36 @@ def get_sample(name="container")
|
|||||||
TZ:
|
TZ:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- "/etc:/etc:ro"
|
- "/etc/localtime:/etc/localtime:ro"
|
||||||
|
|
||||||
links:
|
links:
|
||||||
- "another_container"
|
- "another_container"
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- "nginx"
|
||||||
|
- "mosquitto"
|
||||||
|
- "bridge"
|
||||||
|
|
||||||
|
test: true
|
||||||
|
command: "/bin/bash"
|
||||||
|
entrypoint: "/script.sh"
|
||||||
|
build: "/data/dir"
|
||||||
|
pull: false
|
||||||
container_name: "Something"
|
container_name: "Something"
|
||||||
|
hostname: "foo"
|
||||||
mem_limit: 125M
|
mem_limit: 125M
|
||||||
stdin_open: false
|
stdin_open: false
|
||||||
net: host
|
net: host
|
||||||
tty: false
|
tty: false
|
||||||
detach: true
|
|
||||||
remove: false
|
remove: false
|
||||||
stop_signal: SIGUSR1
|
stop_signal: SIGUSR1
|
||||||
stop_grace_period: 5
|
stop_grace_period: 5
|
||||||
@ -87,12 +109,19 @@ def action_help
|
|||||||
puts "-s, --sample Outputs a sample yml file."
|
puts "-s, --sample Outputs a sample yml file."
|
||||||
puts "-c, --create Create a new container yml file."
|
puts "-c, --create Create a new container yml file."
|
||||||
puts "-p, --pull (Try to) Pull the image(s) before starting the container."
|
puts "-p, --pull (Try to) Pull the image(s) before starting the container."
|
||||||
|
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
|
exit 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def run(cmd, ignore_returnvalue=false)
|
def run(cmd, ignore_returnvalue=false, catch_interrupt=false)
|
||||||
puts "+ #{cmd}"
|
puts "+ #{cmd}" if $dry_run
|
||||||
returnvalue = $dry_run ? true : system("bash -c #{cmd.shellescape}")
|
returnvalue=false
|
||||||
|
begin
|
||||||
|
returnvalue = $dry_run ? true : system("bash -c #{cmd.shellescape}")
|
||||||
|
rescue Interrupt
|
||||||
|
raise if not catch_interrupt
|
||||||
|
end
|
||||||
raise "Command returned a non-zero exit value." if returnvalue!=true && !ignore_returnvalue
|
raise "Command returned a non-zero exit value." if returnvalue!=true && !ignore_returnvalue
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -111,6 +140,7 @@ def action_create(container, file)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def action_run(container, file)
|
def action_run(container, file)
|
||||||
|
# $net_host
|
||||||
raise "File #{file} not found." unless File.exists?(file)
|
raise "File #{file} not found." unless File.exists?(file)
|
||||||
|
|
||||||
data = File.open(file, "r") {|f| YAML.load(f.read)}
|
data = File.open(file, "r") {|f| YAML.load(f.read)}
|
||||||
@ -125,12 +155,28 @@ def action_run(container, file)
|
|||||||
(data["after_build"] || []).each{|cmd| run(cmd)}
|
(data["after_build"] || []).each{|cmd| run(cmd)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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)}
|
(data["before_run"] || []).each{|cmd| run(cmd)}
|
||||||
|
|
||||||
cmd = ["docker", "run"]
|
cmd = ["docker", "create"]
|
||||||
|
cmd << "--net" << data["networks"][0] if data["networks"] && !$net_host
|
||||||
|
cmd << "--net" << "host" if $net_host
|
||||||
|
|
||||||
MAPPINGS.each do |mapping|
|
MAPPINGS.each do |mapping|
|
||||||
yml_name, cmd_name, options = *mapping
|
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 ||= {}
|
options ||= {}
|
||||||
if !data[yml_name]
|
if !data[yml_name]
|
||||||
if options[:type]==:name
|
if options[:type]==:name
|
||||||
@ -139,6 +185,12 @@ def action_run(container, file)
|
|||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if options[:allow_empty] && data[yml_name]==""
|
||||||
|
cmd << cmd_name << '""'
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
if options[:type]==:switch
|
if options[:type]==:switch
|
||||||
cmd << cmd_name
|
cmd << cmd_name
|
||||||
next
|
next
|
||||||
@ -156,36 +208,103 @@ def action_run(container, file)
|
|||||||
if data["pull"] || $pull
|
if data["pull"] || $pull
|
||||||
run("docker pull #{data["image"].shellescape}", true)
|
run("docker pull #{data["image"].shellescape}", true)
|
||||||
end
|
end
|
||||||
run("docker rm -f #{(data["container_name"] || key).shellescape}", true)
|
puts "Stopping and removing old container..."
|
||||||
run(cmd.compact.join(" "))
|
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)}
|
(data["after_run"] || []).each{|cmd| run(cmd)}
|
||||||
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def action_list(base_dir)
|
def list(base_dir)
|
||||||
errors = []
|
result = {:containers=>{}, :errors=>[]}
|
||||||
Dir[File.join(base_dir, "*.yml")].sort.each do |file|
|
Dir[File.join(base_dir, "*.yml")].sort.each do |file|
|
||||||
basename = File.basename(file)[0..-5]
|
basename = File.basename(file)[0..-5]
|
||||||
yaml = File.open(file, "r") {|f| YAML.load(f.read)}
|
yaml = File.open(file, "r") {|f| YAML.load(f.read)}
|
||||||
keys = yaml.keys
|
keys = yaml.keys
|
||||||
|
|
||||||
if keys.include? basename
|
if keys.include? basename
|
||||||
puts basename
|
additional = keys.reject{|k| k==basename}
|
||||||
keys.each {|k| puts " + #{k}" unless k==basename}
|
result[:containers][basename] = additional
|
||||||
else
|
else
|
||||||
errors << "#{file}:\n Missing a key called #{basename}\n Has keys:\n#{keys.map{|k| " #{k}"}.join("\n")}"
|
result[:errors] << "#{file}:\n Missing a key called #{basename}\n Has keys:\n#{keys.map{|k| " #{k}"}.join("\n")}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
return result
|
||||||
if !errors.empty?
|
end
|
||||||
puts
|
|
||||||
puts "ERRORS:"
|
def action_list(base_dir)
|
||||||
puts errors.join("\n\n")
|
list = list(base_dir)
|
||||||
exit 1
|
list[:containers].each do |container, additional|
|
||||||
|
puts container
|
||||||
|
additional.each{|c| puts " + #{c}"}
|
||||||
end
|
end
|
||||||
|
|
||||||
exit 0
|
list[:errors].each do |e|
|
||||||
|
puts "ERROR: #{e}"
|
||||||
|
puts
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
action = :run
|
action = :run
|
||||||
@ -194,6 +313,9 @@ $dry_run = false
|
|||||||
$pull = false
|
$pull = false
|
||||||
needs_container = true
|
needs_container = true
|
||||||
needs_basedir = true
|
needs_basedir = true
|
||||||
|
completion_str = ""
|
||||||
|
silent_basedir = false
|
||||||
|
$net_host = false
|
||||||
|
|
||||||
opts = GetoptLong.new(
|
opts = GetoptLong.new(
|
||||||
[ '--sample', '-s', GetoptLong::NO_ARGUMENT ],
|
[ '--sample', '-s', GetoptLong::NO_ARGUMENT ],
|
||||||
@ -202,6 +324,10 @@ opts = GetoptLong.new(
|
|||||||
[ '--dry-run', '-n', GetoptLong::NO_ARGUMENT ],
|
[ '--dry-run', '-n', GetoptLong::NO_ARGUMENT ],
|
||||||
[ '--list', '-l', GetoptLong::NO_ARGUMENT ],
|
[ '--list', '-l', GetoptLong::NO_ARGUMENT ],
|
||||||
[ '--pull', '-p', GetoptLong::NO_ARGUMENT ],
|
[ '--pull', '-p', GetoptLong::NO_ARGUMENT ],
|
||||||
|
[ '--update', '-u', GetoptLong::NO_ARGUMENT ],
|
||||||
|
[ '--_completion', GetoptLong::OPTIONAL_ARGUMENT ],
|
||||||
|
[ '--host', GetoptLong::NO_ARGUMENT ],
|
||||||
|
[ '--test', GetoptLong::NO_ARGUMENT ]
|
||||||
)
|
)
|
||||||
|
|
||||||
opts.each do |opt, arg|
|
opts.each do |opt, arg|
|
||||||
@ -221,6 +347,21 @@ opts.each do |opt, arg|
|
|||||||
needs_container = false
|
needs_container = false
|
||||||
when '--pull'
|
when '--pull'
|
||||||
$pull = true
|
$pull = true
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -235,7 +376,7 @@ if needs_basedir
|
|||||||
base_dir = ENV['DUP_DIR']
|
base_dir = ENV['DUP_DIR']
|
||||||
else
|
else
|
||||||
base_dir = File.join(Dir.home, ".dup")
|
base_dir = File.join(Dir.home, ".dup")
|
||||||
puts "Environment variable DUP_DIR is not set. Looking for .yml files in #{base_dir}"
|
puts "Environment variable DUP_DIR is not set. Looking for .yml files in #{base_dir}" unless silent_basedir
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -247,6 +388,12 @@ elsif action == :run
|
|||||||
action_run(container, file)
|
action_run(container, file)
|
||||||
elsif action == :list
|
elsif action == :list
|
||||||
action_list(base_dir)
|
action_list(base_dir)
|
||||||
|
elsif action == :update
|
||||||
|
action_update(base_dir)
|
||||||
|
elsif action == :test
|
||||||
|
action_test(container)
|
||||||
|
elsif action == :_completion
|
||||||
|
action_completion(base_dir, completion_str)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user