Merge remote-tracking branch 'origin/main' into gcp_secret_manager_adapter
This commit is contained in:
@@ -18,6 +18,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
execute *accessory.ensure_env_directory
|
execute *accessory.ensure_env_directory
|
||||||
upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
|
upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
|
||||||
execute *accessory.run
|
execute *accessory.run
|
||||||
|
|
||||||
|
if accessory.running_proxy?
|
||||||
|
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
|
||||||
|
execute *accessory.deploy(target: target)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -75,6 +80,10 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
on(hosts) do
|
on(hosts) do
|
||||||
execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
|
execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
|
||||||
execute *accessory.start
|
execute *accessory.start
|
||||||
|
if accessory.running_proxy?
|
||||||
|
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
|
||||||
|
execute *accessory.deploy(target: target)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -87,6 +96,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
on(hosts) do
|
on(hosts) do
|
||||||
execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
|
execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
|
||||||
execute *accessory.stop, raise_on_non_zero_exit: false
|
execute *accessory.stop, raise_on_non_zero_exit: false
|
||||||
|
|
||||||
|
if accessory.running_proxy?
|
||||||
|
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
|
||||||
|
execute *accessory.remove if target
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -112,14 +126,15 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "exec [NAME] [CMD]", "Execute a custom command on servers (use --help to show options)"
|
desc "exec [NAME] [CMD...]", "Execute a custom command on servers within the accessory container (use --help to show options)"
|
||||||
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"
|
||||||
def exec(name, cmd)
|
def exec(name, *cmd)
|
||||||
|
cmd = Kamal::Utils.join_commands(cmd)
|
||||||
with_accessory(name) do |accessory, hosts|
|
with_accessory(name) do |accessory, hosts|
|
||||||
case
|
case
|
||||||
when options[:interactive] && options[:reuse]
|
when options[:interactive] && options[:reuse]
|
||||||
say "Launching interactive command with via SSH from existing container...", :magenta
|
say "Launching interactive command via SSH from existing container...", :magenta
|
||||||
run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
|
run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
|
||||||
|
|
||||||
when options[:interactive]
|
when options[:interactive]
|
||||||
@@ -128,16 +143,16 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
|
|
||||||
when options[:reuse]
|
when options[:reuse]
|
||||||
say "Launching command from existing container...", :magenta
|
say "Launching command from existing container...", :magenta
|
||||||
on(hosts) do
|
on(hosts) do |host|
|
||||||
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
||||||
capture_with_info(*accessory.execute_in_existing_container(cmd))
|
puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd))
|
||||||
end
|
end
|
||||||
|
|
||||||
else
|
else
|
||||||
say "Launching command from new container...", :magenta
|
say "Launching command from new container...", :magenta
|
||||||
on(hosts) do
|
on(hosts) do |host|
|
||||||
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
||||||
capture_with_info(*accessory.execute_in_new_container(cmd))
|
puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
class Kamal::Cli::Secrets < Kamal::Cli::Base
|
class Kamal::Cli::Secrets < Kamal::Cli::Base
|
||||||
desc "fetch [SECRETS...]", "Fetch secrets from a vault"
|
desc "fetch [SECRETS...]", "Fetch secrets from a vault"
|
||||||
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
|
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
|
||||||
option :account, type: :string, required: true, desc: "The account identifier or username"
|
option :account, type: :string, required: false, desc: "The account identifier or username"
|
||||||
option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
|
option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
|
||||||
option :inline, type: :boolean, required: false, hidden: true
|
option :inline, type: :boolean, required: false, hidden: true
|
||||||
def fetch(*secrets)
|
def fetch(*secrets)
|
||||||
results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
|
adapter = initialize_adapter(options[:adapter])
|
||||||
|
|
||||||
|
if adapter.requires_account? && options[:account].blank?
|
||||||
|
return puts "No value provided for required options '--account'"
|
||||||
|
end
|
||||||
|
|
||||||
|
results = adapter.fetch(secrets, **options.slice(:account, :from).symbolize_keys)
|
||||||
|
|
||||||
return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
|
return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
|
||||||
end
|
end
|
||||||
@@ -29,7 +35,7 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def adapter(adapter)
|
def initialize_adapter(adapter)
|
||||||
Kamal::Secrets::Adapters.lookup(adapter)
|
Kamal::Secrets::Adapters.lookup(adapter)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ registry:
|
|||||||
# Configure builder setup.
|
# Configure builder setup.
|
||||||
builder:
|
builder:
|
||||||
arch: amd64
|
arch: amd64
|
||||||
|
# Pass in additional build args needed for your Dockerfile.
|
||||||
|
# args:
|
||||||
|
# RUBY_VERSION: <%= File.read('.ruby-version').strip %>
|
||||||
|
|
||||||
# Inject ENV variables into containers (secrets come from .kamal/secrets).
|
# Inject ENV variables into containers (secrets come from .kamal/secrets).
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
class Kamal::Commands::Accessory < Kamal::Commands::Base
|
class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||||
|
include Proxy
|
||||||
|
|
||||||
attr_reader :accessory_config
|
attr_reader :accessory_config
|
||||||
delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
|
delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
|
||||||
:network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
|
:network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
|
||||||
:secrets_io, :secrets_path, :env_directory,
|
:secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?,
|
||||||
to: :accessory_config
|
to: :accessory_config
|
||||||
|
delegate :proxy_container_name, to: :config
|
||||||
|
|
||||||
|
|
||||||
def initialize(config, name:)
|
def initialize(config, name:)
|
||||||
super(config)
|
super(config)
|
||||||
|
|||||||
16
lib/kamal/commands/accessory/proxy.rb
Normal file
16
lib/kamal/commands/accessory/proxy.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module Kamal::Commands::Accessory::Proxy
|
||||||
|
delegate :proxy_container_name, to: :config
|
||||||
|
|
||||||
|
def deploy(target:)
|
||||||
|
proxy_exec :deploy, service_name, *proxy.deploy_command_args(target: target)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove
|
||||||
|
proxy_exec :remove, service_name
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def proxy_exec(*command)
|
||||||
|
docker :exec, proxy_container_name, "kamal-proxy", *command
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
delegate :argumentize, to: Kamal::Utils
|
delegate :argumentize, to: Kamal::Utils
|
||||||
delegate \
|
delegate \
|
||||||
:args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
|
:args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
|
||||||
:cache_from, :cache_to, :ssh, :provenance, :driver, :docker_driver?,
|
:cache_from, :cache_to, :ssh, :provenance, :sbom, :driver, :docker_driver?,
|
||||||
to: :builder_config
|
to: :builder_config
|
||||||
|
|
||||||
def clean
|
def clean
|
||||||
@@ -37,7 +37,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_options
|
def build_options
|
||||||
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance ]
|
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance, *builder_sbom ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_context
|
def build_context
|
||||||
@@ -101,6 +101,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
argumentize "--provenance", provenance unless provenance.nil?
|
argumentize "--provenance", provenance unless provenance.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def builder_sbom
|
||||||
|
argumentize "--sbom", sbom unless sbom.nil?
|
||||||
|
end
|
||||||
|
|
||||||
def builder_config
|
def builder_config
|
||||||
config.builder
|
config.builder
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class Kamal::Configuration::Accessory
|
|||||||
|
|
||||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
attr_reader :name, :accessory_config, :env
|
attr_reader :name, :accessory_config, :env, :proxy
|
||||||
|
|
||||||
def initialize(name, config:)
|
def initialize(name, config:)
|
||||||
@name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]
|
@name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]
|
||||||
@@ -20,6 +20,8 @@ class Kamal::Configuration::Accessory
|
|||||||
config: accessory_config.fetch("env", {}),
|
config: accessory_config.fetch("env", {}),
|
||||||
secrets: config.secrets,
|
secrets: config.secrets,
|
||||||
context: "accessories/#{name}/env"
|
context: "accessories/#{name}/env"
|
||||||
|
|
||||||
|
initialize_proxy if running_proxy?
|
||||||
end
|
end
|
||||||
|
|
||||||
def service_name
|
def service_name
|
||||||
@@ -106,6 +108,17 @@ class Kamal::Configuration::Accessory
|
|||||||
accessory_config["cmd"]
|
accessory_config["cmd"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def running_proxy?
|
||||||
|
@accessory_config["proxy"].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_proxy
|
||||||
|
@proxy = Kamal::Configuration::Proxy.new \
|
||||||
|
config: config,
|
||||||
|
proxy_config: accessory_config["proxy"],
|
||||||
|
context: "accessories/#{name}/proxy"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
attr_accessor :config
|
attr_accessor :config
|
||||||
|
|
||||||
@@ -176,7 +189,9 @@ class Kamal::Configuration::Accessory
|
|||||||
|
|
||||||
def hosts_from_roles
|
def hosts_from_roles
|
||||||
if accessory_config.key?("roles")
|
if accessory_config.key?("roles")
|
||||||
accessory_config["roles"].flat_map { |role| config.role(role).hosts }
|
accessory_config["roles"].flat_map do |role|
|
||||||
|
config.role(role)&.hosts || raise(Kamal::ConfigurationError, "Unknown role in accessories config: '#{role}'")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,10 @@ class Kamal::Configuration::Builder
|
|||||||
builder_config["provenance"]
|
builder_config["provenance"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sbom
|
||||||
|
builder_config["sbom"]
|
||||||
|
end
|
||||||
|
|
||||||
def git_clone?
|
def git_clone?
|
||||||
Kamal::Git.used? && builder_config["context"].nil?
|
Kamal::Git.used? && builder_config["context"].nil?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -98,3 +98,7 @@ accessories:
|
|||||||
# Defaults to kamal:
|
# Defaults to kamal:
|
||||||
network: custom
|
network: custom
|
||||||
|
|
||||||
|
# Proxy
|
||||||
|
#
|
||||||
|
proxy:
|
||||||
|
...
|
||||||
@@ -108,3 +108,9 @@ builder:
|
|||||||
# It is used to configure provenance attestations for the build result.
|
# It is used to configure provenance attestations for the build result.
|
||||||
# The value can also be a boolean to enable or disable provenance attestations.
|
# The value can also be a boolean to enable or disable provenance attestations.
|
||||||
provenance: mode=max
|
provenance: mode=max
|
||||||
|
|
||||||
|
# SBOM (Software Bill of Materials)
|
||||||
|
#
|
||||||
|
# It is used to configure SBOM generation for the build result.
|
||||||
|
# The value can also be a boolean to enable or disable SBOM generation.
|
||||||
|
sbom: true
|
||||||
|
|||||||
34
lib/kamal/secrets/adapters/aws_secrets_manager.rb
Normal file
34
lib/kamal/secrets/adapters/aws_secrets_manager.rb
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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.shellescape}`.tap do
|
||||||
|
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
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
class Kamal::Secrets::Adapters::Base
|
class Kamal::Secrets::Adapters::Base
|
||||||
delegate :optionize, to: Kamal::Utils
|
delegate :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
def fetch(secrets, account:, from: nil)
|
def fetch(secrets, account: nil, from: nil)
|
||||||
|
raise RuntimeError, "Missing required option '--account'" if requires_account? && account.blank?
|
||||||
|
|
||||||
check_dependencies!
|
check_dependencies!
|
||||||
|
|
||||||
session = login(account)
|
session = login(account)
|
||||||
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
|
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
|
||||||
fetch_secrets(full_secrets, account: account, session: session)
|
fetch_secrets(full_secrets, account: account, session: session)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def requires_account?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def login(...)
|
def login(...)
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
53
lib/kamal/secrets/adapters/doppler.rb
Normal file
53
lib/kamal/secrets/adapters/doppler.rb
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base
|
||||||
|
def requires_account?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def login(*)
|
||||||
|
unless loggedin?
|
||||||
|
`doppler login -y`
|
||||||
|
raise RuntimeError, "Failed to login to Doppler" unless $?.success?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def loggedin?
|
||||||
|
`doppler me --json 2> /dev/null`
|
||||||
|
$?.success?
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
secret_names = secrets.collect { |s| s.split("/").last }
|
||||||
|
|
||||||
|
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
|
||||||
5
lib/kamal/secrets/adapters/test_optional_account.rb
Normal file
5
lib/kamal/secrets/adapters/test_optional_account.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class Kamal::Secrets::Adapters::TestOptionalAccount < Kamal::Secrets::Adapters::Test
|
||||||
|
def requires_account?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,6 +7,18 @@ class CliSecretsTest < CliTestCase
|
|||||||
run_command("fetch", "foo", "bar", "baz", "--account", "myaccount", "--adapter", "test")
|
run_command("fetch", "foo", "bar", "baz", "--account", "myaccount", "--adapter", "test")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "fetch missing --acount" do
|
||||||
|
assert_equal \
|
||||||
|
"No value provided for required options '--account'",
|
||||||
|
run_command("fetch", "foo", "bar", "baz", "--adapter", "test")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch without required --account" do
|
||||||
|
assert_equal \
|
||||||
|
"\\{\\\"foo\\\":\\\"oof\\\",\\\"bar\\\":\\\"rab\\\",\\\"baz\\\":\\\"zab\\\"\\}",
|
||||||
|
run_command("fetch", "foo", "bar", "baz", "--adapter", "test_optional_account")
|
||||||
|
end
|
||||||
|
|
||||||
test "extract" do
|
test "extract" do
|
||||||
assert_equal "oof", run_command("extract", "foo", "{\"foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
|
assert_equal "oof", run_command("extract", "foo", "{\"foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
"busybox" => {
|
"busybox" => {
|
||||||
"service" => "custom-busybox",
|
"service" => "custom-busybox",
|
||||||
"image" => "busybox:latest",
|
"image" => "busybox:latest",
|
||||||
"host" => "1.1.1.7"
|
"host" => "1.1.1.7",
|
||||||
|
"proxy" => {
|
||||||
|
"host" => "busybox.example.com"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,6 +169,18 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
new_command(:mysql).remove_image.join(" ")
|
new_command(:mysql).remove_image.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "deploy" do
|
||||||
|
assert_equal \
|
||||||
|
"docker exec kamal-proxy kamal-proxy deploy custom-busybox --target=\"172.1.0.2:80\" --host=\"busybox.example.com\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
|
||||||
|
new_command(:busybox).deploy(target: "172.1.0.2").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove" do
|
||||||
|
assert_equal \
|
||||||
|
"docker exec kamal-proxy kamal-proxy remove custom-busybox",
|
||||||
|
new_command(:busybox).remove.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_command(accessory)
|
def new_command(accessory)
|
||||||
Kamal::Commands::Accessory.new(Kamal::Configuration.new(@config), name: accessory)
|
Kamal::Commands::Accessory.new(Kamal::Configuration.new(@config), name: accessory)
|
||||||
|
|||||||
@@ -158,6 +158,20 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
builder.push.join(" ")
|
builder.push.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "push with sbom" do
|
||||||
|
builder = new_builder_command(builder: { "sbom" => true })
|
||||||
|
assert_equal \
|
||||||
|
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --sbom true .",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "push with sbom false" do
|
||||||
|
builder = new_builder_command(builder: { "sbom" => false })
|
||||||
|
assert_equal \
|
||||||
|
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --sbom false .",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "mirror count" do
|
test "mirror count" do
|
||||||
command = new_builder_command
|
command = new_builder_command
|
||||||
assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ")
|
assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ")
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
|||||||
"options" => {
|
"options" => {
|
||||||
"cpus" => "4",
|
"cpus" => "4",
|
||||||
"memory" => "2GB"
|
"memory" => "2GB"
|
||||||
|
},
|
||||||
|
"proxy" => {
|
||||||
|
"host" => "monitoring.example.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,4 +164,9 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
|||||||
@deploy[:accessories]["mysql"]["network"] = "database"
|
@deploy[:accessories]["mysql"]["network"] = "database"
|
||||||
assert_equal [ "--network", "database" ], @config.accessory(:mysql).network_args
|
assert_equal [ "--network", "database" ], @config.accessory(:mysql).network_args
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "proxy" do
|
||||||
|
assert @config.accessory(:monitoring).running_proxy?
|
||||||
|
assert_equal [ "monitoring.example.com" ], @config.accessory(:monitoring).proxy.hosts
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -144,6 +144,16 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
|||||||
assert_equal "mode=max", config.builder.provenance
|
assert_equal "mode=max", config.builder.provenance
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "sbom" do
|
||||||
|
assert_nil config.builder.sbom
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setting sbom" do
|
||||||
|
@deploy[:builder]["sbom"] = true
|
||||||
|
|
||||||
|
assert_equal true, config.builder.sbom
|
||||||
|
end
|
||||||
|
|
||||||
test "local disabled but no remote set" do
|
test "local disabled but no remote set" do
|
||||||
@deploy[:builder]["local"] = false
|
@deploy[:builder]["local"] = false
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ COPY *.sh .
|
|||||||
COPY app/ app/
|
COPY app/ app/
|
||||||
COPY app_with_roles/ app_with_roles/
|
COPY app_with_roles/ app_with_roles/
|
||||||
COPY app_with_traefik/ app_with_traefik/
|
COPY app_with_traefik/ app_with_traefik/
|
||||||
|
COPY app_with_proxied_accessory/ app_with_proxied_accessory/
|
||||||
|
|
||||||
RUN rm -rf /root/.ssh
|
RUN rm -rf /root/.ssh
|
||||||
RUN ln -s /shared/ssh /root/.ssh
|
RUN ln -s /shared/ssh /root/.ssh
|
||||||
@@ -30,6 +31,7 @@ RUN git config --global user.name "Deployer"
|
|||||||
RUN cd app && git init && git add . && git commit -am "Initial version"
|
RUN cd app && git init && git add . && git commit -am "Initial version"
|
||||||
RUN cd app_with_roles && git init && git add . && git commit -am "Initial version"
|
RUN cd app_with_roles && git init && git add . && git commit -am "Initial version"
|
||||||
RUN cd app_with_traefik && git init && git add . && git commit -am "Initial version"
|
RUN cd app_with_traefik && git init && git add . && git commit -am "Initial version"
|
||||||
|
RUN cd app_with_proxied_accessory && git init && git add . && git commit -am "Initial version"
|
||||||
|
|
||||||
HEALTHCHECK --interval=1s CMD pgrep sleep
|
HEALTHCHECK --interval=1s CMD pgrep sleep
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
FROM registry:4443/nginx:1-alpine-slim
|
||||||
|
|
||||||
|
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
ARG COMMIT_SHA
|
||||||
|
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
||||||
|
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
|
||||||
|
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
|
||||||
|
RUN echo "Up!" > /usr/share/nginx/html/up
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
service: app_with_proxied_accessory
|
||||||
|
image: app_with_proxied_accessory
|
||||||
|
servers:
|
||||||
|
- vm1
|
||||||
|
env:
|
||||||
|
clear:
|
||||||
|
CLEAR_TOKEN: 4321
|
||||||
|
CLEAR_TAG: ""
|
||||||
|
HOST_TOKEN: "${HOST_TOKEN}"
|
||||||
|
asset_path: /usr/share/nginx/html/versions
|
||||||
|
proxy:
|
||||||
|
host: 127.0.0.1
|
||||||
|
registry:
|
||||||
|
server: registry:4443
|
||||||
|
username: root
|
||||||
|
password: root
|
||||||
|
builder:
|
||||||
|
driver: docker
|
||||||
|
arch: <%= Kamal::Utils.docker_arch %>
|
||||||
|
args:
|
||||||
|
COMMIT_SHA: <%= `git rev-parse HEAD` %>
|
||||||
|
accessories:
|
||||||
|
busybox:
|
||||||
|
service: custom-busybox
|
||||||
|
image: registry:4443/busybox:1.36.0
|
||||||
|
cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done'
|
||||||
|
roles:
|
||||||
|
- web
|
||||||
|
netcat:
|
||||||
|
service: netcat
|
||||||
|
image: registry:4443/busybox:1.36.0
|
||||||
|
cmd: >
|
||||||
|
sh -c 'echo "Starting netcat..."; while true; do echo -e "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHello Ruby" | nc -l -p 80; done'
|
||||||
|
roles:
|
||||||
|
- web
|
||||||
|
port: 12345:80
|
||||||
|
proxy:
|
||||||
|
host: netcat
|
||||||
|
ssl: false
|
||||||
|
healthcheck:
|
||||||
|
interval: 1
|
||||||
|
timeout: 1
|
||||||
|
path: "/"
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
}
|
||||||
|
|
||||||
|
# redirect server error pages to the static page /50x.html
|
||||||
|
#
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
test/integration/proxied_accessory_test.rb
Normal file
63
test/integration/proxied_accessory_test.rb
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
require_relative "integration_test"
|
||||||
|
|
||||||
|
class ProxiedAccessoryTest < IntegrationTest
|
||||||
|
test "boot, stop, start, restart, logs, remove" do
|
||||||
|
@app = "app_with_proxied_accessory"
|
||||||
|
|
||||||
|
kamal :deploy
|
||||||
|
|
||||||
|
kamal :accessory, :boot, :netcat
|
||||||
|
assert_accessory_running :netcat
|
||||||
|
assert_netcat_is_up
|
||||||
|
|
||||||
|
kamal :accessory, :stop, :netcat
|
||||||
|
assert_accessory_not_running :netcat
|
||||||
|
assert_netcat_not_found
|
||||||
|
|
||||||
|
kamal :accessory, :start, :netcat
|
||||||
|
assert_accessory_running :netcat
|
||||||
|
assert_netcat_is_up
|
||||||
|
|
||||||
|
kamal :accessory, :restart, :netcat
|
||||||
|
assert_accessory_running :netcat
|
||||||
|
assert_netcat_is_up
|
||||||
|
|
||||||
|
kamal :accessory, :remove, :netcat, "-y"
|
||||||
|
assert_accessory_not_running :netcat
|
||||||
|
assert_netcat_not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def assert_accessory_running(name)
|
||||||
|
assert_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_accessory_not_running(name)
|
||||||
|
assert_no_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def accessory_details(name)
|
||||||
|
kamal :accessory, :details, name, capture: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_netcat_is_up
|
||||||
|
response = netcat_response
|
||||||
|
debug_response_code(response, "200")
|
||||||
|
assert_equal "200", response.code
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_netcat_not_found
|
||||||
|
response = netcat_response
|
||||||
|
debug_response_code(response, "404")
|
||||||
|
assert_equal "404", response.code
|
||||||
|
end
|
||||||
|
|
||||||
|
def netcat_response
|
||||||
|
uri = URI.parse("http://127.0.0.1:12345/up")
|
||||||
|
http = Net::HTTP.new(uri.host, uri.port)
|
||||||
|
request = Net::HTTP::Get.new(uri)
|
||||||
|
request["Host"] = "netcat"
|
||||||
|
|
||||||
|
http.request(request)
|
||||||
|
end
|
||||||
|
end
|
||||||
98
test/secrets/aws_secrets_manager_adapter_test.rb
Normal file
98
test/secrets/aws_secrets_manager_adapter_test.rb
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
"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 --version 2> /dev/null")
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
Kamal::Cli::Secrets.start \
|
||||||
|
[ *command,
|
||||||
|
"-c", "test/fixtures/deploy_with_accessories.yml",
|
||||||
|
"--adapter", "aws_secrets_manager",
|
||||||
|
"--account", "default" ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
186
test/secrets/doppler_adapter_test.rb
Normal file
186
test/secrets/doppler_adapter_test.rb
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class DopplerAdapterTest < SecretAdapterTestCase
|
||||||
|
setup do
|
||||||
|
`true` # Ensure $? is 0
|
||||||
|
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
|
||||||
|
.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", "--from", "my-project/prd", "SECRET1", "FSECRET1", "FSECRET2")
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
"SECRET1"=>"secret1",
|
||||||
|
"FSECRET1"=>"fsecret1",
|
||||||
|
"FSECRET2"=>"fsecret2"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal expected_json, json
|
||||||
|
end
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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", "FSECRET1", "FSECRET2")
|
||||||
|
end
|
||||||
|
|
||||||
|
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", "--from", "my-project/prd", "SECRET1")))
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
"SECRET1"=>"secret1"
|
||||||
|
}
|
||||||
|
|
||||||
|
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" ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def single_item_json
|
||||||
|
<<~JSON
|
||||||
|
{
|
||||||
|
"SECRET1": {
|
||||||
|
"computed":"secret1",
|
||||||
|
"computedVisibility":"unmasked",
|
||||||
|
"note":""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,6 +2,7 @@ require "bundler/setup"
|
|||||||
require "active_support/test_case"
|
require "active_support/test_case"
|
||||||
require "active_support/testing/autorun"
|
require "active_support/testing/autorun"
|
||||||
require "active_support/testing/stream"
|
require "active_support/testing/stream"
|
||||||
|
require "rails/test_unit/line_filtering"
|
||||||
require "debug"
|
require "debug"
|
||||||
require "mocha/minitest" # using #stubs that can alter returns
|
require "mocha/minitest" # using #stubs that can alter returns
|
||||||
require "minitest/autorun" # using #stub that take args
|
require "minitest/autorun" # using #stub that take args
|
||||||
@@ -32,6 +33,7 @@ end
|
|||||||
|
|
||||||
class ActiveSupport::TestCase
|
class ActiveSupport::TestCase
|
||||||
include ActiveSupport::Testing::Stream
|
include ActiveSupport::Testing::Stream
|
||||||
|
extend Rails::LineFiltering
|
||||||
|
|
||||||
private
|
private
|
||||||
def stdouted
|
def stdouted
|
||||||
|
|||||||
Reference in New Issue
Block a user