dash 2.12.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 (142) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +13 -0
  4. data/bin/dash +18 -0
  5. data/bin/kamal +18 -0
  6. data/lib/kamal/cli/accessory.rb +342 -0
  7. data/lib/kamal/cli/alias/command.rb +10 -0
  8. data/lib/kamal/cli/app/assets.rb +24 -0
  9. data/lib/kamal/cli/app/boot.rb +126 -0
  10. data/lib/kamal/cli/app/error_pages.rb +33 -0
  11. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  12. data/lib/kamal/cli/app.rb +368 -0
  13. data/lib/kamal/cli/base.rb +324 -0
  14. data/lib/kamal/cli/build/clone.rb +59 -0
  15. data/lib/kamal/cli/build/port_forwarding.rb +66 -0
  16. data/lib/kamal/cli/build.rb +242 -0
  17. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  18. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  19. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  20. data/lib/kamal/cli/lock.rb +34 -0
  21. data/lib/kamal/cli/main.rb +299 -0
  22. data/lib/kamal/cli/proxy.rb +419 -0
  23. data/lib/kamal/cli/prune.rb +34 -0
  24. data/lib/kamal/cli/registry.rb +49 -0
  25. data/lib/kamal/cli/secrets.rb +50 -0
  26. data/lib/kamal/cli/server.rb +70 -0
  27. data/lib/kamal/cli/templates/deploy.yml +102 -0
  28. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  29. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  30. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  31. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  32. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  33. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  34. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  35. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +122 -0
  36. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  37. data/lib/kamal/cli/templates/secrets +22 -0
  38. data/lib/kamal/cli.rb +9 -0
  39. data/lib/kamal/commander/specifics.rb +62 -0
  40. data/lib/kamal/commander.rb +230 -0
  41. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  42. data/lib/kamal/commands/accessory.rb +118 -0
  43. data/lib/kamal/commands/app/assets.rb +51 -0
  44. data/lib/kamal/commands/app/containers.rb +31 -0
  45. data/lib/kamal/commands/app/error_pages.rb +9 -0
  46. data/lib/kamal/commands/app/execution.rb +38 -0
  47. data/lib/kamal/commands/app/images.rb +13 -0
  48. data/lib/kamal/commands/app/logging.rb +28 -0
  49. data/lib/kamal/commands/app/proxy.rb +32 -0
  50. data/lib/kamal/commands/app.rb +125 -0
  51. data/lib/kamal/commands/auditor.rb +39 -0
  52. data/lib/kamal/commands/base.rb +147 -0
  53. data/lib/kamal/commands/builder/base.rb +143 -0
  54. data/lib/kamal/commands/builder/clone.rb +32 -0
  55. data/lib/kamal/commands/builder/cloud.rb +22 -0
  56. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  57. data/lib/kamal/commands/builder/local.rb +20 -0
  58. data/lib/kamal/commands/builder/pack.rb +46 -0
  59. data/lib/kamal/commands/builder/remote.rb +75 -0
  60. data/lib/kamal/commands/builder.rb +54 -0
  61. data/lib/kamal/commands/docker.rb +50 -0
  62. data/lib/kamal/commands/hook.rb +20 -0
  63. data/lib/kamal/commands/loadbalancer.rb +130 -0
  64. data/lib/kamal/commands/lock.rb +70 -0
  65. data/lib/kamal/commands/proxy.rb +150 -0
  66. data/lib/kamal/commands/prune.rb +38 -0
  67. data/lib/kamal/commands/registry.rb +38 -0
  68. data/lib/kamal/commands/server.rb +15 -0
  69. data/lib/kamal/commands.rb +2 -0
  70. data/lib/kamal/configuration/accessory.rb +280 -0
  71. data/lib/kamal/configuration/alias.rb +15 -0
  72. data/lib/kamal/configuration/boot.rb +29 -0
  73. data/lib/kamal/configuration/builder.rb +218 -0
  74. data/lib/kamal/configuration/docs/accessory.yml +160 -0
  75. data/lib/kamal/configuration/docs/alias.yml +29 -0
  76. data/lib/kamal/configuration/docs/boot.yml +21 -0
  77. data/lib/kamal/configuration/docs/builder.yml +132 -0
  78. data/lib/kamal/configuration/docs/configuration.yml +228 -0
  79. data/lib/kamal/configuration/docs/env.yml +118 -0
  80. data/lib/kamal/configuration/docs/logging.yml +21 -0
  81. data/lib/kamal/configuration/docs/output.yml +25 -0
  82. data/lib/kamal/configuration/docs/proxy.yml +207 -0
  83. data/lib/kamal/configuration/docs/registry.yml +64 -0
  84. data/lib/kamal/configuration/docs/role.yml +54 -0
  85. data/lib/kamal/configuration/docs/servers.yml +27 -0
  86. data/lib/kamal/configuration/docs/ssh.yml +81 -0
  87. data/lib/kamal/configuration/docs/sshkit.yml +31 -0
  88. data/lib/kamal/configuration/env/tag.rb +13 -0
  89. data/lib/kamal/configuration/env.rb +42 -0
  90. data/lib/kamal/configuration/loadbalancer.rb +34 -0
  91. data/lib/kamal/configuration/logging.rb +33 -0
  92. data/lib/kamal/configuration/output.rb +34 -0
  93. data/lib/kamal/configuration/proxy/boot.rb +124 -0
  94. data/lib/kamal/configuration/proxy/run.rb +152 -0
  95. data/lib/kamal/configuration/proxy.rb +156 -0
  96. data/lib/kamal/configuration/registry.rb +40 -0
  97. data/lib/kamal/configuration/role.rb +247 -0
  98. data/lib/kamal/configuration/servers.rb +25 -0
  99. data/lib/kamal/configuration/ssh.rb +76 -0
  100. data/lib/kamal/configuration/sshkit.rb +26 -0
  101. data/lib/kamal/configuration/validation.rb +27 -0
  102. data/lib/kamal/configuration/validator/accessory.rb +13 -0
  103. data/lib/kamal/configuration/validator/alias.rb +15 -0
  104. data/lib/kamal/configuration/validator/builder.rb +15 -0
  105. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  106. data/lib/kamal/configuration/validator/env.rb +54 -0
  107. data/lib/kamal/configuration/validator/proxy.rb +47 -0
  108. data/lib/kamal/configuration/validator/registry.rb +27 -0
  109. data/lib/kamal/configuration/validator/role.rb +13 -0
  110. data/lib/kamal/configuration/validator/servers.rb +7 -0
  111. data/lib/kamal/configuration/validator.rb +251 -0
  112. data/lib/kamal/configuration/volume.rb +29 -0
  113. data/lib/kamal/configuration.rb +465 -0
  114. data/lib/kamal/docker.rb +30 -0
  115. data/lib/kamal/env_file.rb +44 -0
  116. data/lib/kamal/git.rb +37 -0
  117. data/lib/kamal/otel_shipper.rb +176 -0
  118. data/lib/kamal/output/base_logger.rb +29 -0
  119. data/lib/kamal/output/file_logger.rb +51 -0
  120. data/lib/kamal/output/formatter.rb +36 -0
  121. data/lib/kamal/output/otel_logger.rb +70 -0
  122. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +59 -0
  123. data/lib/kamal/secrets/adapters/base.rb +33 -0
  124. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  125. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  126. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  127. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  128. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  129. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  130. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  131. data/lib/kamal/secrets/adapters/passbolt.rb +129 -0
  132. data/lib/kamal/secrets/adapters/test.rb +16 -0
  133. data/lib/kamal/secrets/adapters.rb +16 -0
  134. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +47 -0
  135. data/lib/kamal/secrets.rb +53 -0
  136. data/lib/kamal/sshkit_with_ext.rb +273 -0
  137. data/lib/kamal/tags.rb +40 -0
  138. data/lib/kamal/utils/sensitive.rb +20 -0
  139. data/lib/kamal/utils.rb +110 -0
  140. data/lib/kamal/version.rb +3 -0
  141. data/lib/kamal.rb +15 -0
  142. metadata +388 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b02db379375fb5102b7464401e6d1d2ca56a0209df5965cd36b87face85a4cb7
4
+ data.tar.gz: 78e9cd7a40f9056abd03f39204e309a01bc4c884705c8a931d9aa5f622045863
5
+ SHA512:
6
+ metadata.gz: 3a6c6bb668d700f596f330283d1c695b6d4120c653cc9bf8ff9be9f123f78b9d0725787d3dc276f8145c9f72cbee9786faac7fe68108337c3ccc622adb9c7ed5
7
+ data.tar.gz: 508b2b00b942d6f47abeb79f17a1d126b025e1b5aec08a65b9dc48088205b2967540f6877328067a64151a333a28c4d9b71cc3201fdad19555dd715d09e9e443
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2023 David Heinemeier Hansson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # Kamal: Deploy web apps anywhere
2
+
3
+ From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to seamlessly switch requests between containers. Works seamlessly across multiple servers, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker.
4
+
5
+ ➡️ See [kamal-deploy.org](https://kamal-deploy.org) for documentation on [installation](https://kamal-deploy.org/docs/installation), [configuration](https://kamal-deploy.org/docs/configuration), and [commands](https://kamal-deploy.org/docs/commands).
6
+
7
+ ## Contributing to the documentation
8
+
9
+ Please help us improve Kamal's documentation on [the basecamp/kamal-site repository](https://github.com/basecamp/kamal-site).
10
+
11
+ ## License
12
+
13
+ Kamal is released under the [MIT License](https://opensource.org/licenses/MIT).
data/bin/dash 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
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,342 @@
1
+ require "active_support/core_ext/array/conversions"
2
+ require "concurrent/array"
3
+
4
+ class Kamal::Cli::Accessory < Kamal::Cli::Base
5
+ desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
6
+ def boot(name, prepare: true)
7
+ modify(lock: true) do
8
+ if name == "all"
9
+ KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
10
+ else
11
+ prepare(name) if prepare
12
+
13
+ with_accessory(name) do |accessory, hosts|
14
+ booted_hosts = Concurrent::Array.new
15
+ on(hosts) do |host|
16
+ booted_hosts << host.to_s if capture_with_info(*accessory.info(all: true, quiet: true)).strip.presence
17
+ end
18
+
19
+ if booted_hosts.any?
20
+ say "Skipping booting `#{name}` on #{booted_hosts.sort.join(", ")}, a container already exists", :yellow
21
+ hosts -= booted_hosts
22
+ end
23
+
24
+ directories(name)
25
+ upload(name)
26
+
27
+ on(hosts) do |host|
28
+ execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
29
+ execute *accessory.ensure_env_directory
30
+ upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
31
+ execute *accessory.run(host: host)
32
+
33
+ if accessory.running_proxy?
34
+ target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
35
+ execute *accessory.deploy(target: target)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ desc "upload [NAME]", "Upload accessory files to host", hide: true
44
+ def upload(name)
45
+ modify(lock: true) do
46
+ with_accessory(name) do |accessory, hosts|
47
+ on(hosts) do
48
+ accessory.files.each do |(local, config)|
49
+ remote = config[:host_path]
50
+ accessory.ensure_local_file_present(local)
51
+
52
+ execute *accessory.make_directory_for(remote)
53
+ upload! local, remote
54
+ execute :chmod, config[:mode], remote
55
+ execute :chown, config[:owner], remote if config[:owner]
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ desc "directories [NAME]", "Create accessory directories on host", hide: true
63
+ def directories(name)
64
+ modify(lock: true) do
65
+ with_accessory(name) do |accessory, hosts|
66
+ on(hosts) do
67
+ accessory.directories.each do |(local, config)|
68
+ execute *accessory.make_directory(local)
69
+ execute :chmod, config[:mode], local if config[:mode]
70
+ execute :chown, config[:owner], local if config[:owner]
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container; use NAME=all to boot all accessories)"
78
+ def reboot(name)
79
+ modify(lock: true) do
80
+ if name == "all"
81
+ KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
82
+ else
83
+ prepare(name)
84
+ pull_image(name)
85
+ stop(name)
86
+ remove_container(name)
87
+ boot(name, prepare: false)
88
+ end
89
+ end
90
+ end
91
+
92
+ desc "start [NAME]", "Start existing accessory container on host"
93
+ def start(name)
94
+ modify(lock: true) do
95
+ with_accessory(name) do |accessory, hosts|
96
+ on(hosts) do
97
+ execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
98
+ execute *accessory.start
99
+ if accessory.running_proxy?
100
+ target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
101
+ execute *accessory.deploy(target: target)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ desc "stop [NAME]", "Stop existing accessory container on host"
109
+ def stop(name)
110
+ modify(lock: true) do
111
+ with_accessory(name) do |accessory, hosts|
112
+ on(hosts) do
113
+ execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
114
+ execute *accessory.stop, raise_on_non_zero_exit: false
115
+
116
+ if accessory.running_proxy?
117
+ target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
118
+ execute *accessory.remove if target
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ desc "restart [NAME]", "Restart existing accessory container on host"
126
+ def restart(name)
127
+ modify(lock: true) do
128
+ stop(name)
129
+ start(name)
130
+ end
131
+ end
132
+
133
+ desc "details [NAME]", "Show details about accessory on host (use NAME=all to show all accessories)"
134
+ def details(name)
135
+ quiet = options[:quiet]
136
+ if name == "all"
137
+ KAMAL.accessory_names.each { |accessory_name| details(accessory_name) }
138
+ else
139
+ type = "Accessory #{name}"
140
+ with_accessory(name) do |accessory, hosts|
141
+ on(hosts) { puts_by_host host, capture_with_info(*accessory.info), type: type, quiet: quiet }
142
+ end
143
+ end
144
+ end
145
+
146
+ desc "exec [NAME] [CMD...]", "Execute a custom command on servers within the accessory container (use --help to show options)"
147
+ option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
148
+ option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
149
+ option :raw, type: :boolean, default: false, desc: "Output raw, unmodified stdout"
150
+ def exec(name, *cmd)
151
+ raw = options[:raw]
152
+
153
+ if raw && options[:interactive]
154
+ raise ArgumentError, "Raw is not compatible with interactive"
155
+ end
156
+
157
+ with_raw_output(raw) do
158
+ pre_connect_if_required
159
+
160
+ cmd = Kamal::Utils.join_commands(cmd)
161
+ quiet = options[:quiet]
162
+
163
+ with_accessory(name) do |accessory, hosts|
164
+ case
165
+ when options[:interactive] && options[:reuse]
166
+ say "Launching interactive command via SSH from existing container...", :magenta
167
+ run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
168
+
169
+ when options[:interactive]
170
+ say "Launching interactive command via SSH from new container...", :magenta
171
+ on(accessory.hosts.first) { execute *KAMAL.registry.login }
172
+ run_locally { exec accessory.execute_in_new_container_over_ssh(cmd) }
173
+
174
+ when options[:reuse]
175
+ say "Launching command from existing container...", :magenta
176
+ on(hosts) do |host|
177
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
178
+ puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd), strip: !raw), quiet: quiet, raw: raw
179
+ end
180
+
181
+ else
182
+ say "Launching command from new container...", :magenta
183
+ on(hosts) do |host|
184
+ execute *KAMAL.registry.login
185
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
186
+ puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd), strip: !raw), quiet: quiet, raw: raw
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ desc "logs [NAME]", "Show log lines from accessory on host (use --help to show options)"
194
+ option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
195
+ option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
196
+ option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
197
+ option :grep_options, desc: "Additional options supplied to grep"
198
+ option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
199
+ option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
200
+ def logs(name)
201
+ with_accessory(name) do |accessory, hosts|
202
+ grep = options[:grep]
203
+ grep_options = options[:grep_options]
204
+ timestamps = !options[:skip_timestamps]
205
+
206
+ if options[:follow]
207
+ run_locally do
208
+ info "Following logs on #{hosts}..."
209
+ info accessory.follow_logs(timestamps: timestamps, grep: grep, grep_options: grep_options)
210
+ exec accessory.follow_logs(timestamps: timestamps, grep: grep, grep_options: grep_options)
211
+ end
212
+ else
213
+ since = options[:since]
214
+ lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
215
+
216
+ on(hosts) do
217
+ puts capture_with_info(*accessory.logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
218
+ end
219
+ end
220
+ end
221
+ end
222
+
223
+ desc "pull_image [NAME]", "Pull accessory image on host", hide: true
224
+ def pull_image(name)
225
+ modify(lock: true) do
226
+ with_accessory(name) do |accessory, hosts|
227
+ on(hosts) do
228
+ execute *KAMAL.auditor.record("Pull #{name} accessory image"), verbosity: :debug
229
+ execute *accessory.pull_image
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
236
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
237
+ def remove(name)
238
+ confirming "This will remove all containers, images and data directories for #{name}. Are you sure?" do
239
+ modify(lock: true) do
240
+ if name == "all"
241
+ KAMAL.accessory_names.each { |accessory_name| remove_accessory(accessory_name) }
242
+ else
243
+ remove_accessory(name)
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ desc "remove_container [NAME]", "Remove accessory container from host", hide: true
250
+ def remove_container(name)
251
+ modify(lock: true) do
252
+ with_accessory(name) do |accessory, hosts|
253
+ on(hosts) do
254
+ execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
255
+ execute *accessory.remove_container
256
+ end
257
+ end
258
+ end
259
+ end
260
+
261
+ desc "remove_image [NAME]", "Remove accessory image from host", hide: true
262
+ def remove_image(name)
263
+ modify(lock: true) do
264
+ with_accessory(name) do |accessory, hosts|
265
+ on(hosts) do
266
+ execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
267
+ execute *accessory.remove_image
268
+ end
269
+ end
270
+ end
271
+ end
272
+
273
+ desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
274
+ def remove_service_directory(name)
275
+ modify(lock: true) do
276
+ with_accessory(name) do |accessory, hosts|
277
+ on(hosts) do
278
+ execute *accessory.remove_service_directory
279
+ end
280
+ end
281
+ end
282
+ end
283
+
284
+ desc "upgrade", "Upgrade accessories from Kamal 1.x to 2.0 (restart them in 'kamal' network)"
285
+ option :rolling, type: :boolean, default: false, desc: "Upgrade one host at a time"
286
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
287
+ def upgrade(name)
288
+ confirming "This will restart all accessories" do
289
+ modify(lock: true) do
290
+ host_groups = options[:rolling] ? KAMAL.accessory_hosts : [ KAMAL.accessory_hosts ]
291
+ host_groups.each do |hosts|
292
+ host_list = Array(hosts).join(",")
293
+ KAMAL.with_specific_hosts(hosts) do
294
+ say "Upgrading #{name} accessories on #{host_list}...", :magenta
295
+ reboot name
296
+ say "Upgraded #{name} accessories on #{host_list}...", :magenta
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
302
+
303
+ private
304
+ def with_accessory(name)
305
+ if KAMAL.config.accessory(name)
306
+ accessory = KAMAL.accessory(name)
307
+ yield accessory, accessory_hosts(accessory)
308
+ else
309
+ error_on_missing_accessory(name)
310
+ end
311
+ end
312
+
313
+ def error_on_missing_accessory(name)
314
+ options = KAMAL.accessory_names.presence
315
+
316
+ error \
317
+ "No accessory by the name of '#{name}'" +
318
+ (options ? " (options: #{options.to_sentence})" : "")
319
+ end
320
+
321
+ def accessory_hosts(accessory)
322
+ KAMAL.accessory_hosts & accessory.hosts
323
+ end
324
+
325
+ def remove_accessory(name)
326
+ stop(name)
327
+ remove_container(name)
328
+ remove_image(name)
329
+ remove_service_directory(name)
330
+ end
331
+
332
+ def prepare(name)
333
+ with_accessory(name) do |accessory, hosts|
334
+ on(hosts) do
335
+ execute *KAMAL.registry.login(registry_config: accessory.registry)
336
+ execute *KAMAL.docker.create_network
337
+ rescue SSHKit::Command::Failed => e
338
+ raise unless e.message.include?("already exists")
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,10 @@
1
+ class Kamal::Cli::Alias::Command < Thor::DynamicCommand
2
+ def run(instance, args = [])
3
+ if (command = KAMAL.resolve_alias(name))
4
+ KAMAL.reset
5
+ Kamal::Cli::Main.start(Shellwords.split(command) + ARGV[1..-1])
6
+ else
7
+ super
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,24 @@
1
+ class Kamal::Cli::App::Assets
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, host: host)
23
+ end
24
+ end
@@ -0,0 +1,126 @@
1
+ class Kamal::Cli::App::Boot
2
+ attr_reader :host, :role, :version, :barrier, :sshkit
3
+ delegate :execute, :capture_with_info, :capture_with_pretty_json, :info, :error, :upload!, to: :sshkit
4
+ delegate :assets?, :running_proxy?, to: :role
5
+
6
+ def initialize(host, role, sshkit, version, barrier)
7
+ @host = host
8
+ @role = role
9
+ @version = version
10
+ @barrier = barrier
11
+ @sshkit = sshkit
12
+ end
13
+
14
+ def run
15
+ old_version = old_version_renamed_if_clashing
16
+
17
+ wait_at_barrier if queuer?
18
+
19
+ begin
20
+ start_new_version
21
+ rescue => e
22
+ close_barrier if gatekeeper?
23
+ stop_new_version
24
+ raise
25
+ end
26
+
27
+ release_barrier if gatekeeper?
28
+
29
+ if old_version
30
+ stop_old_version(old_version)
31
+ end
32
+ end
33
+
34
+ private
35
+ def old_version_renamed_if_clashing
36
+ if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
37
+ renamed_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
38
+ info "Renaming container #{version} to #{renamed_version} as already deployed on #{host}"
39
+ audit("Renaming container #{version} to #{renamed_version}")
40
+ execute *app.rename_container(version: version, new_version: renamed_version)
41
+ end
42
+
43
+ capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip.presence
44
+ end
45
+
46
+ def start_new_version
47
+ audit "Booted app version #{version}"
48
+ hostname = "#{host.to_s[0...51].chomp(".")}-#{SecureRandom.hex(6)}"
49
+
50
+ execute *app.ensure_env_directory
51
+ upload! role.secrets_io(host), role.secrets_path, mode: "0600"
52
+
53
+ execute *app.run(hostname: hostname)
54
+ if running_proxy?
55
+ endpoint = capture_with_info(*app.container_id_for_version(version)).strip
56
+ raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
57
+ execute *app.deploy(target: endpoint)
58
+ else
59
+ Kamal::Cli::Healthcheck::Poller.wait_for_healthy { capture_with_info(*app.status(version: version)) }
60
+ end
61
+ rescue => e
62
+ error "Failed to boot #{role} on #{host}"
63
+ raise e
64
+ end
65
+
66
+ def stop_new_version
67
+ execute *app.stop(version: version), raise_on_non_zero_exit: false
68
+ end
69
+
70
+ def stop_old_version(version)
71
+ execute *app.stop(version: version), raise_on_non_zero_exit: false
72
+ execute *app.clean_up_assets if assets?
73
+ execute *app.clean_up_error_pages if KAMAL.config.error_pages_path
74
+ end
75
+
76
+ def release_barrier
77
+ if barrier.open
78
+ info "First #{KAMAL.primary_role} container is healthy on #{host}, booting any other roles"
79
+ end
80
+ end
81
+
82
+ def wait_at_barrier
83
+ info "Waiting for the first healthy #{KAMAL.primary_role} container before booting #{role} on #{host}..."
84
+ barrier.wait
85
+ info "First #{KAMAL.primary_role} container is healthy, booting #{role} on #{host}..."
86
+ rescue Kamal::Cli::Healthcheck::Error
87
+ info "First #{KAMAL.primary_role} container is unhealthy, not booting #{role} on #{host}"
88
+ raise
89
+ end
90
+
91
+ def close_barrier
92
+ if barrier.close
93
+ info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles"
94
+ begin
95
+ error capture_with_info(*app.logs(container_id: app.container_id_for_version(version)))
96
+ error capture_with_info(*app.container_health_log(version: version))
97
+ rescue SSHKit::Command::Failed
98
+ error "Could not fetch logs for #{version}"
99
+ end
100
+ end
101
+ end
102
+
103
+ def barrier_role?
104
+ role == KAMAL.primary_role
105
+ end
106
+
107
+ def app
108
+ @app ||= KAMAL.app(role: role, host: host)
109
+ end
110
+
111
+ def auditor
112
+ @auditor = KAMAL.auditor(role: role)
113
+ end
114
+
115
+ def audit(message)
116
+ execute *auditor.record(message), verbosity: :debug
117
+ end
118
+
119
+ def gatekeeper?
120
+ barrier && barrier_role?
121
+ end
122
+
123
+ def queuer?
124
+ barrier && !barrier_role?
125
+ end
126
+ end
@@ -0,0 +1,33 @@
1
+ class Kamal::Cli::App::ErrorPages
2
+ ERROR_PAGES_GLOB = "{4??.html,5??.html}"
3
+
4
+ attr_reader :host, :sshkit
5
+ delegate :upload!, :execute, to: :sshkit
6
+
7
+ def initialize(host, sshkit)
8
+ @host = host
9
+ @sshkit = sshkit
10
+ end
11
+
12
+ def run
13
+ if KAMAL.config.error_pages_path
14
+ with_error_pages_tmpdir do |local_error_pages_dir|
15
+ execute *KAMAL.app.create_error_pages_directory
16
+ upload! local_error_pages_dir, KAMAL.config.proxy_boot.error_pages_directory, mode: "0700", recursive: true
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+ def with_error_pages_tmpdir
23
+ Dir.mktmpdir("kamal-error-pages") do |tmpdir|
24
+ error_pages_dir = File.join(tmpdir, KAMAL.config.version)
25
+ FileUtils.mkdir(error_pages_dir)
26
+
27
+ if (files = Dir[File.join(KAMAL.config.error_pages_path, ERROR_PAGES_GLOB)]).any?
28
+ FileUtils.cp(files, error_pages_dir)
29
+ yield error_pages_dir
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ class Kamal::Cli::App::SslCertificates
2
+ attr_reader :host, :role, :sshkit
3
+ delegate :execute, :info, :upload!, to: :sshkit
4
+
5
+ def initialize(host, role, sshkit)
6
+ @host = host
7
+ @role = role
8
+ @sshkit = sshkit
9
+ end
10
+
11
+ def run
12
+ if role.running_proxy? && role.proxy.custom_ssl_certificate?
13
+ info "Writing SSL certificates for #{role.name} on #{host}"
14
+ execute *app.create_ssl_directory
15
+ if cert_content = role.proxy.certificate_pem_content
16
+ upload!(StringIO.new(cert_content), role.proxy.host_tls_cert, mode: "0644")
17
+ end
18
+ if key_content = role.proxy.private_key_pem_content
19
+ upload!(StringIO.new(key_content), role.proxy.host_tls_key, mode: "0644")
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+ def app
26
+ @app ||= KAMAL.app(role: role, host: host)
27
+ end
28
+ end