Files
kamal/lib/kamal/configuration/accessory.rb
Donal McBreen 94bf090657 Copy env files to remote hosts
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.
2023-09-06 14:33:13 +01:00

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