From 237713148314235f6eb08f605f2b1fbe5c6a2855 Mon Sep 17 00:00:00 2001 From: Fabian Schlenz Date: Wed, 17 Apr 2019 09:19:05 +0200 Subject: [PATCH] Code ist jetzt objektorientiert; man kann mehrere Container auf einmal starten. --- dup.rb | 362 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 213 insertions(+), 149 deletions(-) diff --git a/dup.rb b/dup.rb index 9e7e506..1da1135 100755 --- a/dup.rb +++ b/dup.rb @@ -114,10 +114,11 @@ def action_help 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." + puts " --regenerate Generates YAML content from a running container." exit 1 end -def run(cmd, ignore_returnvalue=false, catch_interrupt=false) +def run_cmd(cmd, ignore_returnvalue=false, catch_interrupt=false) puts "+ #{cmd}" if $dry_run returnvalue=false begin @@ -133,7 +134,8 @@ def esc(obj, escape=true) return obj.to_s.shellescape end -def action_create(container, file) +def action_create(container) + file = container.file raise "File #{file} already exists" if File.exists?(file) File.open(file, "w") {|f| f.write(get_sample(container))} @@ -142,110 +144,9 @@ def action_create(container, file) exit 1 # will never be reached because exec replaces this process. But just in case... ;-) end -def action_run(container, file) +def action_run(container) # $net_host - raise "File #{file} not found." unless File.exists?(file) - - data = File.open(file, "r") {|f| YAML.load(f.read)} - - raise "Expected #{file} to define (at least) a container named #{container}." unless data.has_key?(container) - - 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 - - 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", "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, 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 - - 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 - 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 + container.each{|c| c.run(container.count>1)} end def list(base_dir) @@ -288,26 +189,194 @@ def action_completion(base_dir, 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 +def action_regenerate(container) + container.each do |c| + data = c.regenerate + puts data.to_yaml + puts + end +end + +class Container + def initialize(name) + @name = name end - puts ({container=>data}.to_yaml.lines[1..-1].join()) + def load + raise "File #{self.filename} not found." unless File.exists?(self.filename) + + @data = File.open(self.filename, "r") {|f| YAML.load(f.read)} + + if @data.has_key?(@name) + if @data.keys.count > 1 + raise "Expected #{self.filename} to contain all data on top level or underneath a single root element named #{@name}." + end + @data = @data[@name] + end + end + + def filename; "%s/%s.yml" % [$base_dir, @name]; end + + def build_run_command + cmd = ["docker", "create"] + cmd << "--net" << @data["networks"][0] if @data["networks"] && !$net_host + cmd << "--net" << "host" if $net_host + + 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 + + MAPPINGS.each do |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 + cmd << cmd_name << esc(@name) + end + next + end + + 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 + + return cmd + end + + def build + (@data["before_build"] || []).each{|cmd| run_cmd(cmd)} + cmd = ["docker", "build", "-t", @data["image"].shellescape, @data["build"].shellescape] + run_cmd(cmd.join(" ")) + (@data["after_build"] || []).each{|cmd| run_cmd(cmd)} + end + + def pull + run_cmd("docker pull #{@data["image"].shellescape}", true) + end + + def stop_and_remove + puts "Stopping and removing old container..." + run_cmd("docker rm -f #{@name.shellescape} >/dev/null 2>&1", true) + end + + def create_networks + 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_cmd("docker network create #{network.shellescape}", true) + end + end + end + end + + def connect_to_networks + if !$net_host && @data["networks"].count>0 + @data["networks"].each do |network| + puts "Connecting container to network #{network}..." + run_cmd("docker network connect #{network.shellescape} #{@name.shellescape} >/dev/null", true) + end + end + end + + def attach + puts "Attaching container..." + run_cmd("docker attach #{@name.shellescape}") + end + + def log + puts "Opening log... (press Ctrl-C to exit)" + puts + run_cmd("docker logs -f #{@name.shellescape}", true, true) + end + + def run(force_background = false) + raise "No data loaded. Maybe call .load first?" if @data.nil? + + if @data["build"] + self.build + end + + (@data["before_run"] || []).each{|cmd| run_cmd(cmd)} + + if @data["pull"] || $pull + self.pull + end + + self.stop_and_remove + + self.create_networks + + puts "Creating container..." + cmd = build_run_command + run_cmd(cmd.compact.join(" ") + " >/dev/null") + + self.connect_to_networks + + puts "Starting container..." + run_cmd("docker start #{@name.shellescape} >/dev/null") + + (@data["after_run"] || []).each{|cmd| run_cmd(cmd)} + + if !force_background + if ! @data["detach"] + self.attach + else + self.log + end + end + end + + def regenerate + c_data = `docker inspect #{@name}` + 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 + + return data + end end action = :run @@ -315,11 +384,17 @@ container = nil $dry_run = false $pull = false needs_container = true -needs_basedir = true completion_str = "" silent_basedir = false $net_host = false +if ENV['DUP_DIR'] + $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}" unless silent_basedir +end + opts = GetoptLong.new( [ '--sample', '-s', GetoptLong::NO_ARGUMENT ], [ '--create', '-c', GetoptLong::NO_ARGUMENT ], @@ -330,7 +405,7 @@ opts = GetoptLong.new( [ '--update', '-u', GetoptLong::NO_ARGUMENT ], [ '--_completion', GetoptLong::OPTIONAL_ARGUMENT ], [ '--host', GetoptLong::NO_ARGUMENT ], - [ '--test', GetoptLong::NO_ARGUMENT ] + [ '--regenerate', GetoptLong::NO_ARGUMENT ] ) opts.each do |opt, arg| @@ -339,7 +414,6 @@ opts.each do |opt, arg| action_sample when '--create' action = :create - needs_basedir = true when '--help' action_help when '--dry-run' @@ -353,50 +427,40 @@ opts.each do |opt, arg| 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 + when '--regenerate' + action = :regenerate end end -container = ARGV.shift +container = ARGV.map{|c| Container.new(c)} -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") - puts "Environment variable DUP_DIR is not set. Looking for .yml files in #{base_dir}" unless silent_basedir +if needs_container + if container.empty? + raise "No container given." end + + container.each(&:load) end -file = "%s/%s.yml" % [ base_dir, container ] - if action == :create - action_create(container, file) + action_create(container) elsif action == :run - action_run(container, file) + action_run(container) elsif action == :list action_list(base_dir) elsif action == :update action_update(base_dir) -elsif action == :test - action_test(container) +elsif action == :regenerate + action_regenerate(container) elsif action == :_completion - action_completion(base_dir, completion_str) + action_completion($base_dir, completion_str) end