kamal 1.3.1 → 1.5.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +38 -29
  3. data/lib/kamal/cli/app/boot.rb +67 -0
  4. data/lib/kamal/cli/app/prepare_assets.rb +24 -0
  5. data/lib/kamal/cli/app.rb +25 -67
  6. data/lib/kamal/cli/base.rb +23 -8
  7. data/lib/kamal/cli/env.rb +3 -5
  8. data/lib/kamal/cli/main.rb +7 -4
  9. data/lib/kamal/cli/prune.rb +6 -2
  10. data/lib/kamal/cli/server.rb +3 -1
  11. data/lib/kamal/cli/templates/deploy.yml +5 -1
  12. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +7 -0
  13. data/lib/kamal/cli/traefik.rb +16 -13
  14. data/lib/kamal/commander/specifics.rb +49 -0
  15. data/lib/kamal/commander.rb +9 -33
  16. data/lib/kamal/commands/accessory.rb +2 -2
  17. data/lib/kamal/commands/app/assets.rb +12 -12
  18. data/lib/kamal/commands/app/cord.rb +4 -4
  19. data/lib/kamal/commands/app/execution.rb +10 -8
  20. data/lib/kamal/commands/app/images.rb +1 -1
  21. data/lib/kamal/commands/app/logging.rb +2 -2
  22. data/lib/kamal/commands/app.rb +38 -18
  23. data/lib/kamal/commands/auditor.rb +1 -1
  24. data/lib/kamal/commands/base.rb +12 -0
  25. data/lib/kamal/commands/builder/base.rb +22 -5
  26. data/lib/kamal/commands/builder/multiarch.rb +17 -9
  27. data/lib/kamal/commands/builder/native/cached.rb +7 -6
  28. data/lib/kamal/commands/builder/native/remote.rb +9 -9
  29. data/lib/kamal/commands/builder/native.rb +8 -7
  30. data/lib/kamal/commands/docker.rb +10 -1
  31. data/lib/kamal/commands/healthcheck.rb +0 -1
  32. data/lib/kamal/commands/hook.rb +1 -1
  33. data/lib/kamal/commands/lock.rb +19 -9
  34. data/lib/kamal/commands/prune.rb +4 -4
  35. data/lib/kamal/commands/registry.rb +4 -1
  36. data/lib/kamal/commands/server.rb +1 -1
  37. data/lib/kamal/commands/traefik.rb +10 -16
  38. data/lib/kamal/configuration/accessory.rb +10 -20
  39. data/lib/kamal/configuration/boot.rb +1 -1
  40. data/lib/kamal/configuration/builder.rb +11 -3
  41. data/lib/kamal/configuration/env.rb +40 -0
  42. data/lib/kamal/configuration/role.rb +23 -40
  43. data/lib/kamal/configuration.rb +53 -21
  44. data/lib/kamal/env_file.rb +12 -15
  45. data/lib/kamal/sshkit_with_ext.rb +1 -0
  46. data/lib/kamal/utils.rb +7 -3
  47. data/lib/kamal/version.rb +1 -1
  48. data/lib/kamal.rb +1 -1
  49. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2071f73b4e7a6d63939bbfd4f07caacd95aa637964c07fffaa261711f602b5f6
4
- data.tar.gz: 0c265ae0c39673018ece39e7fc16acfcc6e7148efdf6a011827548ffb5a332de
3
+ metadata.gz: d8429a6060103f16d9aadb517e77fbb32fc6c6d2e6b9b3cf2989c8a9309e0067
4
+ data.tar.gz: e997f3c5765639c4b14aad08a81344c5af182904e3d80f4867792ed55a9fc25b
5
5
  SHA512:
6
- metadata.gz: 0d21ac54e6eea13f7e89280d3358a342e54e629f402864b591538d018f19f3275fe88104c06e85a73e3846ddfd606f3106dfb3ab7757629dd931e6b63f111767
7
- data.tar.gz: be9bc4f8a641ef661adb78af0853ee3045d93f40d48c357627f80a5c43a5295125b4629658332b314c45f6134b5b87f03e386b0c432b734a5ca838fda0a027e2
6
+ metadata.gz: 575353b6ea9e9d429a6c6e6b89013fd36f0b371b4edbffbf3ab33f707a9637191872d7463fb670dbe45f9a36bc7a1792335fa9c0709e31d2339b1549c4da32ff
7
+ data.tar.gz: 6a71437092b0ce21cce12a6d127098e2aba2a0b18846026b6df1c9bb1d33f60d27da3ac1c3b553490a2f4daa76a7ae2e725b4f2ee7918d4cc808bc29b556e8f3
@@ -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
@@ -177,7 +177,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
177
177
  if name == "all"
178
178
  KAMAL.accessory_names.each { |accessory_name| remove(accessory_name) }
179
179
  else
180
- if options[:confirmed] || ask("This will remove all containers, images and data directories for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
180
+ confirming "This will remove all containers, images and data directories for #{name}. Are you sure?" do
181
181
  with_accessory(name) do
182
182
  stop(name)
183
183
  remove_container(name)
@@ -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
@@ -226,8 +226,9 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
226
226
 
227
227
  private
228
228
  def with_accessory(name)
229
- if accessory = KAMAL.accessory(name)
230
- yield accessory
229
+ if KAMAL.config.accessory(name)
230
+ accessory = KAMAL.accessory(name)
231
+ yield accessory, accessory_hosts(accessory)
231
232
  else
232
233
  error_on_missing_accessory(name)
233
234
  end
@@ -240,4 +241,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
240
241
  "No accessory by the name of '#{name}'" +
241
242
  (options ? " (options: #{options.to_sentence})" : "")
242
243
  end
244
+
245
+ def accessory_hosts(accessory)
246
+ if KAMAL.specific_hosts&.any?
247
+ KAMAL.specific_hosts & accessory.hosts
248
+ else
249
+ accessory.hosts
250
+ end
251
+ end
243
252
  end
@@ -0,0 +1,67 @@
1
+ class Kamal::Cli::App::Boot
2
+ attr_reader :host, :role, :version, :sshkit
3
+ delegate :execute, :capture_with_info, :info, to: :sshkit
4
+ delegate :uses_cord?, :assets?, to: :role
5
+
6
+ def initialize(host, role, version, sshkit)
7
+ @host = host
8
+ @role = role
9
+ @version = version
10
+ @sshkit = sshkit
11
+ end
12
+
13
+ def run
14
+ old_version = old_version_renamed_if_clashing
15
+
16
+ start_new_version
17
+
18
+ if old_version
19
+ stop_old_version(old_version)
20
+ end
21
+ end
22
+
23
+ private
24
+ def app
25
+ @app ||= KAMAL.app(role: role)
26
+ end
27
+
28
+ def auditor
29
+ @auditor = KAMAL.auditor(role: role)
30
+ end
31
+
32
+ def audit(message)
33
+ execute *auditor.record(message), verbosity: :debug
34
+ end
35
+
36
+ def old_version_renamed_if_clashing
37
+ if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
38
+ renamed_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
39
+ info "Renaming container #{version} to #{renamed_version} as already deployed on #{host}"
40
+ audit("Renaming container #{version} to #{renamed_version}")
41
+ execute *app.rename_container(version: version, new_version: renamed_version)
42
+ end
43
+
44
+ capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip.presence
45
+ end
46
+
47
+ def start_new_version
48
+ audit "Booted app version #{version}"
49
+ execute *app.tie_cord(role.cord_host_file) if uses_cord?
50
+ execute *app.run(hostname: "#{host}-#{SecureRandom.hex(6)}")
51
+ Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
52
+ end
53
+
54
+ def stop_old_version(version)
55
+ if uses_cord?
56
+ cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip
57
+ if cord.present?
58
+ execute *app.cut_cord(cord)
59
+ Kamal::Cli::Healthcheck::Poller.wait_for_unhealthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
60
+ end
61
+ end
62
+
63
+ execute *app.stop(version: version), raise_on_non_zero_exit: false
64
+
65
+ execute *app.clean_up_assets if assets?
66
+ end
67
+ end
@@ -0,0 +1,24 @@
1
+ class Kamal::Cli::App::PrepareAssets
2
+ attr_reader :host, :role, :sshkit
3
+ delegate :execute, :capture_with_info, :info, to: :sshkit
4
+ delegate :assets?, to: :role
5
+
6
+ def initialize(host, role, sshkit)
7
+ @host = host
8
+ @role = role
9
+ @sshkit = sshkit
10
+ end
11
+
12
+ def run
13
+ if assets?
14
+ execute *app.extract_assets
15
+ old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
16
+ execute *app.sync_asset_volumes(old_version: old_version)
17
+ end
18
+ end
19
+
20
+ private
21
+ def app
22
+ @app ||= KAMAL.app(role: role)
23
+ end
24
+ end
data/lib/kamal/cli/app.rb CHANGED
@@ -7,60 +7,23 @@ class Kamal::Cli::App < Kamal::Cli::Base
7
7
  using_version(version_or_latest) do |version|
8
8
  say "Start container with version #{version} using a #{KAMAL.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
9
9
 
10
+ # Assets are prepared in a separate step to ensure they are on all hosts before booting
10
11
  on(KAMAL.hosts) do
11
- execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
12
- execute *KAMAL.app.tag_current_image_as_latest
13
-
14
12
  KAMAL.roles_on(host).each do |role|
15
- app = KAMAL.app(role: role)
16
- role_config = KAMAL.config.role(role)
17
-
18
- if role_config.assets?
19
- execute *app.extract_assets
20
- old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
21
- execute *app.sync_asset_volumes(old_version: old_version)
22
- end
13
+ Kamal::Cli::App::PrepareAssets.new(host, role, self).run
23
14
  end
24
15
  end
25
16
 
26
17
  on(KAMAL.hosts, **KAMAL.boot_strategy) do |host|
27
18
  KAMAL.roles_on(host).each do |role|
28
- app = KAMAL.app(role: role)
29
- auditor = KAMAL.auditor(role: role)
30
- role_config = KAMAL.config.role(role)
31
-
32
- if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
33
- tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
34
- info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
35
- execute *auditor.record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
36
- execute *app.rename_container(version: version, new_version: tmp_version)
37
- end
38
-
39
- old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
40
-
41
- execute *app.tie_cord(role_config.cord_host_file) if role_config.uses_cord?
42
-
43
- execute *auditor.record("Booted app version #{version}"), verbosity: :debug
44
-
45
- execute *app.run(hostname: "#{host}-#{SecureRandom.hex(6)}")
46
-
47
- Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
48
-
49
- if old_version.present?
50
- if role_config.uses_cord?
51
- cord = capture_with_info(*app.cord(version: old_version), raise_on_non_zero_exit: false).strip
52
- if cord.present?
53
- execute *app.cut_cord(cord)
54
- Kamal::Cli::Healthcheck::Poller.wait_for_unhealthy(pause_after_ready: true) { capture_with_info(*app.status(version: old_version)) }
55
- end
56
- end
57
-
58
- execute *app.stop(version: old_version), raise_on_non_zero_exit: false
59
-
60
- execute *app.clean_up_assets if role_config.assets?
61
- end
19
+ Kamal::Cli::App::Boot.new(host, role, version, self).run
62
20
  end
63
21
  end
22
+
23
+ on(KAMAL.hosts) do |host|
24
+ execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
25
+ execute *KAMAL.app.tag_latest_image
26
+ end
64
27
  end
65
28
  end
66
29
  end
@@ -109,13 +72,15 @@ class Kamal::Cli::App < Kamal::Cli::Base
109
72
  desc "exec [CMD]", "Execute a custom command on servers (use --help to show options)"
110
73
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
111
74
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
75
+ option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
112
76
  def exec(cmd)
77
+ env = options[:env]
113
78
  case
114
79
  when options[:interactive] && options[:reuse]
115
80
  say "Get current version of running container...", :magenta unless options[:version]
116
81
  using_version(options[:version] || current_running_version) do |version|
117
82
  say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
118
- run_locally { exec KAMAL.app(role: KAMAL.primary_role).execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host) }
83
+ run_locally { exec KAMAL.app(role: KAMAL.primary_role).execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host, env: env) }
119
84
  end
120
85
 
121
86
  when options[:interactive]
@@ -123,7 +88,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
123
88
  using_version(version_or_latest) do |version|
124
89
  say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
125
90
  run_locally do
126
- exec KAMAL.app(role: KAMAL.primary_role).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host)
91
+ exec KAMAL.app(role: KAMAL.primary_role).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host, env: env)
127
92
  end
128
93
  end
129
94
 
@@ -137,7 +102,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
137
102
 
138
103
  roles.each do |role|
139
104
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
140
- puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_existing_container(cmd))
105
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_existing_container(cmd, env: env))
141
106
  end
142
107
  end
143
108
  end
@@ -151,7 +116,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
151
116
 
152
117
  roles.each do |role|
153
118
  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))
119
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_new_container(cmd, env: env))
155
120
  end
156
121
  end
157
122
  end
@@ -175,7 +140,10 @@ class Kamal::Cli::App < Kamal::Cli::Base
175
140
  roles = KAMAL.roles_on(host)
176
141
 
177
142
  roles.each do |role|
178
- cli.send(:stale_versions, host: host, role: role).each do |version|
143
+ versions = capture_with_info(*KAMAL.app(role: role).list_versions, raise_on_non_zero_exit: false).split("\n")
144
+ versions -= [ capture_with_info(*KAMAL.app(role: role).current_running_version, raise_on_non_zero_exit: false).strip ]
145
+
146
+ versions.each do |version|
179
147
  if stop
180
148
  puts_by_host host, "Stopping stale container for role #{role} with version #{version}"
181
149
  execute *KAMAL.app(role: role).stop(version: version), raise_on_non_zero_exit: false
@@ -202,19 +170,20 @@ class Kamal::Cli::App < Kamal::Cli::Base
202
170
  # FIXME: Catch when app containers aren't running
203
171
 
204
172
  grep = options[:grep]
205
-
173
+ since = options[:since]
206
174
  if options[:follow]
175
+ lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
176
+
207
177
  run_locally do
208
178
  info "Following logs on #{KAMAL.primary_host}..."
209
179
 
210
- KAMAL.specific_roles ||= ["web"]
180
+ KAMAL.specific_roles ||= [ "web" ]
211
181
  role = KAMAL.roles_on(KAMAL.primary_host).first
212
182
 
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)
183
+ info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
184
+ exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
215
185
  end
216
186
  else
217
- since = options[:since]
218
187
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
219
188
 
220
189
  on(KAMAL.hosts) do |host|
@@ -310,18 +279,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
310
279
  version.presence
311
280
  end
312
281
 
313
- def stale_versions(host:, role:)
314
- versions = nil
315
- on(host) do
316
- versions = \
317
- capture_with_info(*KAMAL.app(role: role).list_versions, raise_on_non_zero_exit: false)
318
- .split("\n")
319
- .drop(1)
320
- end
321
- versions
322
- end
323
-
324
282
  def version_or_latest
325
- options[:version] || "latest"
283
+ options[:version] || KAMAL.config.latest_tag
326
284
  end
327
285
  end
@@ -73,7 +73,7 @@ module Kamal::Cli
73
73
  def print_runtime
74
74
  started_at = Time.now
75
75
  yield
76
- return Time.now - started_at
76
+ Time.now - started_at
77
77
  ensure
78
78
  runtime = Time.now - started_at
79
79
  puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
@@ -84,7 +84,7 @@ module Kamal::Cli
84
84
 
85
85
  run_hook "pre-connect"
86
86
 
87
- ensure_run_directory
87
+ ensure_run_and_locks_directory
88
88
 
89
89
  acquire_lock
90
90
 
@@ -103,6 +103,16 @@ module Kamal::Cli
103
103
  release_lock
104
104
  end
105
105
 
106
+ def confirming(question)
107
+ return yield if options[:confirmed]
108
+
109
+ if ask(question, limited_to: %w[ y N ], default: "N") == "y"
110
+ yield
111
+ else
112
+ say "Aborted", :red
113
+ end
114
+ end
115
+
106
116
  def acquire_lock
107
117
  raise_if_locked do
108
118
  say "Acquiring the deploy lock...", :magenta
@@ -123,8 +133,9 @@ module Kamal::Cli
123
133
  yield
124
134
  rescue SSHKit::Runner::ExecuteError => e
125
135
  if e.message =~ /cannot create directory/
136
+ say "Deploy lock already in place!", :red
126
137
  on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
127
- raise LockError, "Deploy lock found"
138
+ raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
128
139
  else
129
140
  raise e
130
141
  end
@@ -146,9 +157,9 @@ module Kamal::Cli
146
157
 
147
158
  say "Running the #{hook} hook...", :magenta
148
159
  run_locally do
149
- KAMAL.with_verbosity(:debug) { execute *KAMAL.hook.run(hook, **details, **extra_details) }
150
- rescue SSHKit::Command::Failed
151
- raise HookError.new("Hook `#{hook}` failed")
160
+ execute *KAMAL.hook.run(hook, **details, **extra_details)
161
+ rescue SSHKit::Command::Failed => e
162
+ raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
152
163
  end
153
164
  end
154
165
  end
@@ -175,10 +186,14 @@ module Kamal::Cli
175
186
  instance_variable_get("@_invocations").first
176
187
  end
177
188
 
178
- def ensure_run_directory
189
+ def ensure_run_and_locks_directory
179
190
  on(KAMAL.hosts) do
180
191
  execute(*KAMAL.server.ensure_run_directory)
181
192
  end
193
+
194
+ on(KAMAL.primary_host) do
195
+ execute(*KAMAL.lock.ensure_locks_directory)
196
+ end
182
197
  end
183
- end
198
+ end
184
199
  end
data/lib/kamal/cli/env.rb CHANGED
@@ -8,22 +8,21 @@ 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! role.env.secrets_io, role.env.secrets_file, mode: 400
14
13
  end
15
14
  end
16
15
 
17
16
  on(KAMAL.traefik_hosts) do
18
17
  execute *KAMAL.traefik.make_env_directory
19
- upload! StringIO.new(KAMAL.traefik.env_file), KAMAL.traefik.host_env_file_path, mode: 400
18
+ upload! KAMAL.traefik.env.secrets_io, KAMAL.traefik.env.secrets_file, mode: 400
20
19
  end
21
20
 
22
21
  on(KAMAL.accessory_hosts) do
23
22
  KAMAL.accessories_on(host).each do |accessory|
24
23
  accessory_config = KAMAL.config.accessory(accessory)
25
24
  execute *KAMAL.accessory(accessory).make_env_directory
26
- upload! StringIO.new(accessory_config.env_file), accessory_config.host_env_file_path, mode: 400
25
+ upload! accessory_config.env.secrets_io, accessory_config.env.secrets_file, mode: 400
27
26
  end
28
27
  end
29
28
  end
@@ -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
@@ -194,7 +197,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
194
197
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
195
198
  def remove
196
199
  mutating do
197
- if options[:confirmed] || ask("This will remove all containers and images. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
200
+ confirming "This will remove all containers and images. Are you sure?" do
198
201
  invoke "kamal:cli:traefik:remove", [], options.without(:confirmed)
199
202
  invoke "kamal:cli:app:remove", [], options.without(:confirmed)
200
203
  invoke "kamal:cli:accessory:remove", [ "all" ], options
@@ -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
@@ -63,7 +63,7 @@ registry:
63
63
  # directories:
64
64
  # - data:/data
65
65
 
66
- # Configure custom arguments for Traefik
66
+ # Configure custom arguments for Traefik. Be sure to reboot traefik when you modify it.
67
67
  # traefik:
68
68
  # args:
69
69
  # accesslog: true
@@ -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