Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Doppler secrets adapter #1183

Closed
Closed
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
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
Loading