Skip to content

Commit

Permalink
Lazily load secrets whenever needed
Browse files Browse the repository at this point in the history
  • Loading branch information
djmb committed Sep 4, 2024
1 parent 6a06efc commit 56754fe
Show file tree
Hide file tree
Showing 43 changed files with 391 additions and 529 deletions.
1 change: 1 addition & 0 deletions lib/kamal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class ConfigurationError < StandardError; end
require "active_support"
require "zeitwerk"
require "yaml"
require "tmpdir"

loader = Zeitwerk::Loader.for_gem
loader.ignore(File.join(__dir__, "kamal", "sshkit_with_ext.rb"))
Expand Down
8 changes: 6 additions & 2 deletions lib/kamal/cli/app/boot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,12 @@ def wait_at_barrier
def close_barrier
if barrier.close
info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles"
error capture_with_info(*app.logs(version: version))
error capture_with_info(*app.container_health_log(version: version))
begin
error capture_with_info(*app.logs(version: version))
error capture_with_info(*app.container_health_log(version: version))
rescue SSHKit::Command::Failed
error "Could not fetch logs for #{version}"
end
end
end

Expand Down
13 changes: 2 additions & 11 deletions lib/kamal/cli/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,15 @@ def initialize(args = [], local_options = {}, config = {})
else
super
end
@original_env = ENV.to_h.dup
initialize_commander(options_with_subcommand_class_options)
initialize_commander unless KAMAL.configured?
end

private
def load_secrets
if destination = options[:destination]
Dotenv.parse(".kamal/secrets.#{destination}", ".kamal/secrets")
else
Dotenv.parse(".kamal/secrets")
end
end

def options_with_subcommand_class_options
options.merge(@_initializer.last[:class_options] || {})
end

def initialize_commander(options)
def initialize_commander
KAMAL.tap do |commander|
if options[:verbose]
ENV["VERBOSE"] = "1" # For backtraces via cli/start
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/cli/build.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def push
push = KAMAL.builder.push

KAMAL.with_verbosity(:debug) do
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.config.builder.secrets }
end
end
end
Expand Down
3 changes: 0 additions & 3 deletions lib/kamal/cli/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,6 @@ def version
desc "build", "Build application image"
subcommand "build", Kamal::Cli::Build

desc "env", "Manage environment files"
subcommand "env", Kamal::Cli::Env

desc "lock", "Manage the deploy lock"
subcommand "lock", Kamal::Cli::Lock

Expand Down
4 changes: 4 additions & 0 deletions lib/kamal/commander.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ def configure(**kwargs)
@config, @config_kwargs = nil, kwargs
end

def configured?
@config || @config_kwargs
end

attr_reader :specific_roles, :specific_hosts

def specific_primary!
Expand Down
8 changes: 0 additions & 8 deletions lib/kamal/commands/accessory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,6 @@ def remove_image
docker :image, :rm, "--force", image
end

def make_env_directory
make_directory accessory_config.env.secrets_directory
end

def remove_env_file
[ :rm, "-f", accessory_config.env.secrets_file ]
end

private
def service_filter
[ "--filter", "label=service=#{service_name}" ]
Expand Down
10 changes: 0 additions & 10 deletions lib/kamal/commands/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,6 @@ def list_versions(*docker_args, statuses: nil)
extract_version_from_name
end


def make_env_directory
make_directory role.env(host).secrets_directory
end

def remove_env_file
[ :rm, "-f", role.env(host).secrets_file ]
end


private
def container_name(version = nil)
[ role.container_prefix, version || config.version ].compact.join("-")
Expand Down
9 changes: 6 additions & 3 deletions lib/kamal/commands/auditor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ def initialize(config, **details)

# Runs remotely
def record(line, **details)
append \
[ :echo, audit_tags(**details).except(:version, :service_version, :service).to_s, line ],
audit_log_file
combine \
[ :mkdir, "-p", config.run_directory ],
append(
[ :echo, audit_tags(**details).except(:version, :service_version, :service).to_s, line ],
audit_log_file
)
end

def reveal
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/commands/builder/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def build_args
end

def build_secrets
argumentize "--secret", secrets.collect { |secret| [ "id", secret ] }
argumentize "--secret", secrets.keys.collect { |secret| [ "id", secret ] }
end

def build_dockerfile
Expand Down
8 changes: 0 additions & 8 deletions lib/kamal/commands/traefik.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,6 @@ def remove_image
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
end

def make_env_directory
make_directory(env.secrets_directory)
end

def remove_env_file
[ :rm, "-f", env.secrets_file ]
end

private
def publish_args
argumentize "--publish", port if publish?
Expand Down
8 changes: 6 additions & 2 deletions lib/kamal/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def initialize(raw_config, destination: nil, version: nil, validate: true)
@aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
@boot = Boot.new(config: self)
@builder = Builder.new(config: self)
@env = Env.new(config: @raw_config.env || {})
@env = Env.new(config: @raw_config.env || {}, secrets: secrets)

@healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck)
@logging = Logging.new(logging_config: @raw_config.logging)
Expand Down Expand Up @@ -224,7 +224,7 @@ def host_env_directory

def env_tags
@env_tags ||= if (tags = raw_config.env["tags"])
tags.collect { |name, config| Env::Tag.new(name, config: config) }
tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
else
[]
end
Expand Down Expand Up @@ -254,6 +254,10 @@ def to_h
}.compact
end

def secrets
@secrets ||= Secrets.new(destination: destination)
end

private
# Will raise ArgumentError if any required config keys are missing
def ensure_destination_if_required
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/configuration/accessory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def initialize(name, config:)

@env = Kamal::Configuration::Env.new \
config: accessory_config.fetch("env", {}),
secrets_file: File.join(config.host_env_directory, "accessories", "#{service_name}.env"),
secrets: config.secrets,
context: "accessories/#{name}/env"
end

Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/configuration/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def args
end

def secrets
builder_config["secrets"] || []
(builder_config["secrets"] || []).to_h { |key| [ key, config.secrets[key] ] }
end

def dockerfile
Expand Down
36 changes: 17 additions & 19 deletions lib/kamal/configuration/env.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,34 @@
class Kamal::Configuration::Env
include Kamal::Configuration::Validation

attr_reader :secrets_keys, :clear, :secrets_file, :context
attr_reader :context, :secrets
attr_reader :clear, :secret_keys
delegate :argumentize, to: Kamal::Utils

def initialize(config:, secrets_file: nil, context: "env")
def initialize(config:, secrets:, context: "env")
@clear = config.fetch("clear", config.key?("secret") || config.key?("tags") ? {} : config)
@secrets_keys = config.fetch("secret", [])
@secrets_file = secrets_file
@secrets = secrets
@secret_keys = config.fetch("secret", [])
@context = context
validate! config, context: context, with: Kamal::Configuration::Validator::Env
end

def args
[ "--env-file", secrets_file, *argumentize("--env", clear) ]
end

def secrets_io
StringIO.new(Kamal::EnvFile.new(secrets).to_s)
end

def secrets
@secrets ||= secrets_keys.to_h { |key| [ key, ENV.fetch(key) ] }
end

def secrets_directory
File.dirname(secrets_file)
[ *clear_args, *secret_args ]
end

def merge(other)
self.class.new \
config: { "clear" => clear.merge(other.clear), "secret" => secrets_keys | other.secrets_keys },
secrets_file: secrets_file || other.secrets_file
config: { "clear" => clear.merge(other.clear), "secret" => secret_keys | other.secret_keys },
secrets: secrets
end

private
def clear_args
argumentize("--env", clear)
end

def secret_args
argumentize("--env", secret_keys.to_h { |key| [ key, secrets[key] ] }, sensitive: true)
end
end
7 changes: 4 additions & 3 deletions lib/kamal/configuration/env/tag.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
class Kamal::Configuration::Env::Tag
attr_reader :name, :config
attr_reader :name, :config, :secrets

def initialize(name, config:)
def initialize(name, config:, secrets:)
@name = name
@config = config
@secrets = secrets
end

def env
Kamal::Configuration::Env.new(config: config)
Kamal::Configuration::Env.new(config: config, secrets: secrets)
end
end
5 changes: 3 additions & 2 deletions lib/kamal/configuration/registry.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
class Kamal::Configuration::Registry
include Kamal::Configuration::Validation

attr_reader :registry_config
attr_reader :registry_config, :secrets

def initialize(config:)
@registry_config = config.raw_config.registry || {}
@secrets = config.secrets
validate! registry_config, with: Kamal::Configuration::Validator::Registry
end

Expand All @@ -23,7 +24,7 @@ def password
private
def lookup(key)
if registry_config[key].is_a?(Array)
ENV.fetch(registry_config[key].first).dup
secrets[registry_config[key].first]
else
registry_config[key]
end
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/configuration/role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def initialize(name, config:)

@specialized_env = Kamal::Configuration::Env.new \
config: specializations.fetch("env", {}),
secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env"),
secrets: config.secrets,
context: "servers/#{name}/env"

@specialized_logging = Kamal::Configuration::Logging.new \
Expand Down
25 changes: 25 additions & 0 deletions lib/kamal/configuration/secrets.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class Kamal::Configuration::Secrets
attr_reader :secret_files

def initialize(destination: nil)
@secret_files = \
(destination ? [ ".kamal/secrets", ".kamal/secrets.#{destination}" ] : [ ".kamal/secrets" ])
.select { |file| File.exist?(file) }
end

def [](key)
@secrets ||= load
@secrets.fetch(key)
rescue KeyError
if secret_files.any?
raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secret_files.join(', ')}"
else
raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files provided"
end
end

private
def load
secret_files.any? ? Dotenv.parse(*secret_files) : {}
end
end
2 changes: 1 addition & 1 deletion lib/kamal/configuration/traefik.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def labels
def env
Kamal::Configuration::Env.new \
config: traefik_config.fetch("env", {}),
secrets_file: File.join(config.host_env_directory, "traefik", "traefik.env"),
secrets: config.secrets,
context: "traefik/env"
end

Expand Down
38 changes: 0 additions & 38 deletions lib/kamal/env_file.rb

This file was deleted.

6 changes: 6 additions & 0 deletions lib/kamal/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ def redacted(value)

# Escape a value to make it safe for shell use.
def escape_shell_value(value)
value.to_s.scan(/[\x00-\x7F]+|[^\x00-\x7F]+/) \
.map { |part| part.ascii_only? ? escape_ascii_shell_value(part) : part }
.join
end

def escape_ascii_shell_value(value)
value.to_s.dump
.gsub(/`/, '\\\\`')
.gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
Expand Down
Loading

0 comments on commit 56754fe

Please sign in to comment.