From 6856742eca5ddcdc62a76a20d530bff695fdbc35 Mon Sep 17 00:00:00 2001 From: Justin Dell Date: Mon, 21 Oct 2024 09:19:06 -0500 Subject: [PATCH 1/4] add secrets adapter for aws secrets manager --- .../secrets/adapters/aws_secretsmanager.rb | 25 ++++++ .../aws_secretsmanager_adapter_test.rb | 87 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 lib/kamal/secrets/adapters/aws_secretsmanager.rb create mode 100644 test/secrets/aws_secretsmanager_adapter_test.rb diff --git a/lib/kamal/secrets/adapters/aws_secretsmanager.rb b/lib/kamal/secrets/adapters/aws_secretsmanager.rb new file mode 100644 index 000000000..8d6f2eadf --- /dev/null +++ b/lib/kamal/secrets/adapters/aws_secretsmanager.rb @@ -0,0 +1,25 @@ +class Kamal::Secrets::Adapters::AwsSecretsmanager < Kamal::Secrets::Adapters::Base + private + def login(_account) + nil + end + + def fetch_secrets(secrets, account:, session:) + {}.tap do |results| + JSON.parse(get_from_secrets_manager(secrets, account: account))["SecretValues"].each do |secret| + secret_name = secret["Name"] + secret_string = JSON.parse(secret["SecretString"]) + + secret_string.each do |key, value| + results["#{secret_name}/#{key}"] = value + end + end + end + end + + def get_from_secrets_manager(secrets, account:) + `aws secretsmanager batch-get-secret-value --secret-id-list #{secrets.map(&:shellescape).join(" ")} --profile #{account}`.tap do + raise RuntimeError, "Could not read #{secret} from AWS Secrets Manager" unless $?.success? + end + end +end diff --git a/test/secrets/aws_secretsmanager_adapter_test.rb b/test/secrets/aws_secretsmanager_adapter_test.rb new file mode 100644 index 000000000..269520340 --- /dev/null +++ b/test/secrets/aws_secretsmanager_adapter_test.rb @@ -0,0 +1,87 @@ +require "test_helper" + +class AwsSecretsmanagerAdapterTest < SecretAdapterTestCase + test "fetch" do + stub_ticks + .with("aws secretsmanager batch-get-secret-value --secret-id-list secret/KEY1 secret/KEY2 secret2/KEY3 --profile default") + .returns(<<~JSON) + { + "SecretValues": [ + { + "ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret", + "Name": "secret", + "VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv", + "SecretString": "{\\"KEY1\\":\\"VALUE1\\", \\"KEY2\\":\\"VALUE2\\"}", + "VersionStages": [ + "AWSCURRENT" + ], + "CreatedDate": "2024-01-01T00:00:00.000000" + }, + { + "ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret2", + "Name": "secret2", + "VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv", + "SecretString": "{\\"KEY3\\":\\"VALUE3\\"}", + "VersionStages": [ + "AWSCURRENT" + ], + "CreatedDate": "2024-01-01T00:00:00.000000" + } + ], + "Errors": [] + } + JSON + + json = JSON.parse(shellunescape(run_command("fetch", "secret/KEY1", "secret/KEY2", "secret2/KEY3"))) + + expected_json = { + "secret/KEY1"=>"VALUE1", + "secret/KEY2"=>"VALUE2", + "secret2/KEY3"=>"VALUE3" + } + + assert_equal expected_json, json + end + + test "fetch with secret names" do + stub_ticks + .with("aws secretsmanager batch-get-secret-value --secret-id-list secret/KEY1 secret/KEY2 --profile default") + .returns(<<~JSON) + { + "SecretValues": [ + { + "ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret", + "Name": "secret", + "VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv", + "SecretString": "{\\"KEY1\\":\\"VALUE1\\", \\"KEY2\\":\\"VALUE2\\"}", + "VersionStages": [ + "AWSCURRENT" + ], + "CreatedDate": "2024-01-01T00:00:00.000000" + } + ], + "Errors": [] + } + JSON + + json = JSON.parse(shellunescape(run_command("fetch", "--from", "secret", "KEY1", "KEY2"))) + + expected_json = { + "secret/KEY1"=>"VALUE1", + "secret/KEY2"=>"VALUE2" + } + + assert_equal expected_json, json + end + + private + def run_command(*command) + stdouted do + Kamal::Cli::Secrets.start \ + [ *command, + "-c", "test/fixtures/deploy_with_accessories.yml", + "--adapter", "aws_secretsmanager", + "--account", "default" ] + end + end +end From c9fff3cb4024f67ff5c4a9c80a06c3675a913bfe Mon Sep 17 00:00:00 2001 From: Justin Dell Date: Mon, 4 Nov 2024 09:14:47 -0600 Subject: [PATCH 2/4] rename secretsmanager to secrets manager --- .../{aws_secretsmanager.rb => aws_secrets_manager.rb} | 2 +- ...er_adapter_test.rb => aws_secrets_manager_adapter_test.rb} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename lib/kamal/secrets/adapters/{aws_secretsmanager.rb => aws_secrets_manager.rb} (92%) rename test/secrets/{aws_secretsmanager_adapter_test.rb => aws_secrets_manager_adapter_test.rb} (96%) diff --git a/lib/kamal/secrets/adapters/aws_secretsmanager.rb b/lib/kamal/secrets/adapters/aws_secrets_manager.rb similarity index 92% rename from lib/kamal/secrets/adapters/aws_secretsmanager.rb rename to lib/kamal/secrets/adapters/aws_secrets_manager.rb index 8d6f2eadf..7f834c3e5 100644 --- a/lib/kamal/secrets/adapters/aws_secretsmanager.rb +++ b/lib/kamal/secrets/adapters/aws_secrets_manager.rb @@ -1,4 +1,4 @@ -class Kamal::Secrets::Adapters::AwsSecretsmanager < Kamal::Secrets::Adapters::Base +class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Base private def login(_account) nil diff --git a/test/secrets/aws_secretsmanager_adapter_test.rb b/test/secrets/aws_secrets_manager_adapter_test.rb similarity index 96% rename from test/secrets/aws_secretsmanager_adapter_test.rb rename to test/secrets/aws_secrets_manager_adapter_test.rb index 269520340..eb4255541 100644 --- a/test/secrets/aws_secretsmanager_adapter_test.rb +++ b/test/secrets/aws_secrets_manager_adapter_test.rb @@ -1,6 +1,6 @@ require "test_helper" -class AwsSecretsmanagerAdapterTest < SecretAdapterTestCase +class AwsSecretsManagerAdapterTest < SecretAdapterTestCase test "fetch" do stub_ticks .with("aws secretsmanager batch-get-secret-value --secret-id-list secret/KEY1 secret/KEY2 secret2/KEY3 --profile default") @@ -80,7 +80,7 @@ def run_command(*command) Kamal::Cli::Secrets.start \ [ *command, "-c", "test/fixtures/deploy_with_accessories.yml", - "--adapter", "aws_secretsmanager", + "--adapter", "aws_secrets_manager", "--account", "default" ] end end From e26694541386acdd13dbb13ea23447b6002d8a29 Mon Sep 17 00:00:00 2001 From: Justin Dell Date: Mon, 4 Nov 2024 09:18:56 -0600 Subject: [PATCH 3/4] implement check_dependencies! --- lib/kamal/secrets/adapters/aws_secrets_manager.rb | 9 +++++++++ test/secrets/aws_secrets_manager_adapter_test.rb | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/kamal/secrets/adapters/aws_secrets_manager.rb b/lib/kamal/secrets/adapters/aws_secrets_manager.rb index 7f834c3e5..1da48b94c 100644 --- a/lib/kamal/secrets/adapters/aws_secrets_manager.rb +++ b/lib/kamal/secrets/adapters/aws_secrets_manager.rb @@ -22,4 +22,13 @@ def get_from_secrets_manager(secrets, account:) raise RuntimeError, "Could not read #{secret} from AWS Secrets Manager" unless $?.success? end end + + def check_dependencies! + raise RuntimeError, "AWS CLI is not installed" unless cli_installed? + end + + def cli_installed? + `aws --version 2> /dev/null` + $?.success? + end end diff --git a/test/secrets/aws_secrets_manager_adapter_test.rb b/test/secrets/aws_secrets_manager_adapter_test.rb index eb4255541..42a0f48ae 100644 --- a/test/secrets/aws_secrets_manager_adapter_test.rb +++ b/test/secrets/aws_secrets_manager_adapter_test.rb @@ -2,6 +2,7 @@ class AwsSecretsManagerAdapterTest < SecretAdapterTestCase test "fetch" do + stub_ticks.with("aws --version 2> /dev/null") stub_ticks .with("aws secretsmanager batch-get-secret-value --secret-id-list secret/KEY1 secret/KEY2 secret2/KEY3 --profile default") .returns(<<~JSON) @@ -44,6 +45,7 @@ class AwsSecretsManagerAdapterTest < SecretAdapterTestCase end test "fetch with secret names" do + stub_ticks.with("aws --version 2> /dev/null") stub_ticks .with("aws secretsmanager batch-get-secret-value --secret-id-list secret/KEY1 secret/KEY2 --profile default") .returns(<<~JSON) @@ -74,6 +76,15 @@ class AwsSecretsManagerAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end + test "fetch without CLI installed" do + stub_ticks_with("aws --version 2> /dev/null", succeed: false) + + error = assert_raises RuntimeError do + JSON.parse(shellunescape(run_command("fetch", "SECRET1"))) + end + assert_equal "AWS CLI is not installed", error.message + end + private def run_command(*command) stdouted do From b4d395cec9247e35c8bb67cf7d247add486c9bb5 Mon Sep 17 00:00:00 2001 From: Justin Dell Date: Mon, 4 Nov 2024 09:46:45 -0600 Subject: [PATCH 4/4] shell escape account name in cli command --- lib/kamal/secrets/adapters/aws_secrets_manager.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kamal/secrets/adapters/aws_secrets_manager.rb b/lib/kamal/secrets/adapters/aws_secrets_manager.rb index 1da48b94c..e23ea1f1e 100644 --- a/lib/kamal/secrets/adapters/aws_secrets_manager.rb +++ b/lib/kamal/secrets/adapters/aws_secrets_manager.rb @@ -18,7 +18,7 @@ def fetch_secrets(secrets, account:, session:) end def get_from_secrets_manager(secrets, account:) - `aws secretsmanager batch-get-secret-value --secret-id-list #{secrets.map(&:shellescape).join(" ")} --profile #{account}`.tap do + `aws secretsmanager batch-get-secret-value --secret-id-list #{secrets.map(&:shellescape).join(" ")} --profile #{account.shellescape}`.tap do raise RuntimeError, "Could not read #{secret} from AWS Secrets Manager" unless $?.success? end end