class Kamal::Configuration::Role include Kamal::Configuration::Validation CORD_FILE = "cord" delegate :argumentize, :optionize, to: Kamal::Utils attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_healthcheck alias to_s name def initialize(name, config:) @name, @config = name.inquiry, config validate! \ specializations, example: validation_yml["servers"]["workers"], context: "servers/#{name}", with: Kamal::Configuration::Validator::Role @specialized_env = Kamal::Configuration::Env.new \ config: specializations.fetch("env", {}), secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env"), context: "servers/#{name}/env" @specialized_logging = Kamal::Configuration::Logging.new \ logging_config: specializations.fetch("logging", {}), context: "servers/#{name}/logging" @specialized_healthcheck = Kamal::Configuration::Healthcheck.new \ healthcheck_config: specializations.fetch("healthcheck", {}), context: "servers/#{name}/healthcheck" end def primary_host hosts.first end def hosts tagged_hosts.keys end def env_tags(host) tagged_hosts.fetch(host).collect { |tag| config.env_tag(tag) } end def cmd specializations["cmd"] end def option_args if args = specializations["options"] optionize args else [] end end def labels default_labels.merge(traefik_labels).merge(custom_labels) end def label_args argumentize "--label", labels end def logging_args logging.args end def logging @logging ||= config.logging.merge(specialized_logging) end def env(host) @envs ||= {} @envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge) end def env_args(host) env(host).args end def asset_volume_args asset_volume&.docker_args end def health_check_args(cord: true) if running_traefik? || healthcheck.set_port_or_path? if cord && uses_cord? optionize({ "health-cmd" => health_check_cmd_with_cord, "health-interval" => healthcheck.interval }) .concat(cord_volume.docker_args) else optionize({ "health-cmd" => healthcheck.cmd, "health-interval" => healthcheck.interval }) end else [] end end def healthcheck @healthcheck ||= if running_traefik? config.healthcheck.merge(specialized_healthcheck) else specialized_healthcheck end end def health_check_cmd_with_cord "(#{healthcheck.cmd}) && (stat #{cord_container_file} > /dev/null || exit 1)" end def running_traefik? if specializations["traefik"].nil? primary? else specializations["traefik"] end end def primary? self == @config.primary_role end def uses_cord? running_traefik? && cord_volume && healthcheck.cmd.present? end def cord_host_directory File.join config.run_directory_as_docker_volume, "cords", [ container_prefix, config.run_id ].join("-") end def cord_volume if (cord = healthcheck.cord) @cord_volume ||= Kamal::Configuration::Volume.new \ host_path: File.join(config.run_directory, "cords", [ container_prefix, config.run_id ].join("-")), container_path: cord end end def cord_host_file File.join cord_volume.host_path, CORD_FILE end def cord_container_directory health_check_options.fetch("cord", nil) end def cord_container_file File.join cord_volume.container_path, CORD_FILE end def container_name(version = nil) [ container_prefix, version || config.version ].compact.join("-") end def container_prefix [ config.service, name, config.destination ].compact.join("-") end def asset_path specializations["asset_path"] || config.asset_path end def assets? asset_path.present? && running_traefik? end def asset_volume(version = nil) if assets? Kamal::Configuration::Volume.new \ host_path: asset_volume_path(version), container_path: asset_path end end def asset_extracted_path(version = nil) File.join config.run_directory, "assets", "extracted", container_name(version) end def asset_volume_path(version = nil) File.join config.run_directory, "assets", "volumes", container_name(version) end private def tagged_hosts {}.tap do |tagged_hosts| extract_hosts_from_config.map do |host_config| if host_config.is_a?(Hash) host, tags = host_config.first tagged_hosts[host] = Array(tags) elsif host_config.is_a?(String) tagged_hosts[host_config] = [] end end end end def extract_hosts_from_config if config.raw_config.servers.is_a?(Array) config.raw_config.servers else servers = config.raw_config.servers[name] servers.is_a?(Array) ? servers : Array(servers["hosts"]) end end def default_labels { "service" => config.service, "role" => name, "destination" => config.destination } end def specializations if config.raw_config.servers.is_a?(Array) || config.raw_config.servers[name].is_a?(Array) {} else config.raw_config.servers[name] end end def traefik_labels if running_traefik? { # Setting a service property ensures that the generated service name will be consistent between versions "traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http", "traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)", "traefik.http.routers.#{traefik_service}.priority" => "2", "traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5", "traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms", "traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker" } else {} end end def traefik_service container_prefix end def custom_labels Hash.new.tap do |labels| labels.merge!(config.labels) if config.labels.present? labels.merge!(specializations["labels"]) if specializations["labels"].present? end end end