From 5a3a1be5a208391b873a365a272f2774b356f6ea Mon Sep 17 00:00:00 2001 From: Fabian Schlenz Date: Wed, 7 Nov 2018 13:21:23 +0100 Subject: [PATCH] Many changes. --- dup.rb | 229 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 188 insertions(+), 41 deletions(-) diff --git a/dup.rb b/dup.rb index 25412f6..f1cf954 100755 --- a/dup.rb +++ b/dup.rb @@ -3,26 +3,32 @@ require 'yaml' require 'pp' require 'shellwords' require 'getoptlong' +require 'ostruct' +require 'json' MAPPINGS = [ - ['stdin_open', '--interactive', {:type=>:switch}], - ['tty', '--tty', {:type=>:switch}], - ['detach', '--detach', {:type=>:switch}], - ['remove', '--rm', {:type=>:switch}], - ['container_name', '--name', {:type=>:name}], - ['ports', '--publish'], - ['restart', '--restart'], - ['environment', '--env'], - ['volumes', '--volume'], - ['mem_limit', '--memory'], + ['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'], - ['stop_signal', '--stop-signal'], - ['stop_grace_period', '--stop-timeout'], + ['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'], - ['entrypoint', '--entrypoint'], - ['image', nil], - ['command', nil, {:escape=>false}] + ['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}] ] @@ -31,10 +37,7 @@ def get_sample(name="container") #{name}: image: "my/image:1.2.3" restart: always - command: "/bin/bash" - entrypoint: "/script.sh" - build: "/data/dir" - pull: false + detach: true ports: - "1234:1234" @@ -46,17 +49,36 @@ def get_sample(name="container") TZ: volumes: - - "/etc:/etc:ro" + - "/etc/localtime:/etc/localtime:ro" links: - "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" + hostname: "foo" mem_limit: 125M stdin_open: false net: host tty: false - detach: true remove: false stop_signal: SIGUSR1 stop_grace_period: 5 @@ -87,12 +109,19 @@ def action_help 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." + 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 -def run(cmd, ignore_returnvalue=false) - puts "+ #{cmd}" - returnvalue = $dry_run ? true : system("bash -c #{cmd.shellescape}") +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 raise "Command returned a non-zero exit value." if returnvalue!=true && !ignore_returnvalue end @@ -111,6 +140,7 @@ def action_create(container, file) end def action_run(container, file) + # $net_host raise "File #{file} not found." unless File.exists?(file) 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)} 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)} - 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| - 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 ||= {} if !data[yml_name] if options[:type]==:name @@ -139,6 +185,12 @@ def action_run(container, file) next end + if options[:allow_empty] && data[yml_name]=="" + cmd << cmd_name << '""' + next + end + + if options[:type]==:switch cmd << cmd_name next @@ -156,36 +208,103 @@ def action_run(container, file) if data["pull"] || $pull run("docker pull #{data["image"].shellescape}", true) end - run("docker rm -f #{(data["container_name"] || key).shellescape}", true) - run(cmd.compact.join(" ")) + 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)} + + 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 -def action_list(base_dir) - errors = [] +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 - puts basename - keys.each {|k| puts " + #{k}" unless k==basename} + additional = keys.reject{|k| k==basename} + result[:containers][basename] = additional 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 - - if !errors.empty? - puts - puts "ERRORS:" - puts errors.join("\n\n") - exit 1 + 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 - 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 action = :run @@ -194,6 +313,9 @@ $dry_run = false $pull = false needs_container = true needs_basedir = true +completion_str = "" +silent_basedir = false +$net_host = false opts = GetoptLong.new( [ '--sample', '-s', GetoptLong::NO_ARGUMENT ], @@ -202,6 +324,10 @@ opts = GetoptLong.new( [ '--dry-run', '-n', GetoptLong::NO_ARGUMENT ], [ '--list', '-l', 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| @@ -221,6 +347,21 @@ opts.each do |opt, arg| needs_container = false when '--pull' $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 @@ -235,7 +376,7 @@ if needs_basedir base_dir = ENV['DUP_DIR'] else 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 @@ -247,6 +388,12 @@ elsif action == :run action_run(container, file) elsif action == :list 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