kamal 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7a1eccb289724a2ca99c9c5e8ffd23749a859f7080606206efec05c417fc751
4
- data.tar.gz: 8a0d9143b580ff01d820746675a98954fe75d6c5a21abcb80ed2b5a5ec0ae657
3
+ metadata.gz: efc8817e4bba2417ad480b7297b323d11bd88ec87993fb4c8a1bb09ffbb3f4a4
4
+ data.tar.gz: 6c1269582ae3ccf90e9b4532b2fecbdd0723493413f82036038a121707f76ce3
5
5
  SHA512:
6
- metadata.gz: 153117a03e1fc92c96b6d8140096233afb792cfb9e227ace80cad5507df38969d5ef66d1342ed1ea838f43e7cef691f200b417c59a92419a9ac00c0cca4b3b6a
7
- data.tar.gz: 0fa78b7cec73a786e4e225e68cfb04a932a893ac8e7698c7361a9600b753564317089f5202ede5c83c2c7063a75d6ea1bd741b02181bf25f0f18833d561b21cd
6
+ metadata.gz: d9862df894f9e105bfe38a88ac93289e9fa89b51d69200cea6f5bd9f4483c74d31e2db39aa2d601aeb620d5f1beac5ca05efe94803e42f79cf3ba61e380b3f6d
7
+ data.tar.gz: 5e03468f451046a73425599db6dba15565d960f244d622bd2248c00e2d3f89db4f96cffed4dd6719d3025211ff0cb725a50020d81b8f09e6a64a6b41a2836b63
data/lib/kamal/cli/app.rb CHANGED
@@ -147,8 +147,12 @@ class Kamal::Cli::App < Kamal::Cli::Base
147
147
  using_version(version_or_latest) do |version|
148
148
  say "Launching command with version #{version} from new container...", :magenta
149
149
  on(KAMAL.hosts) do |host|
150
- execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
151
- puts_by_host host, capture_with_info(*KAMAL.app.execute_in_new_container(cmd))
150
+ roles = KAMAL.roles_on(host)
151
+
152
+ roles.each do |role|
153
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
154
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_new_container(cmd))
155
+ end
152
156
  end
153
157
  end
154
158
  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)"
@@ -24,6 +24,7 @@ module Kamal::Cli
24
24
 
25
25
  def initialize(*)
26
26
  super
27
+ @original_env = ENV.to_h.dup
27
28
  load_envs
28
29
  initialize_commander(options_with_subcommand_class_options)
29
30
  end
@@ -37,6 +38,12 @@ module Kamal::Cli
37
38
  end
38
39
  end
39
40
 
41
+ def reload_envs
42
+ ENV.clear
43
+ ENV.update(@original_env)
44
+ load_envs
45
+ end
46
+
40
47
  def options_with_subcommand_class_options
41
48
  options.merge(@_initializer.last[:class_options] || {})
42
49
  end
@@ -75,8 +82,6 @@ module Kamal::Cli
75
82
  def mutating
76
83
  return yield if KAMAL.holding_lock?
77
84
 
78
- KAMAL.config.ensure_env_available
79
-
80
85
  run_hook "pre-connect"
81
86
 
82
87
  ensure_run_directory
@@ -170,6 +170,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
170
170
  end
171
171
 
172
172
  desc "envify", "Create .env by evaluating .env.erb (or .env.staging.erb -> .env.staging when using -d staging)"
173
+ option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip .env file push"
173
174
  def envify
174
175
  if destination = options[:destination]
175
176
  env_template_path = ".env.#{destination}.erb"
@@ -179,10 +180,12 @@ class Kamal::Cli::Main < Kamal::Cli::Base
179
180
  env_path = ".env"
180
181
  end
181
182
 
182
- File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600)
183
+ File.write(env_path, ERB.new(File.read(env_template_path), trim_mode: "-").result, perm: 0600)
183
184
 
184
- load_envs # reload new file
185
- invoke "kamal:cli:env:push", options
185
+ unless options[:skip_push]
186
+ reload_envs
187
+ invoke "kamal:cli:env:push", options
188
+ end
186
189
  end
187
190
 
188
191
  desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
@@ -83,3 +83,16 @@ 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_web_host. This host takes
88
+ # deploy locks, runs health checks during the deploy, and follow logs, etc.
89
+ # This role should have traefik enabled.
90
+ #
91
+ # Caution: there's no support for role renaming yet, so be careful to cleanup
92
+ # the previous role on the deployed hosts.
93
+ # primary_web_role: web
94
+
95
+ # Controls if we abort when see a role with no hosts. Disabling this may be
96
+ # useful for more complex deploy configurations.
97
+ #
98
+ # allow_empty_roles: false
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ echo "Rebooted Traefik on $KAMAL_HOSTS"
@@ -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
@@ -28,11 +28,11 @@ class Kamal::Commander
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
+ @specific_roles = Kamal::Utils.filter_specific_items(role_names, config.roles) if role_names.present?
32
32
  end
33
33
 
34
34
  def specific_hosts=(hosts)
35
- @specific_hosts = config.all_hosts & hosts if hosts.present?
35
+ @specific_hosts = Kamal::Utils.filter_specific_items(hosts, config.all_hosts) if hosts.present?
36
36
  end
37
37
 
38
38
  def primary_host
@@ -18,6 +18,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
18
18
  "--name", container_name,
19
19
  *(["--hostname", hostname] if hostname),
20
20
  "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
21
+ "-e", "KAMAL_VERSION=\"#{config.version}\"",
21
22
  *role_config.env_args,
22
23
  *role_config.health_check_args,
23
24
  *config.logging_args,
@@ -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
 
@@ -16,6 +16,6 @@ class Kamal::Commands::Docker < Kamal::Commands::Base
16
16
 
17
17
  # Do we have superuser access to install Docker and start system services?
18
18
  def superuser?
19
- [ '[ "${EUID:-$(id -u)}" -eq 0 ]' ]
19
+ [ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ]
20
20
  end
21
21
  end
@@ -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
+ web = config.role(config.primary_web_role)
5
5
 
6
6
  docker :run,
7
7
  "--detach",
@@ -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
@@ -185,6 +185,7 @@ class Kamal::Configuration::Role
185
185
  "traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http",
186
186
 
187
187
  "traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)",
188
+ "traefik.http.routers.#{traefik_service}.priority" => "2",
188
189
  "traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
189
190
  "traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
190
191
  "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
@@ -90,13 +92,20 @@ class Kamal::Configuration
90
92
  end
91
93
 
92
94
  def primary_web_host
93
- role(:web).primary_host
95
+ role(primary_web_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?)
100
+ end
101
+
102
+ def traefik_role_names
103
+ traefik_roles.flat_map(&:name)
98
104
  end
99
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,16 +208,17 @@ class Kamal::Configuration
199
208
  raw_config.asset_path
200
209
  end
201
210
 
211
+ def primary_web_role
212
+ raw_config.primary_web_role || "web"
213
+ end
202
214
 
203
- def valid?
204
- ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version
215
+ def allow_empty_roles?
216
+ raw_config.allow_empty_roles
205
217
  end
206
218
 
207
- # Will raise KeyError if any secret ENVs are missing
208
- def ensure_env_available
209
- roles.collect(&:env_file).each(&:to_s)
210
219
 
211
- true
220
+ def valid?
221
+ ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version
212
222
  end
213
223
 
214
224
  def to_h
@@ -254,9 +264,23 @@ class Kamal::Configuration
254
264
  raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
255
265
  end
256
266
 
257
- roles.each do |role|
258
- if role.hosts.empty?
259
- raise ArgumentError, "No servers specified for the #{role.name} role"
267
+ unless role_names.include?(primary_web_role)
268
+ raise ArgumentError, "The primary_web_role #{primary_web_role} isn't defined"
269
+ end
270
+
271
+ unless traefik_role_names.include?(primary_web_role)
272
+ raise ArgumentError, "Role #{primary_web_role} needs to have traefik enabled"
273
+ end
274
+
275
+ if role(primary_web_role).hosts.empty?
276
+ raise ArgumentError, "No servers specified for the #{primary_web_role} primary_web_role"
277
+ end
278
+
279
+ unless allow_empty_roles?
280
+ roles.each do |role|
281
+ if role.hosts.empty?
282
+ raise ArgumentError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true"
283
+ end
260
284
  end
261
285
  end
262
286
 
@@ -1,4 +1,5 @@
1
1
  require "active_support/core_ext/module/delegation"
2
+ require "sshkit"
2
3
 
3
4
  class Kamal::Utils::Sensitive
4
5
  # So SSHKit knows to redact these values.
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.0.0"
2
+ VERSION = "1.2.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.0.0
4
+ version: 1.2.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-09-19 00:00:00.000000000 Z
11
+ date: 2023-11-15 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.19
275
+ rubygems_version: 3.4.21
274
276
  signing_key:
275
277
  specification_version: 4
276
278
  summary: Deploy web apps in containers to servers running Docker with zero downtime.