Merge branch 'main' into gcp_secret_manager_adapter
This commit is contained in:
@@ -162,7 +162,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
||||
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
||||
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
|
||||
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
||||
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
|
||||
option :grep_options, desc: "Additional options supplied to grep"
|
||||
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
||||
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
|
||||
def logs(name)
|
||||
|
||||
@@ -192,7 +192,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
||||
option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
||||
option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
|
||||
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
||||
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
|
||||
option :grep_options, desc: "Additional options supplied to grep"
|
||||
option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
|
||||
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
|
||||
option :container_id, desc: "Docker container ID to fetch logs"
|
||||
|
||||
@@ -45,7 +45,7 @@ class Kamal::Cli::App::Boot
|
||||
|
||||
def start_new_version
|
||||
audit "Booted app version #{version}"
|
||||
hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"
|
||||
hostname = "#{host.to_s[0...51].chomp(".")}-#{SecureRandom.hex(6)}"
|
||||
|
||||
execute *app.ensure_env_directory
|
||||
upload! role.secrets_io(host), role.secrets_path, mode: "0600"
|
||||
|
||||
@@ -5,7 +5,7 @@ module Kamal::Cli
|
||||
class Base < Thor
|
||||
include SSHKit::DSL
|
||||
|
||||
def self.exit_on_failure?() false end
|
||||
def self.exit_on_failure?() true end
|
||||
def self.dynamic_command_class() Kamal::Cli::Alias::Command end
|
||||
|
||||
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
|
||||
@@ -30,7 +30,8 @@ module Kamal::Cli
|
||||
else
|
||||
super
|
||||
end
|
||||
initialize_commander unless KAMAL.configured?
|
||||
|
||||
initialize_commander unless config[:invoked_via_subcommand]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -23,6 +23,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
||||
|
||||
desc "boot_config <set|get|reset>", "Manage kamal-proxy boot configuration"
|
||||
option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
|
||||
option :publish_host_ip, type: :string, repeatable: true, default: nil, desc: "Host IP address to bind HTTP/HTTPS traffic to. Defaults to all interfaces"
|
||||
option :http_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTP_PORT, desc: "HTTP port to publish on the host"
|
||||
option :https_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTPS_PORT, desc: "HTTPS port to publish on the host"
|
||||
option :log_max_size, type: :string, default: Kamal::Configuration::PROXY_LOG_MAX_SIZE, desc: "Max size of proxy logs"
|
||||
@@ -31,7 +32,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
||||
case subcommand
|
||||
when "set"
|
||||
boot_options = [
|
||||
*(KAMAL.config.proxy_publish_args(options[:http_port], options[:https_port]) if options[:publish]),
|
||||
*(KAMAL.config.proxy_publish_args(options[:http_port], options[:https_port], options[:publish_host_ip]) if options[:publish]),
|
||||
*(KAMAL.config.proxy_logging_args(options[:log_max_size])),
|
||||
*options[:docker_options].map { |option| "--#{option}" }
|
||||
]
|
||||
|
||||
@@ -4,10 +4,10 @@ module Kamal::Commands::App::Assets
|
||||
|
||||
combine \
|
||||
make_directory(role.asset_extracted_directory),
|
||||
[ *docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true" ],
|
||||
docker(:run, "--name", asset_container, "--detach", "--rm", "--entrypoint", "sleep", config.absolute_image, "1000000"),
|
||||
docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_directory),
|
||||
docker(:stop, "-t 1", asset_container),
|
||||
[ *docker(:container, :rm, asset_container, "2> /dev/null"), "|| true" ],
|
||||
docker(:container, :create, "--name", asset_container, config.absolute_image),
|
||||
docker(:container, :cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_directory),
|
||||
docker(:container, :rm, asset_container),
|
||||
by: "&&"
|
||||
end
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class Kamal::Configuration
|
||||
if file.exist?
|
||||
# Newer Psych doesn't load aliases by default
|
||||
load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
|
||||
YAML.send(load_method, ERB.new(IO.read(file)).result).symbolize_keys
|
||||
YAML.send(load_method, ERB.new(File.read(file)).result).symbolize_keys
|
||||
else
|
||||
raise "Configuration file not found in #{file}"
|
||||
end
|
||||
@@ -249,8 +249,16 @@ class Kamal::Configuration
|
||||
env_tags.detect { |t| t.name == name.to_s }
|
||||
end
|
||||
|
||||
def proxy_publish_args(http_port, https_port)
|
||||
argumentize "--publish", [ "#{http_port}:#{PROXY_HTTP_PORT}", "#{https_port}:#{PROXY_HTTPS_PORT}" ]
|
||||
def proxy_publish_args(http_port, https_port, bind_ips = nil)
|
||||
ensure_valid_bind_ips(bind_ips)
|
||||
|
||||
(bind_ips || [ nil ]).map do |bind_ip|
|
||||
bind_ip = format_bind_ip(bind_ip)
|
||||
publish_http = [ bind_ip, http_port, PROXY_HTTP_PORT ].compact.join(":")
|
||||
publish_https = [ bind_ip, https_port, PROXY_HTTPS_PORT ].compact.join(":")
|
||||
|
||||
argumentize "--publish", [ publish_http, publish_https ]
|
||||
end.join(" ")
|
||||
end
|
||||
|
||||
def proxy_logging_args(max_size)
|
||||
@@ -344,6 +352,15 @@ class Kamal::Configuration
|
||||
true
|
||||
end
|
||||
|
||||
def ensure_valid_bind_ips(bind_ips)
|
||||
bind_ips.present? && bind_ips.each do |ip|
|
||||
next if ip =~ Resolv::IPv4::Regex || ip =~ Resolv::IPv6::Regex
|
||||
raise ArgumentError, "Invalid publish IP address: #{ip}"
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def ensure_retain_containers_valid
|
||||
raise Kamal::ConfigurationError, "Must retain at least 1 container" if retain_containers < 1
|
||||
|
||||
@@ -375,6 +392,15 @@ class Kamal::Configuration
|
||||
true
|
||||
end
|
||||
|
||||
def format_bind_ip(ip)
|
||||
# Ensure IPv6 address inside square brackets - e.g. [::1]
|
||||
if ip =~ Resolv::IPv6::Regex && ip !~ /\[.*\]/
|
||||
"[#{ip}]"
|
||||
else
|
||||
ip
|
||||
end
|
||||
end
|
||||
|
||||
def role_names
|
||||
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
|
||||
end
|
||||
|
||||
@@ -142,7 +142,7 @@ class Kamal::Configuration::Accessory
|
||||
end
|
||||
|
||||
def read_dynamic_file(local_file)
|
||||
StringIO.new(ERB.new(IO.read(local_file)).result)
|
||||
StringIO.new(ERB.new(File.read(local_file)).result)
|
||||
end
|
||||
|
||||
def expand_remote_file(remote_file)
|
||||
|
||||
@@ -43,8 +43,8 @@ accessories:
|
||||
|
||||
# Port mappings
|
||||
#
|
||||
# See https://docs.docker.com/network/, and especially note the warning about the security
|
||||
# implications of exposing ports publicly.
|
||||
# See [https://docs.docker.com/network/](https://docs.docker.com/network/), and
|
||||
# especially note the warning about the security implications of exposing ports publicly.
|
||||
port: "127.0.0.1:3306:3306"
|
||||
|
||||
# Labels
|
||||
@@ -101,4 +101,4 @@ accessories:
|
||||
# Proxy
|
||||
#
|
||||
proxy:
|
||||
...
|
||||
...
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
# For example, for a Rails app, you might open a console with:
|
||||
#
|
||||
# ```shell
|
||||
# kamal app exec -i -r console "rails console"
|
||||
# kamal app exec -i --reuse "bin/rails console"
|
||||
# ```
|
||||
#
|
||||
# By defining an alias, like this:
|
||||
aliases:
|
||||
console: app exec -r console -i "rails console"
|
||||
console: app exec -i --reuse "bin/rails console"
|
||||
# You can now open the console with:
|
||||
#
|
||||
# ```shell
|
||||
|
||||
@@ -46,9 +46,22 @@ proxy:
|
||||
# The host value must point to the server we are deploying to, and port 443 must be
|
||||
# open for the Let's Encrypt challenge to succeed.
|
||||
#
|
||||
# If you set `ssl` to `true`, `kamal-proxy` will stop forwarding headers to your app,
|
||||
# unless you explicitly set `forward_headers: true`
|
||||
#
|
||||
# Defaults to `false`:
|
||||
ssl: true
|
||||
|
||||
# Forward headers
|
||||
#
|
||||
# Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
|
||||
#
|
||||
# If you are behind a trusted proxy, you can set this to `true` to forward the headers.
|
||||
#
|
||||
# By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
|
||||
# will forward them if it is set to `false`.
|
||||
forward_headers: true
|
||||
|
||||
# Response timeout
|
||||
#
|
||||
# How long to wait for requests to complete before timing out, defaults to 30 seconds:
|
||||
@@ -93,13 +106,3 @@ proxy:
|
||||
response_headers:
|
||||
- X-Request-ID
|
||||
- X-Request-Start
|
||||
|
||||
# Forward headers
|
||||
#
|
||||
# Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
|
||||
#
|
||||
# If you are behind a trusted proxy, you can set this to `true` to forward the headers.
|
||||
#
|
||||
# By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
|
||||
# will forward them if it is set to `false`.
|
||||
forward_headers: true
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
#
|
||||
# The default registry is Docker Hub, but you can change it using `registry/server`.
|
||||
#
|
||||
# By default, Docker Hub creates public repositories. To avoid making your images public,
|
||||
# set up a private repository before deploying, or change the default repository privacy
|
||||
# settings to private in your [Docker Hub settings](https://hub.docker.com/repository-settings/default-privacy).
|
||||
#
|
||||
# A reference to a secret (in this case, `DOCKER_REGISTRY_TOKEN`) will look up the secret
|
||||
# in the local environment:
|
||||
registry:
|
||||
|
||||
@@ -32,7 +32,7 @@ class Kamal::Secrets
|
||||
private
|
||||
def secrets
|
||||
@secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
|
||||
secrets.merge!(::Dotenv.parse(secrets_file))
|
||||
secrets.merge!(::Dotenv.parse(secrets_file, overwrite: true))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ module Kamal::Secrets::Adapters
|
||||
name = "one_password" if name.downcase == "1password"
|
||||
name = "last_pass" if name.downcase == "lastpass"
|
||||
name = "gcp_secret_manager" if name.downcase == "gcp"
|
||||
name = "bitwarden_secrets_manager" if name.downcase == "bitwarden-sm"
|
||||
adapter_class(name)
|
||||
end
|
||||
|
||||
|
||||
@@ -6,20 +6,28 @@ class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Ba
|
||||
|
||||
def fetch_secrets(secrets, account:, session:)
|
||||
{}.tap do |results|
|
||||
JSON.parse(get_from_secrets_manager(secrets, account: account))["SecretValues"].each do |secret|
|
||||
get_from_secrets_manager(secrets, account: account).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
|
||||
rescue JSON::ParserError
|
||||
results["#{secret_name}"] = secret["SecretString"]
|
||||
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?
|
||||
`aws secretsmanager batch-get-secret-value --secret-id-list #{secrets.map(&:shellescape).join(" ")} --profile #{account.shellescape}`.tap do |secrets|
|
||||
raise RuntimeError, "Could not read #{secrets} from AWS Secrets Manager" unless $?.success?
|
||||
|
||||
secrets = JSON.parse(secrets)
|
||||
|
||||
return secrets["SecretValues"] unless secrets["Errors"].present?
|
||||
|
||||
raise RuntimeError, secrets["Errors"].map { |error| "#{error['SecretId']}: #{error['Message']}" }.join(" ")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
67
lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb
Normal file
67
lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapters::Base
|
||||
def requires_account?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
LIST_ALL_SELECTOR = "all"
|
||||
LIST_ALL_FROM_PROJECT_SUFFIX = "/all"
|
||||
LIST_COMMAND = "secret list -o env"
|
||||
GET_COMMAND = "secret get -o env"
|
||||
|
||||
def fetch_secrets(secrets, account:, session:)
|
||||
raise RuntimeError, "You must specify what to retrieve from Bitwarden Secrets Manager" if secrets.length == 0
|
||||
|
||||
if secrets.length == 1
|
||||
if secrets[0] == LIST_ALL_SELECTOR
|
||||
command = LIST_COMMAND
|
||||
elsif secrets[0].end_with?(LIST_ALL_FROM_PROJECT_SUFFIX)
|
||||
project = secrets[0].split(LIST_ALL_FROM_PROJECT_SUFFIX).first
|
||||
command = "#{LIST_COMMAND} #{project}"
|
||||
end
|
||||
end
|
||||
|
||||
{}.tap do |results|
|
||||
if command.nil?
|
||||
secrets.each do |secret_uuid|
|
||||
secret = run_command("#{GET_COMMAND} #{secret_uuid}")
|
||||
raise RuntimeError, "Could not read #{secret_uuid} from Bitwarden Secrets Manager" unless $?.success?
|
||||
key, value = parse_secret(secret)
|
||||
results[key] = value
|
||||
end
|
||||
else
|
||||
secrets = run_command(command)
|
||||
raise RuntimeError, "Could not read secrets from Bitwarden Secrets Manager" unless $?.success?
|
||||
secrets.split("\n").each do |secret|
|
||||
key, value = parse_secret(secret)
|
||||
results[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def parse_secret(secret)
|
||||
key, value = secret.split("=", 2)
|
||||
value = value.gsub(/^"|"$/, "")
|
||||
[ key, value ]
|
||||
end
|
||||
|
||||
def run_command(command, session: nil)
|
||||
full_command = [ "bws", command ].join(" ")
|
||||
`#{full_command}`
|
||||
end
|
||||
|
||||
def login(account)
|
||||
run_command("run 'echo OK'")
|
||||
raise RuntimeError, "Could not authenticate to Bitwarden Secrets Manager. Did you set a valid access token?" unless $?.success?
|
||||
end
|
||||
|
||||
def check_dependencies!
|
||||
raise RuntimeError, "Bitwarden Secrets Manager CLI is not installed" unless cli_installed?
|
||||
end
|
||||
|
||||
def cli_installed?
|
||||
`bws --version 2> /dev/null`
|
||||
$?.success?
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,3 @@
|
||||
module Kamal
|
||||
VERSION = "2.3.0"
|
||||
VERSION = "2.4.0"
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user