Setting env variables in the docker arguments requires having them on the deploy host. Instead we'll add two new commands `kamal env push` and `kamal env delete` which will manage copying the environment as .env files to the remote host. Docker will pick up the file with `--env-file <path-to-file>`. Env files will be stored under `<kamal run directory>/env`. Running `kamal env push` will create env files for each role and accessory, and traefik if required. `kamal envify` has been updated to also push the env files. By avoiding using `kamal envify` and creating the local and remote secrets manually, you can now avoid accessing secrets needed for the docker runtime environment locally. You will still need build secrets. One thing to note - the Docker doesn't parse the environment variables in the env file, one result of this is that you can't specify multi-line values - see https://github.com/moby/moby/issues/12997. We maybe need to look docker config or docker secrets longer term to get around this. Hattip to @kevinmcconnell - this was all his idea.
182 lines
4.1 KiB
Ruby
182 lines
4.1 KiB
Ruby
class Kamal::Configuration::Accessory
|
|
delegate :argumentize, :env_file_with_secrets, :optionize, to: Kamal::Utils
|
|
|
|
attr_accessor :name, :specifics
|
|
|
|
def initialize(name, config:)
|
|
@name, @config, @specifics = name.inquiry, config, config.raw_config["accessories"][name]
|
|
end
|
|
|
|
def service_name
|
|
"#{config.service}-#{name}"
|
|
end
|
|
|
|
def image
|
|
specifics["image"]
|
|
end
|
|
|
|
def hosts
|
|
if (specifics.keys & ["host", "hosts", "roles"]).size != 1
|
|
raise ArgumentError, "Specify one of `host`, `hosts` or `roles` for accessory `#{name}`"
|
|
end
|
|
|
|
hosts_from_host || hosts_from_hosts || hosts_from_roles
|
|
end
|
|
|
|
def port
|
|
if port = specifics["port"]&.to_s
|
|
port.include?(":") ? port : "#{port}:#{port}"
|
|
end
|
|
end
|
|
|
|
def publish_args
|
|
argumentize "--publish", port if port
|
|
end
|
|
|
|
def labels
|
|
default_labels.merge(specifics["labels"] || {})
|
|
end
|
|
|
|
def label_args
|
|
argumentize "--label", labels
|
|
end
|
|
|
|
def env
|
|
specifics["env"] || {}
|
|
end
|
|
|
|
def env_file
|
|
env_file_with_secrets env
|
|
end
|
|
|
|
def host_env_directory
|
|
File.join config.host_env_directory, "accessories"
|
|
end
|
|
|
|
def host_env_file_path
|
|
File.join host_env_directory, "#{service_name}.env"
|
|
end
|
|
|
|
def env_args
|
|
argumentize "--env-file", host_env_file_path
|
|
end
|
|
|
|
def files
|
|
specifics["files"]&.to_h do |local_to_remote_mapping|
|
|
local_file, remote_file = local_to_remote_mapping.split(":")
|
|
[ expand_local_file(local_file), expand_remote_file(remote_file) ]
|
|
end || {}
|
|
end
|
|
|
|
def directories
|
|
specifics["directories"]&.to_h do |host_to_container_mapping|
|
|
host_relative_path, container_path = host_to_container_mapping.split(":")
|
|
[ expand_host_path(host_relative_path), container_path ]
|
|
end || {}
|
|
end
|
|
|
|
def volumes
|
|
specific_volumes + remote_files_as_volumes + remote_directories_as_volumes
|
|
end
|
|
|
|
def volume_args
|
|
argumentize "--volume", volumes
|
|
end
|
|
|
|
def option_args
|
|
if args = specifics["options"]
|
|
optionize args
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def cmd
|
|
specifics["cmd"]
|
|
end
|
|
|
|
private
|
|
attr_accessor :config
|
|
|
|
def default_labels
|
|
{ "service" => service_name }
|
|
end
|
|
|
|
def expand_local_file(local_file)
|
|
if local_file.end_with?("erb")
|
|
with_clear_env_loaded { read_dynamic_file(local_file) }
|
|
else
|
|
Pathname.new(File.expand_path(local_file)).to_s
|
|
end
|
|
end
|
|
|
|
def with_clear_env_loaded
|
|
(env["clear"] || env).each { |k, v| ENV[k] = v }
|
|
yield
|
|
ensure
|
|
(env["clear"] || env).each { |k, v| ENV.delete(k) }
|
|
end
|
|
|
|
def read_dynamic_file(local_file)
|
|
StringIO.new(ERB.new(IO.read(local_file)).result)
|
|
end
|
|
|
|
def expand_remote_file(remote_file)
|
|
service_name + remote_file
|
|
end
|
|
|
|
def specific_volumes
|
|
specifics["volumes"] || []
|
|
end
|
|
|
|
def remote_files_as_volumes
|
|
specifics["files"]&.collect do |local_to_remote_mapping|
|
|
_, remote_file = local_to_remote_mapping.split(":")
|
|
"#{service_data_directory + remote_file}:#{remote_file}"
|
|
end || []
|
|
end
|
|
|
|
def remote_directories_as_volumes
|
|
specifics["directories"]&.collect do |host_to_container_mapping|
|
|
host_relative_path, container_path = host_to_container_mapping.split(":")
|
|
[ expand_host_path(host_relative_path), container_path ].join(":")
|
|
end || []
|
|
end
|
|
|
|
def expand_host_path(host_relative_path)
|
|
"#{service_data_directory}/#{host_relative_path}"
|
|
end
|
|
|
|
def service_data_directory
|
|
"$PWD/#{service_name}"
|
|
end
|
|
|
|
def hosts_from_host
|
|
if specifics.key?("host")
|
|
host = specifics["host"]
|
|
if host
|
|
[host]
|
|
else
|
|
raise ArgumentError, "Missing host for accessory `#{name}`"
|
|
end
|
|
end
|
|
end
|
|
|
|
def hosts_from_hosts
|
|
if specifics.key?("hosts")
|
|
hosts = specifics["hosts"]
|
|
if hosts.is_a?(Array)
|
|
hosts
|
|
else
|
|
raise ArgumentError, "Hosts should be an Array for accessory `#{name}`"
|
|
end
|
|
end
|
|
end
|
|
|
|
def hosts_from_roles
|
|
if specifics.key?("roles")
|
|
specifics["roles"].flat_map { |role| config.role(role).hosts }
|
|
end
|
|
end
|
|
end
|