kamal 1.0.0 → 1.2.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: 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.