kamal 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1021 -0
  4. data/bin/kamal +18 -0
  5. data/lib/kamal/cli/accessory.rb +239 -0
  6. data/lib/kamal/cli/app.rb +296 -0
  7. data/lib/kamal/cli/base.rb +171 -0
  8. data/lib/kamal/cli/build.rb +106 -0
  9. data/lib/kamal/cli/healthcheck.rb +20 -0
  10. data/lib/kamal/cli/lock.rb +37 -0
  11. data/lib/kamal/cli/main.rb +249 -0
  12. data/lib/kamal/cli/prune.rb +30 -0
  13. data/lib/kamal/cli/registry.rb +18 -0
  14. data/lib/kamal/cli/server.rb +21 -0
  15. data/lib/kamal/cli/templates/deploy.yml +74 -0
  16. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  17. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  18. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  19. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +109 -0
  20. data/lib/kamal/cli/templates/template.env +2 -0
  21. data/lib/kamal/cli/traefik.rb +111 -0
  22. data/lib/kamal/cli.rb +7 -0
  23. data/lib/kamal/commander.rb +154 -0
  24. data/lib/kamal/commands/accessory.rb +113 -0
  25. data/lib/kamal/commands/app.rb +175 -0
  26. data/lib/kamal/commands/auditor.rb +28 -0
  27. data/lib/kamal/commands/base.rb +65 -0
  28. data/lib/kamal/commands/builder/base.rb +60 -0
  29. data/lib/kamal/commands/builder/multiarch/remote.rb +51 -0
  30. data/lib/kamal/commands/builder/multiarch.rb +29 -0
  31. data/lib/kamal/commands/builder/native/cached.rb +16 -0
  32. data/lib/kamal/commands/builder/native/remote.rb +59 -0
  33. data/lib/kamal/commands/builder/native.rb +20 -0
  34. data/lib/kamal/commands/builder.rb +62 -0
  35. data/lib/kamal/commands/docker.rb +21 -0
  36. data/lib/kamal/commands/healthcheck.rb +57 -0
  37. data/lib/kamal/commands/hook.rb +14 -0
  38. data/lib/kamal/commands/lock.rb +63 -0
  39. data/lib/kamal/commands/prune.rb +38 -0
  40. data/lib/kamal/commands/registry.rb +20 -0
  41. data/lib/kamal/commands/traefik.rb +104 -0
  42. data/lib/kamal/commands.rb +2 -0
  43. data/lib/kamal/configuration/accessory.rb +169 -0
  44. data/lib/kamal/configuration/boot.rb +20 -0
  45. data/lib/kamal/configuration/builder.rb +114 -0
  46. data/lib/kamal/configuration/role.rb +155 -0
  47. data/lib/kamal/configuration/ssh.rb +38 -0
  48. data/lib/kamal/configuration/sshkit.rb +20 -0
  49. data/lib/kamal/configuration.rb +251 -0
  50. data/lib/kamal/sshkit_with_ext.rb +104 -0
  51. data/lib/kamal/tags.rb +39 -0
  52. data/lib/kamal/utils/healthcheck_poller.rb +39 -0
  53. data/lib/kamal/utils/sensitive.rb +19 -0
  54. data/lib/kamal/utils.rb +100 -0
  55. data/lib/kamal/version.rb +3 -0
  56. data/lib/kamal.rb +10 -0
  57. metadata +266 -0
data/bin/kamal ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Prevent failures from being reported twice.
4
+ Thread.report_on_exception = false
5
+
6
+ require "kamal"
7
+
8
+ begin
9
+ Kamal::Cli::Main.start(ARGV)
10
+ rescue SSHKit::Runner::ExecuteError => e
11
+ puts " \e[31mERROR (#{e.cause.class}): #{e.message}\e[0m"
12
+ puts e.cause.backtrace if ENV["VERBOSE"]
13
+ exit 1
14
+ rescue => e
15
+ puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
16
+ puts e.backtrace if ENV["VERBOSE"]
17
+ exit 1
18
+ end
@@ -0,0 +1,239 @@
1
+ class Kamal::Cli::Accessory < Kamal::Cli::Base
2
+ desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
3
+ def boot(name, login: true)
4
+ mutating do
5
+ if name == "all"
6
+ KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
7
+ else
8
+ with_accessory(name) do |accessory|
9
+ directories(name)
10
+ upload(name)
11
+
12
+ on(accessory.hosts) do
13
+ execute *KAMAL.registry.login if login
14
+ execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
15
+ execute *accessory.run
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ desc "upload [NAME]", "Upload accessory files to host", hide: true
23
+ def upload(name)
24
+ mutating do
25
+ with_accessory(name) do |accessory|
26
+ on(accessory.hosts) do
27
+ accessory.files.each do |(local, remote)|
28
+ accessory.ensure_local_file_present(local)
29
+
30
+ execute *accessory.make_directory_for(remote)
31
+ upload! local, remote
32
+ execute :chmod, "755", remote
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ desc "directories [NAME]", "Create accessory directories on host", hide: true
40
+ def directories(name)
41
+ mutating do
42
+ with_accessory(name) do |accessory|
43
+ on(accessory.hosts) do
44
+ accessory.directories.keys.each do |host_path|
45
+ execute *accessory.make_directory(host_path)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container)"
53
+ def reboot(name)
54
+ mutating do
55
+ with_accessory(name) do |accessory|
56
+ on(accessory.hosts) do
57
+ execute *KAMAL.registry.login
58
+ end
59
+
60
+ stop(name)
61
+ remove_container(name)
62
+ boot(name, login: false)
63
+ end
64
+ end
65
+ end
66
+
67
+ desc "start [NAME]", "Start existing accessory container on host"
68
+ def start(name)
69
+ mutating do
70
+ with_accessory(name) do |accessory|
71
+ on(accessory.hosts) do
72
+ execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
73
+ execute *accessory.start
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ desc "stop [NAME]", "Stop existing accessory container on host"
80
+ def stop(name)
81
+ mutating do
82
+ with_accessory(name) do |accessory|
83
+ on(accessory.hosts) do
84
+ execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
85
+ execute *accessory.stop, raise_on_non_zero_exit: false
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ desc "restart [NAME]", "Restart existing accessory container on host"
92
+ def restart(name)
93
+ mutating do
94
+ with_accessory(name) do
95
+ stop(name)
96
+ start(name)
97
+ end
98
+ end
99
+ end
100
+
101
+ desc "details [NAME]", "Show details about accessory on host (use NAME=all to show all accessories)"
102
+ def details(name)
103
+ if name == "all"
104
+ KAMAL.accessory_names.each { |accessory_name| details(accessory_name) }
105
+ else
106
+ with_accessory(name) do |accessory|
107
+ on(accessory.hosts) { puts capture_with_info(*accessory.info) }
108
+ end
109
+ end
110
+ end
111
+
112
+ desc "exec [NAME] [CMD]", "Execute a custom command on servers (use --help to show options)"
113
+ option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
114
+ option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
115
+ def exec(name, cmd)
116
+ with_accessory(name) do |accessory|
117
+ case
118
+ when options[:interactive] && options[:reuse]
119
+ say "Launching interactive command with via SSH from existing container...", :magenta
120
+ run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
121
+
122
+ when options[:interactive]
123
+ say "Launching interactive command via SSH from new container...", :magenta
124
+ run_locally { exec accessory.execute_in_new_container_over_ssh(cmd) }
125
+
126
+ when options[:reuse]
127
+ say "Launching command from existing container...", :magenta
128
+ on(accessory.hosts) do
129
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
130
+ capture_with_info(*accessory.execute_in_existing_container(cmd))
131
+ end
132
+
133
+ else
134
+ say "Launching command from new container...", :magenta
135
+ on(accessory.hosts) do
136
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
137
+ capture_with_info(*accessory.execute_in_new_container(cmd))
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ desc "logs [NAME]", "Show log lines from accessory on host (use --help to show options)"
144
+ option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
145
+ option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
146
+ option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
147
+ option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
148
+ def logs(name)
149
+ with_accessory(name) do |accessory|
150
+ grep = options[:grep]
151
+
152
+ if options[:follow]
153
+ run_locally do
154
+ info "Following logs on #{accessory.hosts}..."
155
+ info accessory.follow_logs(grep: grep)
156
+ exec accessory.follow_logs(grep: grep)
157
+ end
158
+ else
159
+ since = options[:since]
160
+ lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
161
+
162
+ on(accessory.hosts) do
163
+ puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep))
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
170
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
171
+ def remove(name)
172
+ mutating do
173
+ if name == "all"
174
+ KAMAL.accessory_names.each { |accessory_name| remove(accessory_name) }
175
+ else
176
+ 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"
177
+ with_accessory(name) do
178
+ stop(name)
179
+ remove_container(name)
180
+ remove_image(name)
181
+ remove_service_directory(name)
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ desc "remove_container [NAME]", "Remove accessory container from host", hide: true
189
+ def remove_container(name)
190
+ mutating do
191
+ with_accessory(name) do |accessory|
192
+ on(accessory.hosts) do
193
+ execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
194
+ execute *accessory.remove_container
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ desc "remove_image [NAME]", "Remove accessory image from host", hide: true
201
+ def remove_image(name)
202
+ mutating do
203
+ with_accessory(name) do |accessory|
204
+ on(accessory.hosts) do
205
+ execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
206
+ execute *accessory.remove_image
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
213
+ def remove_service_directory(name)
214
+ mutating do
215
+ with_accessory(name) do |accessory|
216
+ on(accessory.hosts) do
217
+ execute *accessory.remove_service_directory
218
+ end
219
+ end
220
+ end
221
+ end
222
+
223
+ private
224
+ def with_accessory(name)
225
+ if accessory = KAMAL.accessory(name)
226
+ yield accessory
227
+ else
228
+ error_on_missing_accessory(name)
229
+ end
230
+ end
231
+
232
+ def error_on_missing_accessory(name)
233
+ options = KAMAL.accessory_names.presence
234
+
235
+ error \
236
+ "No accessory by the name of '#{name}'" +
237
+ (options ? " (options: #{options.to_sentence})" : "")
238
+ end
239
+ end
@@ -0,0 +1,296 @@
1
+ class Kamal::Cli::App < Kamal::Cli::Base
2
+ desc "boot", "Boot app on servers (or reboot app if already running)"
3
+ def boot
4
+ mutating do
5
+ hold_lock_on_error do
6
+ say "Get most recent version available as an image...", :magenta unless options[:version]
7
+ using_version(version_or_latest) do |version|
8
+ say "Start container with version #{version} using a #{KAMAL.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
9
+
10
+ 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_as_latest
13
+ end
14
+
15
+ on(KAMAL.hosts, **KAMAL.boot_strategy) do |host|
16
+ roles = KAMAL.roles_on(host)
17
+
18
+ roles.each do |role|
19
+ app = KAMAL.app(role: role)
20
+ auditor = KAMAL.auditor(role: role)
21
+
22
+ if capture_with_info(*app.container_id_for_version(version, only_running: true), raise_on_non_zero_exit: false).present?
23
+ tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
24
+ info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
25
+ execute *auditor.record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
26
+ execute *app.rename_container(version: version, new_version: tmp_version)
27
+ end
28
+
29
+ execute *auditor.record("Booted app version #{version}"), verbosity: :debug
30
+
31
+ old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
32
+ execute *app.start_or_run(hostname: "#{host}-#{SecureRandom.hex(6)}")
33
+
34
+ Kamal::Utils::HealthcheckPoller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
35
+
36
+ execute *app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ desc "start", "Start existing app container on servers"
45
+ def start
46
+ mutating do
47
+ on(KAMAL.hosts) do |host|
48
+ roles = KAMAL.roles_on(host)
49
+
50
+ roles.each do |role|
51
+ execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
52
+ execute *KAMAL.app(role: role).start, raise_on_non_zero_exit: false
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ desc "stop", "Stop app container on servers"
59
+ def stop
60
+ mutating do
61
+ on(KAMAL.hosts) do |host|
62
+ roles = KAMAL.roles_on(host)
63
+
64
+ roles.each do |role|
65
+ execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
66
+ execute *KAMAL.app(role: role).stop, raise_on_non_zero_exit: false
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ # FIXME: Drop in favor of just containers?
73
+ desc "details", "Show details about app containers"
74
+ def details
75
+ on(KAMAL.hosts) do |host|
76
+ roles = KAMAL.roles_on(host)
77
+
78
+ roles.each do |role|
79
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role).info)
80
+ end
81
+ end
82
+ end
83
+
84
+ desc "exec [CMD]", "Execute a custom command on servers (use --help to show options)"
85
+ option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
86
+ option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
87
+ def exec(cmd)
88
+ case
89
+ when options[:interactive] && options[:reuse]
90
+ say "Get current version of running container...", :magenta unless options[:version]
91
+ using_version(options[:version] || current_running_version) do |version|
92
+ say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
93
+ run_locally { exec KAMAL.app(role: "web").execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host) }
94
+ end
95
+
96
+ when options[:interactive]
97
+ say "Get most recent version available as an image...", :magenta unless options[:version]
98
+ using_version(version_or_latest) do |version|
99
+ say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
100
+ run_locally { exec KAMAL.app(role: KAMAL.primary_host.roles.first).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host) }
101
+ end
102
+
103
+ when options[:reuse]
104
+ say "Get current version of running container...", :magenta unless options[:version]
105
+ using_version(options[:version] || current_running_version) do |version|
106
+ say "Launching command with version #{version} from existing container...", :magenta
107
+
108
+ on(KAMAL.hosts) do |host|
109
+ roles = KAMAL.roles_on(host)
110
+
111
+ roles.each do |role|
112
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
113
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_existing_container(cmd))
114
+ end
115
+ end
116
+ end
117
+
118
+ else
119
+ say "Get most recent version available as an image...", :magenta unless options[:version]
120
+ using_version(version_or_latest) do |version|
121
+ say "Launching command with version #{version} from new container...", :magenta
122
+ on(KAMAL.hosts) do |host|
123
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
124
+ puts_by_host host, capture_with_info(*KAMAL.app.execute_in_new_container(cmd))
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ desc "containers", "Show app containers on servers"
131
+ def containers
132
+ on(KAMAL.hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_containers) }
133
+ end
134
+
135
+ desc "stale_containers", "Detect app stale containers"
136
+ option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
137
+ def stale_containers
138
+ mutating do
139
+ stop = options[:stop]
140
+
141
+ cli = self
142
+
143
+ on(KAMAL.hosts) do |host|
144
+ roles = KAMAL.roles_on(host)
145
+
146
+ roles.each do |role|
147
+ cli.send(:stale_versions, host: host, role: role).each do |version|
148
+ if stop
149
+ puts_by_host host, "Stopping stale container for role #{role} with version #{version}"
150
+ execute *KAMAL.app(role: role).stop(version: version), raise_on_non_zero_exit: false
151
+ else
152
+ puts_by_host host, "Detected stale container for role #{role} with version #{version} (use `kamal app stale_containers --stop` to stop)"
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ desc "images", "Show app images on servers"
161
+ def images
162
+ on(KAMAL.hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_images) }
163
+ end
164
+
165
+ desc "logs", "Show log lines from app on servers (use --help to show options)"
166
+ option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
167
+ option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
168
+ option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
169
+ option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
170
+ def logs
171
+ # FIXME: Catch when app containers aren't running
172
+
173
+ grep = options[:grep]
174
+
175
+ if options[:follow]
176
+ run_locally do
177
+ info "Following logs on #{KAMAL.primary_host}..."
178
+
179
+ KAMAL.specific_roles ||= ["web"]
180
+ role = KAMAL.roles_on(KAMAL.primary_host).first
181
+
182
+ info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, grep: grep)
183
+ exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, grep: grep)
184
+ end
185
+ else
186
+ since = options[:since]
187
+ lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
188
+
189
+ on(KAMAL.hosts) do |host|
190
+ roles = KAMAL.roles_on(host)
191
+
192
+ roles.each do |role|
193
+ begin
194
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role).logs(since: since, lines: lines, grep: grep))
195
+ rescue SSHKit::Command::Failed
196
+ puts_by_host host, "Nothing found"
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ desc "remove", "Remove app containers and images from servers"
204
+ def remove
205
+ mutating do
206
+ stop
207
+ remove_containers
208
+ remove_images
209
+ end
210
+ end
211
+
212
+ desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
213
+ def remove_container(version)
214
+ mutating do
215
+ on(KAMAL.hosts) do |host|
216
+ roles = KAMAL.roles_on(host)
217
+
218
+ roles.each do |role|
219
+ execute *KAMAL.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
220
+ execute *KAMAL.app(role: role).remove_container(version: version)
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ desc "remove_containers", "Remove all app containers from servers", hide: true
227
+ def remove_containers
228
+ mutating do
229
+ on(KAMAL.hosts) do |host|
230
+ roles = KAMAL.roles_on(host)
231
+
232
+ roles.each do |role|
233
+ execute *KAMAL.auditor.record("Removed all app containers", role: role), verbosity: :debug
234
+ execute *KAMAL.app(role: role).remove_containers
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ desc "remove_images", "Remove all app images from servers", hide: true
241
+ def remove_images
242
+ mutating do
243
+ on(KAMAL.hosts) do
244
+ execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
245
+ execute *KAMAL.app.remove_images
246
+ end
247
+ end
248
+ end
249
+
250
+ desc "version", "Show app version currently running on servers"
251
+ def version
252
+ on(KAMAL.hosts) do |host|
253
+ role = KAMAL.roles_on(host).first
254
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role).current_running_version).strip
255
+ end
256
+ end
257
+
258
+ private
259
+ def using_version(new_version)
260
+ if new_version
261
+ begin
262
+ old_version = KAMAL.config.version
263
+ KAMAL.config.version = new_version
264
+ yield new_version
265
+ ensure
266
+ KAMAL.config.version = old_version
267
+ end
268
+ else
269
+ yield KAMAL.config.version
270
+ end
271
+ end
272
+
273
+ def current_running_version(host: KAMAL.primary_host)
274
+ version = nil
275
+ on(host) do
276
+ role = KAMAL.roles_on(host).first
277
+ version = capture_with_info(*KAMAL.app(role: role).current_running_version).strip
278
+ end
279
+ version.presence
280
+ end
281
+
282
+ def stale_versions(host:, role:)
283
+ versions = nil
284
+ on(host) do
285
+ versions = \
286
+ capture_with_info(*KAMAL.app(role: role).list_versions, raise_on_non_zero_exit: false)
287
+ .split("\n")
288
+ .drop(1)
289
+ end
290
+ versions
291
+ end
292
+
293
+ def version_or_latest
294
+ options[:version] || "latest"
295
+ end
296
+ end