diff --git a/Gemfile.lock b/Gemfile.lock index e0d6f61e..e1973f80 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,12 @@ PATH remote: . specs: - mrsk (0.10.1) + mrsk (0.11.0) activesupport (>= 7.0) bcrypt_pbkdf (~> 1.0) dotenv (~> 2.8) ed25519 (~> 1.2) + net-ssh (~> 7.0) sshkit (~> 1.21) thor (~> 1.2) zeitwerk (~> 2.5) diff --git a/README.md b/README.md index 531b2408..33d3df10 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Watch the screencast: https://www.youtube.com/watch?v=LL1cV2FXZ5I Join us on Discord: https://discord.gg/YgHVT7GCXS +Ask questions: https://github.com/mrsked/mrsk/discussions + ## Installation If you have a Ruby environment available, you can install MRSK globally with: @@ -14,13 +16,13 @@ If you have a Ruby environment available, you can install MRSK globally with: gem install mrsk ``` -...otherwise, you can run a dockerized version via an alias (add this to your ${SHELL}rc to simplify re-use): +...otherwise, you can run a dockerized version via an alias (add this to your .bashrc or similar to simplify re-use): ```sh alias mrsk='docker run --rm -it -v $HOME/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}/:/workdir ghcr.io/mrsked/mrsk' ``` -Then, inside your app directory, run `mrsk init` (or `mrsk init --bundle` within Rails apps where you want a bin/mrsk binstub). Now edit the new file `config/deploy.yml`. It could look as simple as this: +Then, inside your app directory, run `mrsk init` (or `mrsk init --bundle` within Rails 7+ apps where you want a bin/mrsk binstub). Now edit the new file `config/deploy.yml`. It could look as simple as this: ```yaml service: hey @@ -191,6 +193,15 @@ ssh: user: app ``` +If you are using non-root user, you need to bootstrap your servers manually, before using them with MRSK. On Ubuntu, you'd do: + +```bash +sudo apt update +sudo apt upgrade -y +sudo apt install -y docker.io curl git +sudo usermod -a -G docker ubuntu +``` + ### Using a proxy SSH host If you need to connect to server through a proxy host, you can use `ssh/proxy`: @@ -207,6 +218,13 @@ ssh: proxy: "app@192.168.0.1" ``` +Also if you need specific proxy command to connect to the server: + +```yaml +ssh: + proxy_command: aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p' --region=us-east-1 ## ssh via aws ssm +``` + ### Using env variables You can inject env variables into the app containers using `env`: @@ -288,8 +306,9 @@ You can specialize the default Traefik rules by setting labels on the containers ```yaml labels: - traefik.http.routers.hey.rule: Host(`app.hey.com`) + traefik.http.routers.hey-web.rule: Host(`app.hey.com`) ``` +Traefik rules are in the "service-role-destination" format. The default role will be `web` if no rule is specified. If the destination is not specified, it is not included. To give an example, the above rule would become "traefik.http.routers.hey-web.rule" if it was for the "staging" destination. Note: The backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash! @@ -439,9 +458,9 @@ RUN --mount=type=secret,id=GITHUB_TOKEN \ rm -rf /usr/local/bundle/cache ``` -### Using command arguments for Traefik +### Traefik command arguments -You can customize the traefik command line: +Customize the Traefik command line using `args`: ```yaml traefik: @@ -450,20 +469,38 @@ traefik: accesslog.format: json ``` -This will start the traefik container with `--accesslog=true accesslog.format=json`. +This starts the Traefik container with `--accesslog=true --accesslog.format=json` arguments. -### Traefik's host port binding +### Traefik host port binding -By default Traefik binds to port 80 of the host machine, it can be configured to use an alternative port: +Traefik binds to port 80 by default. Specify an alternative port using `host_port`: ```yaml traefik: host_port: 8080 ``` -### Configure docker options for traefik +### Traefik version, upgrades, and custom images -We allow users to pass additional docker options to the trafik container like +MRSK runs the traefik:v2.9 image to track Traefik 2.9.x releases. + +To pin Traefik to a specific version or an image published to your registry, +specify `image`: + +```yaml +traefik: + image: traefik:v2.10.0-rc1 +``` + +This is useful for downgrading Traefik if there's an unexpected breaking +change in a minor version release, upgrading Traefik to test forthcoming +releases, or running your own Traefik-derived image. + +MRSK has not been tested for compatibility with Traefik 3 betas. Please do! + +### Traefik container configuration + +Pass additional Docker configuration for the Traefik container using `options`: ```yaml traefik: @@ -475,12 +512,27 @@ traefik: memory: 512m ``` -This will start the traefik container with a command like: `docker run ... --volume /tmp/example.json:/tmp/example.json --publish 8080:8080 ` +This starts the Traefik container with `--volume /tmp/example.json:/tmp/example.json --publish 8080:8080 --memory 512m` arguments to `docker run`. +### Traefik container lables -### Configure alternate entrypoints for traefik +Add labels to Traefik Docker container. -You can configure multiple entrypoints for traefik like so: +```yaml +traefik: + lables: + - traefik.enable: true + - traefik.http.routers.dashboard.rule: Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`)) + - traefik.http.routers.dashboard.service: api@internal + - traefik.http.routers.dashboard.middlewares: auth + - traefik.http.middlewares.auth.basicauth.users: test:$2y$05$H2o72tMaO.TwY1wNQUV1K.fhjRgLHRDWohFvUZOJHBEtUXNKrqUKi # test:password +``` + +This labels Traefik container with `--label traefik.http.routers.dashboard.middlewares=\"auth\"` and so on. + +### Traefik alternate entrypoints + +You can configure multiple entrypoints for Traefik like so: ```yaml service: myservice @@ -540,7 +592,7 @@ accessories: memory: "2GB" redis: image: redis:latest - role: + roles: - web port: "36379:6379" volumes: @@ -610,18 +662,21 @@ That'll post a line like follows to a preconfigured chatbot in Basecamp: [My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de ``` -### Using custom healthcheck path or port +### Custom healthcheck -MRSK defaults to checking the health of your application again `/up` on port 3000. You can tailor both with the `healthcheck` setting: +MRSK defaults to checking the health of your application again `/up` on port 3000 up to 7 times. You can tailor the behaviour with the `healthcheck` setting: ```yaml healthcheck: path: /healthz port: 4000 + max_attempts: 7 ``` This will ensure your application is configured with a traefik label for the healthcheck against `/healthz` and that the pre-deploy healthcheck that MRSK performs is done against the same path on port 4000. +The healthcheck also allows for an optional `max_attempts` setting, which will attempt the healthcheck up to the specified number of times before failing the deploy. This is useful for applications that take a while to start up. The default is 7. + ## Commands ### Running commands on servers diff --git a/lib/mrsk/cli.rb b/lib/mrsk/cli.rb index 62323076..c974a814 100644 --- a/lib/mrsk/cli.rb +++ b/lib/mrsk/cli.rb @@ -1,4 +1,5 @@ module Mrsk::Cli + class LockError < StandardError; end end # SSHKit uses instance eval, so we need a global const for ergonomics diff --git a/lib/mrsk/cli/base.rb b/lib/mrsk/cli/base.rb index ee1851bf..a0eb200a 100644 --- a/lib/mrsk/cli/base.rb +++ b/lib/mrsk/cli/base.rb @@ -6,8 +6,6 @@ module Mrsk::Cli class Base < Thor include SSHKit::DSL - class LockError < StandardError; end - def self.exit_on_failure?() true end class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging" @@ -82,8 +80,11 @@ module Mrsk::Cli acquire_lock yield - ensure + release_lock + rescue + error " \e[31mDeploy lock was not released\e[0m" if MRSK.lock_count > 0 + raise end def acquire_lock @@ -95,9 +96,10 @@ module Mrsk::Cli rescue SSHKit::Runner::ExecuteError => e if e.message =~ /cannot create directory/ invoke "mrsk:cli:lock:status", [] + raise LockError, "Deploy lock found" + else + raise e end - - raise LockError, "Deploy lock found" end def release_lock diff --git a/lib/mrsk/cli/healthcheck.rb b/lib/mrsk/cli/healthcheck.rb index 3fda7754..5e9f42dc 100644 --- a/lib/mrsk/cli/healthcheck.rb +++ b/lib/mrsk/cli/healthcheck.rb @@ -1,5 +1,4 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base - MAX_ATTEMPTS = 7 class HealthcheckError < StandardError; end @@ -13,6 +12,7 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base target = "Health check against #{MRSK.config.healthcheck["path"]}" attempt = 1 + max_attempts = MRSK.config.healthcheck["max_attempts"] begin status = capture_with_info(*MRSK.healthcheck.curl) @@ -23,8 +23,8 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base raise HealthcheckError, "#{target} failed with status #{status}" end rescue SSHKit::Command::Failed - if attempt <= MAX_ATTEMPTS - info "#{target} failed to respond, retrying in #{attempt}s..." + if attempt <= max_attempts + info "#{target} failed to respond, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..." sleep attempt attempt += 1 diff --git a/lib/mrsk/cli/lock.rb b/lib/mrsk/cli/lock.rb index 3514e282..f3fdcf4b 100644 --- a/lib/mrsk/cli/lock.rb +++ b/lib/mrsk/cli/lock.rb @@ -12,7 +12,7 @@ class Mrsk::Cli::Lock < Mrsk::Cli::Base message = options[:message] handle_missing_lock do on(MRSK.primary_host) { execute *MRSK.lock.acquire(message, MRSK.config.version) } - say "Set the deploy lock" + say "Acquired the deploy lock" end end @@ -20,7 +20,7 @@ class Mrsk::Cli::Lock < Mrsk::Cli::Base def release handle_missing_lock do on(MRSK.primary_host) { execute *MRSK.lock.release } - say "Removed the deploy lock" + say "Released the deploy lock" end end diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index e39f923c..4dae283b 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -83,21 +83,26 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base with_lock do MRSK.config.version = version - if container_name_available?(MRSK.config.service_with_version) + if container_available?(version) say "Start version #{version}, then wait #{MRSK.config.readiness_delay}s for app to boot before stopping the old version...", :magenta cli = self old_version = nil on(MRSK.hosts) do |host| - old_version = capture_with_info(*MRSK.app.current_running_version).strip.presence + roles = MRSK.roles_on(host) - execute *MRSK.app.start + roles.each do |role| + app = MRSK.app(role: role) + old_version = capture_with_info(*app.current_running_version).strip.presence - if old_version - sleep MRSK.config.readiness_delay + execute *app.start - execute *MRSK.app.stop(version: old_version), raise_on_non_zero_exit: false + if old_version + sleep MRSK.config.readiness_delay + + execute *app.stop(version: old_version), raise_on_non_zero_exit: false + end end end @@ -220,10 +225,15 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base subcommand "lock", Mrsk::Cli::Lock private - def container_name_available?(container_name, host: MRSK.primary_host) - container_names = nil - on(host) { container_names = capture_with_info(*MRSK.app.list_container_names).split("\n") } - Array(container_names).include?(container_name) + def container_available?(version, host: MRSK.primary_host) + available = nil + + on(host) do + first_role = MRSK.roles_on(host).first + available = capture_with_info(*MRSK.app(role: first_role).container_id_for_version(version)).present? + end + + available end def deploy_options diff --git a/lib/mrsk/cli/traefik.rb b/lib/mrsk/cli/traefik.rb index 7be64e8e..df70ab43 100644 --- a/lib/mrsk/cli/traefik.rb +++ b/lib/mrsk/cli/traefik.rb @@ -2,7 +2,10 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base desc "boot", "Boot Traefik on servers" def boot with_lock do - on(MRSK.traefik_hosts) { execute *MRSK.traefik.run, raise_on_non_zero_exit: false } + on(MRSK.traefik_hosts) do + execute *MRSK.registry.login + execute *MRSK.traefik.run, raise_on_non_zero_exit: false + end end end diff --git a/lib/mrsk/commands/traefik.rb b/lib/mrsk/commands/traefik.rb index 248a33ad..da911cfb 100644 --- a/lib/mrsk/commands/traefik.rb +++ b/lib/mrsk/commands/traefik.rb @@ -1,7 +1,7 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base - delegate :optionize, to: Mrsk::Utils + delegate :argumentize, :optionize, to: Mrsk::Utils - IMAGE = "traefik:v2.9.9" + DEFAULT_IMAGE = "traefik:v2.9" CONTAINER_PORT = 80 def run @@ -11,8 +11,9 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base "--publish", port, "--volume", "/var/run/docker.sock:/var/run/docker.sock", *config.logging_args, + *label_args, *docker_options_args, - IMAGE, + image, "--providers.docker", "--log.level=DEBUG", *cmd_option_args @@ -56,6 +57,18 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base end private + def label_args + argumentize "--label", labels + 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 diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index d6cf09f1..a2c2ecf1 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -143,6 +143,8 @@ class Mrsk::Configuration if raw_config.ssh.present? && raw_config.ssh["proxy"] Net::SSH::Proxy::Jump.new \ raw_config.ssh["proxy"].include?("@") ? raw_config.ssh["proxy"] : "root@#{raw_config.ssh["proxy"]}" + elsif raw_config.ssh.present? && raw_config.ssh["proxy_command"] + Net::SSH::Proxy::Command.new(raw_config.ssh["proxy_command"]) end end @@ -156,7 +158,7 @@ class Mrsk::Configuration end def healthcheck - { "path" => "/up", "port" => 3000 }.merge(raw_config.healthcheck || {}) + { "path" => "/up", "port" => 3000, "max_attempts" => 7 }.merge(raw_config.healthcheck || {}) end def readiness_delay diff --git a/lib/mrsk/configuration/role.rb b/lib/mrsk/configuration/role.rb index bfc42fbe..6bca4fb8 100644 --- a/lib/mrsk/configuration/role.rb +++ b/lib/mrsk/configuration/role.rb @@ -74,18 +74,22 @@ class Mrsk::Configuration::Role def traefik_labels if running_traefik? { - "traefik.http.routers.#{config.service}.rule" => "PathPrefix(`/`)", - "traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => config.healthcheck["path"], - "traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s", - "traefik.http.middlewares.#{config.service}-retry.retry.attempts" => "5", - "traefik.http.middlewares.#{config.service}-retry.retry.initialinterval" => "500ms", - "traefik.http.routers.#{config.service}.middlewares" => "#{config.service}-retry@docker" + "traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)", + "traefik.http.services.#{traefik_service}.loadbalancer.healthcheck.path" => config.healthcheck["path"], + "traefik.http.services.#{traefik_service}.loadbalancer.healthcheck.interval" => "1s", + "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/mrsk/version.rb b/lib/mrsk/version.rb index c617e932..2713048d 100644 --- a/lib/mrsk/version.rb +++ b/lib/mrsk/version.rb @@ -1,3 +1,3 @@ module Mrsk - VERSION = "0.10.1" + VERSION = "0.11.0" end diff --git a/mrsk.gemspec b/mrsk.gemspec index d359aa28..24c40bcf 100644 --- a/mrsk.gemspec +++ b/mrsk.gemspec @@ -14,6 +14,7 @@ Gem::Specification.new do |spec| spec.add_dependency "activesupport", ">= 7.0" spec.add_dependency "sshkit", "~> 1.21" + spec.add_dependency "net-ssh", "~> 7.0" spec.add_dependency "thor", "~> 1.2" spec.add_dependency "dotenv", "~> 2.8" spec.add_dependency "zeitwerk", "~> 2.5" diff --git a/test/cli/cli_test_case.rb b/test/cli/cli_test_case.rb index 496d6a27..01cf0019 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -22,4 +22,8 @@ class CliTestCase < ActiveSupport::TestCase def stdouted capture(:stdout) { yield }.strip end -end + + def stderred + capture(:stderr) { yield }.strip + end + end diff --git a/test/cli/healthcheck_test.rb b/test/cli/healthcheck_test.rb index 7a9df6eb..a3441a7e 100644 --- a/test/cli/healthcheck_test.rb +++ b/test/cli/healthcheck_test.rb @@ -23,8 +23,8 @@ class CliHealthcheckTest < CliTestCase .returns("200") run_command("perform").tap do |output| - assert_match "Health check against /up failed to respond, retrying in 1s...", output - assert_match "Health check against /up failed to respond, retrying in 2s...", output + assert_match "Health check against /up failed to respond, retrying in 1s (attempt 1/7)...", output + assert_match "Health check against /up failed to respond, retrying in 2s (attempt 2/7)...", output assert_match "Health check against /up succeeded with 200 OK!", output end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 289fe866..b291ddfc 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -57,6 +57,46 @@ class CliMainTest < CliTestCase end end + test "deploy when locked" do + Thread.report_on_exception = false + + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with { |*arg| arg[0..1] == [:mkdir, :mrsk_lock] } + .raises(RuntimeError, "mkdir: cannot create directory ‘mrsk_lock’: File exists") + + Mrsk::Cli::Base.any_instance.expects(:invoke).with("mrsk:cli:lock:status", []) + + assert_raises(Mrsk::Cli::LockError) do + run_command("deploy") + end + end + + test "deploy error when locking" do + Thread.report_on_exception = false + + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with { |*arg| arg[0..1] == [:mkdir, :mrsk_lock] } + .raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known") + + assert_raises(SSHKit::Runner::ExecuteError) do + run_command("deploy") + end + end + + test "deploy errors leave lock in place" do + invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" } + + Mrsk::Cli::Main.any_instance.expects(:invoke) + .with("mrsk:cli:server:bootstrap", [], invoke_options) + .raises(RuntimeError) + + assert_equal 0, MRSK.lock_count + assert_raises(RuntimeError) do + stderred { run_command("deploy") } + end + assert_equal 1, MRSK.lock_count + end + test "redeploy" do invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" } @@ -86,32 +126,34 @@ class CliMainTest < CliTestCase end test "rollback bad version" do + # Mrsk::Cli::Main.any_instance.stubs(:container_available?).returns(false) run_command("details") # Preheat MRSK const run_command("rollback", "nonsense").tap do |output| - assert_match /docker container ls --all --filter label=service=app --format '{{ .Names }}'/, output + 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 end 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", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-").returns("version-to-rollback\n").at_least_once + Mrsk::Cli::Main.any_instance.stubs(:container_available?).returns(true) + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-").returns("version-to-rollback\n").at_least_once + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=workers", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-").returns("version-to-rollback\n").at_least_once run_command("rollback", "123", config_file: "deploy_with_accessories").tap do |output| assert_match "Start version 123", output - assert_match "docker start app-123", output - assert_match "docker container ls --all --filter name=^app-version-to-rollback$ --quiet | xargs docker stop", output, "Should stop the container that was previously running" + assert_match "docker start app-web-123", output + assert_match "docker container ls --all --filter name=^app-web-version-to-rollback$ --quiet | xargs docker stop", output, "Should stop the container that was previously running" end end 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", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-").returns("").at_least_once + Mrsk::Cli::Main.any_instance.stubs(:container_available?).returns(true) + SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-").returns("").at_least_once run_command("rollback", "123").tap do |output| assert_match "Start version 123", output - assert_match "docker start app-123", output + assert_match "docker start app-web-123", output assert_no_match "docker stop", output end end diff --git a/test/cli/traefik_test.rb b/test/cli/traefik_test.rb index e89fb62b..2cf2513a 100644 --- a/test/cli/traefik_test.rb +++ b/test/cli/traefik_test.rb @@ -3,7 +3,8 @@ 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 --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" traefik:v2.9.9 --providers.docker --log.level=DEBUG", 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 --log-opt max-size=\"10m\" #{Mrsk::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=DEBUG", output end end diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 6727e5ed..cfa7df6c 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 --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", + "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-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app-web.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app-web.loadbalancer.healthcheck.interval=\"1s\" --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", 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 --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", + "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-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app-web.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app-web.loadbalancer.healthcheck.interval=\"1s\" --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", new_command.run.join(" ") end @@ -29,7 +29,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:healthcheck] = { "path" => "/healthz" } assert_equal \ - "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", + "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-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app-web.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app-web.loadbalancer.healthcheck.interval=\"1s\" --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", new_command.run.join(" ") end @@ -44,7 +44,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 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", + "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-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app-web.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app-web.loadbalancer.healthcheck.interval=\"1s\" --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", new_command.run.join(" ") end diff --git a/test/commands/traefik_test.rb b/test/commands/traefik_test.rb index 680c1731..8b807658 100644 --- a/test/commands/traefik_test.rb +++ b/test/commands/traefik_test.rb @@ -2,53 +2,66 @@ require "test_helper" class CommandsTraefikTest < ActiveSupport::TestCase setup do + @image = "traefik:test" + @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - traefik: { "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + traefik: { "image" => @image, "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } } end 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 --log-opt max-size=\"10m\" traefik:v2.9.9 --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\" #{@image} --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 --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" traefik:v2.9.9 --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\" #{@image} --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 --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" traefik:v2.9.9 --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\" #{@image} --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 --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" --publish \"9000:9000\" --publish \"9001:9001\" traefik:v2.9.9 --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\" #{@image} --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 --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" traefik:v2.9.9 --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\" #{@image} --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 --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:v2.9.9 --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\" #{@image} --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 --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" traefik:v2.9.9 --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\" #{@image} --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 --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:v2.9.9 --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\" #{@image} --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 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 --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\"", + 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 --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\"", new_command.run.join(" ") end @@ -56,7 +69,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 --log-opt max-size=\"10m\" traefik:v2.9.9 --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\" #{Mrsk::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=DEBUG", new_command.run.join(" ") end @@ -64,7 +77,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 --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" traefik:v2.9.9 --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-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\"", new_command.run.join(" ") end diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index e61ea51a..20ea39ef 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.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\"" ], @config.role(:web).label_args + assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app-web.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app-web.loadbalancer.healthcheck.interval=\"1s\"", "--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 end test "custom labels" do @@ -66,7 +66,15 @@ 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.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\"" ], config.role(:beta).label_args + assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.routers.app-beta.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app-beta.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app-beta.loadbalancer.healthcheck.interval=\"1s\"", "--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 + end + + test "default traefik label for non-web role with destination" do + config = Mrsk::Configuration.new(@deploy_with_roles.tap { |c| + c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] } + }, destination: "staging") + + assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "destination=\"staging\"", "--label", "traefik.http.routers.app-beta-staging.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app-beta-staging.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app-beta-staging.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app-beta-staging-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-beta-staging-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-beta-staging.middlewares=\"app-beta-staging-retry@docker\"" ], 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 ec65686d..2697673b 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -249,6 +249,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"], :logging=>["--log-opt", "max-size=\"10m\""], :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, "max_attempts" => 7 }}, @config.to_h) end end