kamal 1.3.1 → 1.4.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: 2071f73b4e7a6d63939bbfd4f07caacd95aa637964c07fffaa261711f602b5f6
4
- data.tar.gz: 0c265ae0c39673018ece39e7fc16acfcc6e7148efdf6a011827548ffb5a332de
3
+ metadata.gz: df82a171edf966865ca45112056965b93b50a700c4ae190cebe791992c41a825
4
+ data.tar.gz: 9c5290566c477597460910a80a6e0694285e6d9779b6897bc6c1c10f5bd23b7b
5
5
  SHA512:
6
- metadata.gz: 0d21ac54e6eea13f7e89280d3358a342e54e629f402864b591538d018f19f3275fe88104c06e85a73e3846ddfd606f3106dfb3ab7757629dd931e6b63f111767
7
- data.tar.gz: be9bc4f8a641ef661adb78af0853ee3045d93f40d48c357627f80a5c43a5295125b4629658332b314c45f6134b5b87f03e386b0c432b734a5ca838fda0a027e2
6
+ metadata.gz: c2995cb335465be583037f0a23e2a5d3ce128b47e162b69d7666dbc3e313b542c1d10abef9fa2bb542bdb679003dc8993c3037c4738a9e8544fb346755e8ced0
7
+ data.tar.gz: a0b7d5500f7ad5408409e0f655274e19a833a8158d2013e79a70e7209d87f5c350b715071d8981f9119f9b3d8c66033bfcd23b5290d3d3880511b3c98c7cff83
@@ -5,11 +5,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
5
5
  if name == "all"
6
6
  KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
7
7
  else
8
- with_accessory(name) do |accessory|
8
+ with_accessory(name) do |accessory, hosts|
9
9
  directories(name)
10
10
  upload(name)
11
11
 
12
- on(accessory.hosts) do
12
+ on(hosts) do
13
13
  execute *KAMAL.registry.login if login
14
14
  execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
15
15
  execute *accessory.run
@@ -22,8 +22,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
22
22
  desc "upload [NAME]", "Upload accessory files to host", hide: true
23
23
  def upload(name)
24
24
  mutating do
25
- with_accessory(name) do |accessory|
26
- on(accessory.hosts) do
25
+ with_accessory(name) do |accessory, hosts|
26
+ on(hosts) do
27
27
  accessory.files.each do |(local, remote)|
28
28
  accessory.ensure_local_file_present(local)
29
29
 
@@ -39,8 +39,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
39
39
  desc "directories [NAME]", "Create accessory directories on host", hide: true
40
40
  def directories(name)
41
41
  mutating do
42
- with_accessory(name) do |accessory|
43
- on(accessory.hosts) do
42
+ with_accessory(name) do |accessory, hosts|
43
+ on(hosts) do
44
44
  accessory.directories.keys.each do |host_path|
45
45
  execute *accessory.make_directory(host_path)
46
46
  end
@@ -55,8 +55,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
55
55
  if name == "all"
56
56
  KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
57
57
  else
58
- with_accessory(name) do |accessory|
59
- on(accessory.hosts) do
58
+ with_accessory(name) do |accessory, hosts|
59
+ on(hosts) do
60
60
  execute *KAMAL.registry.login
61
61
  end
62
62
 
@@ -71,8 +71,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
71
71
  desc "start [NAME]", "Start existing accessory container on host"
72
72
  def start(name)
73
73
  mutating do
74
- with_accessory(name) do |accessory|
75
- on(accessory.hosts) do
74
+ with_accessory(name) do |accessory, hosts|
75
+ on(hosts) do
76
76
  execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
77
77
  execute *accessory.start
78
78
  end
@@ -83,8 +83,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
83
83
  desc "stop [NAME]", "Stop existing accessory container on host"
84
84
  def stop(name)
85
85
  mutating do
86
- with_accessory(name) do |accessory|
87
- on(accessory.hosts) do
86
+ with_accessory(name) do |accessory, hosts|
87
+ on(hosts) do
88
88
  execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
89
89
  execute *accessory.stop, raise_on_non_zero_exit: false
90
90
  end
@@ -107,8 +107,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
107
107
  if name == "all"
108
108
  KAMAL.accessory_names.each { |accessory_name| details(accessory_name) }
109
109
  else
110
- with_accessory(name) do |accessory|
111
- on(accessory.hosts) { puts capture_with_info(*accessory.info) }
110
+ with_accessory(name) do |accessory, hosts|
111
+ on(hosts) { puts capture_with_info(*accessory.info) }
112
112
  end
113
113
  end
114
114
  end
@@ -117,7 +117,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
117
117
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
118
118
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
119
119
  def exec(name, cmd)
120
- with_accessory(name) do |accessory|
120
+ with_accessory(name) do |accessory, hosts|
121
121
  case
122
122
  when options[:interactive] && options[:reuse]
123
123
  say "Launching interactive command with via SSH from existing container...", :magenta
@@ -129,14 +129,14 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
129
129
 
130
130
  when options[:reuse]
131
131
  say "Launching command from existing container...", :magenta
132
- on(accessory.hosts) do
132
+ on(hosts) do
133
133
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
134
134
  capture_with_info(*accessory.execute_in_existing_container(cmd))
135
135
  end
136
136
 
137
137
  else
138
138
  say "Launching command from new container...", :magenta
139
- on(accessory.hosts) do
139
+ on(hosts) do
140
140
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
141
141
  capture_with_info(*accessory.execute_in_new_container(cmd))
142
142
  end
@@ -150,12 +150,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
150
150
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
151
151
  option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
152
152
  def logs(name)
153
- with_accessory(name) do |accessory|
153
+ with_accessory(name) do |accessory, hosts|
154
154
  grep = options[:grep]
155
155
 
156
156
  if options[:follow]
157
157
  run_locally do
158
- info "Following logs on #{accessory.hosts}..."
158
+ info "Following logs on #{hosts}..."
159
159
  info accessory.follow_logs(grep: grep)
160
160
  exec accessory.follow_logs(grep: grep)
161
161
  end
@@ -163,7 +163,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
163
163
  since = options[:since]
164
164
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
165
165
 
166
- on(accessory.hosts) do
166
+ on(hosts) do
167
167
  puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep))
168
168
  end
169
169
  end
@@ -192,8 +192,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
192
192
  desc "remove_container [NAME]", "Remove accessory container from host", hide: true
193
193
  def remove_container(name)
194
194
  mutating do
195
- with_accessory(name) do |accessory|
196
- on(accessory.hosts) do
195
+ with_accessory(name) do |accessory, hosts|
196
+ on(hosts) do
197
197
  execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
198
198
  execute *accessory.remove_container
199
199
  end
@@ -204,8 +204,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
204
204
  desc "remove_image [NAME]", "Remove accessory image from host", hide: true
205
205
  def remove_image(name)
206
206
  mutating do
207
- with_accessory(name) do |accessory|
208
- on(accessory.hosts) do
207
+ with_accessory(name) do |accessory, hosts|
208
+ on(hosts) do
209
209
  execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
210
210
  execute *accessory.remove_image
211
211
  end
@@ -216,8 +216,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
216
216
  desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
217
217
  def remove_service_directory(name)
218
218
  mutating do
219
- with_accessory(name) do |accessory|
220
- on(accessory.hosts) do
219
+ with_accessory(name) do |accessory, hosts|
220
+ on(hosts) do
221
221
  execute *accessory.remove_service_directory
222
222
  end
223
223
  end
@@ -227,7 +227,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
227
227
  private
228
228
  def with_accessory(name)
229
229
  if accessory = KAMAL.accessory(name)
230
- yield accessory
230
+ yield accessory, accessory_hosts(accessory)
231
231
  else
232
232
  error_on_missing_accessory(name)
233
233
  end
@@ -240,4 +240,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
240
240
  "No accessory by the name of '#{name}'" +
241
241
  (options ? " (options: #{options.to_sentence})" : "")
242
242
  end
243
+
244
+ def accessory_hosts(accessory)
245
+ if KAMAL.specific_hosts&.any?
246
+ KAMAL.specific_hosts & accessory.hosts
247
+ else
248
+ accessory.hosts
249
+ end
250
+ end
243
251
  end
data/lib/kamal/cli/app.rb CHANGED
@@ -13,9 +13,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
13
13
 
14
14
  KAMAL.roles_on(host).each do |role|
15
15
  app = KAMAL.app(role: role)
16
- role_config = KAMAL.config.role(role)
17
16
 
18
- if role_config.assets?
17
+ if role.assets?
19
18
  execute *app.extract_assets
20
19
  old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
21
20
  execute *app.sync_asset_volumes(old_version: old_version)
@@ -27,7 +26,6 @@ class Kamal::Cli::App < Kamal::Cli::Base
27
26
  KAMAL.roles_on(host).each do |role|
28
27
  app = KAMAL.app(role: role)
29
28
  auditor = KAMAL.auditor(role: role)
30
- role_config = KAMAL.config.role(role)
31
29
 
32
30
  if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
33
31
  tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
@@ -38,7 +36,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
38
36
 
39
37
  old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
40
38
 
41
- execute *app.tie_cord(role_config.cord_host_file) if role_config.uses_cord?
39
+ execute *app.tie_cord(role.cord_host_file) if role.uses_cord?
42
40
 
43
41
  execute *auditor.record("Booted app version #{version}"), verbosity: :debug
44
42
 
@@ -47,7 +45,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
47
45
  Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
48
46
 
49
47
  if old_version.present?
50
- if role_config.uses_cord?
48
+ if role.uses_cord?
51
49
  cord = capture_with_info(*app.cord(version: old_version), raise_on_non_zero_exit: false).strip
52
50
  if cord.present?
53
51
  execute *app.cut_cord(cord)
@@ -57,7 +55,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
57
55
 
58
56
  execute *app.stop(version: old_version), raise_on_non_zero_exit: false
59
57
 
60
- execute *app.clean_up_assets if role_config.assets?
58
+ execute *app.clean_up_assets if role.assets?
61
59
  end
62
60
  end
63
61
  end
@@ -202,19 +200,20 @@ class Kamal::Cli::App < Kamal::Cli::Base
202
200
  # FIXME: Catch when app containers aren't running
203
201
 
204
202
  grep = options[:grep]
205
-
203
+ since = options[:since]
206
204
  if options[:follow]
205
+ lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
206
+
207
207
  run_locally do
208
208
  info "Following logs on #{KAMAL.primary_host}..."
209
209
 
210
210
  KAMAL.specific_roles ||= ["web"]
211
211
  role = KAMAL.roles_on(KAMAL.primary_host).first
212
212
 
213
- info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, grep: grep)
214
- exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, grep: grep)
213
+ info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
214
+ exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
215
215
  end
216
216
  else
217
- since = options[:since]
218
217
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
219
218
 
220
219
  on(KAMAL.hosts) do |host|
@@ -123,8 +123,9 @@ module Kamal::Cli
123
123
  yield
124
124
  rescue SSHKit::Runner::ExecuteError => e
125
125
  if e.message =~ /cannot create directory/
126
+ say "Deploy lock already in place!", :red
126
127
  on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
127
- raise LockError, "Deploy lock found"
128
+ raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
128
129
  else
129
130
  raise e
130
131
  end
data/lib/kamal/cli/env.rb CHANGED
@@ -8,9 +8,8 @@ class Kamal::Cli::Env < Kamal::Cli::Base
8
8
  execute *KAMAL.auditor.record("Pushed env files"), verbosity: :debug
9
9
 
10
10
  KAMAL.roles_on(host).each do |role|
11
- role_config = KAMAL.config.role(role)
12
11
  execute *KAMAL.app(role: role).make_env_directory
13
- upload! StringIO.new(role_config.env_file), role_config.host_env_file_path, mode: 400
12
+ upload! StringIO.new(role.env_file), role.host_env_file_path, mode: 400
14
13
  end
15
14
  end
16
15
 
@@ -36,7 +35,6 @@ class Kamal::Cli::Env < Kamal::Cli::Base
36
35
  execute *KAMAL.auditor.record("Deleted env files"), verbosity: :debug
37
36
 
38
37
  KAMAL.roles_on(host).each do |role|
39
- role_config = KAMAL.config.role(role)
40
38
  execute *KAMAL.app(role: role).remove_env_file
41
39
  end
42
40
  end
@@ -1,15 +1,18 @@
1
1
  class Kamal::Cli::Main < Kamal::Cli::Base
2
2
  desc "setup", "Setup all accessories, push the env, and deploy app to servers"
3
+ option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
3
4
  def setup
4
5
  print_runtime do
5
6
  mutating do
7
+ invoke_options = deploy_options
8
+
6
9
  say "Ensure Docker is installed...", :magenta
7
- invoke "kamal:cli:server:bootstrap"
10
+ invoke "kamal:cli:server:bootstrap", [], invoke_options
8
11
 
9
12
  say "Push env files...", :magenta
10
- invoke "kamal:cli:env:push"
13
+ invoke "kamal:cli:env:push", [], invoke_options
11
14
 
12
- invoke "kamal:cli:accessory:boot", [ "all" ]
15
+ invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options
13
16
  deploy
14
17
  end
15
18
  end
@@ -18,12 +18,16 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
18
18
  end
19
19
  end
20
20
 
21
- desc "containers", "Prune all stopped containers, except the last 5"
21
+ desc "containers", "Prune all stopped containers, except the last n (default 5)"
22
+ option :retain, type: :numeric, default: nil, desc: "Number of containers to retain"
22
23
  def containers
24
+ retain = options.fetch(:retain, KAMAL.config.retain_containers)
25
+ raise "retain must be at least 1" if retain < 1
26
+
23
27
  mutating do
24
28
  on(KAMAL.hosts) do
25
29
  execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
26
- execute *KAMAL.prune.app_containers
30
+ execute *KAMAL.prune.app_containers(retain: retain)
27
31
  execute *KAMAL.prune.healthcheck_containers
28
32
  end
29
33
  end
@@ -17,7 +17,9 @@ class Kamal::Cli::Server < Kamal::Cli::Base
17
17
  end
18
18
 
19
19
  if missing.any?
20
- raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/"
20
+ raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and either `wget` or `curl`. Install Docker manually: https://docs.docker.com/engine/install/"
21
21
  end
22
+
23
+ run_hook "docker-setup"
22
24
  end
23
25
  end
@@ -77,6 +77,10 @@ registry:
77
77
  # Bridge fingerprinted assets, like JS and CSS, between versions to avoid
78
78
  # hitting 404 on in-flight requests. Combines all files from new and old
79
79
  # version inside the asset_path.
80
+ #
81
+ # If your app is using the Sprockets gem, ensure it sets `config.assets.manifest`.
82
+ # See https://github.com/basecamp/kamal/issues/626 for details
83
+ #
80
84
  # asset_path: /rails/public/assets
81
85
 
82
86
  # Configure rolling deploys by setting a wait time between batches of restarts.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # A sample docker-setup hook
4
+ #
5
+ # Sets up a Docker network which can then be used by the application’s containers
6
+
7
+ ssh user@example.com docker network create kamal
@@ -20,7 +20,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
20
20
  on(hosts) do
21
21
  execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug
22
22
  execute *KAMAL.registry.login
23
- execute *KAMAL.traefik.stop
23
+ execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
24
24
  execute *KAMAL.traefik.remove_container
25
25
  execute *KAMAL.traefik.run
26
26
  end
@@ -44,7 +44,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
44
44
  mutating do
45
45
  on(KAMAL.traefik_hosts) do
46
46
  execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
47
- execute *KAMAL.traefik.stop
47
+ execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
48
48
  end
49
49
  end
50
50
  end
@@ -53,7 +53,7 @@ class Kamal::Commander
53
53
 
54
54
  def primary_host
55
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
56
+ specific_hosts&.first || specific_roles&.detect { |role| role == config.primary_role }&.primary_host || specific_roles&.first&.primary_host || config.primary_host
57
57
  end
58
58
 
59
59
  def primary_role
@@ -73,7 +73,7 @@ class Kamal::Commander
73
73
  end
74
74
 
75
75
  def roles_on(host)
76
- roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
76
+ roles.select { |role| role.hosts.include?(host.to_s) }
77
77
  end
78
78
 
79
79
  def traefik_hosts
@@ -1,20 +1,20 @@
1
1
  module Kamal::Commands::App::Assets
2
2
  def extract_assets
3
- asset_container = "#{role_config.container_prefix}-assets"
3
+ asset_container = "#{role.container_prefix}-assets"
4
4
 
5
5
  combine \
6
- make_directory(role_config.asset_extracted_path),
6
+ make_directory(role.asset_extracted_path),
7
7
  [*docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true"],
8
8
  docker(:run, "--name", asset_container, "--detach", "--rm", config.latest_image, "sleep 1000000"),
9
- docker(:cp, "-L", "#{asset_container}:#{role_config.asset_path}/.", role_config.asset_extracted_path),
9
+ docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_path),
10
10
  docker(:stop, "-t 1", asset_container),
11
11
  by: "&&"
12
12
  end
13
13
 
14
14
  def sync_asset_volumes(old_version: nil)
15
- new_extracted_path, new_volume_path = role_config.asset_extracted_path(config.version), role_config.asset_volume.host_path
15
+ new_extracted_path, new_volume_path = role.asset_extracted_path(config.version), role.asset_volume.host_path
16
16
  if old_version.present?
17
- old_extracted_path, old_volume_path = role_config.asset_extracted_path(old_version), role_config.asset_volume(old_version).host_path
17
+ old_extracted_path, old_volume_path = role.asset_extracted_path(old_version), role.asset_volume(old_version).host_path
18
18
  end
19
19
 
20
20
  commands = [make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path)]
@@ -29,8 +29,8 @@ module Kamal::Commands::App::Assets
29
29
 
30
30
  def clean_up_assets
31
31
  chain \
32
- find_and_remove_older_siblings(role_config.asset_extracted_path),
33
- find_and_remove_older_siblings(role_config.asset_volume_path)
32
+ find_and_remove_older_siblings(role.asset_extracted_path),
33
+ find_and_remove_older_siblings(role.asset_volume_path)
34
34
  end
35
35
 
36
36
  private
@@ -39,7 +39,7 @@ module Kamal::Commands::App::Assets
39
39
  :find,
40
40
  Pathname.new(path).dirname.to_s,
41
41
  "-maxdepth 1",
42
- "-name", "'#{role_config.container_prefix}-*'",
42
+ "-name", "'#{role.container_prefix}-*'",
43
43
  "!", "-name", Pathname.new(path).basename.to_s,
44
44
  "-exec rm -rf \"{}\" +"
45
45
  ]
@@ -2,7 +2,7 @@ module Kamal::Commands::App::Cord
2
2
  def cord(version:)
3
3
  pipe \
4
4
  docker(:inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}'", container_name(version)),
5
- [:awk, "'$2 == \"#{role_config.cord_volume.container_path}\" {print $1}'"]
5
+ [:awk, "'$2 == \"#{role.cord_volume.container_path}\" {print $1}'"]
6
6
  end
7
7
 
8
8
  def tie_cord(cord)
@@ -12,8 +12,8 @@ module Kamal::Commands::App::Cord
12
12
  def cut_cord(cord)
13
13
  remove_directory(cord)
14
14
  end
15
-
16
- private
15
+
16
+ private
17
17
  def create_empty_file(file)
18
18
  chain \
19
19
  make_directory_for(file),
@@ -10,9 +10,9 @@ module Kamal::Commands::App::Execution
10
10
  docker :run,
11
11
  ("-it" if interactive),
12
12
  "--rm",
13
- *role_config&.env_args,
13
+ *role&.env_args,
14
14
  *config.volume_args,
15
- *role_config&.option_args,
15
+ *role&.option_args,
16
16
  config.absolute_image,
17
17
  *command
18
18
  end
@@ -6,11 +6,11 @@ module Kamal::Commands::App::Logging
6
6
  ("grep '#{grep}'" if grep)
7
7
  end
8
8
 
9
- def follow_logs(host:, grep: nil)
9
+ def follow_logs(host:, lines: nil, grep: nil)
10
10
  run_over_ssh \
11
11
  pipe(
12
12
  current_running_container_id,
13
- "xargs docker logs --timestamps --tail 10 --follow 2>&1",
13
+ "xargs docker logs --timestamps#{" --tail #{lines}" if lines} --follow 2>&1",
14
14
  (%(grep "#{grep}") if grep)
15
15
  ),
16
16
  host: host
@@ -3,12 +3,11 @@ class Kamal::Commands::App < Kamal::Commands::Base
3
3
 
4
4
  ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
5
5
 
6
- attr_reader :role, :role_config
6
+ attr_reader :role, :role
7
7
 
8
8
  def initialize(config, role: nil)
9
9
  super(config)
10
10
  @role = role
11
- @role_config = config.role(self.role)
12
11
  end
13
12
 
14
13
  def run(hostname: nil)
@@ -19,15 +18,15 @@ class Kamal::Commands::App < Kamal::Commands::Base
19
18
  *(["--hostname", hostname] if hostname),
20
19
  "-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
21
20
  "-e", "KAMAL_VERSION=\"#{config.version}\"",
22
- *role_config.env_args,
23
- *role_config.health_check_args,
24
- *config.logging_args,
21
+ *role.env_args,
22
+ *role.health_check_args,
23
+ *role.logging_args,
25
24
  *config.volume_args,
26
- *role_config.asset_volume_args,
27
- *role_config.label_args,
28
- *role_config.option_args,
25
+ *role.asset_volume_args,
26
+ *role.label_args,
27
+ *role.option_args,
29
28
  config.absolute_image,
30
- role_config.cmd
29
+ role.cmd
31
30
  end
32
31
 
33
32
  def start
@@ -64,22 +63,22 @@ class Kamal::Commands::App < Kamal::Commands::Base
64
63
  def list_versions(*docker_args, statuses: nil)
65
64
  pipe \
66
65
  docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
67
- %(while read line; do echo ${line##{role_config.container_prefix}-}; done) # Extract SHA from "service-role-dest-SHA"
66
+ %(while read line; do echo ${line##{role.container_prefix}-}; done) # Extract SHA from "service-role-dest-SHA"
68
67
  end
69
68
 
70
69
 
71
70
  def make_env_directory
72
- make_directory role_config.host_env_directory
71
+ make_directory role.host_env_directory
73
72
  end
74
73
 
75
74
  def remove_env_file
76
- [ :rm, "-f", role_config.host_env_file_path ]
75
+ [ :rm, "-f", role.host_env_file_path ]
77
76
  end
78
77
 
79
78
 
80
79
  private
81
80
  def container_name(version = nil)
82
- [ role_config.container_prefix, version || config.version ].compact.join("-")
81
+ [ role.container_prefix, version || config.version ].compact.join("-")
83
82
  end
84
83
 
85
84
  def filter_args(statuses: nil)
@@ -62,10 +62,18 @@ module Kamal::Commands
62
62
  combine *commands, by: ">"
63
63
  end
64
64
 
65
+ def any(*commands)
66
+ combine *commands, by: "||"
67
+ end
68
+
65
69
  def xargs(command)
66
70
  [ :xargs, command ].flatten
67
71
  end
68
72
 
73
+ def shell(command)
74
+ [ :sh, "-c", "'#{command.flatten.join(" ").gsub("'", "'\\''")}'" ]
75
+ end
76
+
69
77
  def docker(*args)
70
78
  args.compact.unshift :docker
71
79
  end
@@ -3,7 +3,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
3
3
  class BuilderError < StandardError; end
4
4
 
5
5
  delegate :argumentize, to: Kamal::Utils
6
- delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, to: :builder_config
6
+ delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config
7
7
 
8
8
  def clean
9
9
  docker :image, :rm, "--force", config.absolute_image
@@ -14,7 +14,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
14
14
  end
15
15
 
16
16
  def build_options
17
- [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile ]
17
+ [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_ssh ]
18
18
  end
19
19
 
20
20
  def build_context
@@ -24,7 +24,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
24
24
  def validate_image
25
25
  pipe \
26
26
  docker(:inspect, "-f", "'{{ .Config.Labels.service }}'", config.absolute_image),
27
- [:grep, "-x", config.service, "||", "(echo \"Image #{config.absolute_image} is missing the `service` label\" && exit 1)"]
27
+ any(
28
+ [:grep, "-x", config.service],
29
+ "(echo \"Image #{config.absolute_image} is missing the 'service' label\" && exit 1)"
30
+ )
28
31
  end
29
32
 
30
33
 
@@ -60,6 +63,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
60
63
  end
61
64
  end
62
65
 
66
+ def build_ssh
67
+ argumentize "--ssh", ssh if ssh.present?
68
+ end
69
+
63
70
  def builder_config
64
71
  config.builder
65
72
  end
@@ -10,7 +10,7 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
10
10
  def push
11
11
  docker :buildx, :build,
12
12
  "--push",
13
- "--platform", "linux/amd64,linux/arm64",
13
+ "--platform", platform_names,
14
14
  "--builder", builder_name,
15
15
  *build_options,
16
16
  build_context
@@ -26,4 +26,12 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
26
26
  def builder_name
27
27
  "kamal-#{config.service}-multiarch"
28
28
  end
29
+
30
+ def platform_names
31
+ if local_arch
32
+ "linux/#{local_arch}"
33
+ else
34
+ "linux/amd64,linux/arm64"
35
+ end
36
+ end
29
37
  end
@@ -1,7 +1,7 @@
1
1
  class Kamal::Commands::Docker < Kamal::Commands::Base
2
2
  # Install Docker using the https://github.com/docker/docker-install convenience script.
3
3
  def install
4
- pipe [ :curl, "-fsSL", "https://get.docker.com" ], :sh
4
+ pipe get_docker, :sh
5
5
  end
6
6
 
7
7
  # Checks the Docker client version. Fails if Docker is not installed.
@@ -18,4 +18,13 @@ class Kamal::Commands::Docker < Kamal::Commands::Base
18
18
  def superuser?
19
19
  [ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ]
20
20
  end
21
+
22
+ private
23
+ def get_docker
24
+ shell \
25
+ any \
26
+ [ :curl, "-fsSL", "https://get.docker.com" ],
27
+ [ :wget, "-O -", "https://get.docker.com" ],
28
+ [ :echo, "\"exit 1\"" ]
29
+ end
21
30
  end
@@ -13,10 +13,10 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
13
13
  "while read image tag; do docker rmi $tag; done"
14
14
  end
15
15
 
16
- def app_containers(keep_last: 5)
16
+ def app_containers(retain:)
17
17
  pipe \
18
18
  docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
19
- "tail -n +#{keep_last + 1}",
19
+ "tail -n +#{retain + 1}",
20
20
  "while read container_id; do docker rm $container_id; done"
21
21
  end
22
22
 
@@ -2,7 +2,10 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
2
2
  delegate :registry, to: :config
3
3
 
4
4
  def login
5
- docker :login, registry["server"], "-u", sensitive(lookup("username")), "-p", sensitive(lookup("password"))
5
+ docker :login,
6
+ registry["server"],
7
+ "-u", sensitive(Kamal::Utils.escape_shell_value(lookup("username"))),
8
+ "-p", sensitive(Kamal::Utils.escape_shell_value(lookup("password")))
6
9
  end
7
10
 
8
11
  def logout
@@ -1,7 +1,7 @@
1
1
  class Kamal::Commands::Traefik < Kamal::Commands::Base
2
2
  delegate :argumentize, :optionize, to: Kamal::Utils
3
3
 
4
- DEFAULT_IMAGE = "traefik:v2.9"
4
+ DEFAULT_IMAGE = "traefik:v2.10"
5
5
  CONTAINER_PORT = 80
6
6
  DEFAULT_ARGS = {
7
7
  'log.level' => 'DEBUG'
@@ -39,7 +39,7 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
39
39
  end
40
40
 
41
41
  def start_or_run
42
- combine start, run, by: "||"
42
+ any start, run
43
43
  end
44
44
 
45
45
  def info
@@ -8,7 +8,7 @@ class Kamal::Configuration::Accessory
8
8
  end
9
9
 
10
10
  def service_name
11
- "#{config.service}-#{name}"
11
+ specifics["service"] || "#{config.service}-#{name}"
12
12
  end
13
13
 
14
14
  def image
@@ -8,7 +8,7 @@ class Kamal::Configuration::Boot
8
8
  limit = @options["limit"]
9
9
 
10
10
  if limit.to_s.end_with?("%")
11
- @host_count * limit.to_i / 100
11
+ [@host_count * limit.to_i / 100, 1].max
12
12
  else
13
13
  limit
14
14
  end
@@ -81,6 +81,10 @@ class Kamal::Configuration::Builder
81
81
  end
82
82
  end
83
83
 
84
+ def ssh
85
+ @options["ssh"]
86
+ end
87
+
84
88
  private
85
89
  def valid?
86
90
  if @options["cache"] && @options["cache"]["type"]
@@ -3,9 +3,10 @@ class Kamal::Configuration::Role
3
3
  delegate :argumentize, :optionize, to: Kamal::Utils
4
4
 
5
5
  attr_accessor :name
6
+ alias to_s name
6
7
 
7
8
  def initialize(name, config:)
8
- @name, @config = name.inquiry, config
9
+ @name, @config = name.inquiry, config
9
10
  end
10
11
 
11
12
  def primary_host
@@ -36,6 +37,18 @@ class Kamal::Configuration::Role
36
37
  argumentize "--label", labels
37
38
  end
38
39
 
40
+ def logging_args
41
+ args = config.logging || {}
42
+ args.deep_merge!(specializations["logging"]) if specializations["logging"].present?
43
+
44
+ if args.any?
45
+ optionize({ "log-driver" => args["driver"] }.compact) +
46
+ argumentize("--log-opt", args["options"])
47
+ else
48
+ config.logging_args
49
+ end
50
+ end
51
+
39
52
 
40
53
  def env
41
54
  if config.env && config.env["secret"]
@@ -101,7 +114,7 @@ class Kamal::Configuration::Role
101
114
  end
102
115
 
103
116
  def primary?
104
- @config.primary_role == name
117
+ self == @config.primary_role
105
118
  end
106
119
 
107
120
 
@@ -6,7 +6,7 @@ require "erb"
6
6
  require "net/ssh/proxy/jump"
7
7
 
8
8
  class Kamal::Configuration
9
- delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
9
+ delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, :logging, to: :raw_config, allow_nil: true
10
10
  delegate :argumentize, :optionize, to: Kamal::Utils
11
11
 
12
12
  attr_reader :destination, :raw_config
@@ -92,7 +92,19 @@ class Kamal::Configuration
92
92
  end
93
93
 
94
94
  def primary_host
95
- role(primary_role)&.primary_host
95
+ primary_role&.primary_host
96
+ end
97
+
98
+ def primary_role_name
99
+ raw_config.primary_role || "web"
100
+ end
101
+
102
+ def primary_role
103
+ role(primary_role_name)
104
+ end
105
+
106
+ def allow_empty_roles?
107
+ raw_config.allow_empty_roles
96
108
  end
97
109
 
98
110
  def traefik_roles
@@ -127,6 +139,10 @@ class Kamal::Configuration
127
139
  raw_config.require_destination
128
140
  end
129
141
 
142
+ def retain_containers
143
+ raw_config.retain_containers || 5
144
+ end
145
+
130
146
 
131
147
  def volume_args
132
148
  if raw_config.volumes.present?
@@ -137,9 +153,9 @@ class Kamal::Configuration
137
153
  end
138
154
 
139
155
  def logging_args
140
- if raw_config.logging.present?
141
- optionize({ "log-driver" => raw_config.logging["driver"] }.compact) +
142
- argumentize("--log-opt", raw_config.logging["options"])
156
+ if logging.present?
157
+ optionize({ "log-driver" => logging["driver"] }.compact) +
158
+ argumentize("--log-opt", logging["options"])
143
159
  else
144
160
  argumentize("--log-opt", { "max-size" => "10m" })
145
161
  end
@@ -208,17 +224,9 @@ class Kamal::Configuration
208
224
  raw_config.asset_path
209
225
  end
210
226
 
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
-
219
227
 
220
228
  def valid?
221
- ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version
229
+ ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version && ensure_retain_containers_valid && ensure_valid_service_name
222
230
  end
223
231
 
224
232
  def to_h
@@ -264,12 +272,12 @@ class Kamal::Configuration
264
272
  raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
265
273
  end
266
274
 
267
- unless role_names.include?(primary_role)
268
- raise ArgumentError, "The primary_role #{primary_role} isn't defined"
275
+ unless role_names.include?(primary_role_name)
276
+ raise ArgumentError, "The primary_role #{primary_role_name} isn't defined"
269
277
  end
270
278
 
271
- if role(primary_role).hosts.empty?
272
- raise ArgumentError, "No servers specified for the #{primary_role} primary_role"
279
+ if primary_role.hosts.empty?
280
+ raise ArgumentError, "No servers specified for the #{primary_role.name} primary_role"
273
281
  end
274
282
 
275
283
  unless allow_empty_roles?
@@ -283,6 +291,12 @@ class Kamal::Configuration
283
291
  true
284
292
  end
285
293
 
294
+ def ensure_valid_service_name
295
+ raise ArgumentError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/
296
+
297
+ true
298
+ end
299
+
286
300
  def ensure_valid_kamal_version
287
301
  if minimum_version && Gem::Version.new(minimum_version) > Gem::Version.new(Kamal::VERSION)
288
302
  raise ArgumentError, "Current version is #{Kamal::VERSION}, minimum required is #{minimum_version}"
@@ -291,6 +305,12 @@ class Kamal::Configuration
291
305
  true
292
306
  end
293
307
 
308
+ def ensure_retain_containers_valid
309
+ raise ArgumentError, "Must retain at least 1 container" if retain_containers < 1
310
+
311
+ true
312
+ end
313
+
294
314
 
295
315
  def role_names
296
316
  raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
@@ -1,5 +1,6 @@
1
1
  require "sshkit"
2
2
  require "sshkit/dsl"
3
+ require "net/scp"
3
4
  require "active_support/core_ext/hash/deep_merge"
4
5
  require "json"
5
6
 
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "1.3.1"
2
+ VERSION = "1.4.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.3.1
4
+ version: 1.4.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: 2024-01-10 00:00:00.000000000 Z
11
+ date: 2024-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -217,6 +217,7 @@ files:
217
217
  - lib/kamal/cli/registry.rb
218
218
  - lib/kamal/cli/server.rb
219
219
  - lib/kamal/cli/templates/deploy.yml
220
+ - lib/kamal/cli/templates/sample_hooks/docker-setup.sample
220
221
  - lib/kamal/cli/templates/sample_hooks/post-deploy.sample
221
222
  - lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample
222
223
  - lib/kamal/cli/templates/sample_hooks/pre-build.sample
@@ -286,7 +287,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
286
287
  - !ruby/object:Gem::Version
287
288
  version: '0'
288
289
  requirements: []
289
- rubygems_version: 3.5.1
290
+ rubygems_version: 3.5.6
290
291
  signing_key:
291
292
  specification_version: 4
292
293
  summary: Deploy web apps in containers to servers running Docker with zero downtime.