From 12632aa7f9f80080bad99c1bacdc5f11a8526fc0 Mon Sep 17 00:00:00 2001 From: Dilpreet Singh Date: Mon, 3 Apr 2023 17:14:06 +0530 Subject: [PATCH 01/24] Enable ssh over proxy command --- README.md | 7 +++++++ lib/mrsk/configuration.rb | 2 ++ 2 files changed, 9 insertions(+) diff --git a/README.md b/README.md index a0dd43bf..b5936482 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,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`: diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index d6cf09f1..c8ac5a03 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 From bd8f13dd5ee4aee5b57a1e166e8ec80d7fcf5c0a Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 7 Apr 2023 14:08:25 -0700 Subject: [PATCH 02/24] Traefik image config for version pinning, upgrades, and custom images Accounts for the 2.9.10 security release and allows testing Traefik 3 betas. * Use `image` to configure a specific Traefik Docker image. * Default to `traefik:v2.9` to track future 2.9.x minor releases rather than tightly pinning to `v2.9.9`. * Support images from the configured registry. References #165 --- README.md | 38 ++++++++++++++++++++++++++--------- lib/mrsk/cli/traefik.rb | 5 ++++- lib/mrsk/commands/traefik.rb | 8 ++++++-- test/cli/traefik_test.rb | 3 ++- test/commands/traefik_test.rb | 24 ++++++++++++---------- 5 files changed, 53 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 531b2408..61545f58 100644 --- a/README.md +++ b/README.md @@ -439,9 +439,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 +450,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 +493,12 @@ 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`. -### Configure alternate entrypoints for traefik +### Traefik alternate entrypoints -You can configure multiple entrypoints for traefik like so: +You can configure multiple entrypoints for Traefik like so: ```yaml service: myservice 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..d6cc8ffc 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 - IMAGE = "traefik:v2.9.9" + DEFAULT_IMAGE = "traefik:v2.9" CONTAINER_PORT = 80 def run @@ -12,7 +12,7 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base "--volume", "/var/run/docker.sock:/var/run/docker.sock", *config.logging_args, *docker_options_args, - IMAGE, + image, "--providers.docker", "--log.level=DEBUG", *cmd_option_args @@ -56,6 +56,10 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base end private + def image + config.traefik.fetch("image") { DEFAULT_IMAGE } + end + def docker_options_args optionize(config.traefik["options"] || {}) 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/traefik_test.rb b/test/commands/traefik_test.rb index 680c1731..e6c6178c 100644 --- a/test/commands/traefik_test.rb +++ b/test/commands/traefik_test.rb @@ -2,53 +2,55 @@ 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 @@ -56,7 +58,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 +66,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 From ef04410d77a3f5249e47ecc7464fd20967aa3eee Mon Sep 17 00:00:00 2001 From: Nick Hammond Date: Sat, 8 Apr 2023 13:33:31 -0700 Subject: [PATCH 03/24] Add github discussions link to readme I realize that there's a discussions link on github but I didn't realize mrsk actually utilized it until I saw it mentioned on Discord. I was thinking adding it to the readme would help push people there. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 531b2408..7824d25d 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 +Discss on Github: https://github.com/mrsked/mrsk/discussions + ## Installation If you have a Ruby environment available, you can install MRSK globally with: From cb3c5a53f49de022052c333b9424b4358941ee28 Mon Sep 17 00:00:00 2001 From: Arturo Ojeda Date: Sat, 8 Apr 2023 19:52:53 -0600 Subject: [PATCH 04/24] Configurable max_attempts for healthcheck --- lib/mrsk/cli/healthcheck.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/mrsk/cli/healthcheck.rb b/lib/mrsk/cli/healthcheck.rb index 3fda7754..94950373 100644 --- a/lib/mrsk/cli/healthcheck.rb +++ b/lib/mrsk/cli/healthcheck.rb @@ -1,5 +1,5 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base - MAX_ATTEMPTS = 7 + DEFAULT_MAX_ATTEMPTS = 7 class HealthcheckError < StandardError; end @@ -13,6 +13,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"] || DEFAULT_MAX_ATTEMPTS begin status = capture_with_info(*MRSK.healthcheck.curl) @@ -23,7 +24,7 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base raise HealthcheckError, "#{target} failed with status #{status}" end rescue SSHKit::Command::Failed - if attempt <= MAX_ATTEMPTS + if attempt <= max_attempts info "#{target} failed to respond, retrying in #{attempt}s..." sleep attempt attempt += 1 From c60cc92dfed7630200c1fea53eda007ff730600a Mon Sep 17 00:00:00 2001 From: Kartikey Tanna Date: Sun, 9 Apr 2023 13:44:57 +0530 Subject: [PATCH 05/24] Traefik service name to be derived from role and destination --- lib/mrsk/configuration/role.rb | 16 ++++++++++------ test/commands/app_test.rb | 8 ++++---- test/configuration/role_test.rb | 13 +++++++++++-- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/mrsk/configuration/role.rb b/lib/mrsk/configuration/role.rb index bfc42fbe..44f5a77f 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/test/commands/app_test.rb b/test/commands/app_test.rb index 75a55ae3..6f463d78 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/configuration/role_test.rb b/test/configuration/role_test.rb index e61ea51a..973f12f5 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,16 @@ 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 From 3969f56fa62eecab4e8f8fb9a2bed3457348ec84 Mon Sep 17 00:00:00 2001 From: Arturo Ojeda Date: Sun, 9 Apr 2023 12:07:27 -0600 Subject: [PATCH 06/24] Improved: configurable max_attempts for healthcheck --- lib/mrsk/cli/healthcheck.rb | 5 ++--- lib/mrsk/configuration.rb | 2 +- test/configuration_test.rb | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/mrsk/cli/healthcheck.rb b/lib/mrsk/cli/healthcheck.rb index 94950373..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 - DEFAULT_MAX_ATTEMPTS = 7 class HealthcheckError < StandardError; end @@ -13,7 +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"] || DEFAULT_MAX_ATTEMPTS + max_attempts = MRSK.config.healthcheck["max_attempts"] begin status = capture_with_info(*MRSK.healthcheck.curl) @@ -25,7 +24,7 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base end rescue SSHKit::Command::Failed if attempt <= max_attempts - info "#{target} failed to respond, retrying in #{attempt}s..." + info "#{target} failed to respond, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..." sleep attempt attempt += 1 diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index d6cf09f1..3e58a85a 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -156,7 +156,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/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 From d09cddde8d9833e71463f202f52b94e5c2b9e95a Mon Sep 17 00:00:00 2001 From: Ruslan Gainutdinov Date: Mon, 10 Apr 2023 12:23:06 +0300 Subject: [PATCH 07/24] Update README.md Add sample commands to bootstrap non-root ssh server. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 531b2408..69af95c5 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,11 @@ ssh: user: app ``` +If you are using non-root user, you need to *bootstrap* your server manually, before using it with mrsk. Here is some bootstrap one-liners for popular VMs: + +* Amazon Linux 2: `sudo yum update -y; sudo yum install -y docker curl git; sudo usermod -a -G docker ec2-user; sudo chkconfig docker on; sudo service docker start` +* Ubuntu: `sudo apt update; sudo apt upgrade -y; sudo apt install docker 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`: From fca5b11682022d0361de4a7d2e1128f22e2c51b7 Mon Sep 17 00:00:00 2001 From: Ruslan Gainutdinov Date: Mon, 10 Apr 2023 12:26:57 +0300 Subject: [PATCH 08/24] Update README.md Use docker.io on Ubuntu --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 69af95c5..67e1402a 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ ssh: If you are using non-root user, you need to *bootstrap* your server manually, before using it with mrsk. Here is some bootstrap one-liners for popular VMs: * Amazon Linux 2: `sudo yum update -y; sudo yum install -y docker curl git; sudo usermod -a -G docker ec2-user; sudo chkconfig docker on; sudo service docker start` -* Ubuntu: `sudo apt update; sudo apt upgrade -y; sudo apt install docker curl git; sudo usermod -a -G docker ubuntu` +* Ubuntu: `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 From f3e3196ce5b9aea97c799f983384f8a145c69277 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 10 Apr 2023 14:22:58 +0200 Subject: [PATCH 09/24] Not that --bundle is a Rails 7+ option --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 531b2408..4f00837a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ gem install mrsk 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 From 2a3e576182e427330fcce7b57e6a2abf7f04b993 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 10 Apr 2023 14:24:51 +0200 Subject: [PATCH 10/24] More explicit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 67e1402a..9b283ff0 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ 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' From f386c3bdab43b388ba20d9c961a7905c722b9bdc Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 10 Apr 2023 14:26:49 +0200 Subject: [PATCH 11/24] Make it explicit, focus on Ubuntu --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9b283ff0..e93d5196 100644 --- a/README.md +++ b/README.md @@ -191,10 +191,14 @@ ssh: user: app ``` -If you are using non-root user, you need to *bootstrap* your server manually, before using it with mrsk. Here is some bootstrap one-liners for popular VMs: +If you are using non-root user, you need to bootstrap your servers manually, before using them with MRSK. On Ubuntu, you'd do: -* Amazon Linux 2: `sudo yum update -y; sudo yum install -y docker curl git; sudo usermod -a -G docker ec2-user; sudo chkconfig docker on; sudo service docker start` -* Ubuntu: `sudo apt update; sudo apt upgrade -y; sudo apt install -y docker.io curl git; sudo usermod -a -G docker ubuntu` +```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 From 54a5b90d8fdd48930c9947a87205ad27c5bd5818 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 10 Apr 2023 14:28:52 +0200 Subject: [PATCH 12/24] Simpler --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7824d25d..cec4fd4e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Watch the screencast: https://www.youtube.com/watch?v=LL1cV2FXZ5I Join us on Discord: https://discord.gg/YgHVT7GCXS -Discss on Github: https://github.com/mrsked/mrsk/discussions +Ask questions: https://github.com/mrsked/mrsk/discussions ## Installation From a9488e935de8ed049198cc839036998e7b1e1364 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 10 Apr 2023 14:39:18 +0200 Subject: [PATCH 13/24] Style --- lib/mrsk/configuration/role.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mrsk/configuration/role.rb b/lib/mrsk/configuration/role.rb index 44f5a77f..6bca4fb8 100644 --- a/lib/mrsk/configuration/role.rb +++ b/lib/mrsk/configuration/role.rb @@ -87,7 +87,7 @@ class Mrsk::Configuration::Role end def traefik_service - [config.service, name, config.destination].compact.join("-") + [ config.service, name, config.destination ].compact.join("-") end def custom_labels From 7d17a6c3b51804f642b5f67f7e6a6739f0ca9882 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 10 Apr 2023 15:10:08 +0200 Subject: [PATCH 14/24] Excess CR --- test/configuration/role_test.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index 973f12f5..20ea39ef 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -70,7 +70,6 @@ class ConfigurationRoleTest < ActiveSupport::TestCase 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") From c4df440c79f02b4602f0196ee79fd7b9c3cde80d Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 10 Apr 2023 15:08:48 +0100 Subject: [PATCH 15/24] Improved deploy lock acquisition 1. Don't raise lock error for non-lock issues during lock acquire (see https://github.com/mrsked/mrsk/pull/181) 2. If there is an error while the lock is held, don't release the lock and send a warning to stderr --- lib/mrsk/cli.rb | 1 + lib/mrsk/cli/base.rb | 12 +++++++----- lib/mrsk/cli/lock.rb | 4 ++-- test/cli/cli_test_case.rb | 6 +++++- test/cli/main_test.rb | 40 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 8 deletions(-) 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/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/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/main_test.rb b/test/cli/main_test.rb index b27e5630..8d00b277 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -53,6 +53,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" } From d8c61004e4799902bbf58d8dd3287970a36b3b6a Mon Sep 17 00:00:00 2001 From: Mat Harvard Date: Mon, 10 Apr 2023 07:29:07 -0700 Subject: [PATCH 16/24] Require net-ssh ~> 7.0 for SHA-2 support Versions of net-ssh before 7.0 do not support the SHA-2 algorithm and result in mrsk not being able to connect to hosts using keys generated with it. net-ssh is also a dependency of sshkit, however, sshkit has a version requirement of >= 2.8.0 for net-ssh, so is not effective at ensuring mrsk has the version it needs to be the most compatible. --- Gemfile.lock | 1 + mrsk.gemspec | 1 + 2 files changed, 2 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index e0d6f61e..a915c21d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,6 +6,7 @@ PATH 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/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" From 514b2aa24391864595f8212b34edc16c08dfa209 Mon Sep 17 00:00:00 2001 From: Arturo Ojeda Date: Mon, 10 Apr 2023 09:29:19 -0600 Subject: [PATCH 17/24] Fix test case: console output message was not updated to display the current/total attempts --- test/cli/healthcheck_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 161ebe4bc16fd51c69f7e9efa2e446aaddde19e5 Mon Sep 17 00:00:00 2001 From: Arturo Ojeda Date: Mon, 10 Apr 2023 22:26:10 -0600 Subject: [PATCH 18/24] Updated README.md with new healthcheck.max_attempts option --- .idea/.gitignore | 8 ++++++++ .idea/misc.xml | 6 ++++++ .idea/modules.xml | 8 ++++++++ .idea/mrsk.iml | 14 ++++++++++++++ .idea/vcs.xml | 6 ++++++ README.md | 7 +++++-- 6 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/mrsk.iml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..9fd148f6 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..812d9104 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/mrsk.iml b/.idea/mrsk.iml new file mode 100644 index 00000000..2d83225c --- /dev/null +++ b/.idea/mrsk.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 531b2408..32988c32 100644 --- a/README.md +++ b/README.md @@ -610,18 +610,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 From cfc8fa0590298fb88a290fd6f5514aadb39d3d5f Mon Sep 17 00:00:00 2001 From: Arturo Ojeda Date: Mon, 10 Apr 2023 22:33:20 -0600 Subject: [PATCH 19/24] Remove .idea folder --- .idea/.gitignore | 8 -------- .idea/misc.xml | 6 ------ .idea/modules.xml | 8 -------- .idea/mrsk.iml | 14 -------------- .idea/vcs.xml | 6 ------ 5 files changed, 42 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/mrsk.iml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b81..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 9fd148f6..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 812d9104..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/mrsk.iml b/.idea/mrsk.iml deleted file mode 100644 index 2d83225c..00000000 --- a/.idea/mrsk.iml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From d3936363d053092910e6ed891a34744e62deaca1 Mon Sep 17 00:00:00 2001 From: Kartikey Tanna Date: Tue, 11 Apr 2023 10:17:38 +0530 Subject: [PATCH 20/24] Explained the latest modifications of Traefik container labels --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 941a8885..4ae5172c 100644 --- a/README.md +++ b/README.md @@ -306,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! From 448ea7719f432dab315dab4e6f9319b832e93158 Mon Sep 17 00:00:00 2001 From: Nicolai Reuschling Date: Wed, 12 Apr 2023 10:53:10 +0200 Subject: [PATCH 21/24] fix typo role to roles --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0dd43bf..dc9bab12 100644 --- a/README.md +++ b/README.md @@ -540,7 +540,7 @@ accessories: memory: "2GB" redis: image: redis:latest - role: + roles: - web port: "36379:6379" volumes: From 43f7409de0e9bf2eeaac430dc0fbd386a6c63082 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 12 Apr 2023 09:45:50 +0100 Subject: [PATCH 22/24] Make rollbacks role-aware Rollbacks stopped working after https://github.com/mrsked/mrsk/pull/99. We'll confirm that a container is available for the first role on the primary host before attempting to rollback. --- lib/mrsk/cli/main.rb | 30 ++++++++++++++++++++---------- test/cli/main_test.rb | 18 ++++++++++-------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index 83f71c34..a5dfdacb 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -77,21 +77,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 @@ -214,10 +219,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/test/cli/main_test.rb b/test/cli/main_test.rb index 8d00b277..e08462b3 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -120,32 +120,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", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").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", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").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", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").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", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").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", "--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 - assert_match "docker start app-123", output + assert_match "docker start app-web-123", output assert_no_match "docker stop", output end end From c59eb00dd0406e70dea11a3177ebd6201e86be47 Mon Sep 17 00:00:00 2001 From: Kartikey Tanna Date: Wed, 12 Apr 2023 12:15:09 +0530 Subject: [PATCH 23/24] Labels can be added to Traefik --- README.md | 15 +++++++++++++++ lib/mrsk/commands/traefik.rb | 11 ++++++++++- test/commands/traefik_test.rb | 11 +++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 56e7159a..e7fbaddb 100644 --- a/README.md +++ b/README.md @@ -514,6 +514,21 @@ traefik: 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 + +Add labels to Traefik Docker container. + +```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 diff --git a/lib/mrsk/commands/traefik.rb b/lib/mrsk/commands/traefik.rb index d6cc8ffc..da911cfb 100644 --- a/lib/mrsk/commands/traefik.rb +++ b/lib/mrsk/commands/traefik.rb @@ -1,5 +1,5 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base - delegate :optionize, to: Mrsk::Utils + delegate :argumentize, :optionize, to: Mrsk::Utils DEFAULT_IMAGE = "traefik:v2.9" CONTAINER_PORT = 80 @@ -11,6 +11,7 @@ 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, "--providers.docker", @@ -56,6 +57,14 @@ 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 diff --git a/test/commands/traefik_test.rb b/test/commands/traefik_test.rb index e6c6178c..8b807658 100644 --- a/test/commands/traefik_test.rb +++ b/test/commands/traefik_test.rb @@ -54,6 +54,17 @@ class CommandsTraefikTest < ActiveSupport::TestCase 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 + test "run without configuration" do @config.delete(:traefik) From 60a19f0b3049fbd01dc3f5110e6b666b1bab3433 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 12 Apr 2023 11:45:33 +0200 Subject: [PATCH 24/24] Bump version for 0.11.0 --- Gemfile.lock | 2 +- lib/mrsk/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a915c21d..e1973f80 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - mrsk (0.10.1) + mrsk (0.11.0) activesupport (>= 7.0) bcrypt_pbkdf (~> 1.0) dotenv (~> 2.8) 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