kamal 2.10.1 → 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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/kamal/cli/accessory.rb +48 -39
  4. data/lib/kamal/cli/alias/command.rb +2 -2
  5. data/lib/kamal/cli/app.rb +57 -48
  6. data/lib/kamal/cli/base.rb +118 -17
  7. data/lib/kamal/cli/build.rb +10 -7
  8. data/lib/kamal/cli/lock.rb +5 -16
  9. data/lib/kamal/cli/main.rb +59 -53
  10. data/lib/kamal/cli/proxy.rb +9 -9
  11. data/lib/kamal/cli/prune.rb +3 -3
  12. data/lib/kamal/cli/server.rb +34 -15
  13. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +1 -1
  14. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +1 -1
  15. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +1 -1
  16. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +1 -1
  17. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +1 -1
  18. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +1 -1
  19. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +1 -1
  20. data/lib/kamal/cli/templates/secrets +4 -0
  21. data/lib/kamal/commander.rb +71 -17
  22. data/lib/kamal/commands/accessory.rb +3 -2
  23. data/lib/kamal/commands/app/logging.rb +1 -1
  24. data/lib/kamal/commands/app.rb +1 -1
  25. data/lib/kamal/commands/base.rb +15 -2
  26. data/lib/kamal/commands/builder/clone.rb +2 -1
  27. data/lib/kamal/commands/docker.rb +17 -1
  28. data/lib/kamal/commands/proxy.rb +1 -1
  29. data/lib/kamal/configuration/accessory.rb +13 -5
  30. data/lib/kamal/configuration/docs/alias.yml +3 -0
  31. data/lib/kamal/configuration/docs/configuration.yml +37 -2
  32. data/lib/kamal/configuration/docs/env.yml +6 -4
  33. data/lib/kamal/configuration/docs/output.yml +25 -0
  34. data/lib/kamal/configuration/docs/role.yml +1 -0
  35. data/lib/kamal/configuration/docs/ssh.yml +8 -0
  36. data/lib/kamal/configuration/output.rb +34 -0
  37. data/lib/kamal/configuration/proxy/run.rb +10 -1
  38. data/lib/kamal/configuration/role.rb +18 -6
  39. data/lib/kamal/configuration/ssh.rb +5 -1
  40. data/lib/kamal/configuration/validator.rb +29 -2
  41. data/lib/kamal/configuration.rb +41 -3
  42. data/lib/kamal/git.rb +1 -1
  43. data/lib/kamal/otel_shipper.rb +176 -0
  44. data/lib/kamal/output/base_logger.rb +29 -0
  45. data/lib/kamal/output/file_logger.rb +51 -0
  46. data/lib/kamal/output/formatter.rb +36 -0
  47. data/lib/kamal/output/otel_logger.rb +70 -0
  48. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +10 -2
  49. data/lib/kamal/secrets/adapters/passbolt.rb +1 -1
  50. data/lib/kamal/secrets.rb +1 -1
  51. data/lib/kamal/sshkit_with_ext.rb +9 -4
  52. data/lib/kamal/version.rb +1 -1
  53. metadata +23 -2
@@ -2,22 +2,17 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
2
2
  desc "status", "Report lock status"
3
3
  def status
4
4
  handle_missing_lock do
5
- on(KAMAL.primary_host) do
6
- puts capture_with_debug(*KAMAL.lock.status)
7
- end
5
+ puts capture_lock_status
8
6
  end
9
7
  end
10
8
 
11
9
  desc "acquire", "Acquire the deploy lock"
12
10
  option :message, aliases: "-m", type: :string, desc: "A lock message", required: true
13
11
  def acquire
14
- message = options[:message]
15
12
  ensure_run_directory
16
13
 
17
14
  raise_if_locked do
18
- on(KAMAL.primary_host) do
19
- execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug
20
- end
15
+ execute_lock_acquire(options[:message])
21
16
  say "Acquired the deploy lock"
22
17
  end
23
18
  end
@@ -25,9 +20,7 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
25
20
  desc "release", "Release the deploy lock"
26
21
  def release
27
22
  handle_missing_lock do
28
- on(KAMAL.primary_host) do
29
- execute *KAMAL.lock.release, verbosity: :debug
30
- end
23
+ execute_lock_release
31
24
  say "Released the deploy lock"
32
25
  end
33
26
  end
@@ -35,11 +28,7 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
35
28
  private
36
29
  def handle_missing_lock
37
30
  yield
38
- rescue SSHKit::Runner::ExecuteError => e
39
- if e.message =~ /No such file or directory/
40
- say "There is no deploy lock"
41
- else
42
- raise
43
- end
31
+ rescue LockMissingError
32
+ say "There is no deploy lock"
44
33
  end
45
34
  end
@@ -4,7 +4,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
4
4
  option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
5
5
  def setup
6
6
  print_runtime do
7
- with_lock do
7
+ modify(lock: true) do
8
8
  invoke_options = deploy_options
9
9
 
10
10
  say "Ensure Docker is installed...", :magenta
@@ -19,88 +19,94 @@ class Kamal::Cli::Main < Kamal::Cli::Base
19
19
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
20
20
  option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
21
21
  def deploy(boot_accessories: false)
22
- runtime = print_runtime do
23
- invoke_options = deploy_options
22
+ modify do
23
+ runtime = print_runtime do
24
+ invoke_options = deploy_options
24
25
 
25
- if options[:skip_push]
26
- say "Pull app image...", :magenta
27
- invoke "kamal:cli:build:pull", [], invoke_options
28
- else
29
- say "Build and push app image...", :magenta
30
- invoke "kamal:cli:build:deliver", [], invoke_options
31
- end
26
+ if options[:skip_push]
27
+ say "Pull app image...", :magenta
28
+ invoke "kamal:cli:build:pull", [], invoke_options
29
+ else
30
+ say "Build and push app image...", :magenta
31
+ invoke "kamal:cli:build:deliver", [], invoke_options
32
+ end
32
33
 
33
- with_lock do
34
- run_hook "pre-deploy", secrets: true
34
+ modify(lock: true) do
35
+ run_hook "pre-deploy", secrets: true
35
36
 
36
- say "Ensure kamal-proxy is running...", :magenta
37
- invoke "kamal:cli:proxy:boot", [], invoke_options
37
+ say "Ensure kamal-proxy is running...", :magenta
38
+ invoke "kamal:cli:proxy:boot", [], invoke_options
38
39
 
39
- invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options if boot_accessories
40
+ invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options if boot_accessories
40
41
 
41
- say "Detect stale containers...", :magenta
42
- invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
42
+ say "Detect stale containers...", :magenta
43
+ invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
43
44
 
44
- invoke "kamal:cli:app:boot", [], invoke_options
45
+ invoke "kamal:cli:app:boot", [], invoke_options
45
46
 
46
- say "Prune old containers and images...", :magenta
47
- invoke "kamal:cli:prune:all", [], invoke_options
47
+ say "Prune old containers and images...", :magenta
48
+ invoke "kamal:cli:prune:all", [], invoke_options
49
+ end
48
50
  end
49
- end
50
51
 
51
- run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
52
+ run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
53
+ end
52
54
  end
53
55
 
54
56
  desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy and pruning"
55
57
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
56
58
  option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
57
59
  def redeploy
58
- runtime = print_runtime do
59
- invoke_options = deploy_options
60
+ modify do
61
+ runtime = print_runtime do
62
+ invoke_options = deploy_options
60
63
 
61
- if options[:skip_push]
62
- say "Pull app image...", :magenta
63
- invoke "kamal:cli:build:pull", [], invoke_options
64
- else
65
- say "Build and push app image...", :magenta
66
- invoke "kamal:cli:build:deliver", [], invoke_options
67
- end
64
+ if options[:skip_push]
65
+ say "Pull app image...", :magenta
66
+ invoke "kamal:cli:build:pull", [], invoke_options
67
+ else
68
+ say "Build and push app image...", :magenta
69
+ invoke "kamal:cli:build:deliver", [], invoke_options
70
+ end
68
71
 
69
- with_lock do
70
- run_hook "pre-deploy", secrets: true
72
+ modify(lock: true) do
73
+ run_hook "pre-deploy", secrets: true
71
74
 
72
- say "Detect stale containers...", :magenta
73
- invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
75
+ say "Detect stale containers...", :magenta
76
+ invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
74
77
 
75
- invoke "kamal:cli:app:boot", [], invoke_options
78
+ invoke "kamal:cli:app:boot", [], invoke_options
79
+ end
76
80
  end
77
- end
78
81
 
79
- run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
82
+ run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
83
+ end
80
84
  end
81
85
 
82
86
  desc "rollback [VERSION]", "Rollback app to VERSION"
83
87
  def rollback(version)
84
88
  rolled_back = false
85
- runtime = print_runtime do
86
- with_lock do
87
- invoke_options = deploy_options
88
89
 
89
- KAMAL.config.version = version
90
- old_version = nil
90
+ modify do
91
+ runtime = print_runtime do
92
+ modify(lock: true) do
93
+ invoke_options = deploy_options
91
94
 
92
- if container_available?(version)
93
- run_hook "pre-deploy", secrets: true
95
+ KAMAL.config.version = version
94
96
 
95
- invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version)
96
- rolled_back = true
97
- else
98
- say "The app version '#{version}' is not available as a container (use 'kamal app containers' for available versions)", :red
97
+ if container_available?(version)
98
+ run_hook "pre-deploy", secrets: true
99
+
100
+ invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version)
101
+ rolled_back = true
102
+ else
103
+ say "The app version '#{version}' is not available as a container (use 'kamal app containers' for available versions)", :red
104
+ end
99
105
  end
100
106
  end
101
- end
102
107
 
103
- run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s if rolled_back
108
+ run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s if rolled_back
109
+ end
104
110
  end
105
111
 
106
112
  desc "details", "Show details about all containers"
@@ -182,7 +188,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
182
188
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
183
189
  def remove
184
190
  confirming "This will remove all containers and images. Are you sure?" do
185
- with_lock do
191
+ modify(lock: true) do
186
192
  invoke "kamal:cli:app:remove", [], options.without(:confirmed)
187
193
  invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
188
194
  invoke "kamal:cli:accessory:remove", [ "all" ], options
@@ -196,7 +202,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
196
202
  option :rolling, type: :boolean, default: false, desc: "Upgrade one host at a time"
197
203
  def upgrade
198
204
  confirming "This will replace Traefik with kamal-proxy and restart all accessories" do
199
- with_lock do
205
+ modify(lock: true) do
200
206
  if options[:rolling]
201
207
  KAMAL.hosts.each do |host|
202
208
  KAMAL.with_specific_hosts(host) do
@@ -1,7 +1,7 @@
1
1
  class Kamal::Cli::Proxy < Kamal::Cli::Base
2
2
  desc "boot", "Boot proxy on servers"
3
3
  def boot
4
- with_lock do
4
+ modify(lock: true) do
5
5
  on(KAMAL.hosts) do |host|
6
6
  execute *KAMAL.docker.create_network
7
7
  rescue SSHKit::Command::Failed => e
@@ -108,7 +108,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
108
108
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
109
109
  def reboot
110
110
  confirming "This will cause a brief outage on each host. Are you sure?" do
111
- with_lock do
111
+ modify(lock: true) do
112
112
  host_groups = options[:rolling] ? KAMAL.proxy_hosts : [ KAMAL.proxy_hosts ]
113
113
  host_groups.each do |hosts|
114
114
  host_list = Array(hosts).join(",")
@@ -174,7 +174,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
174
174
 
175
175
  desc "start", "Start existing proxy container on servers"
176
176
  def start
177
- with_lock do
177
+ modify(lock: true) do
178
178
  on(KAMAL.proxy_hosts) do |host|
179
179
  execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
180
180
  execute *KAMAL.proxy(host).start
@@ -184,7 +184,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
184
184
 
185
185
  desc "stop", "Stop existing proxy container on servers"
186
186
  def stop
187
- with_lock do
187
+ modify(lock: true) do
188
188
  on(KAMAL.proxy_hosts) do |host|
189
189
  execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
190
190
  execute *KAMAL.proxy(host).stop, raise_on_non_zero_exit: false
@@ -194,7 +194,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
194
194
 
195
195
  desc "restart", "Restart existing proxy container on servers"
196
196
  def restart
197
- with_lock do
197
+ modify(lock: true) do
198
198
  stop
199
199
  start
200
200
  end
@@ -236,7 +236,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
236
236
  desc "remove", "Remove proxy container and image from servers"
237
237
  option :force, type: :boolean, default: false, desc: "Force removing proxy when apps are still installed"
238
238
  def remove
239
- with_lock do
239
+ modify(lock: true) do
240
240
  if removal_allowed?(options[:force])
241
241
  stop
242
242
  remove_container
@@ -248,7 +248,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
248
248
 
249
249
  desc "remove_container", "Remove proxy container from servers", hide: true
250
250
  def remove_container
251
- with_lock do
251
+ modify(lock: true) do
252
252
  on(KAMAL.proxy_hosts) do
253
253
  execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
254
254
  execute *KAMAL.proxy(host).remove_container
@@ -258,7 +258,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
258
258
 
259
259
  desc "remove_image", "Remove proxy image from servers", hide: true
260
260
  def remove_image
261
- with_lock do
261
+ modify(lock: true) do
262
262
  on(KAMAL.proxy_hosts) do
263
263
  execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
264
264
  execute *KAMAL.proxy(host).remove_image
@@ -268,7 +268,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
268
268
 
269
269
  desc "remove_proxy_directory", "Remove the proxy directory from servers", hide: true
270
270
  def remove_proxy_directory
271
- with_lock do
271
+ modify(lock: true) do
272
272
  on(KAMAL.proxy_hosts) do
273
273
  execute *KAMAL.proxy(host).remove_proxy_directory, raise_on_non_zero_exit: false
274
274
  end
@@ -1,7 +1,7 @@
1
1
  class Kamal::Cli::Prune < Kamal::Cli::Base
2
2
  desc "all", "Prune unused images and stopped containers"
3
3
  def all
4
- with_lock do
4
+ modify(lock: true) do
5
5
  containers
6
6
  images
7
7
  end
@@ -9,7 +9,7 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
9
9
 
10
10
  desc "images", "Prune unused images"
11
11
  def images
12
- with_lock do
12
+ modify(lock: true) do
13
13
  on(KAMAL.hosts) do
14
14
  execute *KAMAL.auditor.record("Pruned images"), verbosity: :debug
15
15
  execute *KAMAL.prune.dangling_images
@@ -24,7 +24,7 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
24
24
  retain = options.fetch(:retain, KAMAL.config.retain_containers)
25
25
  raise "retain must be at least 1" if retain < 1
26
26
 
27
- with_lock do
27
+ modify(lock: true) do
28
28
  on(KAMAL.hosts) do
29
29
  execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
30
30
  execute *KAMAL.prune.app_containers(retain: retain)
@@ -1,33 +1,42 @@
1
1
  class Kamal::Cli::Server < Kamal::Cli::Base
2
2
  desc "exec", "Run a custom command on the server (use --help to show options)"
3
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"
4
5
  def exec(*cmd)
5
- pre_connect_if_required
6
+ raw = options[:raw]
6
7
 
7
- cmd = Kamal::Utils.join_commands(cmd)
8
- hosts = KAMAL.hosts
9
- quiet = options[:quiet]
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]
10
18
 
11
- case
12
- when options[:interactive]
13
- host = KAMAL.primary_host
19
+ case
20
+ when options[:interactive]
21
+ host = KAMAL.primary_host
14
22
 
15
- say "Running '#{cmd}' on #{host} interactively...", :magenta
23
+ say "Running '#{cmd}' on #{host} interactively...", :magenta
16
24
 
17
- run_locally { exec KAMAL.server.run_over_ssh(cmd, host: host) }
18
- else
19
- say "Running '#{cmd}' on #{hosts.join(', ')}...", :magenta
25
+ run_locally { exec KAMAL.server.run_over_ssh(cmd, host: host) }
26
+ else
27
+ say "Running '#{cmd}' on #{hosts.join(', ')}...", :magenta
20
28
 
21
- on(hosts) do |host|
22
- execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{host}"), verbosity: :debug
23
- puts_by_host host, capture_with_info(cmd), quiet: quiet
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
24
33
  end
25
34
  end
26
35
  end
27
36
 
28
37
  desc "bootstrap", "Set up Docker to run Kamal apps"
29
38
  def bootstrap
30
- with_lock do
39
+ modify(lock: true) do
31
40
  missing = []
32
41
 
33
42
  on(KAMAL.hosts) do |host|
@@ -35,6 +44,16 @@ class Kamal::Cli::Server < Kamal::Cli::Base
35
44
  if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
36
45
  info "Missing Docker on #{host}. Installing…"
37
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
38
57
  else
39
58
  missing << host
40
59
  end
@@ -1,3 +1,3 @@
1
- #!/bin/sh
1
+ #!/usr/bin/env sh
2
2
 
3
3
  echo "Docker set up on $KAMAL_HOSTS..."
@@ -1,3 +1,3 @@
1
- #!/bin/sh
1
+ #!/usr/bin/env sh
2
2
 
3
3
  echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..."
@@ -1,4 +1,4 @@
1
- #!/bin/sh
1
+ #!/usr/bin/env sh
2
2
 
3
3
  # A sample post-deploy hook
4
4
  #
@@ -1,3 +1,3 @@
1
- #!/bin/sh
1
+ #!/usr/bin/env sh
2
2
 
3
3
  echo "Rebooted kamal-proxy on $KAMAL_HOSTS"
@@ -1,3 +1,3 @@
1
- #!/bin/sh
1
+ #!/usr/bin/env sh
2
2
 
3
3
  echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..."
@@ -1,4 +1,4 @@
1
- #!/bin/sh
1
+ #!/usr/bin/env sh
2
2
 
3
3
  # A sample pre-build hook
4
4
  #
@@ -1,3 +1,3 @@
1
- #!/bin/sh
1
+ #!/usr/bin/env sh
2
2
 
3
3
  echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."
@@ -1,6 +1,10 @@
1
1
  # Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
2
2
  # and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
3
3
  # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
4
+ #
5
+ # When deploying with destinations, shared secrets can go in .kamal/secrets-common and
6
+ # destination-specific secrets in .kamal/secrets.<destination>. This .kamal/secrets file is used
7
+ # only when no destination is selected.
4
8
 
5
9
  # Option 1: Read secrets from the environment
6
10
  # KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
@@ -1,9 +1,11 @@
1
1
  require "active_support/core_ext/enumerable"
2
2
  require "active_support/core_ext/module/delegation"
3
3
  require "active_support/core_ext/object/blank"
4
+ require "active_support/broadcast_logger"
5
+ require "active_support/notifications"
4
6
 
5
7
  class Kamal::Commander
6
- attr_accessor :verbosity, :holding_lock, :connected
8
+ attr_accessor :verbosity, :holding_lock, :connected, :logging, :lock_wait, :lock_wait_timeout, :lock_wait_interval
7
9
  attr_reader :specific_roles, :specific_hosts
8
10
  delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :app_hosts, :proxy_hosts, :accessory_hosts, to: :specifics
9
11
 
@@ -15,8 +17,14 @@ class Kamal::Commander
15
17
  self.verbosity = :info
16
18
  self.holding_lock = ENV["KAMAL_LOCK"] == "true"
17
19
  self.connected = false
20
+ self.logging = false
21
+ self.lock_wait = false
22
+ self.lock_wait_timeout = 900
23
+ self.lock_wait_interval = 15
24
+ @modify_depth = 0
18
25
  @specifics = @specific_roles = @specific_hosts = nil
19
26
  @config = @config_kwargs = nil
27
+ @output_logger = nil
20
28
  @commands = {}
21
29
  end
22
30
 
@@ -46,27 +54,19 @@ class Kamal::Commander
46
54
 
47
55
  def specific_roles=(role_names)
48
56
  @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
57
+ @specific_roles = if role_names.present?
58
+ filtered = Kamal::Utils.filter_specific_items(role_names, config.roles)
59
+ raise ArgumentError, "No --roles match for #{role_names.join(',')}" if filtered.empty?
60
+ filtered
57
61
  end
58
62
  end
59
63
 
60
64
  def specific_hosts=(hosts)
61
65
  @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
66
+ @specific_hosts = if hosts.present?
67
+ filtered = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
68
+ raise ArgumentError, "No --hosts match for #{hosts.join(',')}" if filtered.empty?
69
+ filtered
70
70
  end
71
71
  end
72
72
 
@@ -129,6 +129,15 @@ class Kamal::Commander
129
129
  config.aliases[name]
130
130
  end
131
131
 
132
+ def resolve_alias(name)
133
+ if @config
134
+ @config.aliases[name]&.command
135
+ else
136
+ raw_config = Kamal::Configuration.load_raw_config(**@config_kwargs.to_h.slice(:config_file, :destination))
137
+ raw_config[:aliases]&.dig(name)
138
+ end
139
+ end
140
+
132
141
  def with_verbosity(level)
133
142
  old_level = self.verbosity
134
143
 
@@ -141,6 +150,22 @@ class Kamal::Commander
141
150
  SSHKit.config.output_verbosity = old_level
142
151
  end
143
152
 
153
+ def modify(command:, subcommand:)
154
+ @logging = true
155
+ if modify_started
156
+ ActiveSupport::Notifications.instrument("modify.kamal",
157
+ command: command, subcommand: subcommand, destination: config.destination, hosts: hosts) { yield }
158
+ else
159
+ yield
160
+ end
161
+ ensure
162
+ output_logger.close if modify_finished
163
+ end
164
+
165
+ def log(line)
166
+ output_logger << "#{line}\n" if logging
167
+ end
168
+
144
169
  def holding_lock?
145
170
  self.holding_lock
146
171
  end
@@ -150,6 +175,20 @@ class Kamal::Commander
150
175
  end
151
176
 
152
177
  private
178
+ def output_logger
179
+ @output_logger ||= ActiveSupport::BroadcastLogger.new
180
+ end
181
+
182
+ def modify_started
183
+ @modify_depth += 1
184
+ @modify_depth == 1
185
+ end
186
+
187
+ def modify_finished
188
+ @modify_depth -= 1
189
+ @modify_depth == 0
190
+ end
191
+
153
192
  # Lazy setup of SSHKit
154
193
  def configure_sshkit_with(config)
155
194
  SSHKit::Backend::Netssh.pool.idle_timeout = config.sshkit.pool_idle_timeout
@@ -160,6 +199,21 @@ class Kamal::Commander
160
199
  end
161
200
  SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
162
201
  SSHKit.config.output_verbosity = verbosity
202
+
203
+ configure_output_with(config)
204
+ end
205
+
206
+ def configure_output_with(config)
207
+ return unless config.output.enabled?
208
+
209
+ config.output.loggers.each { |logger| output_logger.broadcast_to(logger) }
210
+
211
+ SSHKit.config.output = Kamal::Output::Formatter.new($stdout, output_logger)
212
+
213
+ at_exit { @output_logger&.close }
214
+ rescue => e
215
+ $stderr.puts "Output logger setup failed: #{e.class}: #{e.message}"
216
+ $stderr.puts e.backtrace.join("\n") if ENV["VERBOSE"]
163
217
  end
164
218
 
165
219
  def specifics
@@ -4,7 +4,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
4
4
  attr_reader :accessory_config
5
5
  delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
6
6
  :network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
7
- :secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?, :registry,
7
+ :restart_policy, :secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?, :registry,
8
8
  to: :accessory_config
9
9
 
10
10
  def initialize(config, name:)
@@ -16,7 +16,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
16
16
  docker :run,
17
17
  "--name", service_name,
18
18
  "--detach",
19
- "--restart", "unless-stopped",
19
+ "--restart", restart_policy,
20
20
  *network_args,
21
21
  *config.logging_args,
22
22
  *publish_args,
@@ -68,6 +68,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
68
68
  *network_args,
69
69
  *env_args,
70
70
  *volume_args,
71
+ *option_args,
71
72
  image,
72
73
  *command
73
74
  end
@@ -21,7 +21,7 @@ module Kamal::Commands::App::Logging
21
21
  def container_id_command(container_id)
22
22
  case container_id
23
23
  when Array then container_id
24
- when String, Symbol then "echo #{container_id}"
24
+ when String, Symbol then shell([ "echo #{container_id}" ])
25
25
  else current_running_container_id
26
26
  end
27
27
  end
@@ -16,7 +16,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
16
16
  def run(hostname: nil)
17
17
  docker :run,
18
18
  "--detach",
19
- "--restart unless-stopped",
19
+ "--restart", role.restart_policy,
20
20
  "--name", container_name,
21
21
  "--network", "kamal",
22
22
  *([ "--hostname", hostname ] if hostname),