kamal 1.3.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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