Merge branch 'basecamp:main' into buildpacks

This commit is contained in:
Nick Hammond
2025-01-05 15:31:24 -07:00
committed by GitHub
20 changed files with 291 additions and 100 deletions

View File

@@ -4,6 +4,7 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
workflow_dispatch:
jobs: jobs:
rubocop: rubocop:
name: RuboCop name: RuboCop

View File

@@ -1,7 +1,7 @@
PATH PATH
remote: . remote: .
specs: specs:
kamal (2.3.0) kamal (2.4.0)
activesupport (>= 7.0) activesupport (>= 7.0)
base64 (~> 0.2) base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0) bcrypt_pbkdf (~> 1.0)
@@ -16,80 +16,87 @@ PATH
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actionpack (7.1.4.1) actionpack (8.0.0.1)
actionview (= 7.1.4.1) actionview (= 8.0.0.1)
activesupport (= 7.1.4.1) activesupport (= 8.0.0.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
actionview (7.1.4.1) useragent (~> 0.16)
activesupport (= 7.1.4.1) actionview (8.0.0.1)
activesupport (= 8.0.0.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activesupport (7.1.4.1) activesupport (8.0.0.1)
base64 base64
benchmark (>= 0.3)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5) connection_pool (>= 2.2.5)
drb drb
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1) minitest (>= 5.1)
mutex_m securerandom (>= 0.3)
tzinfo (~> 2.0) tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
ast (2.4.2) ast (2.4.2)
base64 (0.2.0) base64 (0.2.0)
bcrypt_pbkdf (1.1.1) bcrypt_pbkdf (1.1.1)
bcrypt_pbkdf (1.1.1-arm64-darwin) bcrypt_pbkdf (1.1.1-arm64-darwin)
bcrypt_pbkdf (1.1.1-x86_64-darwin) bcrypt_pbkdf (1.1.1-x86_64-darwin)
benchmark (0.4.0)
bigdecimal (3.1.8) bigdecimal (3.1.8)
builder (3.3.0) builder (3.3.0)
concurrent-ruby (1.3.4) concurrent-ruby (1.3.4)
connection_pool (2.4.1) connection_pool (2.4.1)
crass (1.0.6) crass (1.0.6)
date (3.4.1)
debug (1.9.2) debug (1.9.2)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
dotenv (3.1.2) dotenv (3.1.5)
drb (2.2.1) drb (2.2.1)
ed25519 (1.3.0) ed25519 (1.3.0)
erubi (1.13.0) erubi (1.13.0)
i18n (1.14.6) i18n (1.14.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
io-console (0.7.2) io-console (0.8.0)
irb (1.14.0) irb (1.14.2)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
json (2.7.2) json (2.9.0)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
loofah (2.22.0) logger (1.6.3)
loofah (2.23.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
minitest (5.25.1) minitest (5.25.4)
mocha (2.4.5) mocha (2.7.1)
ruby2_keywords (>= 0.0.5) ruby2_keywords (>= 0.0.5)
mutex_m (0.2.0)
net-scp (4.0.0) net-scp (4.0.0)
net-ssh (>= 2.6.5, < 8.0.0) net-ssh (>= 2.6.5, < 8.0.0)
net-sftp (4.0.0) net-sftp (4.0.0)
net-ssh (>= 5.0.0, < 8.0.0) net-ssh (>= 5.0.0, < 8.0.0)
net-ssh (7.3.0) net-ssh (7.3.0)
nokogiri (1.16.7-arm64-darwin) nokogiri (1.17.2-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.7-x86_64-darwin) nokogiri (1.17.2-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux) nokogiri (1.17.2-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
parallel (1.25.1) ostruct (0.6.1)
parser (3.3.4.0) parallel (1.26.3)
parser (3.3.6.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
psych (5.1.2) psych (5.2.1)
date
stringio stringio
racc (1.8.1) racc (1.8.1)
rack (3.1.8) rack (3.1.8)
@@ -97,55 +104,52 @@ GEM
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.1.0) rackup (2.2.1)
rack (>= 3) rack (>= 3)
webrick (~> 1.8)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.6.2)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (~> 1.14) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
railties (7.1.4.1) railties (8.0.0.1)
actionpack (= 7.1.4.1) actionpack (= 8.0.0.1)
activesupport (= 7.1.4.1) activesupport (= 8.0.0.1)
irb irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.2.1)
rdoc (6.7.0) rdoc (6.8.1)
psych (>= 4.0.0) psych (>= 4.0.0)
regexp_parser (2.9.2) regexp_parser (2.9.3)
reline (0.5.9) reline (0.5.12)
io-console (~> 0.5) io-console (~> 0.5)
rexml (3.3.9) rubocop (1.69.2)
rubocop (1.65.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.36.2, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.32.0) rubocop-ast (1.36.2)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-minitest (0.35.1) rubocop-minitest (0.36.0)
rubocop (>= 1.61, < 2.0) rubocop (>= 1.61, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-performance (1.21.1) rubocop-performance (1.23.0)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.25.1) rubocop-rails (2.27.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0) rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails-omakase (1.0.0) rubocop-rails-omakase (1.0.0)
rubocop rubocop
@@ -154,17 +158,22 @@ GEM
rubocop-rails rubocop-rails
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
sshkit (1.23.0) securerandom (0.4.0)
sshkit (1.23.2)
base64 base64
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-sftp (>= 2.1.2) net-sftp (>= 2.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
stringio (3.1.1) ostruct
thor (1.3.1) stringio (3.1.2)
thor (1.3.2)
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0) unicode-display_width (3.1.2)
webrick (1.8.2) unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.2)
useragent (0.16.11)
zeitwerk (2.7.1) zeitwerk (2.7.1)
PLATFORMS PLATFORMS

View File

@@ -162,7 +162,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)" option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server" option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)" option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep" option :grep_options, desc: "Additional options supplied to grep"
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)" option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output" option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
def logs(name) def logs(name)

View File

@@ -94,9 +94,15 @@ class Kamal::Cli::App < Kamal::Cli::Base
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)" 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 :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 :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) 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) cmd = Kamal::Utils.join_commands(cmd)
env = options[:env] env = options[:env]
detach = options[:detach]
case case
when options[:interactive] && options[:reuse] when options[:interactive] && options[:reuse]
say "Get current version of running container...", :magenta unless options[:version] say "Get current version of running container...", :magenta unless options[:version]
@@ -138,7 +144,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
roles.each do |role| roles.each do |role|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug 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 end
end end
@@ -186,15 +192,17 @@ class Kamal::Cli::App < Kamal::Cli::Base
option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)" option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server" option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)" option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep" option :grep_options, desc: "Additional options supplied to grep"
option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)" 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 :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 def logs
# FIXME: Catch when app containers aren't running # FIXME: Catch when app containers aren't running
grep = options[:grep] grep = options[:grep]
grep_options = options[:grep_options] grep_options = options[:grep_options]
since = options[:since] since = options[:since]
container_id = options[:container_id]
timestamps = !options[:skip_timestamps] timestamps = !options[:skip_timestamps]
if options[:follow] if options[:follow]
@@ -207,8 +215,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
role = KAMAL.roles_on(KAMAL.primary_host).first role = KAMAL.roles_on(KAMAL.primary_host).first
app = KAMAL.app(role: role, host: host) 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) 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, 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 end
else else
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
@@ -218,7 +226,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
roles.each do |role| roles.each do |role|
begin 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 rescue SSHKit::Command::Failed
puts_by_host host, "Nothing found" puts_by_host host, "Nothing found"
end end

View File

@@ -45,7 +45,7 @@ class Kamal::Cli::App::Boot
def start_new_version def start_new_version
audit "Booted app version #{version}" audit "Booted app version #{version}"
hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}" hostname = "#{host.to_s[0...51].chomp(".")}-#{SecureRandom.hex(6)}"
execute *app.ensure_env_directory execute *app.ensure_env_directory
upload! role.secrets_io(host), role.secrets_path, mode: "0600" upload! role.secrets_io(host), role.secrets_path, mode: "0600"
@@ -91,7 +91,7 @@ class Kamal::Cli::App::Boot
if barrier.close if barrier.close
info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles" info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles"
begin 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)) error capture_with_info(*app.container_health_log(version: version))
rescue SSHKit::Command::Failed rescue SSHKit::Command::Failed
error "Could not fetch logs for #{version}" error "Could not fetch logs for #{version}"

View File

@@ -7,13 +7,15 @@ module Kamal::Commands::App::Execution
*command *command
end end
def execute_in_new_container(*command, interactive: false, env:) def execute_in_new_container(*command, interactive: false, detach: false, env:)
docker :run, docker :run,
("-it" if interactive), ("-it" if interactive),
"--rm", ("--detach" if detach),
("--rm" unless detach),
"--network", "kamal", "--network", "kamal",
*role&.env_args(host), *role&.env_args(host),
*argumentize("--env", env), *argumentize("--env", env),
*role.logging_args,
*config.volume_args, *config.volume_args,
*role&.option_args, *role&.option_args,
config.absolute_image, config.absolute_image,

View File

@@ -1,18 +1,28 @@
module Kamal::Commands::App::Logging 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 \ pipe \
version ? container_id_for_version(version) : 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", "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) ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
end end
def follow_logs(host:, timestamps: true, lines: nil, grep: nil, grep_options: nil) def follow_logs(host:, container_id: nil, timestamps: true, lines: nil, grep: nil, grep_options: nil)
run_over_ssh \ run_over_ssh \
pipe( pipe(
current_running_container_id, container_id_command(container_id),
"xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1", "xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1",
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep) (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
), ),
host: host host: host
end 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 end

View File

@@ -11,7 +11,7 @@ module Kamal::Commands
end end
def run_over_ssh(*command, host:) def run_over_ssh(*command, host:)
"ssh#{ssh_proxy_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'" "ssh#{ssh_proxy_args}#{ssh_keys_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
end end
def container_id_for(container_name:, only_running: false) def container_id_for(container_name:, only_running: false)
@@ -98,5 +98,15 @@ module Kamal::Commands
" -o ProxyCommand='#{config.ssh.proxy.command_line_template}'" " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
end end
end end
def ssh_keys_args
"#{ ssh_keys.join("") if ssh_keys}" + "#{" -o IdentitiesOnly=yes" if config.ssh&.keys_only}"
end
def ssh_keys
config.ssh.keys&.map do |key|
" -i #{key}"
end
end
end end
end end

View File

@@ -14,7 +14,7 @@ class Kamal::Configuration
include Validation include Validation
PROXY_MINIMUM_VERSION = "v0.8.2" PROXY_MINIMUM_VERSION = "v0.8.4"
PROXY_HTTP_PORT = 80 PROXY_HTTP_PORT = 80
PROXY_HTTPS_PORT = 443 PROXY_HTTPS_PORT = 443
PROXY_LOG_MAX_SIZE = "10m" PROXY_LOG_MAX_SIZE = "10m"
@@ -37,7 +37,7 @@ class Kamal::Configuration
if file.exist? if file.exist?
# Newer Psych doesn't load aliases by default # Newer Psych doesn't load aliases by default
load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
YAML.send(load_method, ERB.new(IO.read(file)).result).symbolize_keys YAML.send(load_method, ERB.new(File.read(file)).result).symbolize_keys
else else
raise "Configuration file not found in #{file}" raise "Configuration file not found in #{file}"
end end

View File

@@ -142,7 +142,7 @@ class Kamal::Configuration::Accessory
end end
def read_dynamic_file(local_file) def read_dynamic_file(local_file)
StringIO.new(ERB.new(IO.read(local_file)).result) StringIO.new(ERB.new(File.read(local_file)).result)
end end
def expand_remote_file(remote_file) def expand_remote_file(remote_file)

View File

@@ -43,8 +43,8 @@ accessories:
# Port mappings # Port mappings
# #
# See https://docs.docker.com/network/, and especially note the warning about the security # See [https://docs.docker.com/network/](https://docs.docker.com/network/), and
# implications of exposing ports publicly. # especially note the warning about the security implications of exposing ports publicly.
port: "127.0.0.1:3306:3306" port: "127.0.0.1:3306:3306"
# Labels # Labels

View File

@@ -5,12 +5,12 @@
# For example, for a Rails app, you might open a console with: # For example, for a Rails app, you might open a console with:
# #
# ```shell # ```shell
# kamal app exec -i -r console "rails console" # kamal app exec -i --reuse "bin/rails console"
# ``` # ```
# #
# By defining an alias, like this: # By defining an alias, like this:
aliases: aliases:
console: app exec -r console -i "rails console" console: app exec -i --reuse "bin/rails console"
# You can now open the console with: # You can now open the console with:
# #
# ```shell # ```shell

View File

@@ -46,9 +46,22 @@ proxy:
# The host value must point to the server we are deploying to, and port 443 must be # The host value must point to the server we are deploying to, and port 443 must be
# open for the Let's Encrypt challenge to succeed. # open for the Let's Encrypt challenge to succeed.
# #
# If you set `ssl` to `true`, `kamal-proxy` will stop forwarding headers to your app,
# unless you explicitly set `forward_headers: true`
#
# Defaults to `false`: # Defaults to `false`:
ssl: true ssl: true
# Forward headers
#
# Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
#
# If you are behind a trusted proxy, you can set this to `true` to forward the headers.
#
# By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
# will forward them if it is set to `false`.
forward_headers: true
# Response timeout # Response timeout
# #
# How long to wait for requests to complete before timing out, defaults to 30 seconds: # How long to wait for requests to complete before timing out, defaults to 30 seconds:
@@ -93,13 +106,3 @@ proxy:
response_headers: response_headers:
- X-Request-ID - X-Request-ID
- X-Request-Start - X-Request-Start
# Forward headers
#
# Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
#
# If you are behind a trusted proxy, you can set this to `true` to forward the headers.
#
# By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
# will forward them if it is set to `false`.
forward_headers: true

View File

@@ -2,6 +2,10 @@
# #
# The default registry is Docker Hub, but you can change it using `registry/server`. # The default registry is Docker Hub, but you can change it using `registry/server`.
# #
# By default, Docker Hub creates public repositories. To avoid making your images public,
# set up a private repository before deploying, or change the default repository privacy
# settings to private in your [Docker Hub settings](https://hub.docker.com/repository-settings/default-privacy).
#
# A reference to a secret (in this case, `DOCKER_REGISTRY_TOKEN`) will look up the secret # A reference to a secret (in this case, `DOCKER_REGISTRY_TOKEN`) will look up the secret
# in the local environment: # in the local environment:
registry: registry:

View File

@@ -32,7 +32,7 @@ class Kamal::Secrets
private private
def secrets def secrets
@secrets ||= secrets_files.inject({}) do |secrets, secrets_file| @secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
secrets.merge!(::Dotenv.parse(secrets_file)) secrets.merge!(::Dotenv.parse(secrets_file, overwrite: true))
end end
end end

View File

@@ -6,20 +6,28 @@ class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Ba
def fetch_secrets(secrets, account:, session:) def fetch_secrets(secrets, account:, session:)
{}.tap do |results| {}.tap do |results|
JSON.parse(get_from_secrets_manager(secrets, account: account))["SecretValues"].each do |secret| get_from_secrets_manager(secrets, account: account).each do |secret|
secret_name = secret["Name"] secret_name = secret["Name"]
secret_string = JSON.parse(secret["SecretString"]) secret_string = JSON.parse(secret["SecretString"])
secret_string.each do |key, value| secret_string.each do |key, value|
results["#{secret_name}/#{key}"] = value results["#{secret_name}/#{key}"] = value
end end
rescue JSON::ParserError
results["#{secret_name}"] = secret["SecretString"]
end end
end end
end end
def get_from_secrets_manager(secrets, account:) def get_from_secrets_manager(secrets, account:)
`aws secretsmanager batch-get-secret-value --secret-id-list #{secrets.map(&:shellescape).join(" ")} --profile #{account.shellescape}`.tap do `aws secretsmanager batch-get-secret-value --secret-id-list #{secrets.map(&:shellescape).join(" ")} --profile #{account.shellescape}`.tap do |secrets|
raise RuntimeError, "Could not read #{secret} from AWS Secrets Manager" unless $?.success? raise RuntimeError, "Could not read #{secrets} from AWS Secrets Manager" unless $?.success?
secrets = JSON.parse(secrets)
return secrets["SecretValues"] unless secrets["Errors"].present?
raise RuntimeError, secrets["Errors"].map { |error| "#{error['SecretId']}: #{error['Message']}" }.join(" ")
end end
end end

View File

@@ -1,3 +1,3 @@
module Kamal module Kamal
VERSION = "2.3.0" VERSION = "2.4.0"
end end

View File

@@ -263,13 +263,37 @@ class CliAppTest < CliTestCase
test "exec" do test "exec" do
run_command("exec", "ruby -v").tap do |output| 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
end end
test "exec separate arguments" do test "exec separate arguments" do
run_command("exec", "ruby", " -v").tap do |output| 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 --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 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
end end
@@ -282,7 +306,7 @@ class CliAppTest < CliTestCase
test "exec interactive" do test "exec interactive" do
SSHKit::Backend::Abstract.any_instance.expects(:exec) 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| run_command("exec", "-i", "ruby -v").tap do |output|
assert_match "Get most recent version available as an image...", 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 assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output
@@ -329,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") 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 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 test "logs with follow and grep" do
SSHKit::Backend::Abstract.any_instance.stubs(:exec) 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\"'") .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\"'")

View File

@@ -157,6 +157,12 @@ class CommandsAppTest < ActiveSupport::TestCase
new_command.logs.join(" ") new_command.logs.join(" ")
end 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 test "logs with since" do
assert_equal \ 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", "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",
@@ -208,6 +214,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\"'", "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") 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 \ 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'", "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) new_command.follow_logs(host: "app-1", lines: 123)
@@ -224,29 +234,43 @@ class CommandsAppTest < ActiveSupport::TestCase
test "execute in new container" do test "execute in new container" do
assert_equal \ 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(" ") new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
end end
test "execute in new container with env" do test "execute in new container with env" do
assert_equal \ 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(" ") new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
end end
test "execute in new detached container" do
assert_equal \
"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
test "execute in new container with tags" do test "execute in new container with tags" do
@config[:servers] = [ { "1.1.1.1" => "tag1" } ] @config[:servers] = [ { "1.1.1.1" => "tag1" } ]
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } @config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
assert_equal \ 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(" ") new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
end end
test "execute in new container with custom options" do test "execute in new container with custom options" do
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } } @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
assert_equal \ 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(" ") new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
end end
@@ -263,7 +287,7 @@ class CommandsAppTest < ActiveSupport::TestCase
end end
test "execute in new container over ssh" do 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: {}) new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
end end
@@ -271,13 +295,13 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:servers] = [ { "1.1.1.1" => "tag1" } ] @config[:servers] = [ { "1.1.1.1" => "tag1" } ]
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } } @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: {}) new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
end end
test "execute in new container with custom options over ssh" do 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 } } } @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: {}) new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
end end
@@ -315,6 +339,16 @@ class CommandsAppTest < ActiveSupport::TestCase
assert_equal "ssh -J root@2.2.2.2 -t app@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") assert_equal "ssh -J root@2.2.2.2 -t app@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
end end
test "run over ssh with keys config" do
@config[:ssh] = { "keys" => [ "path_to_key.pem" ] }
assert_equal "ssh -i path_to_key.pem -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
end
test "run over ssh with keys config with keys_only" do
@config[:ssh] = { "keys" => [ "path_to_key.pem" ], "keys_only" => true }
assert_equal "ssh -i path_to_key.pem -o IdentitiesOnly=yes -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
end
test "run over ssh with proxy_command" do test "run over ssh with proxy_command" do
@config[:ssh] = { "proxy_command" => "ssh -W %h:%p user@proxy-server" } @config[:ssh] = { "proxy_command" => "ssh -W %h:%p user@proxy-server" }
assert_equal "ssh -o ProxyCommand='ssh -W %h:%p user@proxy-server' -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1") assert_equal "ssh -o ProxyCommand='ssh -W %h:%p user@proxy-server' -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")

View File

@@ -1,6 +1,35 @@
require "test_helper" require "test_helper"
class AwsSecretsManagerAdapterTest < SecretAdapterTestCase class AwsSecretsManagerAdapterTest < SecretAdapterTestCase
test "fails when errors are present" do
stub_ticks.with("aws --version 2> /dev/null")
stub_ticks
.with("aws secretsmanager batch-get-secret-value --secret-id-list unknown1 unknown2 --profile default")
.returns(<<~JSON)
{
"SecretValues": [],
"Errors": [
{
"SecretId": "unknown1",
"ErrorCode": "ResourceNotFoundException",
"Message": "Secrets Manager can't find the specified secret."
},
{
"SecretId": "unknown2",
"ErrorCode": "ResourceNotFoundException",
"Message": "Secrets Manager can't find the specified secret."
}
]
}
JSON
error = assert_raises RuntimeError do
JSON.parse(shellunescape(run_command("fetch", "unknown1", "unknown2")))
end
assert_equal [ "unknown1: Secrets Manager can't find the specified secret.", "unknown2: Secrets Manager can't find the specified secret." ].join(" "), error.message
end
test "fetch" do test "fetch" do
stub_ticks.with("aws --version 2> /dev/null") stub_ticks.with("aws --version 2> /dev/null")
stub_ticks stub_ticks
@@ -44,6 +73,48 @@ class AwsSecretsManagerAdapterTest < SecretAdapterTestCase
assert_equal expected_json, json assert_equal expected_json, json
end end
test "fetch with string value" do
stub_ticks.with("aws --version 2> /dev/null")
stub_ticks
.with("aws secretsmanager batch-get-secret-value --secret-id-list secret secret2/KEY1 --profile default")
.returns(<<~JSON)
{
"SecretValues": [
{
"ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret",
"Name": "secret",
"VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
"SecretString": "a-string-secret",
"VersionStages": [
"AWSCURRENT"
],
"CreatedDate": "2024-01-01T00:00:00.000000"
},
{
"ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret2",
"Name": "secret2",
"VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
"SecretString": "{\\"KEY2\\":\\"VALUE2\\"}",
"VersionStages": [
"AWSCURRENT"
],
"CreatedDate": "2024-01-01T00:00:00.000000"
}
],
"Errors": []
}
JSON
json = JSON.parse(shellunescape(run_command("fetch", "secret", "secret2/KEY1")))
expected_json = {
"secret"=>"a-string-secret",
"secret2/KEY2"=>"VALUE2"
}
assert_equal expected_json, json
end
test "fetch with secret names" do test "fetch with secret names" do
stub_ticks.with("aws --version 2> /dev/null") stub_ticks.with("aws --version 2> /dev/null")
stub_ticks stub_ticks