From dbe0c3a7f8269db005528b1afd18773e1321722f Mon Sep 17 00:00:00 2001 From: Ali Ismayilov <993934+aliismayilov@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:32:01 +0200 Subject: [PATCH 1/8] Allow running detached app commands this is useful for long running rake tasks or scripts that can be run without having to keep open connection to the server. Example: ``` kamal app exec 'bin/rails db:backfill_task' --detach ``` --- lib/kamal/cli/app.rb | 4 +++- lib/kamal/commands/app/execution.rb | 3 ++- test/cli/app_test.rb | 6 ++++++ test/commands/app_test.rb | 6 ++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 74b7b4df2..0217dbd19 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -94,9 +94,11 @@ def details option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)" option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one" option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command" + option :detach, type: :boolean, default: false, desc: "Execute command in a detached container" def exec(*cmd) cmd = Kamal::Utils.join_commands(cmd) env = options[:env] + detach = options[:detach] case when options[:interactive] && options[:reuse] say "Get current version of running container...", :magenta unless options[:version] @@ -138,7 +140,7 @@ def exec(*cmd) roles.each do |role| execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug - puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env)) + puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env, detach: detach)) end end end diff --git a/lib/kamal/commands/app/execution.rb b/lib/kamal/commands/app/execution.rb index 4434c26aa..d6fa04fdc 100644 --- a/lib/kamal/commands/app/execution.rb +++ b/lib/kamal/commands/app/execution.rb @@ -7,9 +7,10 @@ def execute_in_existing_container(*command, interactive: false, env:) *command end - def execute_in_new_container(*command, interactive: false, env:) + def execute_in_new_container(*command, interactive: false, detach: false, env:) docker :run, ("-it" if interactive), + ("--detach" if detach), "--rm", "--network", "kamal", *role&.env_args(host), diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 5e76179c3..63eade9b5 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -273,6 +273,12 @@ class CliAppTest < CliTestCase end end + test "exec detach" do + run_command("exec", "--detach", "ruby -v").tap do |output| + assert_match "docker run --detach --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output + end + end + test "exec with reuse" do run_command("exec", "--reuse", "ruby -v").tap do |output| assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 1fb59e8a0..abcefed1b 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -234,6 +234,12 @@ class CommandsAppTest < ActiveSupport::TestCase new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ") end + test "execute in new detached container" do + assert_equal \ + "docker run --detach --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup", + new_command.execute_in_new_container("bin/rails", "db:setup", detach: true, env: {}).join(" ") + end + test "execute in new container with tags" do @config[:servers] = [ { "1.1.1.1" => "tag1" } ] @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } From c662b8d578275f0796e5bfa05c5bcea94472ddca Mon Sep 17 00:00:00 2001 From: Ali Ismayilov <993934+aliismayilov@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:57:34 +0200 Subject: [PATCH 2/8] Make --detach incompatible with reuse or interactive --- lib/kamal/cli/app.rb | 4 ++++ test/cli/app_test.rb | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 0217dbd19..3f8130146 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -96,6 +96,10 @@ def details option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command" option :detach, type: :boolean, default: false, desc: "Execute command in a detached container" def exec(*cmd) + if (incompatible_options = [ :interactive, :reuse ].select { |key| options[:detach] && options[key] }.presence) + raise ArgumentError, "Detach is not compatible with #{incompatible_options.join(" or ")}" + end + cmd = Kamal::Utils.join_commands(cmd) env = options[:env] detach = options[:detach] diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 63eade9b5..c5255c4a5 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -279,6 +279,24 @@ class CliAppTest < CliTestCase end end + test "exec detach with reuse" do + assert_raises(ArgumentError, "Detach is not compatible with reuse") do + run_command("exec", "--detach", "--reuse", "ruby -v") + end + end + + test "exec detach with interactive" do + assert_raises(ArgumentError, "Detach is not compatible with interactive") do + run_command("exec", "--interactive", "--detach", "ruby -v") + end + end + + test "exec detach with interactive and reuse" do + assert_raises(ArgumentError, "Detach is not compatible with interactive or reuse") do + run_command("exec", "--interactive", "--detach", "--reuse", "ruby -v") + end + end + test "exec with reuse" do run_command("exec", "--reuse", "ruby -v").tap do |output| assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version From 1da882bb01b4077e6e105664328d14b757124e06 Mon Sep 17 00:00:00 2001 From: Ali Ismayilov <993934+aliismayilov@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:58:38 +0200 Subject: [PATCH 3/8] Enable logging on app exec new containers --- lib/kamal/commands/app/execution.rb | 1 + test/cli/app_test.rb | 8 ++++---- test/commands/app_test.rb | 24 ++++++++++++++++-------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/kamal/commands/app/execution.rb b/lib/kamal/commands/app/execution.rb index d6fa04fdc..3d5ff7569 100644 --- a/lib/kamal/commands/app/execution.rb +++ b/lib/kamal/commands/app/execution.rb @@ -15,6 +15,7 @@ def execute_in_new_container(*command, interactive: false, detach: false, env:) "--network", "kamal", *role&.env_args(host), *argumentize("--env", env), + *role.logging_args, *config.volume_args, *role&.option_args, config.absolute_image, diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index c5255c4a5..e6a51894e 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -263,19 +263,19 @@ class CliAppTest < CliTestCase test "exec" do run_command("exec", "ruby -v").tap do |output| - assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output + assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v", output end end test "exec separate arguments" do run_command("exec", "ruby", " -v").tap do |output| - assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output + assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v", output end end test "exec detach" do run_command("exec", "--detach", "ruby -v").tap do |output| - assert_match "docker run --detach --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output + assert_match "docker run --detach --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v", output end end @@ -306,7 +306,7 @@ class CliAppTest < CliTestCase test "exec interactive" do SSHKit::Backend::Abstract.any_instance.expects(:exec) - .with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v'") + .with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v'") run_command("exec", "-i", "ruby -v").tap do |output| assert_match "Get most recent version available as an image...", output assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index abcefed1b..f3db95683 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -224,19 +224,27 @@ class CommandsAppTest < ActiveSupport::TestCase test "execute in new container" do assert_equal \ - "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup", + "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup", + new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") + end + + test "execute in new container with logging" do + @config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } } + + assert_equal \ + "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end test "execute in new container with env" do assert_equal \ - "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup", + "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --env foo=\"bar\" --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ") end test "execute in new detached container" do assert_equal \ - "docker run --detach --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup", + "docker run --detach --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", detach: true, env: {}).join(" ") end @@ -245,14 +253,14 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } assert_equal \ - "docker run --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup", + "docker run --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end test "execute in new container with custom options" do @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } assert_equal \ - "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", + "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ") end @@ -269,7 +277,7 @@ class CommandsAppTest < ActiveSupport::TestCase end test "execute in new container over ssh" do - assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c}, + assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" dhh/app:999 bin/rails c}, new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end @@ -277,13 +285,13 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:servers] = [ { "1.1.1.1" => "tag1" } ] @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } - assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c'", + assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails c'", new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end test "execute in new container with custom options over ssh" do @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } - assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c}, + assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c}, new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) end From 183fe9e06e56a154b6e9f076df0190b2f72381bd Mon Sep 17 00:00:00 2001 From: Ali Ismayilov <993934+aliismayilov@users.noreply.github.com> Date: Sat, 26 Oct 2024 13:53:55 +0200 Subject: [PATCH 4/8] Follow logs of a specific container --- lib/kamal/cli/app.rb | 6 ++++-- lib/kamal/commands/app/logging.rb | 4 ++-- test/cli/app_test.rb | 7 +++++++ test/commands/app_test.rb | 4 ++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 3f8130146..3ed9a3b26 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -195,12 +195,14 @@ def images option :grep_options, aliases: "-o", desc: "Additional options supplied to grep" option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)" option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output" + option :container_id, desc: "Docker container ID to fetch logs" def logs # FIXME: Catch when app containers aren't running grep = options[:grep] grep_options = options[:grep_options] since = options[:since] + container_id = options[:container_id] timestamps = !options[:skip_timestamps] if options[:follow] @@ -213,8 +215,8 @@ def logs role = KAMAL.roles_on(KAMAL.primary_host).first app = KAMAL.app(role: role, host: host) - info app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options) - exec app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options) + info app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options, container_id: container_id) + exec app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options, container_id: container_id) end else lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set diff --git a/lib/kamal/commands/app/logging.rb b/lib/kamal/commands/app/logging.rb index ad66f370c..4a45a533d 100644 --- a/lib/kamal/commands/app/logging.rb +++ b/lib/kamal/commands/app/logging.rb @@ -6,10 +6,10 @@ def logs(version: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) end - def follow_logs(host:, timestamps: true, lines: nil, grep: nil, grep_options: nil) + def follow_logs(host:, timestamps: true, lines: nil, grep: nil, grep_options: nil, container_id: nil) run_over_ssh \ pipe( - current_running_container_id, + container_id ? "echo #{container_id}" : current_running_container_id, "xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1", (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) ), diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index e6a51894e..63937ea2b 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -353,6 +353,13 @@ class CliAppTest < CliTestCase assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow") end + test "logs with follow and container_id" do + SSHKit::Backend::Abstract.any_instance.stubs(:exec) + .with("ssh -t root@1.1.1.1 -p 22 'echo ID123 | xargs docker logs --timestamps --tail 10 --follow 2>&1'") + + assert_match "echo ID123 | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow", "--container_id", "ID123") + end + test "logs with follow and grep" do SSHKit::Backend::Abstract.any_instance.stubs(:exec) .with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\"'") diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index f3db95683..c9934629f 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -208,6 +208,10 @@ class CommandsAppTest < ActiveSupport::TestCase "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"Completed\"'", new_command.follow_logs(host: "app-1", grep: "Completed") + assert_equal \ + "ssh -t root@app-1 -p 22 'echo ID321 | xargs docker logs --timestamps --follow 2>&1'", + new_command.follow_logs(host: "app-1", container_id: "ID321") + assert_equal \ "ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1'", new_command.follow_logs(host: "app-1", lines: 123) From 75b44cd328dac23013e2ee48e8847164a5291d9a Mon Sep 17 00:00:00 2001 From: Ali Ismayilov <993934+aliismayilov@users.noreply.github.com> Date: Sat, 26 Oct 2024 14:40:35 +0200 Subject: [PATCH 5/8] Capture logs for specific container_id --- lib/kamal/cli/app.rb | 6 +++--- lib/kamal/cli/app/boot.rb | 2 +- lib/kamal/commands/app/execution.rb | 2 +- lib/kamal/commands/app/logging.rb | 6 +++--- test/cli/app_test.rb | 2 +- test/commands/app_test.rb | 8 +++++++- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 3ed9a3b26..2378fa035 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -215,8 +215,8 @@ def logs role = KAMAL.roles_on(KAMAL.primary_host).first app = KAMAL.app(role: role, host: host) - info app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options, container_id: container_id) - exec app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options, container_id: container_id) + info app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options) + exec app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options) end else lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set @@ -226,7 +226,7 @@ def logs roles.each do |role| begin - puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options)) + puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(container_id: container_id, timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options)) rescue SSHKit::Command::Failed puts_by_host host, "Nothing found" end diff --git a/lib/kamal/cli/app/boot.rb b/lib/kamal/cli/app/boot.rb index fd330c71f..b98e0472b 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -91,7 +91,7 @@ def close_barrier if barrier.close info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles" begin - error capture_with_info(*app.logs(version: version)) + error capture_with_info(*app.logs(container_id: "$(#{app.container_id_for_version(version)})")) error capture_with_info(*app.container_health_log(version: version)) rescue SSHKit::Command::Failed error "Could not fetch logs for #{version}" diff --git a/lib/kamal/commands/app/execution.rb b/lib/kamal/commands/app/execution.rb index 3d5ff7569..e1289fd89 100644 --- a/lib/kamal/commands/app/execution.rb +++ b/lib/kamal/commands/app/execution.rb @@ -11,7 +11,7 @@ def execute_in_new_container(*command, interactive: false, detach: false, env:) docker :run, ("-it" if interactive), ("--detach" if detach), - "--rm", + ("--rm" unless detach), "--network", "kamal", *role&.env_args(host), *argumentize("--env", env), diff --git a/lib/kamal/commands/app/logging.rb b/lib/kamal/commands/app/logging.rb index 4a45a533d..56e0679c2 100644 --- a/lib/kamal/commands/app/logging.rb +++ b/lib/kamal/commands/app/logging.rb @@ -1,12 +1,12 @@ module Kamal::Commands::App::Logging - def logs(version: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil) + def logs(container_id: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil) pipe \ - version ? container_id_for_version(version) : current_running_container_id, + container_id ? "echo #{container_id}" : current_running_container_id, "xargs docker logs#{" --timestamps" if timestamps}#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1", ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) end - def follow_logs(host:, timestamps: true, lines: nil, grep: nil, grep_options: nil, container_id: nil) + def follow_logs(host:, container_id: nil, timestamps: true, lines: nil, grep: nil, grep_options: nil) run_over_ssh \ pipe( container_id ? "echo #{container_id}" : current_running_container_id, diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 63937ea2b..7c71d05ea 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -275,7 +275,7 @@ class CliAppTest < CliTestCase test "exec detach" do run_command("exec", "--detach", "ruby -v").tap do |output| - assert_match "docker run --detach --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v", output + assert_match "docker run --detach --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v", output end end diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index c9934629f..c42f535a4 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -157,6 +157,12 @@ class CommandsAppTest < ActiveSupport::TestCase new_command.logs.join(" ") end + test "logs with container_id" do + assert_equal \ + "echo C137 | xargs docker logs --timestamps 2>&1", + new_command.logs(container_id: "C137").join(" ") + end + test "logs with since" do assert_equal \ "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1", @@ -248,7 +254,7 @@ class CommandsAppTest < ActiveSupport::TestCase test "execute in new detached container" do assert_equal \ - "docker run --detach --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup", + "docker run --detach --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup", new_command.execute_in_new_container("bin/rails", "db:setup", detach: true, env: {}).join(" ") end From ac90ee068fd36c31c5a7adf94c22a0f8153a2306 Mon Sep 17 00:00:00 2001 From: Ali Ismayilov <993934+aliismayilov@users.noreply.github.com> Date: Sat, 26 Oct 2024 18:47:10 +0200 Subject: [PATCH 6/8] Prefer dasherized notation --- test/cli/app_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 7c71d05ea..2e532730a 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -357,7 +357,7 @@ class CliAppTest < CliTestCase SSHKit::Backend::Abstract.any_instance.stubs(:exec) .with("ssh -t root@1.1.1.1 -p 22 'echo ID123 | xargs docker logs --timestamps --tail 10 --follow 2>&1'") - assert_match "echo ID123 | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow", "--container_id", "ID123") + assert_match "echo ID123 | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow", "--container-id", "ID123") end test "logs with follow and grep" do From 1cc5406b00048775dce4b75aabd94512ee821abb Mon Sep 17 00:00:00 2001 From: Ali Ismayilov <993934+aliismayilov@users.noreply.github.com> Date: Sat, 23 Nov 2024 21:37:58 +0100 Subject: [PATCH 7/8] Pipe app container id --- lib/kamal/cli/app/boot.rb | 2 +- lib/kamal/commands/app/logging.rb | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/kamal/cli/app/boot.rb b/lib/kamal/cli/app/boot.rb index b98e0472b..41db39a66 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -91,7 +91,7 @@ def close_barrier if barrier.close info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles" begin - error capture_with_info(*app.logs(container_id: "$(#{app.container_id_for_version(version)})")) + error capture_with_info(*app.logs(container_id: app.container_id_for_version(version))) error capture_with_info(*app.container_health_log(version: version)) rescue SSHKit::Command::Failed error "Could not fetch logs for #{version}" diff --git a/lib/kamal/commands/app/logging.rb b/lib/kamal/commands/app/logging.rb index 56e0679c2..d5c8d5c7c 100644 --- a/lib/kamal/commands/app/logging.rb +++ b/lib/kamal/commands/app/logging.rb @@ -1,7 +1,7 @@ module Kamal::Commands::App::Logging def logs(container_id: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil) pipe \ - container_id ? "echo #{container_id}" : current_running_container_id, + container_id_command(container_id), "xargs docker logs#{" --timestamps" if timestamps}#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1", ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep) end @@ -9,10 +9,20 @@ def logs(container_id: nil, timestamps: true, since: nil, lines: nil, grep: nil, def follow_logs(host:, container_id: nil, timestamps: true, lines: nil, grep: nil, grep_options: nil) run_over_ssh \ pipe( - container_id ? "echo #{container_id}" : current_running_container_id, + container_id_command(container_id), "xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1", (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) ), host: host end + + private + + def container_id_command(container_id) + case container_id + when Array then container_id + when String, Symbol then "echo #{container_id}" + else current_running_container_id + end + end end From 8a7843cb35600195f587e171b1ddb5d56bfb3e84 Mon Sep 17 00:00:00 2001 From: Ali Ismayilov <993934+aliismayilov@users.noreply.github.com> Date: Sat, 23 Nov 2024 21:43:34 +0100 Subject: [PATCH 8/8] Allow running the CI manually --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 839bc4e65..8ef7d0aee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: branches: - main pull_request: + workflow_dispatch: jobs: rubocop: name: RuboCop