diff --git a/lib/kamal/secrets/adapters/doppler.rb b/lib/kamal/secrets/adapters/doppler.rb index caa1833e..64d644f7 100644 --- a/lib/kamal/secrets/adapters/doppler.rb +++ b/lib/kamal/secrets/adapters/doppler.rb @@ -1,28 +1,53 @@ class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base + def requires_account? + false + end + private - def login(account) - unless loggedin?(account) + def login(*) + unless loggedin? `doppler login -y` raise RuntimeError, "Failed to login to Doppler" unless $?.success? end end - def loggedin?(account) + def loggedin? `doppler me --json 2> /dev/null` $?.success? end - def fetch_secrets(secrets, account:, session:) - project, config = account.split("/") + def fetch_secrets(secrets, **) + project_and_config_flags = "" + unless service_token_set? + project, config, _ = secrets.first.split("/") + + unless project && config + raise RuntimeError, "Missing project or config from '--from=project/config' option" + end + + project_and_config_flags = "-p #{project.shellescape} -c #{config.shellescape}" + end - raise RuntimeError, "Missing project or config from --acount=project/config option" unless project && config - raise RuntimeError, "Using --from option or FOLDER/SECRET is not supported by Doppler" if secrets.any?(/\//) + secret_names = secrets.collect { |s| s.split("/").last } - items = `doppler secrets get #{secrets.map(&:shellescape).join(" ")} --json -p #{project} -c #{config}` + items = `doppler secrets get #{secret_names.map(&:shellescape).join(" ")} --json #{project_and_config_flags}` raise RuntimeError, "Could not read #{secrets} from Doppler" unless $?.success? items = JSON.parse(items) items.transform_values { |value| value["computed"] } end + + def service_token_set? + ENV["DOPPLER_TOKEN"] && ENV["DOPPLER_TOKEN"][0, 5] == "dp.st" + end + + def check_dependencies! + raise RuntimeError, "Doppler CLI is not installed" unless cli_installed? + end + + def cli_installed? + `doppler --version 2> /dev/null` + $?.success? + end end diff --git a/test/secrets/doppler_adapter_test.rb b/test/secrets/doppler_adapter_test.rb index c7cda494..c2b16468 100644 --- a/test/secrets/doppler_adapter_test.rb +++ b/test/secrets/doppler_adapter_test.rb @@ -6,6 +6,7 @@ class DopplerAdapterTest < SecretAdapterTestCase end test "fetch" do + stub_ticks_with("doppler --version 2> /dev/null", succeed: true) stub_ticks.with("doppler me --json 2> /dev/null") stub_ticks @@ -30,7 +31,9 @@ class DopplerAdapterTest < SecretAdapterTestCase } JSON - json = JSON.parse(shellunescape(run_command("fetch", "SECRET1", "FSECRET1", "FSECRET2"))) + json = JSON.parse( + shellunescape run_command("fetch", "--from", "my-project/prd", "SECRET1", "FSECRET1", "FSECRET2") + ) expected_json = { "SECRET1"=>"secret1", @@ -41,32 +44,106 @@ class DopplerAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end - test "fetch with from" do + test "fetch having DOPPLER_TOKEN" do + ENV["DOPPLER_TOKEN"] = "dp.st.xxxxxxxxxxxxxxxxxxxxxx" + + stub_ticks_with("doppler --version 2> /dev/null", succeed: true) stub_ticks.with("doppler me --json 2> /dev/null") - error = assert_raises RuntimeError do - run_command("fetch", "--from", "FOLDER1", "FSECRET1", "FSECRET2") - end + stub_ticks + .with("doppler secrets get SECRET1 FSECRET1 FSECRET2 --json ") + .returns(<<~JSON) + { + "SECRET1": { + "computed":"secret1", + "computedVisibility":"unmasked", + "note":"" + }, + "FSECRET1": { + "computed":"fsecret1", + "computedVisibility":"unmasked", + "note":"" + }, + "FSECRET2": { + "computed":"fsecret2", + "computedVisibility":"unmasked", + "note":"" + } + } + JSON + + json = JSON.parse( + shellunescape run_command("fetch", "SECRET1", "FSECRET1", "FSECRET2") + ) + + expected_json = { + "SECRET1"=>"secret1", + "FSECRET1"=>"fsecret1", + "FSECRET2"=>"fsecret2" + } + + assert_equal expected_json, json - assert_match(/Using --from option or FOLDER\/SECRET is not supported by Doppler/, error.message) + ENV.delete("DOPPLER_TOKEN") end test "fetch with folder in secret" do + stub_ticks_with("doppler --version 2> /dev/null", succeed: true) + stub_ticks.with("doppler me --json 2> /dev/null") + + stub_ticks + .with("doppler secrets get SECRET1 FSECRET1 FSECRET2 --json -p my-project -c prd") + .returns(<<~JSON) + { + "SECRET1": { + "computed":"secret1", + "computedVisibility":"unmasked", + "note":"" + }, + "FSECRET1": { + "computed":"fsecret1", + "computedVisibility":"unmasked", + "note":"" + }, + "FSECRET2": { + "computed":"fsecret2", + "computedVisibility":"unmasked", + "note":"" + } + } + JSON + + json = JSON.parse( + shellunescape run_command("fetch", "my-project/prd/SECRET1", "my-project/prd/FSECRET1", "my-project/prd/FSECRET2") + ) + + expected_json = { + "SECRET1"=>"secret1", + "FSECRET1"=>"fsecret1", + "FSECRET2"=>"fsecret2" + } + + assert_equal expected_json, json + end + + test "fetch without --from" do + stub_ticks_with("doppler --version 2> /dev/null", succeed: true) stub_ticks.with("doppler me --json 2> /dev/null") error = assert_raises RuntimeError do - run_command("fetch", "FOLDER1/FSECRET1", "SECRET2") + run_command("fetch", "FSECRET1", "FSECRET2") end - assert_match(/Using --from option or FOLDER\/SECRET is not supported by Doppler/, error.message) + assert_equal "Missing project or config from '--from=project/config' option", error.message end test "fetch with signin" do + stub_ticks_with("doppler --version 2> /dev/null", succeed: true) stub_ticks_with("doppler me --json 2> /dev/null", succeed: false) stub_ticks_with("doppler login -y", succeed: true).returns("") stub_ticks.with("doppler secrets get SECRET1 --json -p my-project -c prd").returns(single_item_json) - json = JSON.parse(shellunescape(run_command("fetch", "SECRET1"))) + json = JSON.parse(shellunescape(run_command("fetch", "--from", "my-project/prd", "SECRET1"))) expected_json = { "SECRET1"=>"secret1" @@ -75,14 +152,23 @@ class DopplerAdapterTest < SecretAdapterTestCase assert_equal expected_json, json end + test "fetch without CLI installed" do + stub_ticks_with("doppler --version 2> /dev/null", succeed: false) + + error = assert_raises RuntimeError do + JSON.parse(shellunescape(run_command("fetch", "HOST", "PORT"))) + end + + assert_equal "Doppler CLI is not installed", error.message + end + private def run_command(*command) stdouted do Kamal::Cli::Secrets.start \ [ *command, "-c", "test/fixtures/deploy_with_accessories.yml", - "--adapter", "doppler", - "--account", "my-project/prd" ] + "--adapter", "doppler" ] end end