diff --git a/README.md b/README.md index 59bca2dc..3f4d15e7 100644 --- a/README.md +++ b/README.md @@ -333,6 +333,29 @@ servers: That'll start the job containers with `docker run ... --cap-add --cpu-count 4 ...`. +### Configuring logging + +You can configure the logging driver and options passed to Docker using `logging`: + +```yaml +logging: + driver: awslogs + options: + awslogs-region: "eu-central-2" + awslogs-group: "my-app" +``` + +If nothing is configured, the default option `max-size=10m` is used for all containers. The default logging driver of Docker is `json-file`. + +### Using a different stop wait time + +On a new deploy, each old running container is gracefully shut down with a `SIGTERM`, and after a grace period of `10` seconds a `SIGKILL` is sent. +You can configure this value via the `stop_wait_time` option: + +```yaml +stop_wait_time: 30 +``` + ### Using remote builder for native multi-arch If you're developing on ARM64 (like Apple Silicon), but you want to deploy on AMD64 (x86 64-bit), you can use multi-architecture images. By default, MRSK will setup a local buildx configuration that does this through QEMU emulation. But this can be quite slow, especially on the first build. @@ -512,6 +535,9 @@ accessories: - MYSQL_ROOT_PASSWORD volumes: - /var/lib/mysql:/var/lib/mysql + options: + cpus: 4 + memory: "2GB" redis: image: redis:latest host: 1.1.1.4 diff --git a/lib/mrsk/cli/app.rb b/lib/mrsk/cli/app.rb index 26c33bbc..be09c4ee 100644 --- a/lib/mrsk/cli/app.rb +++ b/lib/mrsk/cli/app.rb @@ -8,24 +8,26 @@ class Mrsk::Cli::App < Mrsk::Cli::Base cli = self - MRSK.config.roles.each do |role| - on(role.hosts) do |host| - execute *MRSK.auditor.record("Booted app version #{version}"), verbosity: :debug + on(MRSK.hosts) do |host| + roles = MRSK.roles_on(host) + + roles.each do |role| + execute *MRSK.auditor(role: role).record("Booted app version #{version}"), verbosity: :debug begin - old_version = capture_with_info(*MRSK.app.current_running_version).strip - execute *MRSK.app.run(role: role.name) + old_version = capture_with_info(*MRSK.app(role: role).current_running_version).strip + execute *MRSK.app(role: role).run sleep MRSK.config.readiness_delay - execute *MRSK.app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present? + execute *MRSK.app(role: role).stop(version: old_version), raise_on_non_zero_exit: false if old_version.present? rescue SSHKit::Command::Failed => e if e.message =~ /already in use/ error "Rebooting container with same version #{version} already deployed on #{host} (may cause gap in zero-downtime promise!)" - execute *MRSK.auditor.record("Rebooted app version #{version}"), verbosity: :debug + execute *MRSK.auditor(role: role).record("Rebooted app version #{version}"), verbosity: :debug - execute *MRSK.app.stop(version: version) - execute *MRSK.app.remove_container(version: version) - execute *MRSK.app.run(role: role.name) + execute *MRSK.app(role: role).stop(version: version) + execute *MRSK.app(role: role).remove_container(version: version) + execute *MRSK.app(role: role).run else raise end @@ -39,9 +41,13 @@ class Mrsk::Cli::App < Mrsk::Cli::Base desc "start", "Start existing app container on servers" def start with_lock do - on(MRSK.hosts) do - execute *MRSK.auditor.record("Started app version #{MRSK.config.version}"), verbosity: :debug - execute *MRSK.app.start, raise_on_non_zero_exit: false + on(MRSK.hosts) do |host| + roles = MRSK.roles_on(host) + + roles.each do |role| + execute *MRSK.auditor.record("Started app version #{MRSK.config.version}"), verbosity: :debug + execute *MRSK.app(role: role).start, raise_on_non_zero_exit: false + end end end end @@ -49,9 +55,13 @@ class Mrsk::Cli::App < Mrsk::Cli::Base desc "stop", "Stop app container on servers" def stop with_lock do - on(MRSK.hosts) do - execute *MRSK.auditor.record("Stopped app"), verbosity: :debug - execute *MRSK.app.stop, raise_on_non_zero_exit: false + on(MRSK.hosts) do |host| + roles = MRSK.roles_on(host) + + roles.each do |role| + execute *MRSK.auditor(role: role).record("Stopped app"), verbosity: :debug + execute *MRSK.app(role: role).stop, raise_on_non_zero_exit: false + end end end end @@ -59,7 +69,13 @@ class Mrsk::Cli::App < Mrsk::Cli::Base # FIXME: Drop in favor of just containers? desc "details", "Show details about app containers" def details - on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.info) } + on(MRSK.hosts) do |host| + roles = MRSK.roles_on(host) + + roles.each do |role| + puts_by_host host, capture_with_info(*MRSK.app(role: role).info) + end + end end desc "exec [CMD]", "Execute a custom command on servers (use --help to show options)" @@ -71,7 +87,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base say "Get current version of running container...", :magenta unless options[:version] using_version(options[:version] || current_running_version) do |version| say "Launching interactive command with version #{version} via SSH from existing container on #{MRSK.primary_host}...", :magenta - run_locally { exec MRSK.app.execute_in_existing_container_over_ssh(cmd, host: MRSK.primary_host) } + run_locally { exec MRSK.app(role: "web").execute_in_existing_container_over_ssh(cmd, host: MRSK.primary_host) } end when options[:interactive] @@ -87,8 +103,12 @@ class Mrsk::Cli::App < Mrsk::Cli::Base say "Launching command with version #{version} from existing container...", :magenta on(MRSK.hosts) do |host| - execute *MRSK.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug - puts_by_host host, capture_with_info(*MRSK.app.execute_in_existing_container(cmd)) + roles = MRSK.roles_on(host) + + roles.each do |role| + execute *MRSK.auditor(role: role).record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug + puts_by_host host, capture_with_info(*MRSK.app(role: role).execute_in_existing_container(cmd)) + end end end @@ -156,9 +176,13 @@ class Mrsk::Cli::App < Mrsk::Cli::Base desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true def remove_container(version) with_lock do - on(MRSK.hosts) do - execute *MRSK.auditor.record("Removed app container with version #{version}"), verbosity: :debug - execute *MRSK.app.remove_container(version: version) + on(MRSK.hosts) do |host| + roles = MRSK.roles_on(host) + + roles.each do |role| + execute *MRSK.auditor(role: role).record("Removed app container with version #{version}"), verbosity: :debug + execute *MRSK.app(role: role).remove_container(version: version) + end end end end @@ -166,9 +190,13 @@ class Mrsk::Cli::App < Mrsk::Cli::Base desc "remove_containers", "Remove all app containers from servers", hide: true def remove_containers with_lock do - on(MRSK.hosts) do - execute *MRSK.auditor.record("Removed all app containers"), verbosity: :debug - execute *MRSK.app.remove_containers + on(MRSK.hosts) do |host| + roles = MRSK.roles_on(host) + + roles.each do |role| + execute *MRSK.auditor(role: role).record("Removed all app containers"), verbosity: :debug + execute *MRSK.app(role: role).remove_containers + end end end end diff --git a/lib/mrsk/commander.rb b/lib/mrsk/commander.rb index abcc48f5..b70357c6 100644 --- a/lib/mrsk/commander.rb +++ b/lib/mrsk/commander.rb @@ -9,7 +9,6 @@ class Mrsk::Commander self.lock_count = 0 end - def config @config ||= Mrsk::Configuration.create_from(**@config_kwargs).tap do |config| @config_kwargs = nil @@ -21,23 +20,38 @@ class Mrsk::Commander @config, @config_kwargs = nil, kwargs end - - attr_accessor :specific_hosts + attr_reader :specific_roles, :specific_hosts def specific_primary! self.specific_hosts = [ config.primary_web_host ] end def specific_roles=(role_names) - self.specific_hosts = config.roles.select { |r| role_names.include?(r.name) }.flat_map(&:hosts) if role_names.present? + @specific_roles = config.roles.select { |r| role_names.include?(r.name) } if role_names.present? + end + + def specific_hosts=(hosts) + @specific_hosts = config.all_hosts & hosts if hosts.present? end def primary_host specific_hosts&.first || config.primary_web_host end + def roles + (specific_roles || config.roles).select do |role| + ((specific_hosts || config.all_hosts) & role.hosts).any? + end + end + def hosts - specific_hosts || config.all_hosts + (specific_hosts || config.all_hosts).select do |host| + (specific_roles || config.roles).flat_map(&:hosts).include?(host) + end + end + + def roles_on(host) + roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name) end def traefik_hosts @@ -53,16 +67,16 @@ class Mrsk::Commander end - def app - @app ||= Mrsk::Commands::App.new(config) + def app(role: nil) + Mrsk::Commands::App.new(config, role: role) end def accessory(name) Mrsk::Commands::Accessory.new(config, name: name) end - def auditor - @auditor ||= Mrsk::Commands::Auditor.new(config) + def auditor(role: nil) + Mrsk::Commands::Auditor.new(config, role: role) end def builder diff --git a/lib/mrsk/commands/accessory.rb b/lib/mrsk/commands/accessory.rb index 2f745267..529e6e32 100644 --- a/lib/mrsk/commands/accessory.rb +++ b/lib/mrsk/commands/accessory.rb @@ -1,6 +1,7 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base attr_reader :accessory_config - delegate :service_name, :image, :host, :port, :files, :directories, :publish_args, :env_args, :volume_args, :label_args, to: :accessory_config + delegate :service_name, :image, :host, :port, :files, :directories, :publish_args, :env_args, :volume_args, + :label_args, :option_args, to: :accessory_config def initialize(config, name:) super(config) @@ -12,11 +13,12 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base "--name", service_name, "--detach", "--restart", "unless-stopped", - "--log-opt", "max-size=#{MAX_LOG_SIZE}", + *config.logging_args, *publish_args, *env_args, *volume_args, *label_args, + *option_args, image end diff --git a/lib/mrsk/commands/app.rb b/lib/mrsk/commands/app.rb index ad0a21d7..f2064e74 100644 --- a/lib/mrsk/commands/app.rb +++ b/lib/mrsk/commands/app.rb @@ -1,14 +1,21 @@ class Mrsk::Commands::App < Mrsk::Commands::Base - def run(role: :web) - role = config.role(role) + attr_reader :role + + def initialize(config, role: nil) + super(config) + @role = role + end + + def run + role = config.role(self.role) docker :run, "--detach", "--restart unless-stopped", - "--log-opt", "max-size=#{MAX_LOG_SIZE}", - "--name", service_with_version_and_destination, - "-e", "MRSK_CONTAINER_NAME=\"#{service_with_version_and_destination}\"", + "--name", container_name, + "-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"", *role.env_args, + *config.logging_args, *config.volume_args, *role.label_args, *role.option_args, @@ -17,13 +24,13 @@ class Mrsk::Commands::App < Mrsk::Commands::Base end def start - docker :start, service_with_version_and_destination + docker :start, container_name end def stop(version: nil) pipe \ version ? container_id_for_version(version) : current_container_id, - xargs(docker(:stop)) + xargs(config.stop_wait_time ? docker(:stop, "-t", config.stop_wait_time) : docker(:stop)) end def info @@ -52,7 +59,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base def execute_in_existing_container(*command, interactive: false) docker :exec, ("-it" if interactive), - service_with_version_and_destination, + container_name, *command end @@ -97,7 +104,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base def remove_container(version:) pipe \ - container_id_for(container_name: service_with_version_and_destination(version)), + container_id_for(container_name: container_name(version)), xargs(docker(:container, :rm)) end @@ -115,12 +122,12 @@ class Mrsk::Commands::App < Mrsk::Commands::Base private - def service_with_version_and_destination(version = nil) - [ config.service, config.destination, version || config.version ].compact.join("-") + def container_name(version = nil) + [ config.service, role, config.destination, version || config.version ].compact.join("-") end def container_id_for_version(version) - container_id_for(container_name: service_with_version_and_destination(version)) + container_id_for(container_name: container_name(version)) end def filter_args @@ -130,6 +137,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base def filters [ "label=service=#{config.service}" ].tap do |filters| filters << "label=destination=#{config.destination}" if config.destination + filters << "label=role=#{role}" if role end end end diff --git a/lib/mrsk/commands/auditor.rb b/lib/mrsk/commands/auditor.rb index a0bc0076..6915a521 100644 --- a/lib/mrsk/commands/auditor.rb +++ b/lib/mrsk/commands/auditor.rb @@ -1,6 +1,13 @@ require "active_support/core_ext/time/conversions" class Mrsk::Commands::Auditor < Mrsk::Commands::Base + attr_reader :role + + def initialize(config, role: nil) + super(config) + @role = role + end + # Runs remotely def record(line) append \ @@ -25,18 +32,26 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base end def tagged_record_line(line) - "'#{recorded_at_tag} #{performer_tag} #{line}'" + tagged_line recorded_at_tag, performer_tag, role_tag, line end def tagged_broadcast_line(line) - "'#{performer_tag} #{line}'" + tagged_line performer_tag, role_tag, line + end + + def tagged_line(*tags_and_line) + "'#{tags_and_line.compact.join(" ")}'" + end + + def recorded_at_tag + "[#{Time.now.to_fs(:db)}]" end def performer_tag "[#{`whoami`.strip}]" end - def recorded_at_tag - "[#{Time.now.to_fs(:db)}]" + def role_tag + "[#{role}]" if role end end diff --git a/lib/mrsk/commands/base.rb b/lib/mrsk/commands/base.rb index df1c4305..9738a91c 100644 --- a/lib/mrsk/commands/base.rb +++ b/lib/mrsk/commands/base.rb @@ -2,8 +2,6 @@ module Mrsk::Commands class Base delegate :redact, :argumentize, to: Mrsk::Utils - MAX_LOG_SIZE = "10m" - attr_accessor :config def initialize(config) diff --git a/lib/mrsk/commands/traefik.rb b/lib/mrsk/commands/traefik.rb index ee86543f..098b7e29 100644 --- a/lib/mrsk/commands/traefik.rb +++ b/lib/mrsk/commands/traefik.rb @@ -7,9 +7,9 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base docker :run, "--name traefik", "--detach", "--restart", "unless-stopped", - "--log-opt", "max-size=#{MAX_LOG_SIZE}", "--publish", port, "--volume", "/var/run/docker.sock:/var/run/docker.sock", + *config.logging_args, *docker_options_args, "traefik", "--providers.docker", @@ -50,7 +50,7 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik" end - def port + def port "#{host_port}:#{CONTAINER_PORT}" end diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index 074c7cf3..d6cf09f1 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -6,8 +6,8 @@ require "erb" require "net/ssh/proxy/jump" class Mrsk::Configuration - delegate :service, :image, :servers, :env, :labels, :registry, :builder, to: :raw_config, allow_nil: true - delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils + delegate :service, :image, :servers, :env, :labels, :registry, :builder, :stop_wait_time, to: :raw_config, allow_nil: true + delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils attr_accessor :destination attr_accessor :raw_config @@ -76,7 +76,7 @@ class Mrsk::Configuration def all_hosts - roles.flat_map(&:hosts) + roles.flat_map(&:hosts).uniq end def primary_web_host @@ -84,7 +84,7 @@ class Mrsk::Configuration end def traefik_hosts - roles.select(&:running_traefik?).flat_map(&:hosts) + roles.select(&:running_traefik?).flat_map(&:hosts).uniq end @@ -121,6 +121,15 @@ class Mrsk::Configuration end end + def logging_args + if raw_config.logging.present? + optionize({ "log-driver" => raw_config.logging["driver"] }.compact) + + argumentize("--log-opt", raw_config.logging["options"]) + else + argumentize("--log-opt", { "max-size" => "10m" }) + end + end + def ssh_user if raw_config.ssh.present? @@ -173,6 +182,7 @@ class Mrsk::Configuration ssh_options: ssh_options, builder: raw_config.builder, accessories: raw_config.accessories, + logging: logging_args, healthcheck: healthcheck }.compact end diff --git a/lib/mrsk/configuration/accessory.rb b/lib/mrsk/configuration/accessory.rb index f1051423..73c8a7dc 100644 --- a/lib/mrsk/configuration/accessory.rb +++ b/lib/mrsk/configuration/accessory.rb @@ -1,5 +1,5 @@ class Mrsk::Configuration::Accessory - delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils + delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils attr_accessor :name, :specifics @@ -67,6 +67,14 @@ class Mrsk::Configuration::Accessory argumentize "--volume", volumes end + def option_args + if args = specifics["options"] + optionize args + else + [] + end + end + private attr_accessor :config diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index b9833b89..a2e4d73d 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -7,7 +7,7 @@ class CliAccessoryTest < CliTestCase run_command("boot", "mysql").tap do |output| assert_match /docker login.*on 1.1.1.3/, output - assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=10m --publish 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output + assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output end end @@ -20,8 +20,8 @@ class CliAccessoryTest < CliTestCase run_command("boot", "all").tap do |output| assert_match /docker login.*on 1.1.1.3/, output assert_match /docker login.*on 1.1.1.4/, output - assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=10m --publish 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output - assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=10m --publish 6379:6379 --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.4", output + assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output + assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.4", output end end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index b2b6786a..fa132007 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -3,14 +3,11 @@ require_relative "cli_test_case" class CliAppTest < CliTestCase test "boot" do # Stub current version fetch - SSHKit::Backend::Abstract.any_instance.stubs(:capture) - .returns("999") # new version - .then - .returns("123") # old version + SSHKit::Backend::Abstract.any_instance.stubs(:capture).returns("123") # old version run_command("boot").tap do |output| assert_match "docker run --detach --restart unless-stopped", output - assert_match "docker container ls --all --filter name=app-123 --quiet | xargs docker stop", output + assert_match "docker container ls --all --filter name=app-web-123 --quiet | xargs docker stop", output end end @@ -20,16 +17,14 @@ class CliAppTest < CliTestCase # Prevent expected failures from outputting to terminal Thread.report_on_exception = false - MRSK.app.stubs(:run) - .raises(SSHKit::Command::Failed.new("already in use")) - .then + Mrsk::Commands::App.any_instance.stubs(:run) .raises(SSHKit::Command::Failed.new("already in use")) .then .returns([ :docker, :run ]) run_command("boot").tap do |output| assert_match "Rebooting container with same version latest already deployed", output # Can't start what's already running - assert_match "docker container ls --all --filter name=app-latest --quiet | xargs docker container rm", output # Remove old container + assert_match "docker container ls --all --filter name=app-web-latest --quiet | xargs docker container rm", output # Remove old container assert_match "docker run", output # Start new container end ensure @@ -38,32 +33,58 @@ class CliAppTest < CliTestCase test "start" do run_command("start").tap do |output| - assert_match "docker start app-999", output + assert_match "docker start app-web-999", output end end test "stop" do run_command("stop").tap do |output| - assert_match "docker ps --quiet --filter label=service=app | xargs docker stop", output + assert_match "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker stop", output end end test "details" do run_command("details").tap do |output| - assert_match "docker ps --filter label=service=app", output + assert_match "docker ps --filter label=service=app --filter label=role=web", output + end + end + + test "remove" do + run_command("remove").tap do |output| + assert_match /#{Regexp.escape("docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker stop")}/, output + assert_match /#{Regexp.escape("docker container prune --force --filter label=service=app")}/, output + assert_match /#{Regexp.escape("docker image prune --all --force --filter label=service=app")}/, output + end + end + + test "remove_container" do + run_command("remove_container", "1234567").tap do |output| + assert_match "docker container ls --all --filter name=app-web-1234567 --quiet | xargs docker container rm", output + end + end + + test "remove_containers" do + run_command("remove_containers").tap do |output| + assert_match "docker container prune --force --filter label=service=app", output + end + end + + test "remove_images" do + run_command("remove_images").tap do |output| + assert_match "docker image prune --all --force --filter label=service=app", output end end test "exec" do run_command("exec", "ruby -v").tap do |output| - assert_match "ruby -v", output + assert_match "docker run --rm dhh/app:latest ruby -v", output end end test "exec with reuse" do run_command("exec", "--reuse", "ruby -v").tap do |output| assert_match "docker ps --filter label=service=app --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1", output # Get current version - assert_match "docker exec app-999 ruby -v", output + assert_match "docker exec app-web-999 ruby -v", output end end @@ -93,32 +114,6 @@ class CliAppTest < CliTestCase assert_match "docker ps --quiet --filter label=service=app | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow") end - test "remove" do - Mrsk::Cli::App.any_instance.expects(:stop) - Mrsk::Cli::App.any_instance.expects(:remove_containers) - Mrsk::Cli::App.any_instance.expects(:remove_images) - - run_command("remove") - end - - test "remove_container" do - run_command("remove_container", "1234567").tap do |output| - assert_match "docker container ls --all --filter name=app-1234567 --quiet | xargs docker container rm", output - end - end - - test "remove_containers" do - run_command("remove_containers").tap do |output| - assert_match "docker container prune --force --filter label=service=app", output - end - end - - test "remove_images" do - run_command("remove_images").tap do |output| - assert_match "docker image prune --all --force --filter label=service=app", output - end - end - test "version" do run_command("version").tap do |output| assert_match "docker ps --filter label=service=app --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1", output @@ -127,6 +122,6 @@ class CliAppTest < CliTestCase private def run_command(*command) - stdouted { Mrsk::Cli::App.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } + stdouted { Mrsk::Cli::App.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml", "--hosts", "1.1.1.1"]) } end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 3a506378..4d75478b 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -90,7 +90,7 @@ class CliMainTest < CliTestCase test "rollback good version" do Mrsk::Cli::Main.any_instance.stubs(:container_name_available?).returns(true) - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").returns("version-to-rollback\n").times(2) + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").returns("version-to-rollback\n").at_least_once run_command("rollback", "123").tap do |output| assert_match "Start version 123", output @@ -101,7 +101,7 @@ class CliMainTest < CliTestCase test "rollback without old version" do Mrsk::Cli::Main.any_instance.stubs(:container_name_available?).returns(true) - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").returns("").times(2) + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").returns("").at_least_once run_command("rollback", "123").tap do |output| assert_match "Start version 123", output @@ -122,8 +122,6 @@ class CliMainTest < CliTestCase run_command("audit").tap do |output| assert_match /tail -n 50 mrsk-app-audit.log on 1.1.1.1/, output assert_match /App Host: 1.1.1.1/, output - assert_match /tail -n 50 mrsk-app-audit.log on 1.1.1.2/, output - assert_match /App Host: 1.1.1.2/, output end end diff --git a/test/cli/prune_test.rb b/test/cli/prune_test.rb index 15e54575..4ff9eedc 100644 --- a/test/cli/prune_test.rb +++ b/test/cli/prune_test.rb @@ -10,15 +10,13 @@ class CliPruneTest < CliTestCase test "images" do run_command("images").tap do |output| - assert_match "docker image prune --all --force --filter label=service=app --filter until=168h on 1.1.1.1", output - assert_match "docker image prune --all --force --filter label=service=app --filter until=168h on 1.1.1.2", output + assert_match /docker image prune --all --force --filter label=service=app --filter until=168h on 1.1.1.\d/, output end end test "containers" do run_command("containers").tap do |output| - assert_match "docker container prune --force --filter label=service=app --filter until=72h on 1.1.1.1", output - assert_match "docker container prune --force --filter label=service=app --filter until=72h on 1.1.1.2", output + assert_match /docker container prune --force --filter label=service=app --filter until=72h on 1.1.1.\d/, output end end diff --git a/test/cli/registry_test.rb b/test/cli/registry_test.rb index d100402c..0647f873 100644 --- a/test/cli/registry_test.rb +++ b/test/cli/registry_test.rb @@ -4,15 +4,13 @@ class CliRegistryTest < CliTestCase test "login" do run_command("login").tap do |output| assert_match /docker login -u \[REDACTED\] -p \[REDACTED\] as .*@localhost/, output - assert_match "docker login -u [REDACTED] -p [REDACTED] on 1.1.1.1", output - assert_match "docker login -u [REDACTED] -p [REDACTED] on 1.1.1.2", output + assert_match /docker login -u \[REDACTED\] -p \[REDACTED\] on 1.1.1.\d/, output end end test "logout" do run_command("logout").tap do |output| - assert_match "docker logout on 1.1.1.1", output - assert_match "docker logout on 1.1.1.2", output + assert_match /docker logout on 1.1.1.\d/, output end end diff --git a/test/cli/traefik_test.rb b/test/cli/traefik_test.rb index 7eda6019..e8626b06 100644 --- a/test/cli/traefik_test.rb +++ b/test/cli/traefik_test.rb @@ -3,7 +3,7 @@ require_relative "cli_test_case" class CliTraefikTest < CliTestCase test "boot" do run_command("boot").tap do |output| - assert_match "docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --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 --log-opt max-size=\"10m\" traefik --providers.docker --log.level=DEBUG", output end end diff --git a/test/commander_test.rb b/test/commander_test.rb index 278d9896..245350c2 100644 --- a/test/commander_test.rb +++ b/test/commander_test.rb @@ -14,18 +14,29 @@ class CommanderTest < ActiveSupport::TestCase test "overwriting hosts" do assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @mrsk.hosts - @mrsk.specific_hosts = [ "1.2.3.4", "1.2.3.5" ] - assert_equal [ "1.2.3.4", "1.2.3.5" ], @mrsk.hosts + @mrsk.specific_hosts = [ "1.1.1.1", "1.1.1.2" ] + assert_equal [ "1.1.1.1", "1.1.1.2" ], @mrsk.hosts end - test "overwriting hosts with roles" do + test "filtering hosts by filtering roles" do assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @mrsk.hosts - @mrsk.specific_roles = [ "workers", "web" ] - assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @mrsk.hosts + @mrsk.specific_roles = [ "web" ] + assert_equal [ "1.1.1.1", "1.1.1.2" ], @mrsk.hosts + end + + test "filtering roles" do + assert_equal [ "web", "workers" ], @mrsk.roles.map(&:name) @mrsk.specific_roles = [ "workers" ] - assert_equal [ "1.1.1.3", "1.1.1.4" ], @mrsk.hosts + assert_equal [ "workers" ], @mrsk.roles.map(&:name) + end + + test "filtering roles by filtering hosts" do + assert_equal [ "web", "workers" ], @mrsk.roles.map(&:name) + + @mrsk.specific_hosts = [ "1.1.1.3" ] + assert_equal [ "workers" ], @mrsk.roles.map(&:name) end test "overwriting hosts with primary" do @@ -39,4 +50,9 @@ class CommanderTest < ActiveSupport::TestCase @mrsk.specific_roles = "web" assert_equal "1.1.1.1", @mrsk.primary_host end + + test "roles_on" do + assert_equal [ "web" ], @mrsk.roles_on("1.1.1.1") + assert_equal [ "workers" ], @mrsk.roles_on("1.1.1.3") + end end diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index 0e343d8c..a7931a90 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -40,11 +40,6 @@ class CommandsAccessoryTest < ActiveSupport::TestCase } } - @config = Mrsk::Configuration.new(@config) - @mysql = Mrsk::Commands::Accessory.new(@config, name: :mysql) - @redis = Mrsk::Commands::Accessory.new(@config, name: :redis) - @busybox = Mrsk::Commands::Accessory.new(@config, name: :busybox) - ENV["MYSQL_ROOT_PASSWORD"] = "secret123" end @@ -54,60 +49,68 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=10m --publish 3306:3306 -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" --label service=\"app-mysql\" private.registry/mysql:8.0", - @mysql.run.join(" ") + "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" --label service=\"app-mysql\" private.registry/mysql:8.0", + new_command(:mysql).run.join(" ") assert_equal \ - "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=10m --publish 6379:6379 -e SOMETHING=\"else\" --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest", - @redis.run.join(" ") + "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 -e SOMETHING=\"else\" --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest", + new_command(:redis).run.join(" ") assert_equal \ - "docker run --name app-busybox --detach --restart unless-stopped --log-opt max-size=10m --label service=\"app-busybox\" busybox:latest", - @busybox.run.join(" ") + "docker run --name app-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --label service=\"app-busybox\" busybox:latest", + new_command(:busybox).run.join(" ") + end + + test "run with logging config" do + @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } + + assert_equal \ + "docker run --name app-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app-busybox\" busybox:latest", + new_command(:busybox).run.join(" ") end test "start" do assert_equal \ "docker container start app-mysql", - @mysql.start.join(" ") + new_command(:mysql).start.join(" ") end test "stop" do assert_equal \ "docker container stop app-mysql", - @mysql.stop.join(" ") + new_command(:mysql).stop.join(" ") end test "info" do assert_equal \ "docker ps --filter label=service=app-mysql", - @mysql.info.join(" ") + new_command(:mysql).info.join(" ") end test "execute in new container" do assert_equal \ "docker run --rm -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root", - @mysql.execute_in_new_container("mysql", "-u", "root").join(" ") + new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ") end test "execute in existing container" do assert_equal \ "docker exec app-mysql mysql -u root", - @mysql.execute_in_existing_container("mysql", "-u", "root").join(" ") + new_command(:mysql).execute_in_existing_container("mysql", "-u", "root").join(" ") end test "execute in new container over ssh" do - @mysql.stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do + new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do assert_match %r|docker run -it --rm -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root|, - @mysql.execute_in_new_container_over_ssh("mysql", "-u", "root") + new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root") end end test "execute in existing container over ssh" do - @mysql.stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do + new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do assert_match %r|docker exec -it app-mysql mysql -u root|, - @mysql.execute_in_existing_container_over_ssh("mysql", "-u", "root") + new_command(:mysql).execute_in_existing_container_over_ssh("mysql", "-u", "root") end end @@ -116,28 +119,33 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "logs" do assert_equal \ "docker logs app-mysql --timestamps 2>&1", - @mysql.logs.join(" ") + new_command(:mysql).logs.join(" ") assert_equal \ "docker logs app-mysql --since 5m --tail 100 --timestamps 2>&1 | grep 'thing'", - @mysql.logs(since: "5m", lines: 100, grep: "thing").join(" ") + new_command(:mysql).logs(since: "5m", lines: 100, grep: "thing").join(" ") end test "follow logs" do assert_equal \ "ssh -t root@1.1.1.5 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'", - @mysql.follow_logs + new_command(:mysql).follow_logs end test "remove container" do assert_equal \ "docker container prune --force --filter label=service=app-mysql", - @mysql.remove_container.join(" ") + new_command(:mysql).remove_container.join(" ") end test "remove image" do assert_equal \ "docker image rm --force private.registry/mysql:8.0", - @mysql.remove_image.join(" ") + new_command(:mysql).remove_image.join(" ") end + + private + def new_command(accessory) + Mrsk::Commands::Accessory.new(Mrsk::Configuration.new(@config), name: accessory) + end end diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 2651abdc..88459bf4 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -13,7 +13,7 @@ class CommandsAppTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", new_command.run.join(" ") end @@ -21,7 +21,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:volumes] = ["/local/path:/container/path" ] assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", new_command.run.join(" ") end @@ -29,90 +29,104 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:healthcheck] = { "path" => "/healthz" } assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", + "docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", new_command.run.join(" ") end test "run with custom options" do @config[:servers] = { "web" => [ "1.1.1.1" ], "jobs" => { "hosts" => [ "1.1.1.2" ], "cmd" => "bin/jobs", "options" => { "mount" => "somewhere", "cap-add" => true } } } + assert_equal \ + "docker run --detach --restart unless-stopped --name app-jobs-999 -e MRSK_CONTAINER_NAME=\"app-jobs-999\" -e RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs", + new_command(role: "jobs").run.join(" ") + end + + test "run with logging config" do + @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs", - new_command.run(role: :jobs).join(" ") + "docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", + new_command.run.join(" ") end test "start" do assert_equal \ - "docker start app-999", + "docker start app-web-999", new_command.start.join(" ") end test "start with destination" do @destination = "staging" assert_equal \ - "docker start app-staging-999", + "docker start app-web-staging-999", new_command.start.join(" ") end test "stop" do assert_equal \ - "docker ps --quiet --filter label=service=app | xargs docker stop", + "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker stop", + new_command.stop.join(" ") + end + + test "stop with custom stop wait time" do + @config[:stop_wait_time] = 30 + assert_equal \ + "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker stop -t 30", new_command.stop.join(" ") end test "stop with version" do assert_equal \ - "docker container ls --all --filter name=app-123 --quiet | xargs docker stop", + "docker container ls --all --filter name=app-web-123 --quiet | xargs docker stop", new_command.stop(version: "123").join(" ") end test "info" do assert_equal \ - "docker ps --filter label=service=app", + "docker ps --filter label=service=app --filter label=role=web", new_command.info.join(" ") end test "info with destination" do @destination = "staging" assert_equal \ - "docker ps --filter label=service=app --filter label=destination=staging", + "docker ps --filter label=service=app --filter label=destination=staging --filter label=role=web", new_command.info.join(" ") end test "logs" do assert_equal \ - "docker ps --quiet --filter label=service=app | xargs docker logs 2>&1", + "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs 2>&1", new_command.logs.join(" ") assert_equal \ - "docker ps --quiet --filter label=service=app | xargs docker logs --since 5m 2>&1", + "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --since 5m 2>&1", new_command.logs(since: "5m").join(" ") assert_equal \ - "docker ps --quiet --filter label=service=app | xargs docker logs --tail 100 2>&1", + "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --tail 100 2>&1", new_command.logs(lines: "100").join(" ") assert_equal \ - "docker ps --quiet --filter label=service=app | xargs docker logs --since 5m --tail 100 2>&1", + "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --since 5m --tail 100 2>&1", new_command.logs(since: "5m", lines: "100").join(" ") assert_equal \ - "docker ps --quiet --filter label=service=app | xargs docker logs 2>&1 | grep 'my-id'", + "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs 2>&1 | grep 'my-id'", new_command.logs(grep: "my-id").join(" ") assert_equal \ - "docker ps --quiet --filter label=service=app | xargs docker logs --since 5m 2>&1 | grep 'my-id'", + "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --since 5m 2>&1 | grep 'my-id'", new_command.logs(since: "5m", grep: "my-id").join(" ") end test "follow logs" do assert_match \ - "docker ps --quiet --filter label=service=app | xargs docker logs --timestamps --tail 10 --follow 2>&1", + "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --timestamps --tail 10 --follow 2>&1", new_command.follow_logs(host: "app-1") assert_match \ - "docker ps --quiet --filter label=service=app | xargs docker logs --timestamps --tail 10 --follow 2>&1 | grep \"Completed\"", + "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --timestamps --tail 10 --follow 2>&1 | grep \"Completed\"", new_command.follow_logs(host: "app-1", grep: "Completed") end @@ -125,7 +139,7 @@ class CommandsAppTest < ActiveSupport::TestCase test "execute in existing container" do assert_equal \ - "docker exec app-999 bin/rails db:setup", + "docker exec app-web-999 bin/rails db:setup", new_command.execute_in_existing_container("bin/rails", "db:setup").join(" ") end @@ -135,7 +149,7 @@ class CommandsAppTest < ActiveSupport::TestCase end test "execute in existing container over ssh" do - assert_match %r|docker exec -it app-999 bin/rails c|, + assert_match %r|docker exec -it app-web-999 bin/rails c|, new_command.execute_in_existing_container_over_ssh("bin/rails", "c", host: "app-1") end @@ -166,14 +180,14 @@ class CommandsAppTest < ActiveSupport::TestCase test "current_container_id" do assert_equal \ - "docker ps --quiet --filter label=service=app", + "docker ps --quiet --filter label=service=app --filter label=role=web", new_command.current_container_id.join(" ") end test "current_container_id with destination" do @destination = "staging" assert_equal \ - "docker ps --quiet --filter label=service=app --filter label=destination=staging", + "docker ps --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web", new_command.current_container_id.join(" ") end @@ -185,52 +199,52 @@ class CommandsAppTest < ActiveSupport::TestCase test "current_running_version" do assert_equal \ - "docker ps --filter label=service=app --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1", + "docker ps --filter label=service=app --filter label=role=web --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1", new_command.current_running_version.join(" ") end test "list_containers" do assert_equal \ - "docker container ls --all --filter label=service=app", + "docker container ls --all --filter label=service=app --filter label=role=web", new_command.list_containers.join(" ") end test "list_containers with destination" do @destination = "staging" assert_equal \ - "docker container ls --all --filter label=service=app --filter label=destination=staging", + "docker container ls --all --filter label=service=app --filter label=destination=staging --filter label=role=web", new_command.list_containers.join(" ") end test "list_container_names" do assert_equal \ - "docker container ls --all --filter label=service=app --format '{{ .Names }}'", + "docker container ls --all --filter label=service=app --filter label=role=web --format '{{ .Names }}'", new_command.list_container_names.join(" ") end test "remove_container" do assert_equal \ - "docker container ls --all --filter name=app-999 --quiet | xargs docker container rm", + "docker container ls --all --filter name=app-web-999 --quiet | xargs docker container rm", new_command.remove_container(version: "999").join(" ") end test "remove_container with destination" do @destination = "staging" assert_equal \ - "docker container ls --all --filter name=app-staging-999 --quiet | xargs docker container rm", + "docker container ls --all --filter name=app-web-staging-999 --quiet | xargs docker container rm", new_command.remove_container(version: "999").join(" ") end test "remove_containers" do assert_equal \ - "docker container prune --force --filter label=service=app", + "docker container prune --force --filter label=service=app --filter label=role=web", new_command.remove_containers.join(" ") end test "remove_containers with destination" do @destination = "staging" assert_equal \ - "docker container prune --force --filter label=service=app --filter label=destination=staging", + "docker container prune --force --filter label=service=app --filter label=destination=staging --filter label=role=web", new_command.remove_containers.join(" ") end @@ -242,19 +256,19 @@ class CommandsAppTest < ActiveSupport::TestCase test "remove_images" do assert_equal \ - "docker image prune --all --force --filter label=service=app", + "docker image prune --all --force --filter label=service=app --filter label=role=web", new_command.remove_images.join(" ") end test "remove_images with destination" do @destination = "staging" assert_equal \ - "docker image prune --all --force --filter label=service=app --filter label=destination=staging", + "docker image prune --all --force --filter label=service=app --filter label=destination=staging --filter label=role=web", new_command.remove_images.join(" ") end private - def new_command - Mrsk::Commands::App.new(Mrsk::Configuration.new(@config, destination: @destination, version: "999")) + def new_command(role: "web") + Mrsk::Commands::App.new(Mrsk::Configuration.new(@config, destination: @destination, version: "999"), role: role) end end diff --git a/test/commands/auditor_test.rb b/test/commands/auditor_test.rb index f35e110c..55c3a32e 100644 --- a/test/commands/auditor_test.rb +++ b/test/commands/auditor_test.rb @@ -22,6 +22,14 @@ class CommandsAuditorTest < ActiveSupport::TestCase new_command.record("app removed container").join(" ") end + test "record with role" do + @role = "web" + + assert_match \ + /echo '.* \[web\] app removed container' >> mrsk-app-audit.log/, + new_command.record("app removed container").join(" ") + end + test "broadcast" do assert_match \ /bin\/audit_broadcast '\[.*\] app removed container'/, @@ -30,6 +38,6 @@ class CommandsAuditorTest < ActiveSupport::TestCase private def new_command - Mrsk::Commands::Auditor.new(Mrsk::Configuration.new(@config, destination: @destination, version: "123")) + Mrsk::Commands::Auditor.new(Mrsk::Configuration.new(@config, destination: @destination, version: "123"), role: @role) end end diff --git a/test/commands/traefik_test.rb b/test/commands/traefik_test.rb index f2b67f91..3f6084d1 100644 --- a/test/commands/traefik_test.rb +++ b/test/commands/traefik_test.rb @@ -10,45 +10,45 @@ class CommandsTraefikTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --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 --log-opt max-size=\"10m\" traefik --providers.docker --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 --log-opt max-size=10m --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --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 --log-opt max-size=\"10m\" traefik --providers.docker --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 --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --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 --log-opt max-size=\"10m\" traefik --providers.docker --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 --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --publish \"9000:9000\" --publish \"9001:9001\" traefik --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 --log-opt max-size=\"10m\" --publish \"9000:9000\" --publish \"9001:9001\" traefik --providers.docker --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 --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --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 --log-opt max-size=\"10m\" traefik --providers.docker --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 --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" traefik --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 --log-opt max-size=\"10m\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" traefik --providers.docker --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 --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --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 --log-opt max-size=\"10m\" traefik --providers.docker --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 --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" traefik --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 --log-opt max-size=\"10m\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" traefik --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end @@ -56,7 +56,15 @@ class CommandsTraefikTest < ActiveSupport::TestCase @config.delete(:traefik) assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --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 --log-opt max-size=\"10m\" traefik --providers.docker --log.level=DEBUG", + new_command.run.join(" ") + end + + test "run with logging config" do + @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 --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" traefik --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index 7062de37..fd070949 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -39,7 +39,11 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase }, "volumes" => [ "/var/lib/redis:/data" - ] + ], + "options" => { + "cpus" => 4, + "memory" => "2GB" + } } } } @@ -104,4 +108,8 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase test "directories" do assert_equal({"$PWD/app-mysql/data"=>"/var/lib/mysql"}, @config.accessory(:mysql).directories) end + + test "options" do + assert_equal ["--cpus", "\"4\"", "--memory", "\"2GB\""], @config.accessory(:redis).option_args + end end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index ee381449..b5282379 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -16,7 +16,7 @@ class ConfigurationTest < ActiveSupport::TestCase @config = Mrsk::Configuration.new(@deploy) @deploy_with_roles = @deploy.dup.merge({ - servers: { "web" => [ "1.1.1.1", "1.1.1.2" ], "workers" => { "hosts" => [ "1.1.1.3", "1.1.1.4" ] } } }) + servers: { "web" => [ "1.1.1.1", "1.1.1.2" ], "workers" => { "hosts" => [ "1.1.1.1", "1.1.1.3" ] } } }) @config_with_roles = Mrsk::Configuration.new(@deploy_with_roles) end @@ -55,7 +55,7 @@ class ConfigurationTest < ActiveSupport::TestCase test "all hosts" do assert_equal [ "1.1.1.1", "1.1.1.2"], @config.all_hosts - assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @config_with_roles.all_hosts + assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], @config_with_roles.all_hosts end test "primary web host" do @@ -69,7 +69,7 @@ class ConfigurationTest < ActiveSupport::TestCase @deploy_with_roles[:servers]["workers"]["traefik"] = true config = Mrsk::Configuration.new(@deploy_with_roles) - assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], config.traefik_hosts + assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.traefik_hosts end test "version" do @@ -209,6 +209,20 @@ class ConfigurationTest < ActiveSupport::TestCase assert_equal ["--volume", "/local/path:/container/path"], @config.volume_args end + test "logging args default" do + assert_equal ["--log-opt", "max-size=\"10m\""], @config.logging_args + end + + test "logging args with configured options" do + config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!(logging: { "options" => { "max-size" => "100m", "max-file" => 5 } }) }) + assert_equal ["--log-opt", "max-size=\"100m\"", "--log-opt", "max-file=\"5\""], @config.logging_args + end + + test "logging args with configured driver and options" do + config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!(logging: { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => 5 } }) }) + assert_equal ["--log-driver", "\"local\"", "--log-opt", "max-size=\"100m\"", "--log-opt", "max-file=\"5\""], @config.logging_args + end + test "erb evaluation of yml config" do config = Mrsk::Configuration.create_from config_file: Pathname.new(File.expand_path("fixtures/deploy.erb.yml", __dir__)) assert_equal "my-user", config.registry["username"] @@ -233,6 +247,6 @@ class ConfigurationTest < ActiveSupport::TestCase end test "to_h" do - assert_equal({ :roles=>["web"], :hosts=>["1.1.1.1", "1.1.1.2"], :primary_host=>"1.1.1.1", :version=>"missing", :repository=>"dhh/app", :absolute_image=>"dhh/app:missing", :service_with_version=>"app-missing", :env_args=>["-e", "REDIS_URL=\"redis://x/y\""], :ssh_options=>{:user=>"root", :auth_methods=>["publickey"]}, :volume_args=>["--volume", "/local/path:/container/path"], :healthcheck=>{"path"=>"/up", "port"=>3000 }}, @config.to_h) + assert_equal({ :roles=>["web"], :hosts=>["1.1.1.1", "1.1.1.2"], :primary_host=>"1.1.1.1", :version=>"missing", :repository=>"dhh/app", :absolute_image=>"dhh/app:missing", :service_with_version=>"app-missing", :env_args=>["-e", "REDIS_URL=\"redis://x/y\""], :ssh_options=>{:user=>"root", :auth_methods=>["publickey"]}, :volume_args=>["--volume", "/local/path:/container/path"], :logging=>["--log-opt", "max-size=\"10m\""], :healthcheck=>{"path"=>"/up", "port"=>3000 }}, @config.to_h) end end diff --git a/test/fixtures/deploy_with_accessories.yml b/test/fixtures/deploy_with_accessories.yml index 0d0c86f3..4440a1f9 100644 --- a/test/fixtures/deploy_with_accessories.yml +++ b/test/fixtures/deploy_with_accessories.yml @@ -28,4 +28,4 @@ accessories: directories: - data:/data -readiness_delay: 0 \ No newline at end of file +readiness_delay: 0