Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions lib/kamal/cli/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,18 @@ def details
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"
option :push_secrets, type: :boolean, default: false, desc: "Push env files to servers before executing command"
def exec(*cmd)
pre_connect_if_required

if (incompatible_options = [ :interactive, :reuse ].select { |key| options[:detach] && options[key] }.presence)
raise ArgumentError, "Detach is not compatible with #{incompatible_options.join(" or ")}"
end

if options[:push_secrets] && options[:reuse]
raise ArgumentError, "Push secrets is not compatible with reuse"
end

if cmd.empty?
raise ArgumentError, "No command provided. You must specify a command to execute."
end
Expand All @@ -121,6 +126,16 @@ def exec(*cmd)
using_version(version_or_latest) do |version|
say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
on(KAMAL.primary_host) { execute *KAMAL.registry.login }

if options[:push_secrets]
say "Pushing env files to servers...", :magenta
on(KAMAL.primary_host) do
app = KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host)
execute *app.ensure_env_directory
upload! KAMAL.primary_role.secrets_io(KAMAL.primary_host), KAMAL.primary_role.secrets_path, mode: "0600"
end
end

run_locally do
exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_new_container_over_ssh(cmd, env: env)
end
Expand All @@ -143,6 +158,15 @@ def exec(*cmd)
say "Launching command with version #{version} from new container...", :magenta
on(KAMAL.app_hosts) { execute *KAMAL.registry.login }

if options[:push_secrets]
say "Pushing env files to servers...", :magenta
on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
app = KAMAL.app(role: role, host: host)
execute *app.ensure_env_directory
upload! role.secrets_io(host), role.secrets_path, mode: "0600"
end
end

on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, 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, detach: detach)), quiet: quiet
Expand Down
53 changes: 53 additions & 0 deletions test/cli/app_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,59 @@ class CliAppTest < CliTestCase
end
end

test "exec with push-secrets" do
run_command("exec", "--push-secrets", "ruby -v").tap do |output|
assert_match "Pushing env files to servers...", output
assert_match "mkdir -p .kamal/apps/app/env/roles", output
assert_match "Uploading", output
assert_match ".kamal/apps/app/env/roles/web.env", output
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
assert_match %r{docker run --rm --name app-web-exec-latest-[0-9a-f]{6} --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 without push-secrets does not push env files" do
run_command("exec", "ruby -v").tap do |output|
assert_no_match "Pushing env files to servers...", output
assert_no_match "Uploading", output
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
assert_match %r{docker run --rm --name app-web-exec-latest-[0-9a-f]{6} --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 with push-secrets and primary" do
run_command("exec", "--push-secrets", "--primary", "ruby -v").tap do |output|
assert_match "Pushing env files to servers...", output
assert_match "mkdir -p .kamal/apps/app/env/roles", output
assert_match "Uploading", output
end
end

test "exec with push-secrets and reuse" do
assert_raises(ArgumentError, "Push secrets is not compatible with reuse") do
run_command("exec", "--push-secrets", "--reuse", "ruby -v")
end
end

test "exec interactive with push-secrets" do
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
SSHKit::Backend::Abstract.any_instance.expects(:exec)
.with(regexp_matches(%r{ssh -t root@1\.1\.1\.1 -p 22 'docker run -it --rm --name app-web-exec-latest-[0-9a-f]{6} --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" dhh/app:latest ruby -v'}))

stub_stdin_tty do
run_command("exec", "-i", "--push-secrets", "ruby -v").tap do |output|
assert_hook_ran "pre-connect", output
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
assert_match "Pushing env files to servers...", output
assert_match "mkdir -p .kamal/apps/app/env/roles", output
assert_match "Uploading", output
assert_match ".kamal/apps/app/env/roles/web.env", 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
end
end
end

test "containers" do
run_command("containers").tap do |output|
assert_match "docker container ls --all --filter label=service=app", output
Expand Down
Loading