kamal 2.7.0 → 2.11.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/kamal/cli/accessory.rb +27 -7
  4. data/lib/kamal/cli/alias/command.rb +2 -2
  5. data/lib/kamal/cli/app/boot.rb +1 -1
  6. data/lib/kamal/cli/app.rb +74 -115
  7. data/lib/kamal/cli/base.rb +19 -6
  8. data/lib/kamal/cli/build/clone.rb +0 -2
  9. data/lib/kamal/cli/build/port_forwarding.rb +66 -0
  10. data/lib/kamal/cli/build.rb +70 -35
  11. data/lib/kamal/cli/healthcheck/poller.rb +1 -1
  12. data/lib/kamal/cli/main.rb +9 -3
  13. data/lib/kamal/cli/proxy.rb +42 -35
  14. data/lib/kamal/cli/registry.rb +37 -7
  15. data/lib/kamal/cli/secrets.rb +2 -1
  16. data/lib/kamal/cli/server.rb +12 -1
  17. data/lib/kamal/cli/templates/deploy.yml +4 -3
  18. data/lib/kamal/cli/templates/secrets +2 -1
  19. data/lib/kamal/commander.rb +21 -19
  20. data/lib/kamal/commands/accessory.rb +5 -0
  21. data/lib/kamal/commands/app/execution.rb +7 -1
  22. data/lib/kamal/commands/app.rb +1 -0
  23. data/lib/kamal/commands/base.rb +15 -2
  24. data/lib/kamal/commands/builder/base.rb +20 -1
  25. data/lib/kamal/commands/builder/hybrid.rb +3 -3
  26. data/lib/kamal/commands/builder/local.rb +8 -2
  27. data/lib/kamal/commands/builder/pack.rb +5 -5
  28. data/lib/kamal/commands/builder/remote.rb +15 -3
  29. data/lib/kamal/commands/builder.rb +8 -2
  30. data/lib/kamal/commands/docker.rb +17 -1
  31. data/lib/kamal/commands/proxy.rb +22 -3
  32. data/lib/kamal/commands/registry.rb +22 -0
  33. data/lib/kamal/configuration/accessory.rb +56 -25
  34. data/lib/kamal/configuration/boot.rb +4 -0
  35. data/lib/kamal/configuration/builder.rb +10 -3
  36. data/lib/kamal/configuration/docs/accessory.yml +37 -5
  37. data/lib/kamal/configuration/docs/alias.yml +3 -0
  38. data/lib/kamal/configuration/docs/boot.yml +12 -10
  39. data/lib/kamal/configuration/docs/configuration.yml +30 -1
  40. data/lib/kamal/configuration/docs/proxy.yml +48 -16
  41. data/lib/kamal/configuration/docs/registry.yml +12 -4
  42. data/lib/kamal/configuration/docs/ssh.yml +7 -4
  43. data/lib/kamal/configuration/docs/sshkit.yml +8 -0
  44. data/lib/kamal/configuration/env.rb +7 -3
  45. data/lib/kamal/configuration/proxy/boot.rb +4 -9
  46. data/lib/kamal/configuration/proxy/run.rb +143 -0
  47. data/lib/kamal/configuration/proxy.rb +7 -3
  48. data/lib/kamal/configuration/registry.rb +8 -0
  49. data/lib/kamal/configuration/role.rb +15 -3
  50. data/lib/kamal/configuration/ssh.rb +18 -3
  51. data/lib/kamal/configuration/sshkit.rb +4 -0
  52. data/lib/kamal/configuration/validator/proxy.rb +20 -0
  53. data/lib/kamal/configuration/validator/registry.rb +5 -3
  54. data/lib/kamal/configuration/validator.rb +52 -4
  55. data/lib/kamal/configuration/volume.rb +11 -4
  56. data/lib/kamal/configuration.rb +89 -5
  57. data/lib/kamal/secrets/adapters/one_password.rb +1 -1
  58. data/lib/kamal/secrets/adapters/passbolt.rb +1 -2
  59. data/lib/kamal/secrets/adapters/test.rb +3 -1
  60. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +15 -1
  61. data/lib/kamal/secrets.rb +17 -6
  62. data/lib/kamal/sshkit_with_ext.rb +135 -10
  63. data/lib/kamal/utils.rb +3 -3
  64. data/lib/kamal/version.rb +1 -1
  65. data/lib/kamal.rb +1 -0
  66. metadata +18 -2
@@ -1,5 +1,3 @@
1
- require "uri"
2
-
3
1
  class Kamal::Cli::Build < Kamal::Cli::Base
4
2
  class BuildError < StandardError; end
5
3
 
@@ -11,15 +9,16 @@ class Kamal::Cli::Build < Kamal::Cli::Base
11
9
 
12
10
  desc "push", "Build and push app image to registry"
13
11
  option :output, type: :string, default: "registry", banner: "export_type", desc: "Exported type for the build result, and may be any exported type supported by 'buildx --output'."
12
+ option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
14
13
  def push
15
14
  cli = self
16
15
 
17
- # Ensure pre-connect hooks run before the build, they may needed for a remote builder
16
+ # Ensure pre-connect hooks run before the build, they may be needed for a remote builder
18
17
  # or the pre-build hooks.
19
18
  pre_connect_if_required
20
19
 
21
20
  ensure_docker_installed
22
- login_to_registry_locally
21
+ login_to_registry_locally if KAMAL.builder.login_to_registry_locally?
23
22
 
24
23
  run_hook "pre-build"
25
24
 
@@ -37,29 +36,31 @@ class Kamal::Cli::Build < Kamal::Cli::Base
37
36
  say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
38
37
  end
39
38
 
40
- with_env(KAMAL.config.builder.secrets) do
41
- run_locally do
42
- begin
43
- execute *KAMAL.builder.inspect_builder
44
- rescue SSHKit::Command::Failed => e
45
- if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
46
- warn "Missing compatible builder, so creating a new one first"
47
- begin
48
- cli.remove
49
- rescue SSHKit::Command::Failed
50
- raise unless e.message =~ /(context not found|no builder|does not exist)/
39
+ forward_local_registry_port_for_remote_builder do
40
+ with_env(KAMAL.config.builder.secrets) do
41
+ run_locally do
42
+ begin
43
+ execute *KAMAL.builder.inspect_builder
44
+ rescue SSHKit::Command::Failed => e
45
+ if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
46
+ warn "Missing compatible builder, so creating a new one first"
47
+ begin
48
+ cli.remove
49
+ rescue SSHKit::Command::Failed
50
+ raise unless e.message =~ /(context not found|no builder|does not exist)/
51
+ end
52
+ cli.create
53
+ else
54
+ raise
51
55
  end
52
- cli.create
53
- else
54
- raise
55
56
  end
56
- end
57
57
 
58
- # Get the command here to ensure the Dir.chdir doesn't interfere with it
59
- push = KAMAL.builder.push(cli.options[:output])
58
+ # Get the command here to ensure the Dir.chdir doesn't interfere with it
59
+ push = KAMAL.builder.push(cli.options[:output], no_cache: cli.options[:no_cache])
60
60
 
61
- KAMAL.with_verbosity(:debug) do
62
- Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
61
+ KAMAL.with_verbosity(:debug) do
62
+ Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.builder.push_env }
63
+ end
63
64
  end
64
65
  end
65
66
  end
@@ -67,16 +68,18 @@ class Kamal::Cli::Build < Kamal::Cli::Base
67
68
 
68
69
  desc "pull", "Pull app image from registry onto servers"
69
70
  def pull
70
- login_to_registry_remotely
71
-
72
- if (first_hosts = mirror_hosts).any?
73
- #  Pull on a single host per mirror first to seed them
74
- say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
75
- pull_on_hosts(first_hosts)
76
- say "Pulling image on remaining hosts...", :magenta
77
- pull_on_hosts(KAMAL.app_hosts - first_hosts)
78
- else
79
- pull_on_hosts(KAMAL.app_hosts)
71
+ login_to_registry_remotely unless KAMAL.registry.local?
72
+
73
+ forward_local_registry_port(KAMAL.hosts, **KAMAL.config.ssh.options) do
74
+ if (first_hosts = mirror_hosts).any?
75
+ #  Pull on a single host per mirror first to seed them
76
+ say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
77
+ pull_on_hosts(first_hosts)
78
+ say "Pulling image on remaining hosts...", :magenta
79
+ pull_on_hosts(KAMAL.app_hosts - first_hosts)
80
+ else
81
+ pull_on_hosts(KAMAL.app_hosts)
82
+ end
80
83
  end
81
84
  end
82
85
 
@@ -119,6 +122,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
119
122
 
120
123
  desc "dev", "Build using the working directory, tag it as dirty, and push to local image store."
121
124
  option :output, type: :string, default: "docker", banner: "export_type", desc: "Exported type for the build result, and may be any exported type supported by 'buildx --output'."
125
+ option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
122
126
  def dev
123
127
  cli = self
124
128
 
@@ -144,7 +148,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
144
148
 
145
149
  with_env(KAMAL.config.builder.secrets) do
146
150
  run_locally do
147
- build = KAMAL.builder.push(cli.options[:output], tag_as_dirty: true)
151
+ build = KAMAL.builder.push(cli.options[:output], tag_as_dirty: true, no_cache: cli.options[:no_cache])
148
152
  KAMAL.with_verbosity(:debug) do
149
153
  execute(*build)
150
154
  end
@@ -192,7 +196,11 @@ class Kamal::Cli::Build < Kamal::Cli::Base
192
196
 
193
197
  def login_to_registry_locally
194
198
  run_locally do
195
- execute *KAMAL.registry.login
199
+ if KAMAL.registry.local?
200
+ execute *KAMAL.registry.setup
201
+ else
202
+ execute *KAMAL.registry.login
203
+ end
196
204
  end
197
205
  end
198
206
 
@@ -201,4 +209,31 @@ class Kamal::Cli::Build < Kamal::Cli::Base
201
209
  execute *KAMAL.registry.login
202
210
  end
203
211
  end
212
+
213
+ def forward_local_registry_port_for_remote_builder(&block)
214
+ if KAMAL.builder.remote?
215
+ remote_uri = URI(KAMAL.config.builder.remote)
216
+ forward_local_registry_port([ remote_uri.host ], **remote_builder_ssh_options(remote_uri), &block)
217
+ else
218
+ yield
219
+ end
220
+ end
221
+
222
+ def forward_local_registry_port(hosts, **ssh_options, &block)
223
+ if KAMAL.config.registry.local?
224
+ say "Setting up local registry port forwarding to #{hosts.join(', ')}..."
225
+ PortForwarding.new(hosts, KAMAL.config.registry.local_port, **ssh_options).forward(&block)
226
+ else
227
+ yield
228
+ end
229
+ end
230
+
231
+ def remote_builder_ssh_options(remote_uri)
232
+ { user: remote_uri.user,
233
+ port: remote_uri.port,
234
+ keepalive: KAMAL.config.ssh.options[:keepalive],
235
+ keepalive_interval: KAMAL.config.ssh.options[:keepalive_interval],
236
+ logger: KAMAL.config.ssh.options[:logger]
237
+ }.compact
238
+ end
204
239
  end
@@ -1,7 +1,7 @@
1
1
  module Kamal::Cli::Healthcheck::Poller
2
2
  extend self
3
3
 
4
- def wait_for_healthy(role, &block)
4
+ def wait_for_healthy(&block)
5
5
  attempt = 1
6
6
  timeout_at = Time.now + KAMAL.config.deploy_timeout
7
7
  readiness_delay = KAMAL.config.readiness_delay
@@ -1,6 +1,7 @@
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
3
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
4
+ option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
4
5
  def setup
5
6
  print_runtime do
6
7
  with_lock do
@@ -16,6 +17,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
16
17
 
17
18
  desc "deploy", "Deploy app to servers"
18
19
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
20
+ option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
19
21
  def deploy(boot_accessories: false)
20
22
  runtime = print_runtime do
21
23
  invoke_options = deploy_options
@@ -51,6 +53,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
51
53
 
52
54
  desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy and pruning"
53
55
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
56
+ option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
54
57
  def redeploy
55
58
  runtime = print_runtime do
56
59
  invoke_options = deploy_options
@@ -109,8 +112,9 @@ class Kamal::Cli::Main < Kamal::Cli::Base
109
112
 
110
113
  desc "audit", "Show audit log from servers"
111
114
  def audit
115
+ quiet = options[:quiet]
112
116
  on(KAMAL.hosts) do |host|
113
- puts_by_host host, capture_with_info(*KAMAL.auditor.reveal)
117
+ puts_by_host host, capture_with_info(*KAMAL.auditor.reveal), quiet: quiet
114
118
  end
115
119
  end
116
120
 
@@ -182,7 +186,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
182
186
  invoke "kamal:cli:app:remove", [], options.without(:confirmed)
183
187
  invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
184
188
  invoke "kamal:cli:accessory:remove", [ "all" ], options
185
- invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true)
189
+ invoke "kamal:cli:registry:remove", [], options.without(:confirmed).merge(skip_local: true)
186
190
  end
187
191
  end
188
192
  end
@@ -272,6 +276,8 @@ class Kamal::Cli::Main < Kamal::Cli::Base
272
276
  end
273
277
 
274
278
  def deploy_options
275
- { "version" => KAMAL.config.version }.merge(options.without("skip_push"))
279
+ base_options = options.without("skip_push")
280
+ base_options = base_options.except("no_cache") unless base_options["no_cache"]
281
+ { "version" => KAMAL.config.version }.merge(base_options)
276
282
  end
277
283
  end
@@ -11,13 +11,13 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
11
11
  on(KAMAL.proxy_hosts) do |host|
12
12
  execute *KAMAL.registry.login
13
13
 
14
- version = capture_with_info(*KAMAL.proxy.version).strip.presence
14
+ version = capture_with_info(*KAMAL.proxy(host).version).strip.presence
15
15
 
16
- if version && Kamal::Utils.older_version?(version, Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION)
17
- raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}"
16
+ if version && Kamal::Utils.older_version?(version, Kamal::Configuration::Proxy::Run::MINIMUM_VERSION)
17
+ 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}"
18
18
  end
19
- execute *KAMAL.proxy.ensure_apps_config_directory
20
- execute *KAMAL.proxy.start_or_run
19
+ execute *KAMAL.proxy(host).ensure_apps_config_directory
20
+ execute *KAMAL.proxy(host).start_or_run
21
21
  end
22
22
  end
23
23
  end
@@ -25,9 +25,9 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
25
25
  desc "boot_config <set|get|reset>", "Manage kamal-proxy boot configuration"
26
26
  option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
27
27
  option :publish_host_ip, type: :string, repeatable: true, default: nil, desc: "Host IP address to bind HTTP/HTTPS traffic to. Defaults to all interfaces"
28
- option :http_port, type: :numeric, default: Kamal::Configuration::Proxy::Boot::DEFAULT_HTTP_PORT, desc: "HTTP port to publish on the host"
29
- option :https_port, type: :numeric, default: Kamal::Configuration::Proxy::Boot::DEFAULT_HTTPS_PORT, desc: "HTTPS port to publish on the host"
30
- option :log_max_size, type: :string, default: Kamal::Configuration::Proxy::Boot::DEFAULT_LOG_MAX_SIZE, desc: "Max size of proxy logs"
28
+ option :http_port, type: :numeric, default: Kamal::Configuration::Proxy::Run::DEFAULT_HTTP_PORT, desc: "HTTP port to publish on the host"
29
+ option :https_port, type: :numeric, default: Kamal::Configuration::Proxy::Run::DEFAULT_HTTPS_PORT, desc: "HTTPS port to publish on the host"
30
+ option :log_max_size, type: :string, default: Kamal::Configuration::Proxy::Run::DEFAULT_LOG_MAX_SIZE, desc: "Max size of proxy logs"
31
31
  option :registry, type: :string, default: nil, desc: "Registry to use for the proxy image"
32
32
  option :repository, type: :string, default: nil, desc: "Repository for the proxy image"
33
33
  option :image_version, type: :string, default: nil, desc: "Version of the proxy to run"
@@ -35,6 +35,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
35
35
  option :debug, type: :boolean, default: false, desc: "Whether to run the proxy in debug mode"
36
36
  option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
37
37
  def boot_config(subcommand)
38
+ say "The proxy boot_config command is deprecated - set the config in the deploy YAML at proxy/run instead", :yellow
38
39
  proxy_boot_config = KAMAL.config.proxy_boot
39
40
 
40
41
  case subcommand
@@ -58,42 +59,44 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
58
59
  run_command = "kamal-proxy run #{Kamal::Utils.optionize(run_command_options).join(" ")}" if run_command_options.any?
59
60
 
60
61
  on(KAMAL.proxy_hosts) do |host|
61
- execute(*KAMAL.proxy.ensure_proxy_directory)
62
+ proxy = KAMAL.proxy(host)
63
+ execute(*proxy.ensure_proxy_directory)
62
64
  if boot_options != proxy_boot_config.default_boot_options
63
65
  upload! StringIO.new(boot_options.join(" ")), proxy_boot_config.options_file
64
66
  else
65
- execute *KAMAL.proxy.reset_boot_options, raise_on_non_zero_exit: false
67
+ execute *proxy.reset_boot_options, raise_on_non_zero_exit: false
66
68
  end
67
69
 
68
70
  if image != proxy_boot_config.image_default
69
71
  upload! StringIO.new(image), proxy_boot_config.image_file
70
72
  else
71
- execute *KAMAL.proxy.reset_image, raise_on_non_zero_exit: false
73
+ execute *proxy.reset_image, raise_on_non_zero_exit: false
72
74
  end
73
75
 
74
76
  if image_version
75
77
  upload! StringIO.new(image_version), proxy_boot_config.image_version_file
76
78
  else
77
- execute *KAMAL.proxy.reset_image_version, raise_on_non_zero_exit: false
79
+ execute *proxy.reset_image_version, raise_on_non_zero_exit: false
78
80
  end
79
81
 
80
82
  if run_command
81
83
  upload! StringIO.new(run_command), proxy_boot_config.run_command_file
82
84
  else
83
- execute *KAMAL.proxy.reset_run_command, raise_on_non_zero_exit: false
85
+ execute *proxy.reset_run_command, raise_on_non_zero_exit: false
84
86
  end
85
87
  end
86
88
  when "get"
87
89
 
88
90
  on(KAMAL.proxy_hosts) do |host|
89
- puts "Host #{host}: #{capture_with_info(*KAMAL.proxy.boot_config)}"
91
+ puts "Host #{host}: #{capture_with_info(*KAMAL.proxy(host).boot_config)}"
90
92
  end
91
93
  when "reset"
92
94
  on(KAMAL.proxy_hosts) do |host|
93
- execute *KAMAL.proxy.reset_boot_options, raise_on_non_zero_exit: false
94
- execute *KAMAL.proxy.reset_image, raise_on_non_zero_exit: false
95
- execute *KAMAL.proxy.reset_image_version, raise_on_non_zero_exit: false
96
- execute *KAMAL.proxy.reset_run_command, raise_on_non_zero_exit: false
95
+ proxy = KAMAL.proxy(host)
96
+ execute *proxy.reset_boot_options, raise_on_non_zero_exit: false
97
+ execute *proxy.reset_image, raise_on_non_zero_exit: false
98
+ execute *proxy.reset_image_version, raise_on_non_zero_exit: false
99
+ execute *proxy.reset_run_command, raise_on_non_zero_exit: false
97
100
  end
98
101
  else
99
102
  raise ArgumentError, "Unknown boot_config subcommand #{subcommand}"
@@ -111,15 +114,16 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
111
114
  host_list = Array(hosts).join(",")
112
115
  run_hook "pre-proxy-reboot", hosts: host_list
113
116
  on(hosts) do |host|
117
+ proxy = KAMAL.proxy(host)
114
118
  execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
115
119
  execute *KAMAL.registry.login
116
120
 
117
121
  "Stopping and removing kamal-proxy on #{host}, if running..."
118
- execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
119
- execute *KAMAL.proxy.remove_container
120
- execute *KAMAL.proxy.ensure_apps_config_directory
122
+ execute *proxy.stop, raise_on_non_zero_exit: false
123
+ execute *proxy.remove_container
124
+ execute *proxy.ensure_apps_config_directory
121
125
 
122
- execute *KAMAL.proxy.run
126
+ execute *proxy.run
123
127
  end
124
128
  run_hook "post-proxy-reboot", hosts: host_list
125
129
  end
@@ -140,16 +144,17 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
140
144
  say "Upgrading proxy on #{host_list}...", :magenta
141
145
  run_hook "pre-proxy-reboot", hosts: host_list
142
146
  on(hosts) do |host|
147
+ proxy = KAMAL.proxy(host)
143
148
  execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
144
149
  execute *KAMAL.registry.login
145
150
 
146
151
  "Stopping and removing Traefik on #{host}, if running..."
147
- execute *KAMAL.proxy.cleanup_traefik
152
+ execute *proxy.cleanup_traefik
148
153
 
149
154
  "Stopping and removing kamal-proxy on #{host}, if running..."
150
- execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
151
- execute *KAMAL.proxy.remove_container
152
- execute *KAMAL.proxy.remove_image
155
+ execute *proxy.stop, raise_on_non_zero_exit: false
156
+ execute *proxy.remove_container
157
+ execute *proxy.remove_image
153
158
  end
154
159
 
155
160
  KAMAL.with_specific_hosts(hosts) do
@@ -172,7 +177,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
172
177
  with_lock do
173
178
  on(KAMAL.proxy_hosts) do |host|
174
179
  execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
175
- execute *KAMAL.proxy.start
180
+ execute *KAMAL.proxy(host).start
176
181
  end
177
182
  end
178
183
  end
@@ -182,7 +187,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
182
187
  with_lock do
183
188
  on(KAMAL.proxy_hosts) do |host|
184
189
  execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
185
- execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
190
+ execute *KAMAL.proxy(host).stop, raise_on_non_zero_exit: false
186
191
  end
187
192
  end
188
193
  end
@@ -197,7 +202,8 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
197
202
 
198
203
  desc "details", "Show details about proxy container from servers"
199
204
  def details
200
- on(KAMAL.proxy_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.proxy.info), type: "Proxy" }
205
+ quiet = options[:quiet]
206
+ on(KAMAL.proxy_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.proxy(host).info), type: "Proxy", quiet: quiet }
201
207
  end
202
208
 
203
209
  desc "logs", "Show log lines from proxy on servers"
@@ -212,16 +218,17 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
212
218
 
213
219
  if options[:follow]
214
220
  run_locally do
221
+ proxy = KAMAL.proxy(KAMAL.primary_host)
215
222
  info "Following logs on #{KAMAL.primary_host}..."
216
- info KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
217
- exec KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
223
+ info proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
224
+ exec proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
218
225
  end
219
226
  else
220
227
  since = options[:since]
221
228
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
222
229
 
223
230
  on(KAMAL.proxy_hosts) do |host|
224
- puts_by_host host, capture(*KAMAL.proxy.logs(timestamps: timestamps, since: since, lines: lines, grep: grep)), type: "Proxy"
231
+ puts_by_host host, capture(*KAMAL.proxy(host).logs(timestamps: timestamps, since: since, lines: lines, grep: grep)), type: "Proxy"
225
232
  end
226
233
  end
227
234
  end
@@ -244,7 +251,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
244
251
  with_lock do
245
252
  on(KAMAL.proxy_hosts) do
246
253
  execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
247
- execute *KAMAL.proxy.remove_container
254
+ execute *KAMAL.proxy(host).remove_container
248
255
  end
249
256
  end
250
257
  end
@@ -254,7 +261,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
254
261
  with_lock do
255
262
  on(KAMAL.proxy_hosts) do
256
263
  execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
257
- execute *KAMAL.proxy.remove_image
264
+ execute *KAMAL.proxy(host).remove_image
258
265
  end
259
266
  end
260
267
  end
@@ -263,7 +270,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
263
270
  def remove_proxy_directory
264
271
  with_lock do
265
272
  on(KAMAL.proxy_hosts) do
266
- execute *KAMAL.proxy.remove_proxy_directory, raise_on_non_zero_exit: false
273
+ execute *KAMAL.proxy(host).remove_proxy_directory, raise_on_non_zero_exit: false
267
274
  end
268
275
  end
269
276
  end
@@ -1,19 +1,49 @@
1
1
  class Kamal::Cli::Registry < Kamal::Cli::Base
2
- desc "login", "Log in to registry locally and remotely"
2
+ desc "setup", "Setup local registry or log in to remote registry locally and remotely"
3
3
  option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
4
4
  option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
5
- def login
5
+ def setup
6
6
  ensure_docker_installed unless options[:skip_local]
7
7
 
8
- run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
9
- on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
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
10
37
  end
11
38
 
12
- desc "logout", "Log out of registry locally and remotely"
39
+ desc "logout", "Log out of remote registry locally and remotely"
13
40
  option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
14
41
  option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
15
42
  def logout
16
- run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
17
- on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
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
18
48
  end
19
49
  end
@@ -12,8 +12,9 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
12
12
  end
13
13
 
14
14
  results = adapter.fetch(secrets, **options.slice(:account, :from).symbolize_keys)
15
+ json = JSON.dump(results)
15
16
 
16
- return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
17
+ return_or_puts options[:inline] ? json.shellescape : json, inline: options[:inline]
17
18
  end
18
19
 
19
20
  desc "extract", "Extract a single secret from the results of a fetch call"
@@ -6,6 +6,7 @@ class Kamal::Cli::Server < Kamal::Cli::Base
6
6
 
7
7
  cmd = Kamal::Utils.join_commands(cmd)
8
8
  hosts = KAMAL.hosts
9
+ quiet = options[:quiet]
9
10
 
10
11
  case
11
12
  when options[:interactive]
@@ -19,7 +20,7 @@ class Kamal::Cli::Server < Kamal::Cli::Base
19
20
 
20
21
  on(hosts) do |host|
21
22
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{host}"), verbosity: :debug
22
- puts_by_host host, capture_with_info(cmd)
23
+ puts_by_host host, capture_with_info(cmd), quiet: quiet
23
24
  end
24
25
  end
25
26
  end
@@ -34,6 +35,16 @@ class Kamal::Cli::Server < Kamal::Cli::Base
34
35
  if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
35
36
  info "Missing Docker on #{host}. Installing…"
36
37
  execute *KAMAL.docker.install
38
+
39
+ unless execute(*KAMAL.docker.root?, raise_on_non_zero_exit: false) ||
40
+ execute(*KAMAL.docker.in_docker_group?, raise_on_non_zero_exit: false)
41
+ execute *KAMAL.docker.add_to_docker_group
42
+ begin
43
+ execute *KAMAL.docker.refresh_session
44
+ rescue IOError
45
+ info "Session refreshed due to group change."
46
+ end
47
+ end
37
48
  else
38
49
  missing << host
39
50
  end
@@ -25,13 +25,14 @@ proxy:
25
25
 
26
26
  # Credentials for your image host.
27
27
  registry:
28
+ server: localhost:5555
28
29
  # Specify the registry server, if you're not using Docker Hub
29
30
  # server: registry.digitalocean.com / ghcr.io / ...
30
- username: my-user
31
+ # username: my-user
31
32
 
32
33
  # Always use an access token rather than real password (pulled from .kamal/secrets).
33
- password:
34
- - KAMAL_REGISTRY_PASSWORD
34
+ # password:
35
+ # - KAMAL_REGISTRY_PASSWORD
35
36
 
36
37
  # Configure builder setup.
37
38
  builder:
@@ -3,10 +3,11 @@
3
3
  # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
4
4
 
5
5
  # Option 1: Read secrets from the environment
6
- KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
6
+ # KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
7
7
 
8
8
  # Option 2: Read secrets via a command
9
9
  # RAILS_MASTER_KEY=$(cat config/master.key)
10
+ # KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)
10
11
 
11
12
  # Option 3: Read secrets via kamal secrets helpers
12
13
  # These will handle logging in and fetching the secrets in as few calls as possible
@@ -21,7 +21,7 @@ class Kamal::Commander
21
21
  end
22
22
 
23
23
  def config
24
- @config ||= Kamal::Configuration.create_from(**@config_kwargs).tap do |config|
24
+ @config ||= Kamal::Configuration.create_from(**@config_kwargs.to_h).tap do |config|
25
25
  @config_kwargs = nil
26
26
  configure_sshkit_with(config)
27
27
  end
@@ -46,27 +46,19 @@ class Kamal::Commander
46
46
 
47
47
  def specific_roles=(role_names)
48
48
  @specifics = nil
49
- if role_names.present?
50
- @specific_roles = Kamal::Utils.filter_specific_items(role_names, config.roles)
51
-
52
- if @specific_roles.empty?
53
- raise ArgumentError, "No --roles match for #{role_names.join(',')}"
54
- end
55
-
56
- @specific_roles
49
+ @specific_roles = if role_names.present?
50
+ filtered = Kamal::Utils.filter_specific_items(role_names, config.roles)
51
+ raise ArgumentError, "No --roles match for #{role_names.join(',')}" if filtered.empty?
52
+ filtered
57
53
  end
58
54
  end
59
55
 
60
56
  def specific_hosts=(hosts)
61
57
  @specifics = nil
62
- if hosts.present?
63
- @specific_hosts = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
64
-
65
- if @specific_hosts.empty?
66
- raise ArgumentError, "No --hosts match for #{hosts.join(',')}"
67
- end
68
-
69
- @specific_hosts
58
+ @specific_hosts = if hosts.present?
59
+ filtered = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
60
+ raise ArgumentError, "No --hosts match for #{hosts.join(',')}" if filtered.empty?
61
+ filtered
70
62
  end
71
63
  end
72
64
 
@@ -109,8 +101,8 @@ class Kamal::Commander
109
101
  @commands[:lock] ||= Kamal::Commands::Lock.new(config)
110
102
  end
111
103
 
112
- def proxy
113
- @commands[:proxy] ||= Kamal::Commands::Proxy.new(config)
104
+ def proxy(host)
105
+ Kamal::Commands::Proxy.new(config, host: host)
114
106
  end
115
107
 
116
108
  def prune
@@ -129,6 +121,15 @@ class Kamal::Commander
129
121
  config.aliases[name]
130
122
  end
131
123
 
124
+ def resolve_alias(name)
125
+ if @config
126
+ @config.aliases[name]&.command
127
+ else
128
+ raw_config = Kamal::Configuration.load_raw_config(**@config_kwargs.to_h.slice(:config_file, :destination))
129
+ raw_config[:aliases]&.dig(name)
130
+ end
131
+ end
132
+
132
133
  def with_verbosity(level)
133
134
  old_level = self.verbosity
134
135
 
@@ -155,6 +156,7 @@ class Kamal::Commander
155
156
  SSHKit::Backend::Netssh.pool.idle_timeout = config.sshkit.pool_idle_timeout
156
157
  SSHKit::Backend::Netssh.configure do |sshkit|
157
158
  sshkit.max_concurrent_starts = config.sshkit.max_concurrent_starts
159
+ sshkit.dns_retries = config.sshkit.dns_retries
158
160
  sshkit.ssh_options = config.ssh.options
159
161
  end
160
162
  SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs