diff --git a/bin/kamal b/bin/kamal index 96c0dc64..62290b89 100755 --- a/bin/kamal +++ b/bin/kamal @@ -9,10 +9,10 @@ begin Kamal::Cli::Main.start(ARGV) rescue SSHKit::Runner::ExecuteError => e puts " \e[31mERROR (#{e.cause.class}): #{e.message}\e[0m" - puts e.cause.backtrace if ENV["VERBOSE"] + puts e.cause.backtrace exit 1 rescue => e puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m" - puts e.backtrace if ENV["VERBOSE"] + puts e.backtrace exit 1 end diff --git a/lib/kamal/cli.rb b/lib/kamal/cli.rb index c1501ddb..051d9416 100644 --- a/lib/kamal/cli.rb +++ b/lib/kamal/cli.rb @@ -1,6 +1,7 @@ module Kamal::Cli class LockError < StandardError; end class HookError < StandardError; end + class TraefikError < StandardError; end end # SSHKit uses instance eval, so we need a global const for ergonomics diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 4892acd2..01fdd80e 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -2,6 +2,8 @@ class Kamal::Cli::App < Kamal::Cli::Base desc "boot", "Boot app on servers (or reboot app if already running)" def boot mutating do + ensure_traefik_file_provider_enabled + hold_lock_on_error do say "Get most recent version available as an image...", :magenta unless options[:version] using_version(version_or_latest) do |version| @@ -18,6 +20,8 @@ class Kamal::Cli::App < Kamal::Cli::Base roles.each do |role| app = KAMAL.app(role: role) auditor = KAMAL.auditor(role: role) + traefik_dynamic = KAMAL.traefik_dynamic(role: role) + role_config = KAMAL.config.role(role) if capture_with_info(*app.container_id_for_version(version, only_running: true), raise_on_non_zero_exit: false).present? tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}" @@ -33,6 +37,12 @@ class Kamal::Cli::App < Kamal::Cli::Base Kamal::Utils::HealthcheckPoller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) } + if role_config.running_traefik? + ip_address = capture_with_info(*app.ip_address(version: version)).strip + execute *traefik_dynamic.write_config(ip_address: ip_address) + Kamal::Utils::SwitchPoller.wait_for_switch(traefik_dynamic) { capture_with_info(*traefik_dynamic.run_id)&.strip } + end + execute *app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present? end end @@ -44,12 +54,23 @@ class Kamal::Cli::App < Kamal::Cli::Base desc "start", "Start existing app container on servers" def start mutating do + ensure_traefik_file_provider_enabled + on(KAMAL.hosts) do |host| roles = KAMAL.roles_on(host) roles.each do |role| + app = KAMAL.app(role: role) + role_config = KAMAL.config.role(role) + execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug - execute *KAMAL.app(role: role).start, raise_on_non_zero_exit: false + execute *app.start, raise_on_non_zero_exit: false + version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip + + if role_config.running_traefik? + ip_address = capture_with_info(*app.ip_address(version: version)).strip + execute *KAMAL.traefik_dynamic(role: role).write_config(ip_address: ip_address) + end end end end @@ -62,8 +83,10 @@ class Kamal::Cli::App < Kamal::Cli::Base roles = KAMAL.roles_on(host) roles.each do |role| + app = KAMAL.app(role: role) execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug - execute *KAMAL.app(role: role).stop, raise_on_non_zero_exit: false + execute *KAMAL.traefik_dynamic(role: role).remove_config if KAMAL.config.role(role).running_traefik? + execute *app.stop, raise_on_non_zero_exit: false end end end @@ -293,4 +316,13 @@ class Kamal::Cli::App < Kamal::Cli::Base def version_or_latest options[:version] || "latest" end + + def ensure_traefik_file_provider_enabled + # Ensure traefik has been rebooted to switch to the file provider + on(KAMAL.traefik_hosts) do + unless capture_with_info(*KAMAL.traefik_static.docker_entrypoint_args).include?("--providers.file.directory=") + raise Kamal::Cli::TraefikError, "File provider not enabled, you'll need to run `kamal traefik reboot` to deploy" + end + end + end end diff --git a/lib/kamal/cli/env.rb b/lib/kamal/cli/env.rb index 7d4dac10..071742ee 100644 --- a/lib/kamal/cli/env.rb +++ b/lib/kamal/cli/env.rb @@ -13,8 +13,9 @@ class Kamal::Cli::Env < Kamal::Cli::Base end on(KAMAL.traefik_hosts) do - execute *KAMAL.traefik.make_env_directory - upload! StringIO.new(KAMAL.traefik.env_file), KAMAL.traefik.host_env_file_path, mode: 400 + traefik_static_config = KAMAL.traefik_static.static_config + execute *KAMAL.traefik_static.make_env_directory + upload! StringIO.new(traefik_static_config.env_file), traefik_static_config.host_env_file_path, mode: 400 end on(KAMAL.accessory_hosts) do @@ -38,7 +39,7 @@ class Kamal::Cli::Env < Kamal::Cli::Base end on(KAMAL.traefik_hosts) do - execute *KAMAL.traefik.remove_env_file + execute *KAMAL.traefik_static.remove_env_file end on(KAMAL.accessory_hosts) do diff --git a/lib/kamal/cli/traefik.rb b/lib/kamal/cli/traefik.rb index 9bc4f644..b6b267bf 100644 --- a/lib/kamal/cli/traefik.rb +++ b/lib/kamal/cli/traefik.rb @@ -4,7 +4,8 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base mutating do on(KAMAL.traefik_hosts) do execute *KAMAL.registry.login - execute *KAMAL.traefik.start_or_run + execute *KAMAL.traefik_static.ensure_config_directory + execute *KAMAL.traefik_static.start_or_run end end end @@ -16,9 +17,10 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base on(KAMAL.traefik_hosts, in: options[:rolling] ? :sequence : :parallel) do execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug execute *KAMAL.registry.login - execute *KAMAL.traefik.stop - execute *KAMAL.traefik.remove_container - execute *KAMAL.traefik.run + execute *KAMAL.traefik_static.stop + execute *KAMAL.traefik_static.remove_container + execute *KAMAL.traefik_static.ensure_config_directory + execute *KAMAL.traefik_static.run end end end @@ -28,7 +30,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base mutating do on(KAMAL.traefik_hosts) do execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug - execute *KAMAL.traefik.start + execute *KAMAL.traefik_static.start end end end @@ -38,7 +40,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base mutating do on(KAMAL.traefik_hosts) do execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug - execute *KAMAL.traefik.stop + execute *KAMAL.traefik_static.stop end end end @@ -53,7 +55,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base desc "details", "Show details about Traefik container from servers" def details - on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik.info), type: "Traefik" } + on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik_static.info), type: "Traefik" } end desc "logs", "Show log lines from Traefik on servers" @@ -67,15 +69,15 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base if options[:follow] run_locally do info "Following logs on #{KAMAL.primary_host}..." - info KAMAL.traefik.follow_logs(host: KAMAL.primary_host, grep: grep) - exec KAMAL.traefik.follow_logs(host: KAMAL.primary_host, grep: grep) + info KAMAL.traefik_static.follow_logs(host: KAMAL.primary_host, grep: grep) + exec KAMAL.traefik_static.follow_logs(host: KAMAL.primary_host, grep: grep) end else since = options[:since] lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set on(KAMAL.traefik_hosts) do |host| - puts_by_host host, capture(*KAMAL.traefik.logs(since: since, lines: lines, grep: grep)), type: "Traefik" + puts_by_host host, capture(*KAMAL.traefik_static.logs(since: since, lines: lines, grep: grep)), type: "Traefik" end end end @@ -94,7 +96,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base mutating do on(KAMAL.traefik_hosts) do execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug - execute *KAMAL.traefik.remove_container + execute *KAMAL.traefik_static.remove_container end end end @@ -104,7 +106,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base mutating do on(KAMAL.traefik_hosts) do execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug - execute *KAMAL.traefik.remove_image + execute *KAMAL.traefik_static.remove_image end end end diff --git a/lib/kamal/commander.rb b/lib/kamal/commander.rb index a98ac2b5..170eb279 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -81,7 +81,7 @@ class Kamal::Commander def app(role: nil) - Kamal::Commands::App.new(config, role: role) + Kamal::Commands::App.new(config, role: role || config.roles.first.name) end def accessory(name) @@ -124,8 +124,12 @@ class Kamal::Commander @server ||= Kamal::Commands::Server.new(config) end - def traefik - @traefik ||= Kamal::Commands::Traefik.new(config) + def traefik_static + @traefik_static ||= Kamal::Commands::Traefik::Static.new(config) + end + + def traefik_dynamic(role: nil) + Kamal::Commands::Traefik::Dynamic.new(config, role: role || config.roles.first.name) end def with_verbosity(level) diff --git a/lib/kamal/commands/accessory.rb b/lib/kamal/commands/accessory.rb index bd70ac5c..c6416c9d 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -86,6 +86,10 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base end end + def make_directory_for(remote_file) + make_directory Pathname.new(remote_file).dirname.to_s + end + def remove_service_directory [ :rm, "-rf", service_name ] end diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index cb1f7091..ae20cfa4 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -13,22 +13,20 @@ class Kamal::Commands::App < Kamal::Commands::Base end def run(hostname: nil) - role = config.role(self.role) - docker :run, "--detach", "--restart unless-stopped", "--name", container_name, *(["--hostname", hostname] if hostname), "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"", - *role.env_args, - *role.health_check_args, + *role_config.env_args, + *role_config.health_check_args, *config.logging_args, *config.volume_args, - *role.label_args, - *role.option_args, + *role_config.label_args, + *role_config.option_args, config.absolute_image, - role.cmd + role_config.cmd end def start @@ -49,6 +47,10 @@ class Kamal::Commands::App < Kamal::Commands::Base docker :ps, *filter_args end + def ip_address(version:) + docker :inspect, "-f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'", container_name(version) + end + def logs(since: nil, lines: nil, grep: nil) pipe \ @@ -76,14 +78,12 @@ class Kamal::Commands::App < Kamal::Commands::Base end def execute_in_new_container(*command, interactive: false) - role = config.role(self.role) - docker :run, ("-it" if interactive), "--rm", - *role&.env_args, + *role_config&.env_args, *config.volume_args, - *role&.option_args, + *role_config&.option_args, config.absolute_image, *command end @@ -112,7 +112,7 @@ class Kamal::Commands::App < Kamal::Commands::Base def list_versions(*docker_args, statuses: nil) pipe \ docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'), - %(while read line; do echo ${line##{service_role_dest}-}; done) # Extract SHA from "service-role-dest-SHA" + %(while read line; do echo ${line##{role_config.full_name}-}; done) # Extract SHA from "service-role-dest-SHA" end def list_containers @@ -157,19 +157,19 @@ class Kamal::Commands::App < Kamal::Commands::Base [:rm, "-f", config.role(role).host_env_file_path] end + def service_role_dest + [config.service, role, config.destination].compact.join("-") + end + private def container_name(version = nil) - [ config.service, role, config.destination, version || config.version ].compact.join("-") + [ role_config.full_name, version || config.version ].compact.join("-") end def filter_args(statuses: nil) argumentize "--filter", filters(statuses: statuses) end - def service_role_dest - [config.service, role, config.destination].compact.join("-") - end - def filters(statuses: nil) [ "label=service=#{config.service}" ].tap do |filters| filters << "label=destination=#{config.destination}" if config.destination @@ -179,4 +179,8 @@ class Kamal::Commands::App < Kamal::Commands::Base end end end + + def role_config + @role_config ||= config.role(self.role) + end end diff --git a/lib/kamal/commands/server.rb b/lib/kamal/commands/server.rb index 5b3ad194..1368e96e 100644 --- a/lib/kamal/commands/server.rb +++ b/lib/kamal/commands/server.rb @@ -1,5 +1,5 @@ class Kamal::Commands::Server < Kamal::Commands::Base def ensure_run_directory - [:mkdir, "-p", config.run_directory] + make_directory config.run_directory end end diff --git a/lib/kamal/commands/traefik.rb b/lib/kamal/commands/traefik.rb deleted file mode 100644 index fbbcea0f..00000000 --- a/lib/kamal/commands/traefik.rb +++ /dev/null @@ -1,122 +0,0 @@ -class Kamal::Commands::Traefik < Kamal::Commands::Base - delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils - - DEFAULT_IMAGE = "traefik:v2.9" - CONTAINER_PORT = 80 - DEFAULT_ARGS = { - 'log.level' => 'DEBUG' - } - - def run - docker :run, "--name traefik", - "--detach", - "--restart", "unless-stopped", - *publish_args, - "--volume", "/var/run/docker.sock:/var/run/docker.sock", - *env_args, - *config.logging_args, - *label_args, - *docker_options_args, - image, - "--providers.docker", - *cmd_option_args - end - - def start - docker :container, :start, "traefik" - end - - def stop - docker :container, :stop, "traefik" - end - - def start_or_run - combine start, run, by: "||" - end - - def info - docker :ps, "--filter", "name=^traefik$" - end - - def logs(since: nil, lines: nil, grep: nil) - pipe \ - docker(:logs, "traefik", (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"), - ("grep '#{grep}'" if grep) - end - - def follow_logs(host:, grep: nil) - run_over_ssh pipe( - docker(:logs, "traefik", "--timestamps", "--tail", "10", "--follow", "2>&1"), - (%(grep "#{grep}") if grep) - ).join(" "), host: host - end - - def remove_container - docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik" - end - - def remove_image - docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik" - end - - def port - "#{host_port}:#{CONTAINER_PORT}" - end - - def env_file - env_file_with_secrets config.traefik.fetch("env", {}) - end - - def host_env_file_path - File.join host_env_directory, "traefik.env" - end - - def make_env_directory - make_directory(host_env_directory) - end - - def remove_env_file - [:rm, "-f", host_env_file_path] - end - - private - def publish_args - argumentize "--publish", port unless config.traefik["publish"] == false - end - - def label_args - argumentize "--label", labels - end - - def env_args - argumentize "--env-file", host_env_file_path - end - - def host_env_directory - File.join config.host_env_directory, "traefik" - end - - def labels - config.traefik["labels"] || [] - end - - def image - config.traefik.fetch("image") { DEFAULT_IMAGE } - end - - def docker_options_args - optionize(config.traefik["options"] || {}) - end - - def cmd_option_args - if args = config.traefik["args"] - optionize DEFAULT_ARGS.merge(args), with: "=" - else - optionize DEFAULT_ARGS, with: "=" - end - end - - def host_port - config.traefik["host_port"] || CONTAINER_PORT - end -end diff --git a/lib/kamal/commands/traefik/dynamic.rb b/lib/kamal/commands/traefik/dynamic.rb new file mode 100644 index 00000000..9b3a0e1f --- /dev/null +++ b/lib/kamal/commands/traefik/dynamic.rb @@ -0,0 +1,43 @@ +class Kamal::Commands::Traefik::Dynamic < Kamal::Commands::Base + attr_reader :static_config, :dynamic_config + + def initialize(config, role: nil) + super(config) + @static_config = Kamal::Configuration::Traefik::Static.new(config: config) + @dynamic_config = Kamal::Configuration::Traefik::Dynamic.new(config: config, role: role) + end + + def run_id + pipe \ + [:docker, :exec, :traefik, :wget, "-qSO", "/dev/null", "http://localhost:#{Kamal::Configuration::Traefik::Static::CONTAINER_PORT}#{config.healthcheck["path"]}", "2>&1"], + [:grep, "-i", Kamal::Configuration::Traefik::Dynamic::RUN_ID_HEADER], + [:cut, "-d ' ' -f 4"] + end + + def write_config(ip_address:) + # Write to tmp then mv for an atomic copy. If you write directly traefik sees an empty file + # and removes the service before picking up the new config. + temp_config_file = "/tmp/kamal-traefik-config-#{rand(10000000)}" + chain \ + write([:echo, dynamic_config.config(ip_address: ip_address).to_yaml.shellescape], temp_config_file), + [:mv, temp_config_file, host_file] + end + + def remove_config + [:rm, host_file] + end + + def boot_check? + dynamic_config.boot_check? + end + + def config_run_id + dynamic_config.run_id + end + + private + def host_file + "#{static_config.host_directory}/#{dynamic_config.host_file}" + end +end + diff --git a/lib/kamal/commands/traefik/static.rb b/lib/kamal/commands/traefik/static.rb new file mode 100644 index 00000000..587120df --- /dev/null +++ b/lib/kamal/commands/traefik/static.rb @@ -0,0 +1,75 @@ +class Kamal::Commands::Traefik::Static < Kamal::Commands::Base + attr_reader :static_config, :dynamic_config + + def initialize(config, role: nil) + super(config) + @static_config = Kamal::Configuration::Traefik::Static.new(config: config) + @dynamic_config = Kamal::Configuration::Traefik::Dynamic.new(config: config, role: role) + end + + def run + docker :run, static_config.docker_args, static_config.image, static_config.traefik_args + end + + def start + docker :container, :start, "traefik" + end + + def stop + docker :container, :stop, "traefik" + end + + def start_or_run + combine start, run, by: "||" + end + + def info + docker :ps, "--filter", "name=^traefik$" + end + + def logs(since: nil, lines: nil, grep: nil) + pipe \ + docker(:logs, "traefik", (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"), + ("grep '#{grep}'" if grep) + end + + def follow_logs(host:, grep: nil) + run_over_ssh pipe( + docker(:logs, "traefik", "--timestamps", "--tail", "10", "--follow", "2>&1"), + (%(grep "#{grep}") if grep) + ).join(" "), host: host + end + + def remove_container + docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik" + end + + def remove_image + docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik" + end + + def port + "#{host_port}:#{CONTAINER_PORT}" + end + + def make_env_directory + make_directory(static_config.host_env_directory) + end + + def remove_env_file + [:rm, "-f", static_config.host_env_file_path] + end + + def ensure_config_directory + make_directory(static_config.host_directory) + end + + def docker_entrypoint_args + docker :inspect, "-f '{{index .Args 1 }}'", :traefik + end + + def boot_check? + dynamic_config.boot_check? + end +end + diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 300a9d8d..95c74ead 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -7,7 +7,7 @@ require "net/ssh/proxy/jump" class Kamal::Configuration delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true - delegate :argumentize, :optionize, to: Kamal::Utils + delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils attr_accessor :destination attr_accessor :raw_config @@ -153,7 +153,7 @@ class Kamal::Configuration end def valid? - ensure_required_keys_present && ensure_valid_kamal_version + ensure_required_keys_present && ensure_valid_kamal_version && ensure_no_traefik_labels end @@ -231,6 +231,17 @@ class Kamal::Configuration true end + def ensure_no_traefik_labels + # The switch to a traefik file provider means that traefik labels on app containers are ignored + # We'll raise an error and suggest moving them + + if roles.any? { |role| role.labels.keys.any? { |label| label.start_with?("traefik.") } } + raise ArgumentError, "Traefik is not configured to read labels, move traefik config to dynamic:" + end + + true + end + def role_names raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index f549d459..07f52562 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -4,7 +4,7 @@ class Kamal::Configuration::Role attr_accessor :name def initialize(name, config:) - @name, @config = name.inquiry, config + @name, @config = name.inquiry, config end def primary_host @@ -16,7 +16,7 @@ class Kamal::Configuration::Role end def labels - default_labels.merge(traefik_labels).merge(custom_labels) + default_labels.merge(custom_labels) end def label_args @@ -82,9 +82,23 @@ class Kamal::Configuration::Role end def running_traefik? - name.web? || specializations["traefik"] + name.web? || (specializations["traefik"] != nil && specializations["traefik"] != false) end + def traefik + case specializations["traefik"] + when NilClass, TrueClass, FalseClass + {} + else + specializations["traefik"] + end + end + + def full_name + [ config.service, name, config.destination ].compact.join("-") + end + + private attr_accessor :config @@ -105,26 +119,6 @@ class Kamal::Configuration::Role end end - def traefik_labels - if running_traefik? - { - # Setting a service property ensures that the generated service name will be consistent between versions - "traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http", - - "traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)", - "traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5", - "traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms", - "traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker" - } - else - {} - end - end - - def traefik_service - [ config.service, name, config.destination ].compact.join("-") - end - def custom_labels Hash.new.tap do |labels| labels.merge!(config.labels) if config.labels.present? diff --git a/lib/kamal/configuration/traefik/dynamic.rb b/lib/kamal/configuration/traefik/dynamic.rb new file mode 100644 index 00000000..75ee5e5d --- /dev/null +++ b/lib/kamal/configuration/traefik/dynamic.rb @@ -0,0 +1,66 @@ +class Kamal::Configuration::Traefik::Dynamic + RUN_ID_HEADER = "X-Kamal-Run-ID" + + delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils + + attr_reader :traefik_config, :role_config, :role_traefik_config + + def initialize(config:, role:) + @traefik_config = config.traefik || {} + @role_config = config.role(role) + @role_traefik_config = role_config&.traefik || {} + end + + def host_file + "#{role_config.full_name}.yml" + end + + def config(ip_address:) + default_config(ip_address:).deep_merge!(custom_config) + end + + def boot_check? + role_traefik_config.fetch("boot_check") { traefik_config.fetch("boot_check", true) } + end + + def run_id + @run_id ||= SecureRandom.hex(16) + end + + private + def default_config(ip_address:) + run_id_header_middleware = "#{role_config.full_name}-id-header" + + { + "http" => { + "routers" => { + role_config.full_name => { + "rule" => "PathPrefix(`/`)", + "middlewares" => [ run_id_header_middleware ], + "service" => role_config.full_name + } + }, + "services" => { + role_config.full_name => { + "loadbalancer" => { + "servers" => [ { "url" => "http://#{ip_address}:80" } ] + } + } + }, + "middlewares" => { + run_id_header_middleware => { + "headers" => { + "customresponseheaders" => { + RUN_ID_HEADER => run_id + } + } + } + } + } + } + end + + def custom_config + traefik_config.fetch("dynamic", {}).deep_merge(role_traefik_config.fetch("dynamic", {})) + end +end diff --git a/lib/kamal/configuration/traefik/static.rb b/lib/kamal/configuration/traefik/static.rb new file mode 100644 index 00000000..dd816635 --- /dev/null +++ b/lib/kamal/configuration/traefik/static.rb @@ -0,0 +1,84 @@ +class Kamal::Configuration::Traefik::Static + CONTAINER_PORT = 80 + DEFAULT_IMAGE = "traefik:v2.9" + CONFIG_DIRECTORY = "/var/run/traefik-config" + DEFAULT_ARGS = { + "providers.docker": true, # Obsolete now but required for zero-downtime upgrade from previous versions + "providers.file.directory" => "/var/run/traefik-config", + "providers.file.watch": true, + "log.level" => "DEBUG", + } + + delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils + + attr_reader :config, :traefik_config + + def initialize(config:) + @config = config + @traefik_config = config.traefik || {} + end + + def docker_args + [ + "--name traefik", + "--detach", + "--restart", "unless-stopped", + *publish_args, + "--volume", "/var/run/docker.sock:/var/run/docker.sock", + "--volume", "#{host_directory}:#{CONFIG_DIRECTORY}", + *env_args, + *config.logging_args, + *label_args, + *docker_options_args + ] + end + + def image + traefik_config.fetch("image") { DEFAULT_IMAGE } + end + + def traefik_args + optionize DEFAULT_ARGS.merge(traefik_config.fetch("args", {})), with: "=" + end + + def host_directory + if Pathname.new(config.run_directory).absolute? + "#{config.run_directory}/traefik-config" + else + "$(pwd)/#{config.run_directory}/traefik-config" + end + end + + def host_env_file_path + File.join host_env_directory, "traefik.env" + end + + def host_env_directory + File.join config.host_env_directory, "traefik" + end + + def env_file + env_file_with_secrets config.traefik.fetch("env", {}) + end + + private + def host_port + traefik_config.fetch("host_port", CONTAINER_PORT) + end + + def publish_args + argumentize "--publish", "#{host_port}:#{CONTAINER_PORT}" unless traefik_config["publish"] == false + end + + def env_args + argumentize "--env-file", host_env_file_path + end + + def label_args + argumentize "--label", traefik_config.fetch("labels", []) + end + + def docker_options_args + optionize(traefik_config["options"] || {}) + end +end diff --git a/lib/kamal/utils.rb b/lib/kamal/utils.rb index 6ab1648b..b3104849 100644 --- a/lib/kamal/utils.rb +++ b/lib/kamal/utils.rb @@ -115,4 +115,25 @@ module Kamal::Utils "#{key.to_s}=#{value.to_s}\n" end + + def poll(max_attempts:, exception:, &block) + attempt = 1 + + begin + block.call + rescue exception => e + if attempt <= max_attempts + info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..." + sleep attempt + attempt += 1 + retry + else + raise + end + end + end + + def info(message) + SSHKit.config.output.info(message) + end end diff --git a/lib/kamal/utils/healthcheck_poller.rb b/lib/kamal/utils/healthcheck_poller.rb index ddb09ec6..990c7672 100644 --- a/lib/kamal/utils/healthcheck_poller.rb +++ b/lib/kamal/utils/healthcheck_poller.rb @@ -5,10 +5,7 @@ class Kamal::Utils::HealthcheckPoller class << self def wait_for_healthy(pause_after_ready: false, &block) - attempt = 1 - max_attempts = KAMAL.config.healthcheck["max_attempts"] - - begin + Kamal::Utils.poll(max_attempts: KAMAL.config.healthcheck["max_attempts"], exception: HealthcheckError) do case status = block.call when "healthy" sleep TRAEFIK_HEALTHY_DELAY if pause_after_ready @@ -17,23 +14,9 @@ class Kamal::Utils::HealthcheckPoller else raise HealthcheckError, "container not ready (#{status})" end - rescue HealthcheckError => e - if attempt <= max_attempts - info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..." - sleep attempt - attempt += 1 - retry - else - raise - end - end - info "Container is healthy!" + SSHKit.config.output.info "Container is healthy!" + end end - - private - def info(message) - SSHKit.config.output.info(message) - end end end diff --git a/lib/kamal/utils/switch_poller.rb b/lib/kamal/utils/switch_poller.rb new file mode 100644 index 00000000..1706fc36 --- /dev/null +++ b/lib/kamal/utils/switch_poller.rb @@ -0,0 +1,17 @@ +class Kamal::Utils::SwitchPoller + class SwitchError < StandardError; end + + class << self + TRAEFIK_SWITCH_DELAY = 2 + def wait_for_switch(traefik_dynamic, &block) + if traefik_dynamic.boot_check? + Kamal::Utils.poll(max_attempts: 5, exception: SwitchError) do + polled_run_id = block.call + raise SwitchError, "Waiting for #{traefik_dynamic.config_run_id}, currently #{polled_run_id}" unless polled_run_id == traefik_dynamic.config_run_id + end + else + sleep TRAEFIK_SWITCH_DELAY + end + end + end +end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 8a90e90a..de320e5f 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -5,6 +5,7 @@ class CliAppTest < CliTestCase stub_running run_command("boot").tap do |output| assert_match "docker tag dhh/app:latest dhh/app:latest", output + assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output end @@ -13,26 +14,12 @@ class CliAppTest < CliTestCase test "boot will rename if same version is already running" do run_command("details") # Preheat Kamal const - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false) - .returns("12345678") # running version - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") - .returns("running") # health check - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false) - .returns("123") # old version - + stub_running run_command("boot").tap do |output| assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename - assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output end - ensure - Thread.report_on_exception = true end test "boot uses group strategy when specified" do @@ -45,9 +32,27 @@ class CliAppTest < CliTestCase run_command("boot", config: :with_boot_strategy) end + test "boot without traefik file provider raises exception" do + Thread.report_on_exception = false + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "-f '{{index .Args 1 }}'", :traefik) + .returns("[--providers.docker --log.level=DEBUG --accesslog --accesslog.format=json]").at_least_once + + assert_raises(SSHKit::Runner::ExecuteError, "Exception while executing on host 1.1.1.1: File provider not enabled, you'll need to run `kamal traefik reboot` to deploy") do + run_command("boot") + end + ensure + Thread.report_on_exception = true + end + test "boot errors leave lock in place" do invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999" } + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "-f '{{index .Args 1 }}'", :traefik) + .returns("[--providers.docker --providers.file.directory=/var/run/traefik-config --providers.file.watch --log.level=DEBUG --accesslog --accesslog.format=json]").at_least_once + Kamal::Cli::App.any_instance.expects(:using_version).raises(RuntimeError) assert !KAMAL.holding_lock? @@ -58,6 +63,13 @@ class CliAppTest < CliTestCase end test "start" do + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info) + .with { |*args| args == [ :docker, :inspect, "-f '{{index .Args 1 }}'", :traefik ] } + .returns("[--providers.docker --providers.file.directory=/var/run/traefik-config --providers.file.watch --log.level=DEBUG --accesslog --accesslog.format=json]").at_least_once + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info) + .with { |*args| args != [ :docker, :inspect, "-f '{{index .Args 1 }}'", :traefik ] } + .returns("").at_least_once + run_command("start").tap do |output| assert_match "docker start app-web-999", output end @@ -124,7 +136,7 @@ class CliAppTest < CliTestCase test "exec" do run_command("exec", "ruby -v").tap do |output| - assert_match "docker run --rm dhh/app:latest ruby -v", output + assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output end end @@ -180,10 +192,26 @@ class CliAppTest < CliTestCase end def stub_running + SecureRandom.stubs(:hex).with(16).returns("12345678901234567890123456789012") + SecureRandom.stubs(:hex).with(6).returns("123456789012") + SecureRandom.stubs(:hex).with(8).returns("1234567890123456") + SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "-f '{{index .Args 1 }}'", :traefik) + .returns("[--providers.docker --providers.file.directory=/var/run/traefik-config --providers.file.watch --log.level=DEBUG --accesslog --accesslog.format=json]").at_least_once + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") .returns("running") # health check + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "-f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'", "app-web-latest") + .returns("172.17.0.3").at_least_once + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :exec, :traefik, :wget, "-qSO", "/dev/null", "http://localhost:80/up", "2>&1", "|", :grep, "-i", "X-Kamal-Run-ID", "|", :cut, "-d ' ' -f 4") + .returns("12345678901234567890123456789012").at_least_once end end diff --git a/test/cli/healthcheck_test.rb b/test/cli/healthcheck_test.rb index f9c3aa9c..47d61860 100644 --- a/test/cli/healthcheck_test.rb +++ b/test/cli/healthcheck_test.rb @@ -5,7 +5,7 @@ class CliHealthcheckTest < CliTestCase # Prevent expected failures from outputting to terminal Thread.report_on_exception = false - Kamal::Utils::HealthcheckPoller.stubs(:sleep) # No sleeping when retrying + Object.any_instance.stubs(:sleep) # No sleeping when retrying SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false) @@ -28,13 +28,15 @@ class CliHealthcheckTest < CliTestCase assert_match "container not ready (unhealthy), retrying in 2s (attempt 2/7)...", output assert_match "Container is healthy!", output end + ensure + Thread.report_on_exception = true end test "perform failing to become healthy" do # Prevent expected failures from outputting to terminal Thread.report_on_exception = false - Kamal::Utils::HealthcheckPoller.stubs(:sleep) # No sleeping when retrying + Object.any_instance.stubs(:sleep) # No sleeping when retrying SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false) @@ -62,6 +64,8 @@ class CliHealthcheckTest < CliTestCase run_command("perform") end assert_match "container not ready (unhealthy)", exception.message + ensure + Thread.report_on_exception = true end private diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 04f11575..e8677a6d 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -66,7 +66,7 @@ class CliMainTest < CliTestCase .with { |*args| args == [ :mkdir, "-p", ".kamal" ] } SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*arg| arg[0..1] == [:mkdir, ".kamal/lock-app"] } + .with { |*args| args[0..1] == [:mkdir, ".kamal/lock-app"] } .raises(RuntimeError, "mkdir: cannot create directory ‘kamal_lock-app’: File exists") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_debug) @@ -75,6 +75,8 @@ class CliMainTest < CliTestCase assert_raises(Kamal::Cli::LockError) do run_command("deploy") end + ensure + Thread.report_on_exception = true end test "deploy error when locking" do @@ -90,6 +92,8 @@ class CliMainTest < CliTestCase assert_raises(SSHKit::Runner::ExecuteError) do run_command("deploy") end + ensure + Thread.report_on_exception = true end test "deploy errors during outside section leave remove lock" do @@ -173,9 +177,14 @@ class CliMainTest < CliTestCase assert_match /docker container ls --all --filter name=\^app-web-nonsense\$ --quiet/, output assert_match /The app version 'nonsense' is not available as a container/, output end + ensure + Thread.report_on_exception = true end test "rollback good version" do + SecureRandom.stubs(:hex).with(16).returns("12345678901234567890123456789012") + SecureRandom.stubs(:hex).with(6).returns("123456789012") + [ "web", "workers" ].each do |role| SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :container, :ls, "--filter", "name=^app-#{role}-123$", "--quiet", raise_on_non_zero_exit: false) @@ -191,6 +200,18 @@ class CliMainTest < CliTestCase .returns("running").at_least_once # health check end + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "-f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'", "app-web-123") + .returns("172.17.0.3").at_least_once + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "-f '{{index .Args 1 }}'", :traefik) + .returns("[--providers.docker --providers.file.directory=/var/run/traefik-config --providers.file.watch --log.level=DEBUG --accesslog --accesslog.format=json]").at_least_once + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :exec, :traefik, :wget, "-qSO", "/dev/null", "http://localhost:80/up", "2>&1", "|", :grep, "-i", "X-Kamal-Run-ID", "|", :cut, "-d ' ' -f 4") + .returns("12345678901234567890123456789012").at_least_once + Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true) hook_variables = { version: 123, service_version: "app@123", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "rollback" } @@ -207,8 +228,13 @@ class CliMainTest < CliTestCase test "rollback without old version" do Kamal::Cli::Main.any_instance.stubs(:container_available?).returns(true) - Kamal::Utils::HealthcheckPoller.stubs(:sleep) + Object.stubs(:sleep) + SecureRandom.stubs(:hex).with(16).returns("12345678901234567890123456789012") + SecureRandom.stubs(:hex).with(6).returns("123456789012") + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "-f '{{index .Args 1 }}'", :traefik) + .returns("[--providers.docker --providers.file.directory=/var/run/traefik-config --providers.file.watch --log.level=DEBUG --accesslog --accesslog.format=json]").at_least_once SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :container, :ls, "--filter", "name=^app-web-123$", "--quiet", raise_on_non_zero_exit: false) .returns("").at_least_once @@ -218,6 +244,13 @@ class CliMainTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") .returns("running").at_least_once # health check + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :inspect, "-f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'", "app-web-123") + .returns("172.17.0.3").at_least_once + + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) + .with(:docker, :exec, :traefik, :wget, "-qSO", "/dev/null", "http://localhost:80/up", "2>&1", "|", :grep, "-i", "X-Kamal-Run-ID", "|", :cut, "-d ' ' -f 4") + .returns("12345678901234567890123456789012").at_least_once run_command("rollback", "123").tap do |output| assert_match "Start container with version 123", output diff --git a/test/cli/traefik_test.rb b/test/cli/traefik_test.rb index 1b19b033..dad8a6b2 100644 --- a/test/cli/traefik_test.rb +++ b/test/cli/traefik_test.rb @@ -4,7 +4,7 @@ class CliTraefikTest < CliTestCase test "boot" do run_command("boot").tap do |output| assert_match "docker login", output - assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output + assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{Kamal::Configuration::Traefik::Static::DEFAULT_IMAGE} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\"", output end end @@ -14,11 +14,13 @@ class CliTraefikTest < CliTestCase run_command("reboot").tap do |output| assert_match "docker container stop traefik", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output - assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output + assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{Kamal::Configuration::Traefik::Static::DEFAULT_IMAGE} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\"", output end end test "reboot --rolling" do + Object.any_instance.stubs(:sleep) + run_command("reboot", "--rolling").tap do |output| assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output end diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index ab3f6ace..0a874d51 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -13,13 +13,13 @@ class CommandsAppTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" dhh/app:999", new_command.run.join(" ") end test "run with hostname" do assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" dhh/app:999", new_command.run(hostname: "myhost").join(" ") end @@ -27,7 +27,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:volumes] = ["/local/path:/container/path" ] assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" dhh/app:999", new_command.run.join(" ") end @@ -35,7 +35,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:healthcheck] = { "path" => "/healthz" } assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/healthz || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/healthz || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" dhh/app:999", new_command.run.join(" ") end @@ -43,7 +43,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:healthcheck] = { "cmd" => "/bin/up" } assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"/bin/up\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"/bin/up\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" dhh/app:999", new_command.run.join(" ") end @@ -51,7 +51,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "healthcheck" => { "cmd" => "/bin/healthy" } } } assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"/bin/healthy\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"/bin/healthy\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" dhh/app:999", new_command.run.join(" ") end @@ -66,7 +66,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" dhh/app:999", new_command.run.join(" ") end @@ -85,13 +85,13 @@ class CommandsAppTest < ActiveSupport::TestCase test "start_or_run" do assert_equal \ - "docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" dhh/app:999", new_command.start_or_run.join(" ") end test "start_or_run with hostname" do assert_equal \ - "docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999", + "docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" --env-file .kamal/env/roles/app-web.env --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" dhh/app:999", new_command.start_or_run(hostname: "myhost").join(" ") end diff --git a/test/commands/traefik_test.rb b/test/commands/traefik/static_test.rb similarity index 53% rename from test/commands/traefik_test.rb rename to test/commands/traefik/static_test.rb index 5a651dee..67ba2039 100644 --- a/test/commands/traefik_test.rb +++ b/test/commands/traefik/static_test.rb @@ -1,6 +1,6 @@ require "test_helper" -class CommandsTraefikTest < ActiveSupport::TestCase +class CommandsTraefikStaticTest < ActiveSupport::TestCase setup do @image = "traefik:test" @@ -18,72 +18,72 @@ class CommandsTraefikTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["host_port"] = "8080" assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["publish"] = false assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end test "run with ports configured" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["options"] = {"publish" => %w[9000:9000 9001:9001]} assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --publish \"9000:9000\" --publish \"9001:9001\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --publish \"9000:9000\" --publish \"9001:9001\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end test "run with volumes configured" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json] } assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end test "run with several options configured" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json], "publish" => %w[8080:8080], "memory" => "512m"} assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end test "run with labels configured" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["labels"] = { "traefik.http.routers.dashboard.service" => "api@internal", "traefik.http.routers.dashboard.middlewares" => "auth" } assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.dashboard.service=\"api@internal\" --label traefik.http.routers.dashboard.middlewares=\"auth\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.dashboard.service=\"api@internal\" --label traefik.http.routers.dashboard.middlewares=\"auth\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end test "run with env configured" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] } assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end @@ -91,7 +91,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase @config.delete(:traefik) assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{Kamal::Configuration::Traefik::Static::DEFAULT_IMAGE} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\"", new_command.run.join(" ") end @@ -99,7 +99,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end @@ -107,7 +107,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase @config[:traefik]["args"]["log.level"] = "ERROR" assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=\"ERROR\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/traefik-config:/var/run/traefik-config --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" #{@image} --providers.docker --providers.file.directory=\"/var/run/traefik-config\" --providers.file.watch --log.level=\"ERROR\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end @@ -183,10 +183,6 @@ class CommandsTraefikTest < ActiveSupport::TestCase assert_equal "EXAMPLE_API_KEY=456\n", new_command.env_file end - test "host_env_file_path" do - assert_equal ".kamal/env/traefik/traefik.env", new_command.host_env_file_path - end - test "make_env_directory" do assert_equal "mkdir -p .kamal/env/traefik", new_command.make_env_directory.join(" ") end @@ -197,6 +193,6 @@ class CommandsTraefikTest < ActiveSupport::TestCase private def new_command - Kamal::Commands::Traefik.new(Kamal::Configuration.new(@config, version: "123")) + Kamal::Commands::Traefik::Static.new(Kamal::Configuration.new(@config, version: "123")) end end diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index b29ac2b5..55298d3d 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -42,7 +42,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase end test "special label args for web" do - assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "traefik.http.services.app-web.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.middlewares.app-web-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\"" ], @config.role(:web).label_args + assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\""], @config.role(:web).label_args end test "custom labels" do @@ -66,7 +66,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] } }) - assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.services.app-beta.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-beta.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-beta.middlewares=\"app-beta-retry@docker\"" ], config.role(:beta).label_args + assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"" ], config.role(:beta).label_args end test "env overwritten by role" do diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 36429e56..b1194baf 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -252,4 +252,10 @@ class ConfigurationTest < ActiveSupport::TestCase config = Kamal::Configuration.new(@deploy.merge!(run_directory: "/root/kamal")) assert_equal "/root/kamal", config.run_directory end + + test "app traefik labels raise ArgumentError" do + assert_raises(ArgumentError) do + Kamal::Configuration.new(@deploy.tap { |c| c.merge!(labels: { "traefik.enabled": true }) }) + end + end end diff --git a/test/integration/docker/deployer/app/config/deploy.yml b/test/integration/docker/deployer/app/config/deploy.yml index 6ecb94b3..c9063f68 100644 --- a/test/integration/docker/deployer/app/config/deploy.yml +++ b/test/integration/docker/deployer/app/config/deploy.yml @@ -19,6 +19,7 @@ builder: COMMIT_SHA: <%= `git rev-parse HEAD` %> healthcheck: cmd: wget -qO- http://localhost > /dev/null + path: / traefik: args: accesslog: true diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index fcf82267..58eefce3 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -39,6 +39,12 @@ class MainTest < IntegrationTest assert_no_remote_env_file end + test "envify" do + kamal :envify + + assert_equal "SECRET_TOKEN=1234", deployer_exec("cat .env", capture: true) + end + test "config" do config = YAML.load(kamal(:config, capture: true)) version = latest_app_version