kamal 1.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2470f58e93ad7f181c9a422a0eef08c741190cb897c962b83aeda9effd4862f5
4
- data.tar.gz: df8ddd4bf5dcf6687fab747bcfb11689dbe58b846ab6e392e99694ce8bfdff88
3
+ metadata.gz: 4a11bf8b2153b8bc4323e4b03e289d7c0c5e60a9dcc9012103ae701ae2c3256b
4
+ data.tar.gz: bc4b5c88e63a717fc539c71c00fb8207ee78693ade9658ac8effd3ecdd5249ab
5
5
  SHA512:
6
- metadata.gz: 7cef4c1ea22ffe3a0f80dbd2985cb3f3941e70dfcb35509a4ec7210124e517b0b7336ddd571370c32868d9d99442b9cc1186eb6a0cdefaa5db1b0475f117faae
7
- data.tar.gz: 1f6d43624775bb9f1320692c3b4e5d061528b92763886f963cf184978e2405e478c5c6a55347316c69416f512bb5f70c970999d56e2e5c6937b6b83613338106
6
+ metadata.gz: b8cbc84077617cfd72ad225cb458b0df9d36fe88b2f2a640bec9f012b4f92272d5cd62617580e75f217715be97e88c126bfe842dcdc1ab262c82db3fc9e0c746
7
+ data.tar.gz: c3e1de191ebcf4621fe15e59ee8b976491a58e8ffbd40a8862d15040837b7669f7e26372382711b118eb357035f047d920dd7817de7fc415aa5b8aa8a7e74a60
@@ -49,17 +49,21 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
49
49
  end
50
50
  end
51
51
 
52
- desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container)"
52
+ desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container; use NAME=all to boot all accessories)"
53
53
  def reboot(name)
54
54
  mutating do
55
- with_accessory(name) do |accessory|
56
- on(accessory.hosts) do
57
- execute *KAMAL.registry.login
58
- end
55
+ if name == "all"
56
+ KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
57
+ else
58
+ with_accessory(name) do |accessory|
59
+ on(accessory.hosts) do
60
+ execute *KAMAL.registry.login
61
+ end
59
62
 
60
- stop(name)
61
- remove_container(name)
62
- boot(name, login: false)
63
+ stop(name)
64
+ remove_container(name)
65
+ boot(name, login: false)
66
+ end
63
67
  end
64
68
  end
65
69
  end
@@ -14,8 +14,8 @@ module Kamal::Cli
14
14
  class_option :version, desc: "Run commands against a specific app version"
15
15
 
16
16
  class_option :primary, type: :boolean, aliases: "-p", desc: "Run commands only on primary host instead of all"
17
- class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma)"
18
- class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma)"
17
+ class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma, supports wildcards with *)"
18
+ class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma, supports wildcards with *)"
19
19
 
20
20
  class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
21
21
  class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
@@ -3,6 +3,7 @@ class Kamal::Cli::Healthcheck < Kamal::Cli::Base
3
3
 
4
4
  desc "perform", "Health check current app version"
5
5
  def perform
6
+ raise "The primary host is not configured to run Traefik" unless KAMAL.config.role(KAMAL.config.primary_role).running_traefik?
6
7
  on(KAMAL.primary_host) do
7
8
  begin
8
9
  execute *KAMAL.healthcheck.run
@@ -38,8 +38,10 @@ class Kamal::Cli::Main < Kamal::Cli::Base
38
38
  say "Ensure Traefik is running...", :magenta
39
39
  invoke "kamal:cli:traefik:boot", [], invoke_options
40
40
 
41
- say "Ensure app can pass healthcheck...", :magenta
42
- invoke "kamal:cli:healthcheck:perform", [], invoke_options
41
+ if KAMAL.config.role(KAMAL.config.primary_role).running_traefik?
42
+ say "Ensure app can pass healthcheck...", :magenta
43
+ invoke "kamal:cli:healthcheck:perform", [], invoke_options
44
+ end
43
45
 
44
46
  say "Detect stale containers...", :magenta
45
47
  invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
@@ -83,3 +83,15 @@ registry:
83
83
  # boot:
84
84
  # limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
85
85
  # wait: 2
86
+
87
+ # Configure the role used to determine the primary_host. This host takes
88
+ # deploy locks, runs health checks during the deploy, and follow logs, etc.
89
+ #
90
+ # Caution: there's no support for role renaming yet, so be careful to cleanup
91
+ # the previous role on the deployed hosts.
92
+ # primary_role: web
93
+
94
+ # Controls if we abort when see a role with no hosts. Disabling this may be
95
+ # useful for more complex deploy configurations.
96
+ #
97
+ # allow_empty_roles: false
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ echo "Rebooted Traefik on $KAMAL_HOSTS"
@@ -32,7 +32,7 @@ fi
32
32
  current_branch=$(git branch --show-current)
33
33
 
34
34
  if [ -z "$current_branch" ]; then
35
- echo "No git remote set, aborting..." >&2
35
+ echo "Not on a git branch, aborting..." >&2
36
36
  exit 1
37
37
  fi
38
38
 
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ echo "Rebooting Traefik on $KAMAL_HOSTS..."
@@ -13,12 +13,18 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
13
13
  option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
14
14
  def reboot
15
15
  mutating do
16
- on(KAMAL.traefik_hosts, in: options[:rolling] ? :sequence : :parallel) do
17
- execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug
18
- execute *KAMAL.registry.login
19
- execute *KAMAL.traefik.stop
20
- execute *KAMAL.traefik.remove_container
21
- execute *KAMAL.traefik.run
16
+ host_groups = options[:rolling] ? KAMAL.traefik_hosts : [KAMAL.traefik_hosts]
17
+ host_groups.each do |hosts|
18
+ host_list = Array(hosts).join(",")
19
+ run_hook "pre-traefik-reboot", hosts: host_list
20
+ on(hosts) do
21
+ execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug
22
+ execute *KAMAL.registry.login
23
+ execute *KAMAL.traefik.stop
24
+ execute *KAMAL.traefik.remove_container
25
+ execute *KAMAL.traefik.run
26
+ end
27
+ run_hook "post-traefik-reboot", hosts: host_list
22
28
  end
23
29
  end
24
30
  end
@@ -24,19 +24,36 @@ class Kamal::Commander
24
24
  attr_reader :specific_roles, :specific_hosts
25
25
 
26
26
  def specific_primary!
27
- self.specific_hosts = [ config.primary_web_host ]
27
+ self.specific_hosts = [ config.primary_host ]
28
28
  end
29
29
 
30
30
  def specific_roles=(role_names)
31
- @specific_roles = config.roles.select { |r| role_names.include?(r.name) } if role_names.present?
31
+ if role_names.present?
32
+ @specific_roles = Kamal::Utils.filter_specific_items(role_names, config.roles)
33
+
34
+ if @specific_roles.empty?
35
+ raise ArgumentError, "No --roles match for #{role_names.join(',')}"
36
+ end
37
+
38
+ @specific_roles
39
+ end
32
40
  end
33
41
 
34
42
  def specific_hosts=(hosts)
35
- @specific_hosts = config.all_hosts & hosts if hosts.present?
43
+ if hosts.present?
44
+ @specific_hosts = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
45
+
46
+ if @specific_hosts.empty?
47
+ raise ArgumentError, "No --hosts match for #{hosts.join(',')}"
48
+ end
49
+
50
+ @specific_hosts
51
+ end
36
52
  end
37
53
 
38
54
  def primary_host
39
- specific_hosts&.first || specific_roles&.first&.primary_host || config.primary_web_host
55
+ # Given a list of specific roles, make an effort to match up with the primary_role
56
+ specific_hosts&.first || specific_roles&.detect { |role| role.name == config.primary_role }&.primary_host || specific_roles&.first&.primary_host || config.primary_host
40
57
  end
41
58
 
42
59
  def primary_role
@@ -18,7 +18,7 @@ module Kamal::Commands
18
18
  elsif config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Command)
19
19
  cmd << " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
20
20
  end
21
- cmd << " -t #{config.ssh.user}@#{host} '#{command.join(" ")}'"
21
+ cmd << " -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ")}'"
22
22
  end
23
23
  end
24
24
 
@@ -1,7 +1,7 @@
1
1
  class Kamal::Commands::Healthcheck < Kamal::Commands::Base
2
2
 
3
3
  def run
4
- web = config.role(:web)
4
+ primary = config.role(config.primary_role)
5
5
 
6
6
  docker :run,
7
7
  "--detach",
@@ -9,12 +9,12 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base
9
9
  "--publish", "#{exposed_port}:#{config.healthcheck["port"]}",
10
10
  "--label", "service=#{config.healthcheck_service}",
11
11
  "-e", "KAMAL_CONTAINER_NAME=\"#{config.healthcheck_service}\"",
12
- *web.env_args,
13
- *web.health_check_args(cord: false),
12
+ *primary.env_args,
13
+ *primary.health_check_args(cord: false),
14
14
  *config.volume_args,
15
- *web.option_args,
15
+ *primary.option_args,
16
16
  config.absolute_image,
17
- web.cmd
17
+ primary.cmd
18
18
  end
19
19
 
20
20
  def status
@@ -6,6 +6,14 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
6
6
  DEFAULT_ARGS = {
7
7
  'log.level' => 'DEBUG'
8
8
  }
9
+ DEFAULT_LABELS = {
10
+ # These ensure we serve a 502 rather than a 404 if no containers are available
11
+ "traefik.http.routers.catchall.entryPoints" => "http",
12
+ "traefik.http.routers.catchall.rule" => "PathPrefix(`/`)",
13
+ "traefik.http.routers.catchall.service" => "unavailable",
14
+ "traefik.http.routers.catchall.priority" => 1,
15
+ "traefik.http.services.unavailable.loadbalancer.server.port" => "0"
16
+ }
9
17
 
10
18
  def run
11
19
  docker :run, "--name traefik",
@@ -97,7 +105,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
97
105
  end
98
106
 
99
107
  def labels
100
- config.traefik["labels"] || []
108
+ DEFAULT_LABELS.merge(config.traefik["labels"] || {})
101
109
  end
102
110
 
103
111
  def image
@@ -70,8 +70,8 @@ class Kamal::Configuration::Accessory
70
70
 
71
71
  def directories
72
72
  specifics["directories"]&.to_h do |host_to_container_mapping|
73
- host_relative_path, container_path = host_to_container_mapping.split(":")
74
- [ expand_host_path(host_relative_path), container_path ]
73
+ host_path, container_path = host_to_container_mapping.split(":")
74
+ [ expand_host_path(host_path), container_path ]
75
75
  end || {}
76
76
  end
77
77
 
@@ -138,13 +138,17 @@ class Kamal::Configuration::Accessory
138
138
 
139
139
  def remote_directories_as_volumes
140
140
  specifics["directories"]&.collect do |host_to_container_mapping|
141
- host_relative_path, container_path = host_to_container_mapping.split(":")
142
- [ expand_host_path(host_relative_path), container_path ].join(":")
141
+ host_path, container_path = host_to_container_mapping.split(":")
142
+ [ expand_host_path(host_path), container_path ].join(":")
143
143
  end || []
144
144
  end
145
145
 
146
- def expand_host_path(host_relative_path)
147
- "#{service_data_directory}/#{host_relative_path}"
146
+ def expand_host_path(host_path)
147
+ absolute_path?(host_path) ? host_path : "#{service_data_directory}/#{host_path}"
148
+ end
149
+
150
+ def absolute_path?(path)
151
+ Pathname.new(path).absolute?
148
152
  end
149
153
 
150
154
  def service_data_directory
@@ -93,7 +93,15 @@ class Kamal::Configuration::Role
93
93
 
94
94
 
95
95
  def running_traefik?
96
- name.web? || specializations["traefik"]
96
+ if specializations["traefik"].nil?
97
+ primary?
98
+ else
99
+ specializations["traefik"]
100
+ end
101
+ end
102
+
103
+ def primary?
104
+ @config.primary_role == name
97
105
  end
98
106
 
99
107
 
@@ -185,6 +193,7 @@ class Kamal::Configuration::Role
185
193
  "traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http",
186
194
 
187
195
  "traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)",
196
+ "traefik.http.routers.#{traefik_service}.priority" => "2",
188
197
  "traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
189
198
  "traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
190
199
  "traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker"
@@ -9,6 +9,10 @@ class Kamal::Configuration::Ssh
9
9
  config.fetch("user", "root")
10
10
  end
11
11
 
12
+ def port
13
+ config.fetch("port", 22)
14
+ end
15
+
12
16
  def proxy
13
17
  if (proxy = config["proxy"])
14
18
  Net::SSH::Proxy::Jump.new(proxy.include?("@") ? proxy : "root@#{proxy}")
@@ -18,7 +22,7 @@ class Kamal::Configuration::Ssh
18
22
  end
19
23
 
20
24
  def options
21
- { user: user, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30 }.compact
25
+ { user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30 }.compact
22
26
  end
23
27
 
24
28
  def to_h
@@ -25,7 +25,9 @@ class Kamal::Configuration
25
25
 
26
26
  def load_config_file(file)
27
27
  if file.exist?
28
- YAML.load(ERB.new(IO.read(file)).result).symbolize_keys
28
+ # Newer Psych doesn't load aliases by default
29
+ load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
30
+ YAML.send(load_method, ERB.new(IO.read(file)).result).symbolize_keys
29
31
  else
30
32
  raise "Configuration file not found in #{file}"
31
33
  end
@@ -89,14 +91,21 @@ class Kamal::Configuration
89
91
  roles.flat_map(&:hosts).uniq
90
92
  end
91
93
 
92
- def primary_web_host
93
- role(:web).primary_host
94
+ def primary_host
95
+ role(primary_role)&.primary_host
94
96
  end
95
97
 
96
- def traefik_hosts
97
- roles.select(&:running_traefik?).flat_map(&:hosts).uniq
98
+ def traefik_roles
99
+ roles.select(&:running_traefik?)
98
100
  end
99
101
 
102
+ def traefik_role_names
103
+ traefik_roles.flat_map(&:name)
104
+ end
105
+
106
+ def traefik_hosts
107
+ traefik_roles.flat_map(&:hosts).uniq
108
+ end
100
109
 
101
110
  def repository
102
111
  [ raw_config.registry["server"], image ].compact.join("/")
@@ -199,6 +208,14 @@ class Kamal::Configuration
199
208
  raw_config.asset_path
200
209
  end
201
210
 
211
+ def primary_role
212
+ raw_config.primary_role || "web"
213
+ end
214
+
215
+ def allow_empty_roles?
216
+ raw_config.allow_empty_roles
217
+ end
218
+
202
219
 
203
220
  def valid?
204
221
  ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version
@@ -208,7 +225,7 @@ class Kamal::Configuration
208
225
  {
209
226
  roles: role_names,
210
227
  hosts: all_hosts,
211
- primary_host: primary_web_host,
228
+ primary_host: primary_host,
212
229
  version: version,
213
230
  repository: repository,
214
231
  absolute_image: absolute_image,
@@ -247,9 +264,19 @@ class Kamal::Configuration
247
264
  raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
248
265
  end
249
266
 
250
- roles.each do |role|
251
- if role.hosts.empty?
252
- raise ArgumentError, "No servers specified for the #{role.name} role"
267
+ unless role_names.include?(primary_role)
268
+ raise ArgumentError, "The primary_role #{primary_role} isn't defined"
269
+ end
270
+
271
+ if role(primary_role).hosts.empty?
272
+ raise ArgumentError, "No servers specified for the #{primary_role} primary_role"
273
+ end
274
+
275
+ unless allow_empty_roles?
276
+ roles.each do |role|
277
+ if role.hosts.empty?
278
+ raise ArgumentError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true"
279
+ end
253
280
  end
254
281
  end
255
282
 
data/lib/kamal/utils.rb CHANGED
@@ -58,4 +58,20 @@ module Kamal::Utils
58
58
  .gsub(/`/, '\\\\`')
59
59
  .gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
60
60
  end
61
+
62
+ # Apply a list of host or role filters, including wildcard matches
63
+ def filter_specific_items(filters, items)
64
+ matches = []
65
+
66
+ Array(filters).select do |filter|
67
+ matches += Array(items).select do |item|
68
+ # Only allow * for a wildcard
69
+ pattern = Regexp.escape(filter).gsub('\*', '.*')
70
+ # items are roles or hosts
71
+ (item.respond_to?(:name) ? item.name : item).match(/^#{pattern}$/)
72
+ end
73
+ end
74
+
75
+ matches
76
+ end
61
77
  end
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "1.1.0"
2
+ VERSION = "1.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-03 00:00:00.000000000 Z
11
+ date: 2023-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -204,9 +204,11 @@ files:
204
204
  - lib/kamal/cli/server.rb
205
205
  - lib/kamal/cli/templates/deploy.yml
206
206
  - lib/kamal/cli/templates/sample_hooks/post-deploy.sample
207
+ - lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample
207
208
  - lib/kamal/cli/templates/sample_hooks/pre-build.sample
208
209
  - lib/kamal/cli/templates/sample_hooks/pre-connect.sample
209
210
  - lib/kamal/cli/templates/sample_hooks/pre-deploy.sample
211
+ - lib/kamal/cli/templates/sample_hooks/pre-traefik-reboot.sample
210
212
  - lib/kamal/cli/templates/template.env
211
213
  - lib/kamal/cli/traefik.rb
212
214
  - lib/kamal/commander.rb
@@ -270,7 +272,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
270
272
  - !ruby/object:Gem::Version
271
273
  version: '0'
272
274
  requirements: []
273
- rubygems_version: 3.4.21
275
+ rubygems_version: 3.4.22
274
276
  signing_key:
275
277
  specification_version: 4
276
278
  summary: Deploy web apps in containers to servers running Docker with zero downtime.