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
@@ -0,0 +1,419 @@
1
+ class Kamal::Cli::Proxy < Kamal::Cli::Base
2
+ desc "boot", "Boot proxy on servers"
3
+ def boot
4
+ modify(lock: true) do
5
+ on(KAMAL.hosts) do |host|
6
+ execute *KAMAL.docker.create_network
7
+ rescue SSHKit::Command::Failed => e
8
+ raise unless e.message.include?("already exists")
9
+ end
10
+
11
+ # Skip proxy on loadbalancer host - the loadbalancer will handle it
12
+ proxy_hosts = KAMAL.proxy_hosts
13
+ if KAMAL.config.proxy.loadbalancer_on_proxy_host?
14
+ proxy_hosts = proxy_hosts - [ KAMAL.config.proxy.effective_loadbalancer ]
15
+ end
16
+
17
+ on(proxy_hosts) do |host|
18
+ execute *KAMAL.registry.login
19
+
20
+ version = capture_with_info(*KAMAL.proxy(host).version).strip.presence
21
+
22
+ if version && Kamal::Utils.older_version?(version, Kamal::Configuration::Proxy::Run::MINIMUM_VERSION)
23
+ raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::Proxy::Run::MINIMUM_VERSION}"
24
+ end
25
+ execute *KAMAL.proxy(host).ensure_apps_config_directory
26
+ execute *KAMAL.proxy(host).start_or_run
27
+ end
28
+
29
+ if KAMAL.config.proxy.load_balancing?
30
+ on(KAMAL.config.proxy.effective_loadbalancer) do |host|
31
+ info "Starting loadbalancer on #{host}..."
32
+ execute *KAMAL.registry.login
33
+ execute *KAMAL.loadbalancer.ensure_apps_config_directory
34
+ execute *KAMAL.loadbalancer.start_or_run
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ desc "boot_config <set|get|reset>", "Manage kamal-proxy boot configuration"
41
+ option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
42
+ option :publish_host_ip, type: :string, repeatable: true, default: nil, desc: "Host IP address to bind HTTP/HTTPS traffic to. Defaults to all interfaces"
43
+ option :http_port, type: :numeric, default: Kamal::Configuration::Proxy::Run::DEFAULT_HTTP_PORT, desc: "HTTP port to publish on the host"
44
+ option :https_port, type: :numeric, default: Kamal::Configuration::Proxy::Run::DEFAULT_HTTPS_PORT, desc: "HTTPS port to publish on the host"
45
+ option :log_max_size, type: :string, default: Kamal::Configuration::Proxy::Run::DEFAULT_LOG_MAX_SIZE, desc: "Max size of proxy logs"
46
+ option :registry, type: :string, default: nil, desc: "Registry to use for the proxy image"
47
+ option :repository, type: :string, default: nil, desc: "Repository for the proxy image"
48
+ option :image_version, type: :string, default: nil, desc: "Version of the proxy to run"
49
+ option :metrics_port, type: :numeric, default: nil, desc: "Port to report prometheus metrics on"
50
+ option :debug, type: :boolean, default: false, desc: "Whether to run the proxy in debug mode"
51
+ option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
52
+ def boot_config(subcommand)
53
+ say "The proxy boot_config command is deprecated - set the config in the deploy YAML at proxy/run instead", :yellow
54
+ proxy_boot_config = KAMAL.config.proxy_boot
55
+
56
+ case subcommand
57
+ when "set"
58
+ boot_options = [
59
+ *(proxy_boot_config.publish_args(options[:http_port], options[:https_port], options[:publish_host_ip]) if options[:publish]),
60
+ *(proxy_boot_config.logging_args(options[:log_max_size])),
61
+ *("--expose=#{options[:metrics_port]}" if options[:metrics_port]),
62
+ *options[:docker_options].map { |option| "--#{option}" }
63
+ ]
64
+
65
+ image = [
66
+ options[:registry].presence,
67
+ options[:repository].presence || proxy_boot_config.repository_name,
68
+ proxy_boot_config.image_name
69
+ ].compact.join("/")
70
+
71
+ image_version = options[:image_version]
72
+
73
+ run_command_options = { debug: options[:debug] || nil, "metrics-port": options[:metrics_port] }.compact
74
+ run_command = "kamal-proxy run #{Kamal::Utils.optionize(run_command_options).join(" ")}" if run_command_options.any?
75
+
76
+ on(KAMAL.proxy_hosts) do |host|
77
+ proxy = KAMAL.proxy(host)
78
+ execute(*proxy.ensure_proxy_directory)
79
+ if boot_options != proxy_boot_config.default_boot_options
80
+ upload! StringIO.new(boot_options.join(" ")), proxy_boot_config.options_file
81
+ else
82
+ execute *proxy.reset_boot_options, raise_on_non_zero_exit: false
83
+ end
84
+
85
+ if image != proxy_boot_config.image_default
86
+ upload! StringIO.new(image), proxy_boot_config.image_file
87
+ else
88
+ execute *proxy.reset_image, raise_on_non_zero_exit: false
89
+ end
90
+
91
+ if image_version
92
+ upload! StringIO.new(image_version), proxy_boot_config.image_version_file
93
+ else
94
+ execute *proxy.reset_image_version, raise_on_non_zero_exit: false
95
+ end
96
+
97
+ if run_command
98
+ upload! StringIO.new(run_command), proxy_boot_config.run_command_file
99
+ else
100
+ execute *proxy.reset_run_command, raise_on_non_zero_exit: false
101
+ end
102
+ end
103
+ when "get"
104
+
105
+ on(KAMAL.proxy_hosts) do |host|
106
+ puts "Host #{host}: #{capture_with_info(*KAMAL.proxy(host).boot_config)}"
107
+ end
108
+ when "reset"
109
+ on(KAMAL.proxy_hosts) do |host|
110
+ proxy = KAMAL.proxy(host)
111
+ execute *proxy.reset_boot_options, raise_on_non_zero_exit: false
112
+ execute *proxy.reset_image, raise_on_non_zero_exit: false
113
+ execute *proxy.reset_image_version, raise_on_non_zero_exit: false
114
+ execute *proxy.reset_run_command, raise_on_non_zero_exit: false
115
+ end
116
+ else
117
+ raise ArgumentError, "Unknown boot_config subcommand #{subcommand}"
118
+ end
119
+ end
120
+
121
+ desc "reboot", "Reboot proxy on servers (stop container, remove container, start new container)"
122
+ option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
123
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
124
+ def reboot
125
+ confirming "This will cause a brief outage on each host. Are you sure?" do
126
+ modify(lock: true) do
127
+ # Skip proxy on loadbalancer host - it will be handled by loadbalancer reboot
128
+ proxy_hosts = KAMAL.proxy_hosts
129
+ if KAMAL.config.proxy.loadbalancer_on_proxy_host?
130
+ proxy_hosts = proxy_hosts - [ KAMAL.config.proxy.effective_loadbalancer ]
131
+ end
132
+
133
+ host_groups = options[:rolling] ? proxy_hosts : [ proxy_hosts ]
134
+ host_groups.each do |hosts|
135
+ next if Array(hosts).empty?
136
+
137
+ host_list = Array(hosts).join(",")
138
+ run_hook "pre-proxy-reboot", hosts: host_list
139
+ on(hosts) do |host|
140
+ proxy = KAMAL.proxy(host)
141
+ execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
142
+ execute *KAMAL.registry.login
143
+
144
+ info "Stopping and removing kamal-proxy on #{host}, if running..."
145
+ execute *proxy.stop, raise_on_non_zero_exit: false
146
+ execute *proxy.remove_container
147
+ execute *proxy.ensure_apps_config_directory
148
+
149
+ execute *proxy.run
150
+ end
151
+ run_hook "post-proxy-reboot", hosts: host_list
152
+ end
153
+
154
+ if KAMAL.config.proxy.load_balancing?
155
+ lb_host = KAMAL.config.proxy.effective_loadbalancer
156
+ run_hook "pre-loadbalancer-reboot", hosts: lb_host
157
+
158
+ on(lb_host) do |host|
159
+ execute *KAMAL.auditor.record("Rebooted loadbalancer"), verbosity: :debug
160
+ execute *KAMAL.registry.login
161
+
162
+ info "Stopping and removing #{KAMAL.loadbalancer.container_name} on #{host}, if running..."
163
+ execute *KAMAL.loadbalancer.stop, raise_on_non_zero_exit: false
164
+ execute *KAMAL.loadbalancer.remove_container
165
+ execute *KAMAL.loadbalancer.ensure_apps_config_directory
166
+
167
+ execute *KAMAL.loadbalancer.run
168
+ end
169
+
170
+ run_hook "post-loadbalancer-reboot", hosts: lb_host
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ desc "upgrade", "Upgrade to kamal-proxy on servers (stop container, remove container, start new container, reboot app)", hide: true
177
+ option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
178
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
179
+ def upgrade
180
+ invoke_options = { "version" => KAMAL.config.latest_tag }.merge(options)
181
+
182
+ confirming "This will cause a brief outage on each host. Are you sure?" do
183
+ host_groups = options[:rolling] ? KAMAL.hosts : [ KAMAL.hosts ]
184
+ host_groups.each do |hosts|
185
+ host_list = Array(hosts).join(",")
186
+ say "Upgrading proxy on #{host_list}...", :magenta
187
+ run_hook "pre-proxy-reboot", hosts: host_list
188
+ on(hosts) do |host|
189
+ proxy = KAMAL.proxy(host)
190
+ execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
191
+ execute *KAMAL.registry.login
192
+
193
+ info "Stopping and removing Traefik on #{host}, if running..."
194
+ execute *proxy.cleanup_traefik
195
+
196
+ info "Stopping and removing kamal-proxy on #{host}, if running..."
197
+ execute *proxy.stop, raise_on_non_zero_exit: false
198
+ execute *proxy.remove_container
199
+ execute *proxy.remove_image
200
+ end
201
+
202
+ KAMAL.with_specific_hosts(hosts) do
203
+ invoke "kamal:cli:proxy:boot", [], invoke_options
204
+ reset_invocation(Kamal::Cli::Proxy)
205
+ invoke "kamal:cli:app:boot", [], invoke_options
206
+ reset_invocation(Kamal::Cli::App)
207
+ invoke "kamal:cli:prune:all", [], invoke_options
208
+ reset_invocation(Kamal::Cli::Prune)
209
+ end
210
+
211
+ run_hook "post-proxy-reboot", hosts: host_list
212
+ say "Upgraded proxy on #{host_list}", :magenta
213
+ end
214
+ end
215
+ end
216
+
217
+ desc "start", "Start existing proxy container on servers"
218
+ def start
219
+ modify(lock: true) do
220
+ on(KAMAL.proxy_hosts) do |host|
221
+ execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
222
+ execute *KAMAL.proxy(host).start
223
+ end
224
+ end
225
+ end
226
+
227
+ desc "stop", "Stop existing proxy container on servers"
228
+ def stop
229
+ modify(lock: true) do
230
+ on(KAMAL.proxy_hosts) do |host|
231
+ execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
232
+ execute *KAMAL.proxy(host).stop, raise_on_non_zero_exit: false
233
+ end
234
+ end
235
+ end
236
+
237
+ desc "restart", "Restart existing proxy container on servers"
238
+ def restart
239
+ modify(lock: true) do
240
+ stop
241
+ start
242
+ end
243
+ end
244
+
245
+ desc "details", "Show details about proxy container from servers"
246
+ def details
247
+ quiet = options[:quiet]
248
+ on(KAMAL.proxy_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.proxy(host).info), type: "Proxy", quiet: quiet }
249
+
250
+ if KAMAL.config.proxy.load_balancing?
251
+ on(KAMAL.config.proxy.effective_loadbalancer) do |host|
252
+ puts_by_host host, capture_with_info(*KAMAL.loadbalancer.info), type: "Loadbalancer"
253
+ end
254
+ end
255
+ end
256
+
257
+ desc "logs", "Show log lines from proxy on servers"
258
+ option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
259
+ option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
260
+ option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
261
+ option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
262
+ option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
263
+ def logs
264
+ grep = options[:grep]
265
+ timestamps = !options[:skip_timestamps]
266
+
267
+ if options[:follow]
268
+ run_locally do
269
+ proxy = KAMAL.proxy(KAMAL.primary_host)
270
+ info "Following logs on #{KAMAL.primary_host}..."
271
+ info proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
272
+ exec proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
273
+ end
274
+ else
275
+ since = options[:since]
276
+ lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
277
+
278
+ on(KAMAL.proxy_hosts) do |host|
279
+ puts_by_host host, capture(*KAMAL.proxy(host).logs(timestamps: timestamps, since: since, lines: lines, grep: grep)), type: "Proxy"
280
+ end
281
+ end
282
+ end
283
+
284
+ desc "remove", "Remove proxy container and image from servers"
285
+ option :force, type: :boolean, default: false, desc: "Force removing proxy when apps are still installed"
286
+ def remove
287
+ modify(lock: true) do
288
+ if removal_allowed?(options[:force])
289
+ stop
290
+ remove_container
291
+ remove_image
292
+ remove_proxy_directory
293
+ end
294
+ end
295
+ end
296
+
297
+ desc "loadbalancer STATUS", "Manage the load balancer"
298
+ def loadbalancer(status)
299
+ case status
300
+ when "info"
301
+ if KAMAL.config.proxy.load_balancing?
302
+ on(KAMAL.config.proxy.effective_loadbalancer) do |host|
303
+ puts "Loadbalancer status on #{host}:"
304
+ puts capture_with_info(*KAMAL.loadbalancer.info)
305
+ end
306
+ else
307
+ puts "Load balancing is not configured"
308
+ end
309
+ when "start"
310
+ if KAMAL.config.proxy.load_balancing?
311
+ on(KAMAL.config.proxy.effective_loadbalancer) do |host|
312
+ execute *KAMAL.registry.login
313
+ execute *KAMAL.loadbalancer.start_or_run
314
+ end
315
+ else
316
+ puts "Load balancing is not configured"
317
+ end
318
+ when "stop"
319
+ if KAMAL.config.proxy.load_balancing?
320
+ on(KAMAL.config.proxy.effective_loadbalancer) do |host|
321
+ execute *KAMAL.loadbalancer.stop, raise_on_non_zero_exit: false
322
+ end
323
+ else
324
+ puts "Load balancing is not configured"
325
+ end
326
+ when "logs"
327
+ if KAMAL.config.proxy.load_balancing?
328
+ on(KAMAL.config.proxy.effective_loadbalancer) do |host|
329
+ puts_by_host host, capture(*KAMAL.loadbalancer.logs(timestamps: true)), type: "Loadbalancer"
330
+ end
331
+ else
332
+ puts "Load balancing is not configured"
333
+ end
334
+ when "deploy"
335
+ if KAMAL.config.proxy.load_balancing?
336
+ targets = []
337
+ KAMAL.config.roles.each do |role|
338
+ next unless role.running_proxy?
339
+
340
+ role.hosts.each do |host|
341
+ targets << host
342
+ end
343
+ end
344
+
345
+ on(KAMAL.config.proxy.effective_loadbalancer) do |host|
346
+ info "Deploying to loadbalancer on #{host} with targets: #{targets.join(', ')}"
347
+ execute *KAMAL.loadbalancer.deploy(targets: targets)
348
+ end
349
+ else
350
+ puts "Load balancing is not configured"
351
+ end
352
+ else
353
+ puts "Unknown loadbalancer subcommand: #{status}. Available: info, start, stop, logs, deploy"
354
+ end
355
+ end
356
+
357
+ desc "remove_container", "Remove proxy container from servers", hide: true
358
+ def remove_container
359
+ modify(lock: true) do
360
+ on(KAMAL.proxy_hosts) do
361
+ execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
362
+ execute *KAMAL.proxy(host).remove_container
363
+ end
364
+
365
+ if KAMAL.config.proxy.load_balancing?
366
+ on(KAMAL.config.proxy.effective_loadbalancer) do
367
+ execute *KAMAL.auditor.record("Removed loadbalancer container"), verbosity: :debug
368
+ execute *KAMAL.loadbalancer.remove_container
369
+ end
370
+ end
371
+ end
372
+ end
373
+
374
+ desc "remove_image", "Remove proxy image from servers", hide: true
375
+ def remove_image
376
+ modify(lock: true) do
377
+ on(KAMAL.proxy_hosts) do
378
+ execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
379
+ execute *KAMAL.proxy(host).remove_image
380
+ end
381
+
382
+ if KAMAL.config.proxy.load_balancing?
383
+ on(KAMAL.config.proxy.effective_loadbalancer) do
384
+ execute *KAMAL.auditor.record("Removed loadbalancer image"), verbosity: :debug
385
+ execute *KAMAL.loadbalancer.remove_image
386
+ end
387
+ end
388
+ end
389
+ end
390
+
391
+ desc "remove_proxy_directory", "Remove the proxy directory from servers", hide: true
392
+ def remove_proxy_directory
393
+ modify(lock: true) do
394
+ on(KAMAL.proxy_hosts) do
395
+ execute *KAMAL.proxy(host).remove_proxy_directory, raise_on_non_zero_exit: false
396
+ end
397
+ end
398
+ end
399
+
400
+ private
401
+ def removal_allowed?(force)
402
+ on(KAMAL.proxy_hosts) do |host|
403
+ app_count = capture_with_info(*KAMAL.server.app_directory_count).chomp.to_i
404
+ raise "The are other applications installed on #{host}" if app_count > 0
405
+ end
406
+
407
+ true
408
+ rescue SSHKit::Runner::ExecuteError => e
409
+ raise unless e.message.include?("The are other applications installed on")
410
+
411
+ if force
412
+ say "Forcing, so removing the proxy, even though other apps are installed", :magenta
413
+ else
414
+ say "Not removing the proxy, as other apps are installed, ignore this check with kamal proxy remove --force", :magenta
415
+ end
416
+
417
+ force
418
+ end
419
+ end
@@ -0,0 +1,34 @@
1
+ class Kamal::Cli::Prune < Kamal::Cli::Base
2
+ desc "all", "Prune unused images and stopped containers"
3
+ def all
4
+ modify(lock: true) do
5
+ containers
6
+ images
7
+ end
8
+ end
9
+
10
+ desc "images", "Prune unused images"
11
+ def images
12
+ modify(lock: true) do
13
+ on(KAMAL.hosts) do
14
+ execute *KAMAL.auditor.record("Pruned images"), verbosity: :debug
15
+ execute *KAMAL.prune.dangling_images
16
+ execute *KAMAL.prune.tagged_images
17
+ end
18
+ end
19
+ end
20
+
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"
23
+ def containers
24
+ retain = options.fetch(:retain, KAMAL.config.retain_containers)
25
+ raise "retain must be at least 1" if retain < 1
26
+
27
+ modify(lock: true) do
28
+ on(KAMAL.hosts) do
29
+ execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
30
+ execute *KAMAL.prune.app_containers(retain: retain)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,49 @@
1
+ class Kamal::Cli::Registry < Kamal::Cli::Base
2
+ desc "setup", "Setup local registry or log in to remote registry locally and remotely"
3
+ option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
4
+ option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
5
+ def setup
6
+ ensure_docker_installed unless options[:skip_local]
7
+
8
+ if KAMAL.registry.local?
9
+ run_locally { execute *KAMAL.registry.setup } unless options[:skip_local]
10
+ else
11
+ run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
12
+ on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
13
+ end
14
+ end
15
+
16
+ desc "remove", "Remove local registry or log out of remote registry locally and remotely"
17
+ option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
18
+ option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
19
+ def remove
20
+ if KAMAL.registry.local?
21
+ run_locally { execute *KAMAL.registry.remove, raise_on_non_zero_exit: false } unless options[:skip_local]
22
+ else
23
+ run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
24
+ on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
25
+ end
26
+ end
27
+
28
+ desc "login", "Log in to remote registry locally and remotely"
29
+ option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
30
+ option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
31
+ def login
32
+ if KAMAL.registry.local?
33
+ raise "Cannot use login command with a local registry. Use `kamal registry setup` instead."
34
+ end
35
+
36
+ setup
37
+ end
38
+
39
+ desc "logout", "Log out of remote registry locally and remotely"
40
+ option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
41
+ option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
42
+ def logout
43
+ if KAMAL.registry.local?
44
+ raise "Cannot use logout command with a local registry. Use `kamal registry remove` instead."
45
+ end
46
+
47
+ remove
48
+ end
49
+ end
@@ -0,0 +1,50 @@
1
+ class Kamal::Cli::Secrets < Kamal::Cli::Base
2
+ desc "fetch [SECRETS...]", "Fetch secrets from a vault"
3
+ option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
4
+ option :account, type: :string, required: false, desc: "The account identifier or username"
5
+ option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
6
+ option :inline, type: :boolean, required: false, hidden: true
7
+ def fetch(*secrets)
8
+ adapter = initialize_adapter(options[:adapter])
9
+
10
+ if adapter.requires_account? && options[:account].blank?
11
+ return puts "No value provided for required options '--account'"
12
+ end
13
+
14
+ results = adapter.fetch(secrets, **options.slice(:account, :from).symbolize_keys)
15
+ json = JSON.dump(results)
16
+
17
+ return_or_puts options[:inline] ? json.shellescape : json, inline: options[:inline]
18
+ end
19
+
20
+ desc "extract", "Extract a single secret from the results of a fetch call"
21
+ option :inline, type: :boolean, required: false, hidden: true
22
+ def extract(name, secrets)
23
+ parsed_secrets = JSON.parse(secrets)
24
+ value = parsed_secrets[name] || parsed_secrets.find { |k, v| k.end_with?("/#{name}") }&.last
25
+
26
+ raise "Could not find secret #{name}" if value.nil?
27
+
28
+ return_or_puts value, inline: options[:inline]
29
+ end
30
+
31
+ desc "print", "Print the secrets (for debugging)"
32
+ def print
33
+ KAMAL.config.secrets.to_h.each do |key, value|
34
+ puts "#{key}=#{value}"
35
+ end
36
+ end
37
+
38
+ private
39
+ def initialize_adapter(adapter)
40
+ Kamal::Secrets::Adapters.lookup(adapter)
41
+ end
42
+
43
+ def return_or_puts(value, inline: nil)
44
+ if inline
45
+ value
46
+ else
47
+ puts value
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,70 @@
1
+ class Kamal::Cli::Server < Kamal::Cli::Base
2
+ desc "exec", "Run a custom command on the server (use --help to show options)"
3
+ option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)"
4
+ option :raw, type: :boolean, default: false, desc: "Output raw, unmodified stdout"
5
+ def exec(*cmd)
6
+ raw = options[:raw]
7
+
8
+ if raw && options[:interactive]
9
+ raise ArgumentError, "Raw is not compatible with interactive"
10
+ end
11
+
12
+ with_raw_output(raw) do
13
+ pre_connect_if_required
14
+
15
+ cmd = Kamal::Utils.join_commands(cmd)
16
+ hosts = KAMAL.hosts
17
+ quiet = options[:quiet]
18
+
19
+ case
20
+ when options[:interactive]
21
+ host = KAMAL.primary_host
22
+
23
+ say "Running '#{cmd}' on #{host} interactively...", :magenta
24
+
25
+ run_locally { exec KAMAL.server.run_over_ssh(cmd, host: host) }
26
+ else
27
+ say "Running '#{cmd}' on #{hosts.join(', ')}...", :magenta
28
+
29
+ on(hosts) do |host|
30
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{host}"), verbosity: :debug
31
+ puts_by_host host, capture_with_info(cmd, strip: !raw), quiet: quiet, raw: raw
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ desc "bootstrap", "Set up Docker to run Kamal apps"
38
+ def bootstrap
39
+ modify(lock: true) do
40
+ missing = []
41
+
42
+ on(KAMAL.hosts) do |host|
43
+ unless execute(*KAMAL.docker.installed?, raise_on_non_zero_exit: false)
44
+ if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
45
+ info "Missing Docker on #{host}. Installing…"
46
+ execute *KAMAL.docker.install
47
+
48
+ unless execute(*KAMAL.docker.root?, raise_on_non_zero_exit: false) ||
49
+ execute(*KAMAL.docker.in_docker_group?, raise_on_non_zero_exit: false)
50
+ execute *KAMAL.docker.add_to_docker_group
51
+ begin
52
+ execute *KAMAL.docker.refresh_session
53
+ rescue IOError
54
+ info "Session refreshed due to group change."
55
+ end
56
+ end
57
+ else
58
+ missing << host
59
+ end
60
+ end
61
+ end
62
+
63
+ if missing.any?
64
+ 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/"
65
+ end
66
+
67
+ run_hook "docker-setup"
68
+ end
69
+ end
70
+ end