Skip to content

Commit

Permalink
Add Doppler secrets adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
watsonian committed Nov 1, 2024
1 parent cd4e183 commit d0ac138
Show file tree
Hide file tree
Showing 2 changed files with 224 additions and 0 deletions.
59 changes: 59 additions & 0 deletions lib/kamal/secrets/adapters/doppler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base
private
def login(account)
unless loggedin?
raise RuntimeError, "Doppler CLI not logged in and no DOPPLER_TOKEN found in environment"
end
end

def loggedin?
`doppler me 2> /dev/null`
$?.success?
end

def fetch_secrets(secrets, account:, session:)
if secrets.empty?
raise RuntimeError, "No secrets were fetched. Please specify which secrets to fetch or use 'all' to fetch all secrets."
end

project_and_config_flags = ""
unless service_token_set?
project, config, _ = secrets.first.split("/")

unless project && config
raise RuntimeError, "You must pass the Doppler project and config in using --from PROJECT/CONFIG"
end

project_and_config_flags = " -p #{project.shellescape} -c #{config.shellescape}"
end

secret_names = secrets.collect{|s| s.split("/").last}

if secret_names.first.downcase == "all"
raw_secrets_json = `doppler secrets download --no-file --json#{project_and_config_flags}`
else
raw_secrets_json = `doppler secrets get --json#{project_and_config_flags} #{secret_names.map(&:shellescape).join(" ")}`
end
raise RuntimeError, "Could not read #{secrets} from Doppler" unless $?.success?

secrets_json = JSON.parse(raw_secrets_json)
{}.tap do |results|
secrets_json.each do |k, v|
results[k] = v["computed"] || v
end
end
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
165 changes: 165 additions & 0 deletions test/secrets/doppler_adapter_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
require "test_helper"

class DopplerAdapterTest < SecretAdapterTestCase
setup do
ENV.delete("DOPPLER_TOKEN")
`true` # Ensure $? is 0
end

test "fetch" do
ENV["DOPPLER_TOKEN"] = "dp.st.xxxxxxxxxxxxxxxxxxxxxx"
stub_ticks.with("doppler --version 2> /dev/null")
stub_ticks.with("doppler me 2> /dev/null")

stub_ticks
.with("doppler secrets get --json HOST PORT")
.returns(secrets_get_json)

json = JSON.parse(shellunescape(run_command("fetch", "HOST", "PORT")))

expected_json = {
"HOST"=>"0.0.0.0",
"PORT"=>"8080"
}

assert_equal expected_json, json
ENV.delete("DOPPLER_TOKEN")
end

test "fetch with from" do
stub_ticks.with("doppler --version 2> /dev/null")
stub_ticks.with("doppler me 2> /dev/null")

stub_ticks
.with("doppler secrets get --json -p example -c dev HOST PORT")
.returns(secrets_get_json)

json = JSON.parse(shellunescape(run_command("fetch", "--from", "example/dev", "HOST", "PORT")))

expected_json = {
"HOST"=>"0.0.0.0",
"PORT"=>"8080"
}

assert_equal expected_json, json
end

test "fetch all" do
ENV["DOPPLER_TOKEN"] = "dp.st.xxxxxxxxxxxxxxxxxxxxxx"
stub_ticks.with("doppler --version 2> /dev/null")
stub_ticks.with("doppler me 2> /dev/null")

stub_ticks
.with("doppler secrets download --no-file --json")
.returns(secrets_download_json)

json = JSON.parse(shellunescape(run_command("fetch", "all")))

expected_json = {
"DOPPLER_PROJECT"=>"example",
"DOPPLER_ENVIRONMENT"=>"dev",
"DOPPLER_CONFIG"=>"dev",
"HOST"=>"0.0.0.0",
"PORT"=>"8080"
}

assert_equal expected_json, json
ENV.delete("DOPPLER_TOKEN")
end

test "fetch all with from" do
stub_ticks.with("doppler --version 2> /dev/null")
stub_ticks.with("doppler me 2> /dev/null")

stub_ticks
.with("doppler secrets download --no-file --json -p example -c dev")
.returns(secrets_download_json)

json = JSON.parse(shellunescape(run_command("fetch", "--from", "example/dev", "all")))

expected_json = {
"DOPPLER_PROJECT"=>"example",
"DOPPLER_ENVIRONMENT"=>"dev",
"DOPPLER_CONFIG"=>"dev",
"HOST"=>"0.0.0.0",
"PORT"=>"8080"
}

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

test "fetch without being logged in and without DOPPLER_TOKEN" do
stub_ticks_with("doppler --version 2> /dev/null")
stub_ticks_with("doppler me 2> /dev/null", succeed: false)

error = assert_raises RuntimeError do
JSON.parse(shellunescape(run_command("fetch", "HOST", "PORT")))
end
assert_equal "Doppler CLI not logged in and no DOPPLER_TOKEN found in environment", error.message
end

test "fetch with from and no secrets or 'all' specified" do
stub_ticks.with("doppler --version 2> /dev/null")
stub_ticks.with("doppler me 2> /dev/null")

error = assert_raises RuntimeError do
JSON.parse(shellunescape(run_command("fetch", "--from", "example/dev")))
end
assert_equal "No secrets were fetched. Please specify which secrets to fetch or use 'all' to fetch all secrets.", 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", "-" ]
end
end

def secrets_get_json
<<~JSON
{
"HOST": {
"computed": "0.0.0.0",
"computedValueType": {
"type": "string"
},
"computedVisibility": "masked",
"note": ""
},
"PORT": {
"computed": "8080",
"computedValueType": {
"type": "string"
},
"computedVisibility": "masked",
"note": ""
}
}
JSON
end

def secrets_download_json
<<~JSON
{
"DOPPLER_CONFIG": "dev",
"DOPPLER_ENVIRONMENT": "dev",
"DOPPLER_PROJECT": "example",
"HOST": "0.0.0.0",
"PORT": "8080"
}
JSON
end
end

0 comments on commit d0ac138

Please sign in to comment.