Class: Kamal::Configuration::Role

Inherits:
Object
  • Object
show all
Includes:
Validation
Defined in:
lib/kamal/configuration/role.rb

Defined Under Namespace

Classes: Healthcheck

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Validation

#validate!, #validation_yml

Constructor Details

#initialize(name, config:) ⇒ Role

Returns a new instance of Role.



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/kamal/configuration/role.rb', line 10

def initialize(name, config:)
  @name, @config = name.inquiry, config
  validate! \
    role_config,
    example: validation_yml["servers"]["workers"],
    context: "servers/#{name}",
    with: Kamal::Configuration::Validator::Role

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

  @specialized_logging = Kamal::Configuration::Logging.new \
    logging_config: specializations.fetch("logging", {}),
    context: "servers/#{name}/logging"

  # `healthcheck: false` is an opt-out, not a healthcheck — it leaves @healthcheck nil.
  if healthcheck_config = specializations["healthcheck"]
    @healthcheck = Kamal::Configuration::Role::Healthcheck.new \
      healthcheck_config: healthcheck_config,
      context: "servers/#{name}/healthcheck"
  end

  initialize_specialized_proxy
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



6
7
8
# File 'lib/kamal/configuration/role.rb', line 6

def config
  @config
end

#healthcheckObject (readonly)

Returns the value of attribute healthcheck.



6
7
8
# File 'lib/kamal/configuration/role.rb', line 6

def healthcheck
  @healthcheck
end

#nameObject (readonly) Also known as: to_s

Returns the value of attribute name.



6
7
8
# File 'lib/kamal/configuration/role.rb', line 6

def name
  @name
end

#specialized_envObject (readonly)

Returns the value of attribute specialized_env.



6
7
8
# File 'lib/kamal/configuration/role.rb', line 6

def specialized_env
  @specialized_env
end

#specialized_loggingObject (readonly)

Returns the value of attribute specialized_logging.



6
7
8
# File 'lib/kamal/configuration/role.rb', line 6

def specialized_logging
  @specialized_logging
end

#specialized_proxyObject (readonly)

Returns the value of attribute specialized_proxy.



6
7
8
# File 'lib/kamal/configuration/role.rb', line 6

def specialized_proxy
  @specialized_proxy
end

Instance Method Details

#asset_extracted_directory(version = config.version) ⇒ Object



247
248
249
# File 'lib/kamal/configuration/role.rb', line 247

def asset_extracted_directory(version = config.version)
  File.join config.assets_directory, "extracted", [ name, version ].join("-")
end

#asset_pathObject



228
229
230
# File 'lib/kamal/configuration/role.rb', line 228

def asset_path
  asset_path_config&.dig(0)
end

#asset_path_optionsObject



243
244
245
# File 'lib/kamal/configuration/role.rb', line 243

def asset_path_options
  asset_path_config&.dig(1)
end

#asset_volume(version = config.version) ⇒ Object



236
237
238
239
240
241
# File 'lib/kamal/configuration/role.rb', line 236

def asset_volume(version = config.version)
  if assets?
    Kamal::Configuration::Volume.new \
      host_path: asset_volume_directory(version), container_path: asset_path, options: asset_path_options
  end
end

#asset_volume_argsObject



209
210
211
# File 'lib/kamal/configuration/role.rb', line 209

def asset_volume_args
  asset_volume&.docker_args
end

#asset_volume_directory(version = config.version) ⇒ Object



251
252
253
# File 'lib/kamal/configuration/role.rb', line 251

def asset_volume_directory(version = config.version)
  File.join config.assets_directory, "volumes", [ name, version ].join("-")
end

#assets?Boolean

Returns:

  • (Boolean)


232
233
234
# File 'lib/kamal/configuration/role.rb', line 232

def assets?
  asset_path.present? && running_proxy?
end

#bootObject

nil unless the role paces its own hosts. Deliberately not falling back to the global boot: that limit is already spent slicing the cross-role host list in Cli::App#host_boot_groups, and handing it to the per-role runner as well would sleep boot.wait a second time inside every group.

Built on first read rather than in the initializer — Servers.new constructs every Role before Kamal::Configuration#initialize has finished assigning its own collaborators.



98
99
100
101
102
103
104
105
106
# File 'lib/kamal/configuration/role.rb', line 98

def boot
  return @boot if defined?(@boot)

  @boot =
    if (boot_config = specializations["boot"])
      Kamal::Configuration::Boot.new \
        config: config, boot_config: boot_config, context: "servers/#{name}/boot"
    end
end

#boot_runner_options(hosts) ⇒ Object

hosts is what on_roles is about to pace — role.hosts & the run's hosts, so --roles/--hosts have already narrowed it. A percentage limit has to count that, not the role's configured hosts.



111
112
113
# File 'lib/kamal/configuration/role.rb', line 111

def boot_runner_options(hosts)
  boot&.runner_options_for(hosts) || {}
end

#cmdObject



57
58
59
# File 'lib/kamal/configuration/role.rb', line 57

def cmd
  specializations["cmd"]
end

#container_name(version = nil) ⇒ Object



219
220
221
# File 'lib/kamal/configuration/role.rb', line 219

def container_name(version = nil)
  [ container_prefix, version || config.version ].compact.join("-")
end

#container_prefixObject



223
224
225
# File 'lib/kamal/configuration/role.rb', line 223

def container_prefix
  [ config.service, name, config.destination ].compact.join("-")
end

#ensure_one_host_for_sslObject



255
256
257
258
259
260
# File 'lib/kamal/configuration/role.rb', line 255

def ensure_one_host_for_ssl
  # Skip SSL validation when a loadbalancer is present or custom certificates are provided
  if running_proxy? && proxy.ssl? && hosts.size > 1 && !proxy.loadbalancer.present? && !proxy.custom_ssl_certificate?
    raise Kamal::ConfigurationError, "SSL is only supported on a single server unless you provide custom certificates or configure a loadbalancer, found #{hosts.size} servers for role #{name}"
  end
end

#env(host) ⇒ Object



188
189
190
191
# File 'lib/kamal/configuration/role.rb', line 188

def env(host)
  @envs ||= {}
  @envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
end

#env_args(host) ⇒ Object



193
194
195
# File 'lib/kamal/configuration/role.rb', line 193

def env_args(host)
  [ *env(host).clear_args, *argumentize("--env-file", secrets_path) ]
end

#env_directoryObject



197
198
199
# File 'lib/kamal/configuration/role.rb', line 197

def env_directory
  File.join(config.env_directory, "roles")
end

#env_tags(host) ⇒ Object



45
46
47
# File 'lib/kamal/configuration/role.rb', line 45

def env_tags(host)
  tagged_hosts.fetch(host).collect { |tag| config.env_tag(tag) }.compact
end

#healthcheck_argsObject

Kept out of option_args on purpose: Commands::App::Execution splats those into one-shot kamal app exec containers, which must not inherit a service healthcheck.



83
84
85
# File 'lib/kamal/configuration/role.rb', line 83

def healthcheck_args
  healthcheck&.args || []
end

#hostsObject



41
42
43
# File 'lib/kamal/configuration/role.rb', line 41

def hosts
  tagged_hosts.keys
end

#hosts_with_tag(tag) ⇒ Object

The role's own hosts carrying tag in deploy.yml. Accessory tag:/tags: resolution comes through here rather than re-walking raw_config.servers, so it inherits extract_hosts_from_config's handling of every legal role shape — bare list, hosts: mapping, and the top-level servers: array.



53
54
55
# File 'lib/kamal/configuration/role.rb', line 53

def hosts_with_tag(tag)
  tagged_hosts.select { |_host, tags| tags.include?(tag) }.keys
end

#label_argsObject



73
74
75
# File 'lib/kamal/configuration/role.rb', line 73

def label_args
  argumentize "--label", labels
end

#labelsObject



69
70
71
# File 'lib/kamal/configuration/role.rb', line 69

def labels
  default_labels.merge(custom_labels)
end

#loggingObject



87
88
89
# File 'lib/kamal/configuration/role.rb', line 87

def logging
  @logging ||= config.logging.merge(specialized_logging)
end

#logging_argsObject



77
78
79
# File 'lib/kamal/configuration/role.rb', line 77

def logging_args
  logging.args
end

#option_argsObject



61
62
63
# File 'lib/kamal/configuration/role.rb', line 61

def option_args
  optionize docker_options.reject { |key, _| key.to_s == "restart" }
end

#primary?Boolean

Returns:

  • (Boolean)


214
215
216
# File 'lib/kamal/configuration/role.rb', line 214

def primary?
  name == @config.primary_role_name
end

#primary_hostObject



37
38
39
# File 'lib/kamal/configuration/role.rb', line 37

def primary_host
  hosts.first
end

#proxyObject



115
116
117
# File 'lib/kamal/configuration/role.rb', line 115

def proxy
  @proxy ||= specialized_proxy.merge(config.proxy) if running_proxy?
end

#readiness_delayObject

How long a role that has no healthcheck must merely keep running before the deploy accepts it. Role-specialized so a role that legitimately opts out can be tuned without slowing every other role down.



184
185
186
# File 'lib/kamal/configuration/role.rb', line 184

def readiness_delay
  specializations["readiness_delay"] || config.readiness_delay
end

#readiness_descriptionObject

One-line rendering of readiness_source, shared by the deploy banner and kamal doctor so both name the same gate the same way.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/kamal/configuration/role.rb', line 148

def readiness_description
  case readiness_source
  when :proxy
    [ "kamal-proxy health check", proxy.healthcheck_path ].compact.join(" ")
  when :healthcheck
    healthcheck.port ? "healthcheck #{healthcheck.path}:#{healthcheck.port}" : "healthcheck (custom cmd)"
  when :healthcheck_exec
    "healthcheck exec probe (#{healthcheck.exec})"
  when :docker_options
    "docker healthcheck (options: health-cmd)"
  else
    "NONE (old container stops #{readiness_delay}s after boot)"
  end
end

#readiness_gated?Boolean

Whether the operator has made a readiness decision for this role at all — declared a healthcheck, hand-rolled a health-cmd option, or accepted the gap with healthcheck: false. Distinct from readiness_source, which reports what actually gates the deploy.

Returns:

  • (Boolean)


166
167
168
# File 'lib/kamal/configuration/role.rb', line 166

def readiness_gated?
  healthcheck.present? || health_cmd_option? || healthcheck_disabled?
end

#readiness_sourceObject

Where a deploy of this role actually waits for readiness before stopping the old container. Without a proxy or a docker healthcheck, Healthcheck::Poller only sees .State.Status, so staying running for the readiness delay is the whole gate. :healthcheck_exec is the odd one out: the container declares no docker healthcheck, the deploy host polls the probe itself.



132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/kamal/configuration/role.rb', line 132

def readiness_source
  if running_proxy?
    :proxy
  elsif healthcheck&.exec?
    :healthcheck_exec
  elsif healthcheck
    :healthcheck
  elsif health_cmd_option?
    :docker_options
  else
    :none
  end
end

#restart_policyObject



65
66
67
# File 'lib/kamal/configuration/role.rb', line 65

def restart_policy
  restart_policy_option || "unless-stopped"
end

#running_proxy?Boolean

Returns:

  • (Boolean)


119
120
121
# File 'lib/kamal/configuration/role.rb', line 119

def running_proxy?
  @running_proxy
end

#secrets_io(host) ⇒ Object



201
202
203
# File 'lib/kamal/configuration/role.rb', line 201

def secrets_io(host)
  env(host).secrets_io
end

#secrets_pathObject



205
206
207
# File 'lib/kamal/configuration/role.rb', line 205

def secrets_path
  File.join(config.env_directory, "roles", "#{name}.env")
end

#ssl?Boolean

Returns:

  • (Boolean)


123
124
125
# File 'lib/kamal/configuration/role.rb', line 123

def ssl?
  running_proxy? && proxy.ssl?
end

#stop_argsObject



170
171
172
173
174
175
# File 'lib/kamal/configuration/role.rb', line 170

def stop_args
  # When deploying with the proxy, kamal-proxy will drain request before returning so we don't need to wait.
  timeout = stop_timeout || (running_proxy? ? nil : config.drain_timeout)

  [ *argumentize("-t", timeout) ]
end

#stop_timeoutObject



177
178
179
# File 'lib/kamal/configuration/role.rb', line 177

def stop_timeout
  specializations["stop_timeout"] || config.stop_timeout
end