Files
kamal/lib/mrsk/configuration.rb
David Heinemeier Hansson 93423f2f20 Merge branch 'main' into pr/99
* main:
  Wording
  Remove accessory images using tags rather than labels
  Update readme to point to ghcr.io/mrsked/mrsk
  Validate that all roles have hosts
  Commander needn't accumulate configuration
  Pull latest image tag, so we can identity it
  Default to deploying the config version
  Remove unneeded Dockerfile.dind, update Readme
  add D-in-D dockerfile, update Readme
2023-03-24 14:26:31 +01:00

229 lines
5.4 KiB
Ruby

require "active_support/ordered_options"
require "active_support/core_ext/string/inquiry"
require "active_support/core_ext/module/delegation"
require "pathname"
require "erb"
require "net/ssh/proxy/jump"
class Mrsk::Configuration
delegate :service, :image, :servers, :env, :labels, :registry, :builder, to: :raw_config, allow_nil: true
delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils
attr_accessor :destination
attr_accessor :raw_config
class << self
def create_from(config_file:, destination: nil, version: nil)
raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
new raw_config, destination: destination, version: version
end
private
def load_config_files(*files)
files.inject({}) { |config, file| config.deep_merge! load_config_file(file) }
end
def load_config_file(file)
if file.exist?
YAML.load(ERB.new(IO.read(file)).result).symbolize_keys
else
raise "Configuration file not found in #{file}"
end
end
def destination_config_file(base_config_file, destination)
base_config_file.sub_ext(".#{destination}.yml") if destination
end
end
def initialize(raw_config, destination: nil, version: nil, validate: true)
@raw_config = ActiveSupport::InheritableOptions.new(raw_config)
@destination = destination
@declared_version = version
valid? if validate
end
def version=(version)
@declared_version = version
end
def version
@declared_version.presence || ENV["VERSION"] || current_commit_hash
end
def abbreviated_version
Mrsk::Utils.abbreviate_version(version)
end
def roles
@roles ||= role_names.collect { |role_name| Role.new(role_name, config: self) }
end
def role(name)
roles.detect { |r| r.name == name.to_s }
end
def accessories
@accessories ||= raw_config.accessories&.keys&.collect { |name| Mrsk::Configuration::Accessory.new(name, config: self) } || []
end
def accessory(name)
accessories.detect { |a| a.name == name.to_s }
end
def all_hosts
roles.flat_map(&:hosts).uniq
end
def primary_web_host
role(:web).primary_host
end
def traefik_hosts
roles.select(&:running_traefik?).flat_map(&:hosts).uniq
end
def repository
[ raw_config.registry["server"], image ].compact.join("/")
end
def absolute_image
"#{repository}:#{version}"
end
def latest_image
"#{repository}:latest"
end
def service_with_version
"#{service}-#{version}"
end
def env_args
if raw_config.env.present?
argumentize_env_with_secrets(raw_config.env)
else
[]
end
end
def volume_args
if raw_config.volumes.present?
argumentize "--volume", raw_config.volumes
else
[]
end
end
def ssh_user
if raw_config.ssh.present?
raw_config.ssh["user"] || "root"
else
"root"
end
end
def ssh_proxy
if raw_config.ssh.present? && raw_config.ssh["proxy"]
Net::SSH::Proxy::Jump.new \
raw_config.ssh["proxy"].include?("@") ? raw_config.ssh["proxy"] : "root@#{raw_config.ssh["proxy"]}"
end
end
def ssh_options
{ user: ssh_user, proxy: ssh_proxy, auth_methods: [ "publickey" ] }.compact
end
def audit_broadcast_cmd
raw_config.audit_broadcast_cmd
end
def healthcheck
{ "path" => "/up", "port" => 3000 }.merge(raw_config.healthcheck || {})
end
def readiness_delay
raw_config.readiness_delay || 7
end
def valid?
ensure_required_keys_present && ensure_env_available
end
def to_h
{
roles: role_names,
hosts: all_hosts,
primary_host: primary_web_host,
version: version,
repository: repository,
absolute_image: absolute_image,
service_with_version: service_with_version,
env_args: env_args,
volume_args: volume_args,
ssh_options: ssh_options,
builder: raw_config.builder,
accessories: raw_config.accessories,
healthcheck: healthcheck
}.compact
end
def traefik
raw_config.traefik || {}
end
private
# Will raise ArgumentError if any required config keys are missing
def ensure_required_keys_present
%i[ service image registry servers ].each do |key|
raise ArgumentError, "Missing required configuration for #{key}" unless raw_config[key].present?
end
if raw_config.registry["username"].blank?
raise ArgumentError, "You must specify a username for the registry in config/deploy.yml"
end
if raw_config.registry["password"].blank?
raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
end
roles.each do |role|
if role.hosts.empty?
raise ArgumentError, "No servers specified for the #{role.name} role"
end
end
true
end
# Will raise KeyError if any secret ENVs are missing
def ensure_env_available
env_args
roles.each(&:env_args)
true
end
def role_names
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
end
def current_commit_hash
@current_commit_hash ||=
if system("git rev-parse")
`git rev-parse HEAD`.strip
else
raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
end
end
end