Compare commits

...

38 Commits

Author SHA1 Message Date
448f645f6f dps: Support für authelia_policy=none 2022-11-02 13:05:58 +01:00
a30978d20c dup: Added switch -q / --quiet to disable opening the log after running the new container. 2022-10-27 15:08:11 +02:00
a0bb698c02 Add authelia stuff with descriptions to --sample. 2022-10-27 15:04:26 +02:00
8cad86560d Don't show certificate details. 2022-10-27 15:04:08 +02:00
e2bd375198 Show authelia status. 2022-10-27 15:03:52 +02:00
3af1ffa11a Fix typo: dlr is drl (restart & log). 2022-10-27 15:03:32 +02:00
03b8b4baca Renamed dlr to drl (first Restart, then Logs). 2022-02-15 09:24:00 +01:00
7ff0f28ed4 dup: Message about unset DUP_DIR is now being output to STDERR to prevent it from clobbering up redirections of --sample and stuff. 2021-11-04 11:42:50 +01:00
99370803e3 Better source directory detection. 2021-11-03 16:18:03 +01:00
9a07f9fc74 dstats: Removed debugging code. 2021-11-02 15:34:16 +01:00
28445e30d2 dps: Fixed duplicated ports. 2021-11-02 15:07:30 +01:00
f21c3fdb52 install: Use pwd instead of calculating the local path. 2021-11-02 15:00:15 +01:00
f46b00c896 install: Fix for sh instead of bash. 2021-11-02 14:58:20 +01:00
40ab734bb3 install: Fix. 2021-11-02 14:55:07 +01:00
154e73b291 install: Fixed file mode. 2021-11-02 14:53:41 +01:00
8dbec29c45 Added an installer. 2021-11-02 14:52:15 +01:00
ee25ee5fe1 More commands. 2021-11-02 14:38:12 +01:00
bb0b5f7b6f dps: Added. 2021-11-02 14:14:18 +01:00
d879c633a3 Merge branch 'master' of https://git.schle.nz/fabian/dup 2021-11-02 14:13:11 +01:00
45c5612efb dup: Added --regenerate to print all command necessary to regenerate a container from scratch. 2021-11-02 14:13:09 +01:00
5a0094f4d9 Typo gefixt. 2021-11-02 09:48:50 +00:00
3879c20814 Added ofelia functionality. 2021-04-20 11:57:52 +02:00
f7b9535089 (Try to) stop old container before removing it. 2020-11-06 06:45:57 +01:00
5a562e2e90 Added shm_size. 2020-11-06 06:45:33 +01:00
6e7624a245 Extended examples for nginx_type and nginx_client_max_body_size. 2020-09-04 06:20:49 +02:00
eee7b009a4 Updated mappings, sample yml and options. 2020-09-04 06:18:38 +02:00
18eb80ebf6 Added mapping auto_update to set the label for watchtower or pyouroboros or whatever. 2020-09-04 06:17:46 +02:00
1eb3a8ded7 Fixes for network stuff. 2020-09-04 06:16:55 +02:00
73c3f5aacb Added option mapping to set the user and group for the container. 2020-09-04 06:15:05 +02:00
1ee6de03c1 Differentiate between creating a fresh container and recreating an already existing one. 2020-09-04 06:14:16 +02:00
33a8df5ac1 Added --all to regenerate all available containers. 2020-09-04 06:13:10 +02:00
61b3f2b98a Reduced the verbosity and in turn added -v/--verbose to reactivate the verbosity. 2020-09-04 06:11:48 +02:00
2377131483 Code ist jetzt objektorientiert; man kann mehrere Container auf einmal starten. 2019-04-17 09:19:05 +02:00
bf53c905e5 Added switch init. 2019-03-21 15:52:01 +01:00
ea79b9b98c Merge branch 'master' of https://git.schle.nz/fabian/dup 2019-03-21 15:51:07 +01:00
e2b14af74c Extended the sample to contain test: true 2019-03-21 15:49:13 +01:00
6c2bf950a0 Added a switch for ipv6. 2019-03-21 15:48:53 +01:00
7b102d25aa Use the default editor. 2019-02-26 09:55:40 +01:00
12 changed files with 639 additions and 207 deletions

13
_get_container.sh Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
if [ -z "$1" ]; then
base=`realpath . --relative-to="/data/containers" | cut -d"/" -f1`
if [ "$base" = ".." -o "$base" = "." ]; then
echo "No container given and not within /data/containers"
exit 1
fi
container="$base"
echo "Working on container $container."
else
container="$1"
fi

20
_install.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
SOURCE=$(pwd)
TARGET="/usr/local/bin"
if [ ! -e "$SOURCE/dbash" ]; then
echo "Please cd to the directory containing this file and try to run _install.sh again."
exit 1
fi
ln -sf "$SOURCE/dbash" "$TARGET/dbash"
ln -sf "$SOURCE/dhelp" "$TARGET/dhelp"
ln -sf "$SOURCE/dlogs" "$TARGET/dlogs"
ln -sf "$SOURCE/dlogsl" "$TARGET/dlogsl"
ln -sf "$SOURCE/drl" "$TARGET/drl"
ln -sf "$SOURCE/dps.rb" "$TARGET/dps"
ln -sf "$SOURCE/dsh" "$TARGET/dsh"
ln -sf "$SOURCE/dstats" "$TARGET/dstats"
ln -sf "$SOURCE/dup.rb" "$TARGET/dup"
ln -sf "$SOURCE/ofelias.rb" "$TARGET/ofelias"

5
dbash Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
. $(dirname "$(readlink -f "$0")")/_get_container.sh
docker exec -it "$container" /bin/bash

9
dhelp Executable file
View File

@@ -0,0 +1,9 @@
echo "Docker-Tools"
echo
echo "dps - Shows a tabular list of running containers."
echo "dlogs - Show latest log entries of container."
echo "dlogsl - Shows latest log entries using less."
echo "dup - Creates and runs containers from YAML files."
echo "dbash - Starts a bash shell in the container."
echo "dsh - Starts a sh shell in the container."
echo "dstats - Shows stats for all running containers."

5
dlogs Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
. $(dirname "$(readlink -f "$0")")/_get_container.sh
docker logs --tail=100 -f "$container"

5
dlogsl Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
. $(dirname "$(readlink -f "$0")")/_get_container.sh
docker logs -f "$container" 2>&1 | less

144
dps.rb Executable file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env ruby
require 'json'
require 'socket'
require 'net/http'
COLOR_GREEN = ""
COLOR_RED = ""
COLOR_GRAY = ""
COLOR_RESET = ""
class Container
FIELDS = [:state, :dup, :ouroboros, :name, :vhosts, :authelia, :status, :image, :ports]
@@lengths = Hash.new(0)
def initialize(data)
@strings = {}
@data = data
set :state, (@data[:State]=="running" ? COLOR_GREEN : COLOR_RED) + @data[:State].capitalize + COLOR_RESET
set :dup, @data[:Labels][:"de.fabianonline.dup"]=="true" ? "" : ""
set :ouroboros, @data[:Labels][:"com.centurylinklabs.watchtower.enable"]=="true" ? "" : ""
set :name, @data[:Names].first[1..-1]
vh = @data[:Labels][:"nginx_virtual_host"]&.split(",")&.map{|h| h.split("@").first.strip}
if vh.nil? || vh.count==0
vh = ""
elsif vh.count==1
vh = vh.first
else
if FULL
vh = vh.join(", ")
else
vh = vh.first + ",…"
end
end
set :vhosts, vh
p = @data[:Ports].sort_by{|p| p[:PrivatePort]}.collect do |p|
type = p[:Type]=="tcp" ? "" : "/#{p[:Type]}"
if p[:PrivatePort]==p[:PublicPort]
"#{COLOR_GREEN}#{p[:PrivatePort]}#{type}"
elsif p[:PublicPort]==nil
"#{COLOR_GRAY}#{p[:PrivatePort]}#{type}"
else
"#{COLOR_RED}#{p[:PrivatePort]}->#{p[:PublicPort]}#{type}"
end
end.uniq.join(" ")
#set :nginx_allow, @data[:Labels][:"nginx_allow"]
if vh==""
set :authelia, ""
elsif @data[:Labels][:"authelia_policy"]=="one_factor"
set :authelia, "1#{COLOR_GRAY}FA#{COLOR_RESET}"
elsif @data[:Labels][:"authelia_policy"]=="two_factor"
set :authelia, "2#{COLOR_GRAY}FA#{COLOR_RESET}"
elsif @data[:Labels][:"authelia_policy"]=="bypass"
set :authelia, "#{COLOR_GREEN}BYP#{COLOR_RESET}"
elsif @data[:Labels][:"authelia_policy"]=="none"
set :authelia, "#{COLOR_GREEN}NONE#{COLOR_RESET}"
else
set :authelia, "#{COLOR_RED}?#{COLOR_RESET}"
end
st = @data[:Status]
if st.end_with?("(healthy)")
st = COLOR_GREEN + st.gsub("(healthy)", "(v)") + COLOR_RESET
elsif st.end_with?("(unhealthy)")
st = COLOR_RED + st.gsub("(unhealthy)", "(x)") + COLOR_RESET
end
set :status, st
im = @data[:Image]
im = im.split(":")[0] unless FULL
set :image, im
set :ports, p
end
def set(key, value)
@data[key] = value
@@lengths[key] = [@@lengths[key], value.cleaned.length].max if value
end
def self.lengths; FIELDS.map{|k| @@lengths[k]}; end
def data
FIELDS.map{|k| @data[k]}
end
end
class String
def cleaned
self.gsub(/\[.+?m/, "")
end
def fix_length(len)
l = self.cleaned.length
shortened = self.length - l
if l >= len
return self.slice(0, len + shortened)
else
return self + (" " * (len - l))
end
end
end
def fetch_data
socket = UNIXSocket.new("/var/run/docker.sock")
request = "GET /v1.24/containers/json?all=1 HTTP/1.0\r\n\r\n"#Host: localhost\r\n\r\n"
socket.write(request)
response = ""
loop do
break if socket.eof?
line = socket.gets
break if line=="\r\n"
end
until socket.eof?
line = socket.gets
response += line
end
return JSON.parse(response, symbolize_names: true)
end
FULL = ARGV[0]=="--full"
data = fetch_data
data = data.map{|c| Container.new(c).data}
lengths = Container.lengths
data.sort_by{|d| d[3]}.each do |row|
row.each_with_index do |d, index|
if index==row.length-1
print d.to_s
else
print d.to_s.fix_length(lengths[index]) + " "
end
end
puts
end

5
drl Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
. $(dirname "$(readlink -f "$0")")/_get_container.sh
docker restart "$container" && docker logs --tail=100 -f "$container"

5
dsh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
. $(dirname "$(readlink -f "$0")")/_get_container.sh
docker exec -it "$container" /bin/sh

3
dstats Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker stats $(docker inspect --format "{{.Name}}" $(docker ps -q))

463
dup.rb
View File

@@ -7,6 +7,16 @@ require 'ostruct'
require 'json' require 'json'
MAPPINGS = [ MAPPINGS = [
# Syntax:
# [name_in_yml, docker_option_name, lambda_for_creating_yml_for_container=nil, options={}]
# Options are:
# type: Sets type of this option
# switch: Only adds the docker_option_name to the command, ignoring the value.
# name: Value will be set to the name of the container (from yml filename or root element)
# hidden: Will not be added to the run command
# allow_empty: May be empty.
# escape: If set to false, will not be shellescaped
['stdin_open', '--interactive', lambda{|c,i| c.Config.OpenStdin}, {:type=>:switch}], ['stdin_open', '--interactive', lambda{|c,i| c.Config.OpenStdin}, {:type=>:switch}],
['tty', '--tty', lambda{|c,i| c.Config.Tty}, {:type=>:switch}], ['tty', '--tty', lambda{|c,i| c.Config.Tty}, {:type=>:switch}],
#['detach', '--detach', {:type=>:switch}], #['detach', '--detach', {:type=>:switch}],
@@ -22,22 +32,30 @@ MAPPINGS = [
['stop_grace_period', '--stop-timeout', lambda{|c,i| c.Config.StopTimeout}], ['stop_grace_period', '--stop-timeout', lambda{|c,i| c.Config.StopTimeout}],
['devices', '--device'], ['devices', '--device'],
['net', '--net'], ['net', '--net'],
['ipv6', '--ipv6'],
['user', '--user'],
['networks', '--network', lambda{|c,i| c.NetworkSettings.Networks.to_h.keys.map(&:to_s)}, {:type=>:hidden}], ['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}], ['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}], ['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'], ['hostname', '--hostname'],
['shm_size', '--shm-size'],
['working_dir', '--workdir', lambda{|c,i| (wd=c.Config.WorkingDir) == i.Config.WorkingDir ? nil : wd}], ['working_dir', '--workdir', lambda{|c,i| (wd=c.Config.WorkingDir) == i.Config.WorkingDir ? nil : wd}],
['init', '--init', lambda{|c,i| c.Config.Init}, {:type=>:switch}],
['image', nil, lambda{|c,i| c.Config.Image}], ['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}] ['command', nil, lambda{|c,i| (cmd=c.Config.Cmd.join(" ") rescue nil) == (i.Config.Cmd.join(" ") rescue nil)? nil : cmd}, {:escape=>false}],
['test', nil, nil, {type: :hidden}],
['auto_update', nil, nil, {type: :hidden}],
['ofelia', nil, nil, {type: :hidden}]
] ]
def get_sample(name="container") def get_sample(name="container")
return <<~HEREDOC return <<~HEREDOC
#{name}:
image: "my/image:1.2.3" image: "my/image:1.2.3"
restart: always restart: always
detach: true detach: true
test: true # Having this set to true will prevent dup from creating a label de.fabianonline.dup
auto_update: true # Enabling this adds a label to allow watchtower to automatically update this container
ports: ports:
- "1234:1234" - "1234:1234"
@@ -51,24 +69,34 @@ def get_sample(name="container")
volumes: volumes:
- "/etc/localtime:/etc/localtime:ro" - "/etc/localtime:/etc/localtime:ro"
links:
- "another_container"
labels: labels:
com.centurylinklabs.watchtower.enable: true # Syntax:
nginx_virtual_host: container.home.fabianonline.de # <hostname>: Uses the given hostname and the default cert (*.schle.nz)
# <hostname>@: Uses the given hostname and the cert with the same name
# <hostname>@<cert>: Uses the given hostname and cert
nginx_virtual_host: container.home.schle.nz
nginx_port: 80 nginx_port: 80
nginx_allow: fabian # "user" or "@group" or "user, user, @group" or "all" nginx_additional_ports: "443, 5050"
nginx_no_robots: true nginx_no_robots: true
nginx_public_paths: "/public, /api" nginx_public_paths: "/public, /api"
nginx_type: # "http" (default), "fastcgi", "skip" (doesn't create any entries) nginx_type: # "http" (default), "https", "fastcgi", "skip" (doesn't create any entries)
nginx_client_max_body_size: "25M"
# Syntax:
# user:<username>
# group:<groupname>
authelia_allow: "user:fabian"
# Policies:
# one_factor
# two_factor
# bypass
authelia_policy: one_factor
networks: networks:
- "nginx" - "nginx"
- "mosquitto" - "mosquitto"
- "bridge" - "bridge"
test: true user: "1000:1000"
command: "/bin/bash" command: "/bin/bash"
entrypoint: "/script.sh" entrypoint: "/script.sh"
build: "/data/dir" build: "/data/dir"
@@ -82,6 +110,7 @@ def get_sample(name="container")
remove: false remove: false
stop_signal: SIGUSR1 stop_signal: SIGUSR1
stop_grace_period: 5 stop_grace_period: 5
shm_size: 256M
before_build: before_build:
- "echo 'Starting build'" - "echo 'Starting build'"
@@ -95,6 +124,15 @@ def get_sample(name="container")
devices: devices:
- "/dev/ttyUSB0:/dev/ttyUSB0" - "/dev/ttyUSB0:/dev/ttyUSB0"
ofelia:
- name: update # No spaces!
schedule: "@every 4h" # go cron format! See https://pkg.go.dev/github.com/robfig/cron?utm_source=godoc
command: uname -a
user: foo
tty: false
no-overlap: true
- name: Test 2
HEREDOC HEREDOC
end end
@@ -111,18 +149,32 @@ def action_help
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 "-l, --list Lists all available containers."
puts " --host Starts a container with net: host - which implies not using ports, networks and/or links." 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."
puts " --all Use all containers defined in yml files."
puts "-v, --verbose More verbose output."
puts "-q, --quiet Don't open log after starting a new container."
exit 1 exit 1
end end
def run(cmd, ignore_returnvalue=false, catch_interrupt=false) def run_cmd(cmd, ignore_returnvalue=false, catch_interrupt=false, ignore_dry_run: false)
puts "+ #{cmd}" if $dry_run verbose "+ #{cmd}"
returnvalue=false returnvalue=false
begin begin
returnvalue = $dry_run ? true : system("bash -c #{cmd.shellescape}") returnvalue = ($dry_run && !ignore_dry_run) ? true : system("bash -c #{cmd.shellescape}")
rescue Interrupt rescue Interrupt
raise if not catch_interrupt raise if not catch_interrupt
end 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
return returnvalue
end
def collect_commands(&block)
$commands = []
def run_cmd(cmd, ignore_returnvalue=false, catch_interrupt=false, ignore_dry_run: false); $commands<<cmd; return 0; end
yield
return $commands
end end
def esc(obj, escape=true) def esc(obj, escape=true)
@@ -130,119 +182,19 @@ def esc(obj, escape=true)
return obj.to_s.shellescape return obj.to_s.shellescape
end end
def action_create(container, file) def action_create(container)
file = container.file
raise "File #{file} already exists" if File.exists?(file) raise "File #{file} already exists" if File.exists?(file)
File.open(file, "w") {|f| f.write(get_sample(container))} File.open(file, "w") {|f| f.write(get_sample(container))}
exec("joe #{file.shellescape}") exec("/usr/bin/editor #{file.shellescape}")
exit 1 # will never be reached because exec replaces this process. But just in case... ;-) exit 1 # will never be reached because exec replaces this process. But just in case... ;-)
end end
def action_run(container, file) def action_run(container)
# $net_host # $net_host
raise "File #{file} not found." unless File.exists?(file) container.each{|c| c.run(container.count>1)}
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
end end
def list(base_dir) def list(base_dir)
@@ -285,8 +237,215 @@ def action_completion(base_dir, complete_str)
puts containers.join(" ") puts containers.join(" ")
end end
def action_test(container) def action_regenerate(container)
c_data = `docker inspect #{container}` container.each do |c|
data = c.regenerate
p collect_commands{ c.run }
end
end
class Container
def initialize(name)
@name = name
end
def exists?
run_cmd("docker inspect --format='1' #{@name.shellescape} >/dev/null 2>&1", true, ignore_dry_run: true)
end
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 add_label(key, value)
if @data["labels"].is_a? Array
@data["labels"] << "#{key}=#{value}"
elsif @data["labels"].is_a? Hash
@data["labels"][key] = value
else
raise "data['labels'] is of an unexpected type: #{@data["labels"].class}"
end
end
def build_run_command
$net_host = true if @data["net"]=="host"
cmd = ["docker", "create"]
cmd << "--net" << @data["networks"][0] if @data["networks"] && @data["networks"].count>0 && !$net_host
cmd << "--net" << "host" if $net_host
@data["labels"] ||= []
if !@data["test"]
add_label("de.fabianonline.dup", "true")
end
if @data["auto_update"]
add_label("com.centurylinklabs.watchtower.enable", "true")
end
if @data["ofelia"] && @data["ofelia"].count > 0
add_label("ofelia.enabled", "true")
@data["ofelia"].each_with_index do |job, index|
name = job["name"] || "job-#{index}"
name = name.downcase.gsub(/[^a-z0-9\-]/, "-").gsub(/-+/, "-")
schedule = job["schedule"]
next unless schedule
command = job["command"]
next unless command
add_label("ofelia.job-exec.#{name}.schedule", schedule)
add_label("ofelia.job-exec.#{name}.command", command)
add_label("ofelia.job-exec.#{name}.user", job["user"]) if job["user"]
add_label("ofelia.job-exec.#{name}.tty", job["tty"].to_s) if job["tty"]!=nil
ol = job["no-overlap"]
add_label("ofelia.job-exec.#{name}.no-overlap", ol!=nil ? ol.to_s : "true")
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.compact.join(" ")
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
verbose "Stopping and removing old container..."
run_cmd("docker stop #{@name.shellescape} >/dev/null 2>&1", true)
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)
verbose "Network #{network} exists."
else
verbose "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|
verbose "Connecting container to network #{network}..."
run_cmd("docker network connect #{network.shellescape} #{@name.shellescape} >/dev/null", true)
end
end
end
def attach
verbose "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
if self.exists?
puts "Recreating #{@name}..."
else
puts "Creating #{@name}..."
end
self.stop_and_remove
self.create_networks
verbose "Creating container..."
cmd = build_run_command
run_cmd(cmd + " >/dev/null")
self.connect_to_networks
verbose "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
unless $quiet
self.log
end
end
end
end
def regenerate
c_data = `docker inspect #{@name}`
c = JSON.parse(c_data, object_class: OpenStruct).first c = JSON.parse(c_data, object_class: OpenStruct).first
i_data = `docker inspect #{c.Image}` i_data = `docker inspect #{c.Image}`
i = JSON.parse(i_data, object_class: OpenStruct).first i = JSON.parse(i_data, object_class: OpenStruct).first
@@ -304,18 +463,30 @@ def action_test(container)
data[name] = result data[name] = result
end end
puts ({container=>data}.to_yaml.lines[1..-1].join()) @data = data
return data
end
end end
def verbose(str); puts(str) if $verbose; end
action = :run action = :run
container = nil container = nil
$dry_run = false $dry_run = false
$verbose = false
$pull = false $pull = false
needs_container = true needs_container = true
needs_basedir = true
completion_str = "" completion_str = ""
silent_basedir = false silent_basedir = false
$net_host = false $net_host = false
$quiet = false
if ENV['DUP_DIR']
$base_dir = ENV['DUP_DIR']
else
$base_dir = File.join(Dir.home, ".dup")
STDERR.puts "Environment variable DUP_DIR is not set. Looking for .yml files in #{$base_dir}" unless silent_basedir
end
opts = GetoptLong.new( opts = GetoptLong.new(
[ '--sample', '-s', GetoptLong::NO_ARGUMENT ], [ '--sample', '-s', GetoptLong::NO_ARGUMENT ],
@@ -327,7 +498,10 @@ opts = GetoptLong.new(
[ '--update', '-u', GetoptLong::NO_ARGUMENT ], [ '--update', '-u', GetoptLong::NO_ARGUMENT ],
[ '--_completion', GetoptLong::OPTIONAL_ARGUMENT ], [ '--_completion', GetoptLong::OPTIONAL_ARGUMENT ],
[ '--host', GetoptLong::NO_ARGUMENT ], [ '--host', GetoptLong::NO_ARGUMENT ],
[ '--test', GetoptLong::NO_ARGUMENT ] [ '--regenerate', GetoptLong::NO_ARGUMENT ],
[ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
[ '--all', GetoptLong::NO_ARGUMENT ],
[ '--quiet', '-q', GetoptLong::NO_ARGUMENT ]
) )
opts.each do |opt, arg| opts.each do |opt, arg|
@@ -336,7 +510,6 @@ opts.each do |opt, arg|
action_sample action_sample
when '--create' when '--create'
action = :create action = :create
needs_basedir = true
when '--help' when '--help'
action_help action_help
when '--dry-run' when '--dry-run'
@@ -350,50 +523,48 @@ opts.each do |opt, arg|
when '--update' when '--update'
action = :update action = :update
needs_container = false needs_container = false
needs_basedir = true
when '--_completion' when '--_completion'
action = :_completion action = :_completion
completion_str = arg completion_str = arg
needs_container = false needs_container = false
needs_basedir = true
silent_basedir = true silent_basedir = true
when '--host' when '--host'
$net_host = true $net_host = true
when '--test' when '--regenerate'
action = :test action = :regenerate
needs_basedir = false needs_container = false
when '--verbose'
$verbose = true
when '--all'
container = Dir[$base_dir + "/*.yml"].sort.map{|f| Container.new(File.basename(f, ".yml"))}
when '--quiet'
$quiet = true
end end
end end
container = ARGV.shift container ||= ARGV.map{|c| Container.new(c)}
if needs_container && (container==nil || container=="") if needs_container
if container.empty?
raise "No container given." 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
end end
end
file = "%s/%s.yml" % [ base_dir, container ] container.each(&:load)
end
if action == :create if action == :create
action_create(container, file) action_create(container)
elsif action == :run elsif action == :run
action_run(container, file) action_run(container)
elsif action == :list elsif action == :list
action_list(base_dir) action_list(base_dir)
elsif action == :update elsif action == :update
action_update(base_dir) action_update(base_dir)
elsif action == :test elsif action == :regenerate
action_test(container) action_regenerate(container)
container.each {|c| puts c.build_run_command}
elsif action == :_completion elsif action == :_completion
action_completion(base_dir, completion_str) action_completion($base_dir, completion_str)
end end

47
ofelias.rb Executable file
View File

@@ -0,0 +1,47 @@
#!/usr/bin/env ruby
require 'socket'
require 'json'
require 'terminal-table'
def fetch_data
socket = UNIXSocket.new("/var/run/docker.sock")
request = "GET /v1.24/containers/json?all=1 HTTP/1.0\r\n\r\n"
socket.write(request)
response = ""
loop do
break if socket.eof?
line = socket.gets
break if line=="\r\n"
end
until socket.eof?
line = socket.gets
response += line
end
return JSON.parse(response, symbolize_names: true)
end
rows = []
data = fetch_data
data.each do |d|
next unless labels = d[:Labels]
name = d[:Names][0][1..-1]
next unless labels[:"ofelia.enabled"]
tasks = labels.keys.map(&:to_s).map{|s| s.scan(/\Aofelia\.job-([a-z]+)\.([^.]*)\./).first}.uniq.compact
next unless tasks.count>0
tasks.each do |task|
type, taskname = *task
schedule = labels[:"ofelia.job-#{type}.#{taskname}.schedule"]
command = labels[:"ofelia.job-#{type}.#{taskname}.command"]
rows << [name, type, taskname, schedule, command]
end
end
exit 0 unless rows.count>0
puts Terminal::Table.new(rows: rows, headings: ["Container", "Type", "Task", "Schedule", "Command"])