kamal 2.2.2 → 2.4.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +23 -8
  3. data/lib/kamal/cli/app/boot.rb +2 -2
  4. data/lib/kamal/cli/app.rb +13 -5
  5. data/lib/kamal/cli/proxy.rb +1 -1
  6. data/lib/kamal/cli/secrets.rb +9 -3
  7. data/lib/kamal/cli/templates/deploy.yml +10 -6
  8. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  9. data/lib/kamal/commands/accessory.rb +8 -4
  10. data/lib/kamal/commands/app/containers.rb +2 -2
  11. data/lib/kamal/commands/app/execution.rb +4 -2
  12. data/lib/kamal/commands/app/images.rb +1 -1
  13. data/lib/kamal/commands/app/logging.rb +14 -4
  14. data/lib/kamal/commands/app.rb +15 -7
  15. data/lib/kamal/commands/base.rb +11 -1
  16. data/lib/kamal/commands/builder/base.rb +10 -2
  17. data/lib/kamal/configuration/accessory.rb +28 -3
  18. data/lib/kamal/configuration/builder.rb +9 -1
  19. data/lib/kamal/configuration/docs/accessory.yml +14 -2
  20. data/lib/kamal/configuration/docs/alias.yml +2 -2
  21. data/lib/kamal/configuration/docs/builder.yml +12 -0
  22. data/lib/kamal/configuration/docs/proxy.yml +13 -10
  23. data/lib/kamal/configuration/docs/registry.yml +4 -0
  24. data/lib/kamal/configuration.rb +3 -3
  25. data/lib/kamal/env_file.rb +3 -1
  26. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +42 -0
  27. data/lib/kamal/secrets/adapters/base.rb +13 -1
  28. data/lib/kamal/secrets/adapters/bitwarden.rb +23 -8
  29. data/lib/kamal/secrets/adapters/doppler.rb +53 -0
  30. data/lib/kamal/secrets/adapters/last_pass.rb +9 -0
  31. data/lib/kamal/secrets/adapters/one_password.rb +9 -0
  32. data/lib/kamal/secrets/adapters/test.rb +4 -0
  33. data/lib/kamal/secrets/adapters/test_optional_account.rb +5 -0
  34. data/lib/kamal/secrets.rb +12 -7
  35. data/lib/kamal/utils.rb +2 -0
  36. data/lib/kamal/version.rb +1 -1
  37. metadata +18 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7da17dcbc307380fd036004d67e3007ea6824a4b634f161c2882776b5c2020df
4
- data.tar.gz: c394f629044adecac7c2bc65a8ea6615269a010d68be8051c4fd4c579e5be686
3
+ metadata.gz: 246d570a9a6f85698245aa14237986648b39f5e32750bd6f76fd401f96e2398a
4
+ data.tar.gz: 02a6b0c3aff13021ee86381e5c10bd4f5b7708d8f9c13c92e03ddd46ed5fa9d5
5
5
  SHA512:
6
- metadata.gz: 41028ff1893c92dfea1d1de5f5289e60fd719f28445f7f827b513482463e364e3640a88003ef68b24b4fcb4f502f88dbb3daf9c4e04d956fc8a03e09d4f64d90
7
- data.tar.gz: b6db1eb9d4acbd57ce4fcce79798a2f1ad47cd043e2624c03ee940a86fc32bf49bf0446c70142e98e7d87e06cc5c6ee6dd89448533b5c2577fd2d6ee57faaa87
6
+ metadata.gz: 31365397457be212570677a8dfa4fd3aac1701a4f251bc989100f4ea043edc0f29265d92c9bdde432b323ceadb62c7ccc491c8f6bc9c88b49d0835d193447f83
7
+ data.tar.gz: cc6f0cbce224c0234bbaba0ed5bf78001cbb9e888a969154e2728b07b36e8fb6e7a24fa1db2b5a52159f49de5fcee66d28de816ba5b9a8f95285a0e75f08a090
@@ -18,6 +18,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
18
18
  execute *accessory.ensure_env_directory
19
19
  upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
20
20
  execute *accessory.run
21
+
22
+ if accessory.running_proxy?
23
+ target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
24
+ execute *accessory.deploy(target: target)
25
+ end
21
26
  end
22
27
  end
23
28
  end
@@ -75,6 +80,10 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
75
80
  on(hosts) do
76
81
  execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
77
82
  execute *accessory.start
83
+ if accessory.running_proxy?
84
+ target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
85
+ execute *accessory.deploy(target: target)
86
+ end
78
87
  end
79
88
  end
80
89
  end
@@ -87,6 +96,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
87
96
  on(hosts) do
88
97
  execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
89
98
  execute *accessory.stop, raise_on_non_zero_exit: false
99
+
100
+ if accessory.running_proxy?
101
+ target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
102
+ execute *accessory.remove if target
103
+ end
90
104
  end
91
105
  end
92
106
  end
@@ -112,14 +126,15 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
112
126
  end
113
127
  end
114
128
 
115
- desc "exec [NAME] [CMD]", "Execute a custom command on servers (use --help to show options)"
129
+ desc "exec [NAME] [CMD...]", "Execute a custom command on servers within the accessory container (use --help to show options)"
116
130
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
117
131
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
118
- def exec(name, cmd)
132
+ def exec(name, *cmd)
133
+ cmd = Kamal::Utils.join_commands(cmd)
119
134
  with_accessory(name) do |accessory, hosts|
120
135
  case
121
136
  when options[:interactive] && options[:reuse]
122
- say "Launching interactive command with via SSH from existing container...", :magenta
137
+ say "Launching interactive command via SSH from existing container...", :magenta
123
138
  run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
124
139
 
125
140
  when options[:interactive]
@@ -128,16 +143,16 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
128
143
 
129
144
  when options[:reuse]
130
145
  say "Launching command from existing container...", :magenta
131
- on(hosts) do
146
+ on(hosts) do |host|
132
147
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
133
- capture_with_info(*accessory.execute_in_existing_container(cmd))
148
+ puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd))
134
149
  end
135
150
 
136
151
  else
137
152
  say "Launching command from new container...", :magenta
138
- on(hosts) do
153
+ on(hosts) do |host|
139
154
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
140
- capture_with_info(*accessory.execute_in_new_container(cmd))
155
+ puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd))
141
156
  end
142
157
  end
143
158
  end
@@ -147,7 +162,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
147
162
  option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
148
163
  option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
149
164
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
150
- option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
165
+ option :grep_options, desc: "Additional options supplied to grep"
151
166
  option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
152
167
  option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
153
168
  def logs(name)
@@ -45,7 +45,7 @@ class Kamal::Cli::App::Boot
45
45
 
46
46
  def start_new_version
47
47
  audit "Booted app version #{version}"
48
- hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"
48
+ hostname = "#{host.to_s[0...51].chomp(".")}-#{SecureRandom.hex(6)}"
49
49
 
50
50
  execute *app.ensure_env_directory
51
51
  upload! role.secrets_io(host), role.secrets_path, mode: "0600"
@@ -91,7 +91,7 @@ class Kamal::Cli::App::Boot
91
91
  if barrier.close
92
92
  info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles"
93
93
  begin
94
- error capture_with_info(*app.logs(version: version))
94
+ error capture_with_info(*app.logs(container_id: app.container_id_for_version(version)))
95
95
  error capture_with_info(*app.container_health_log(version: version))
96
96
  rescue SSHKit::Command::Failed
97
97
  error "Could not fetch logs for #{version}"
data/lib/kamal/cli/app.rb CHANGED
@@ -94,9 +94,15 @@ class Kamal::Cli::App < Kamal::Cli::Base
94
94
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
95
95
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
96
96
  option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
97
+ option :detach, type: :boolean, default: false, desc: "Execute command in a detached container"
97
98
  def exec(*cmd)
99
+ if (incompatible_options = [ :interactive, :reuse ].select { |key| options[:detach] && options[key] }.presence)
100
+ raise ArgumentError, "Detach is not compatible with #{incompatible_options.join(" or ")}"
101
+ end
102
+
98
103
  cmd = Kamal::Utils.join_commands(cmd)
99
104
  env = options[:env]
105
+ detach = options[:detach]
100
106
  case
101
107
  when options[:interactive] && options[:reuse]
102
108
  say "Get current version of running container...", :magenta unless options[:version]
@@ -138,7 +144,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
138
144
 
139
145
  roles.each do |role|
140
146
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
141
- puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env))
147
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env, detach: detach))
142
148
  end
143
149
  end
144
150
  end
@@ -186,15 +192,17 @@ class Kamal::Cli::App < Kamal::Cli::Base
186
192
  option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
187
193
  option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
188
194
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
189
- option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
195
+ option :grep_options, desc: "Additional options supplied to grep"
190
196
  option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
191
197
  option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
198
+ option :container_id, desc: "Docker container ID to fetch logs"
192
199
  def logs
193
200
  # FIXME: Catch when app containers aren't running
194
201
 
195
202
  grep = options[:grep]
196
203
  grep_options = options[:grep_options]
197
204
  since = options[:since]
205
+ container_id = options[:container_id]
198
206
  timestamps = !options[:skip_timestamps]
199
207
 
200
208
  if options[:follow]
@@ -207,8 +215,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
207
215
  role = KAMAL.roles_on(KAMAL.primary_host).first
208
216
 
209
217
  app = KAMAL.app(role: role, host: host)
210
- info app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
211
- exec app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
218
+ info app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
219
+ exec app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
212
220
  end
213
221
  else
214
222
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
@@ -218,7 +226,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
218
226
 
219
227
  roles.each do |role|
220
228
  begin
221
- puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
229
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(container_id: container_id, timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
222
230
  rescue SSHKit::Command::Failed
223
231
  puts_by_host host, "Nothing found"
224
232
  end
@@ -14,7 +14,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
14
14
  version = capture_with_info(*KAMAL.proxy.version).strip.presence
15
15
 
16
16
  if version && Kamal::Utils.older_version?(version, Kamal::Configuration::PROXY_MINIMUM_VERSION)
17
- raise "kamal-proxy version #{version} is too old, please reboot to update to at least #{Kamal::Configuration::PROXY_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_MINIMUM_VERSION}"
18
18
  end
19
19
  execute *KAMAL.proxy.start_or_run
20
20
  end
@@ -1,11 +1,17 @@
1
1
  class Kamal::Cli::Secrets < Kamal::Cli::Base
2
2
  desc "fetch [SECRETS...]", "Fetch secrets from a vault"
3
3
  option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
4
- option :account, type: :string, required: true, desc: "The account identifier or username"
4
+ option :account, type: :string, required: false, desc: "The account identifier or username"
5
5
  option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
6
6
  option :inline, type: :boolean, required: false, hidden: true
7
7
  def fetch(*secrets)
8
- results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
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)
9
15
 
10
16
  return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
11
17
  end
@@ -29,7 +35,7 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
29
35
  end
30
36
 
31
37
  private
32
- def adapter(adapter)
38
+ def initialize_adapter(adapter)
33
39
  Kamal::Secrets::Adapters.lookup(adapter)
34
40
  end
35
41
 
@@ -13,13 +13,14 @@ servers:
13
13
  # - 192.168.0.1
14
14
  # cmd: bin/jobs
15
15
 
16
- # Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server).
17
- # If using something like Cloudflare, it is recommended to set encryption mode
18
- # in Cloudflare's SSL/TLS setting to "Full" to enable end-to-end encryption.
19
- proxy:
16
+ # Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
17
+ # Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
18
+ #
19
+ # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
20
+ proxy:
20
21
  ssl: true
21
22
  host: app.example.com
22
- # kamal-proxy connects to your container over port 80, use `app_port` to specify a different port.
23
+ # Proxy connects to your container on port 80 by default.
23
24
  # app_port: 3000
24
25
 
25
26
  # Credentials for your image host.
@@ -35,6 +36,9 @@ registry:
35
36
  # Configure builder setup.
36
37
  builder:
37
38
  arch: amd64
39
+ # Pass in additional build args needed for your Dockerfile.
40
+ # args:
41
+ # RUBY_VERSION: <%= File.read('.ruby-version').strip %>
38
42
 
39
43
  # Inject ENV variables into containers (secrets come from .kamal/secrets).
40
44
  #
@@ -90,7 +94,7 @@ builder:
90
94
  # directories:
91
95
  # - data:/var/lib/mysql
92
96
  # redis:
93
- # image: redis:7.0
97
+ # image: valkey/valkey:8
94
98
  # host: 192.168.0.2
95
99
  # port: 6379
96
100
  # directories:
@@ -0,0 +1,16 @@
1
+ module Kamal::Commands::Accessory::Proxy
2
+ delegate :proxy_container_name, to: :config
3
+
4
+ def deploy(target:)
5
+ proxy_exec :deploy, service_name, *proxy.deploy_command_args(target: target)
6
+ end
7
+
8
+ def remove
9
+ proxy_exec :remove, service_name
10
+ end
11
+
12
+ private
13
+ def proxy_exec(*command)
14
+ docker :exec, proxy_container_name, "kamal-proxy", *command
15
+ end
16
+ end
@@ -1,9 +1,13 @@
1
1
  class Kamal::Commands::Accessory < Kamal::Commands::Base
2
+ include Proxy
3
+
2
4
  attr_reader :accessory_config
3
5
  delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
4
- :publish_args, :env_args, :volume_args, :label_args, :option_args,
5
- :secrets_io, :secrets_path, :env_directory,
6
+ :network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
7
+ :secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?,
6
8
  to: :accessory_config
9
+ delegate :proxy_container_name, to: :config
10
+
7
11
 
8
12
  def initialize(config, name:)
9
13
  super(config)
@@ -15,7 +19,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
15
19
  "--name", service_name,
16
20
  "--detach",
17
21
  "--restart", "unless-stopped",
18
- "--network", "kamal",
22
+ *network_args,
19
23
  *config.logging_args,
20
24
  *publish_args,
21
25
  *env_args,
@@ -64,7 +68,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
64
68
  docker :run,
65
69
  ("-it" if interactive),
66
70
  "--rm",
67
- "--network", "kamal",
71
+ *network_args,
68
72
  *env_args,
69
73
  *volume_args,
70
74
  image,
@@ -2,7 +2,7 @@ module Kamal::Commands::App::Containers
2
2
  DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
3
3
 
4
4
  def list_containers
5
- docker :container, :ls, "--all", *filter_args
5
+ docker :container, :ls, "--all", *container_filter_args
6
6
  end
7
7
 
8
8
  def list_container_names
@@ -20,7 +20,7 @@ module Kamal::Commands::App::Containers
20
20
  end
21
21
 
22
22
  def remove_containers
23
- docker :container, :prune, "--force", *filter_args
23
+ docker :container, :prune, "--force", *container_filter_args
24
24
  end
25
25
 
26
26
  def container_health_log(version:)
@@ -7,13 +7,15 @@ module Kamal::Commands::App::Execution
7
7
  *command
8
8
  end
9
9
 
10
- def execute_in_new_container(*command, interactive: false, env:)
10
+ def execute_in_new_container(*command, interactive: false, detach: false, env:)
11
11
  docker :run,
12
12
  ("-it" if interactive),
13
- "--rm",
13
+ ("--detach" if detach),
14
+ ("--rm" unless detach),
14
15
  "--network", "kamal",
15
16
  *role&.env_args(host),
16
17
  *argumentize("--env", env),
18
+ *role.logging_args,
17
19
  *config.volume_args,
18
20
  *role&.option_args,
19
21
  config.absolute_image,
@@ -4,7 +4,7 @@ module Kamal::Commands::App::Images
4
4
  end
5
5
 
6
6
  def remove_images
7
- docker :image, :prune, "--all", "--force", *filter_args
7
+ docker :image, :prune, "--all", "--force", *image_filter_args
8
8
  end
9
9
 
10
10
  def tag_latest_image
@@ -1,18 +1,28 @@
1
1
  module Kamal::Commands::App::Logging
2
- def logs(version: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
2
+ def logs(container_id: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
3
3
  pipe \
4
- version ? container_id_for_version(version) : current_running_container_id,
4
+ container_id_command(container_id),
5
5
  "xargs docker logs#{" --timestamps" if timestamps}#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
6
6
  ("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
7
7
  end
8
8
 
9
- def follow_logs(host:, timestamps: true, lines: nil, grep: nil, grep_options: nil)
9
+ def follow_logs(host:, container_id: nil, timestamps: true, lines: nil, grep: nil, grep_options: nil)
10
10
  run_over_ssh \
11
11
  pipe(
12
- current_running_container_id,
12
+ container_id_command(container_id),
13
13
  "xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1",
14
14
  (%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
15
15
  ),
16
16
  host: host
17
17
  end
18
+
19
+ private
20
+
21
+ def container_id_command(container_id)
22
+ case container_id
23
+ when Array then container_id
24
+ when String, Symbol then "echo #{container_id}"
25
+ else current_running_container_id
26
+ end
27
+ end
18
28
  end
@@ -47,7 +47,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
47
47
  end
48
48
 
49
49
  def info
50
- docker :ps, *filter_args
50
+ docker :ps, *container_filter_args
51
51
  end
52
52
 
53
53
 
@@ -67,7 +67,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
67
67
 
68
68
  def list_versions(*docker_args, statuses: nil)
69
69
  pipe \
70
- docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
70
+ docker(:ps, *container_filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
71
71
  extract_version_from_name
72
72
  end
73
73
 
@@ -91,11 +91,15 @@ class Kamal::Commands::App < Kamal::Commands::Base
91
91
  end
92
92
 
93
93
  def latest_container(format:, filters: nil)
94
- docker :ps, "--latest", *format, *filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
94
+ docker :ps, "--latest", *format, *container_filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
95
95
  end
96
96
 
97
- def filter_args(statuses: nil)
98
- argumentize "--filter", filters(statuses: statuses)
97
+ def container_filter_args(statuses: nil)
98
+ argumentize "--filter", container_filters(statuses: statuses)
99
+ end
100
+
101
+ def image_filter_args
102
+ argumentize "--filter", image_filters
99
103
  end
100
104
 
101
105
  def extract_version_from_name
@@ -103,13 +107,17 @@ class Kamal::Commands::App < Kamal::Commands::Base
103
107
  %(while read line; do echo ${line##{role.container_prefix}-}; done)
104
108
  end
105
109
 
106
- def filters(statuses: nil)
110
+ def container_filters(statuses: nil)
107
111
  [ "label=service=#{config.service}" ].tap do |filters|
108
- filters << "label=destination=#{config.destination}" if config.destination
112
+ filters << "label=destination=#{config.destination}"
109
113
  filters << "label=role=#{role}" if role
110
114
  statuses&.each do |status|
111
115
  filters << "status=#{status}"
112
116
  end
113
117
  end
114
118
  end
119
+
120
+ def image_filters
121
+ [ "label=service=#{config.service}" ]
122
+ end
115
123
  end
@@ -11,7 +11,7 @@ module Kamal::Commands
11
11
  end
12
12
 
13
13
  def run_over_ssh(*command, host:)
14
- "ssh#{ssh_proxy_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
14
+ "ssh#{ssh_proxy_args}#{ssh_keys_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
15
15
  end
16
16
 
17
17
  def container_id_for(container_name:, only_running: false)
@@ -94,5 +94,15 @@ module Kamal::Commands
94
94
  " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
95
95
  end
96
96
  end
97
+
98
+ def ssh_keys_args
99
+ "#{ ssh_keys.join("") if ssh_keys}" + "#{" -o IdentitiesOnly=yes" if config.ssh&.keys_only}"
100
+ end
101
+
102
+ def ssh_keys
103
+ config.ssh.keys&.map do |key|
104
+ " -i #{key}"
105
+ end
106
+ end
97
107
  end
98
108
  end
@@ -6,7 +6,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
6
6
  delegate :argumentize, to: Kamal::Utils
7
7
  delegate \
8
8
  :args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
9
- :cache_from, :cache_to, :ssh, :driver, :docker_driver?,
9
+ :cache_from, :cache_to, :ssh, :provenance, :sbom, :driver, :docker_driver?,
10
10
  to: :builder_config
11
11
 
12
12
  def clean
@@ -37,7 +37,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
37
37
  end
38
38
 
39
39
  def build_options
40
- [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ]
40
+ [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance, *builder_sbom ]
41
41
  end
42
42
 
43
43
  def build_context
@@ -97,6 +97,14 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
97
97
  argumentize "--ssh", ssh if ssh.present?
98
98
  end
99
99
 
100
+ def builder_provenance
101
+ argumentize "--provenance", provenance unless provenance.nil?
102
+ end
103
+
104
+ def builder_sbom
105
+ argumentize "--sbom", sbom unless sbom.nil?
106
+ end
107
+
100
108
  def builder_config
101
109
  config.builder
102
110
  end
@@ -1,9 +1,11 @@
1
1
  class Kamal::Configuration::Accessory
2
2
  include Kamal::Configuration::Validation
3
3
 
4
+ DEFAULT_NETWORK = "kamal"
5
+
4
6
  delegate :argumentize, :optionize, to: Kamal::Utils
5
7
 
6
- attr_reader :name, :accessory_config, :env
8
+ attr_reader :name, :accessory_config, :env, :proxy
7
9
 
8
10
  def initialize(name, config:)
9
11
  @name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]
@@ -18,6 +20,8 @@ class Kamal::Configuration::Accessory
18
20
  config: accessory_config.fetch("env", {}),
19
21
  secrets: config.secrets,
20
22
  context: "accessories/#{name}/env"
23
+
24
+ initialize_proxy if running_proxy?
21
25
  end
22
26
 
23
27
  def service_name
@@ -38,6 +42,10 @@ class Kamal::Configuration::Accessory
38
42
  end
39
43
  end
40
44
 
45
+ def network_args
46
+ argumentize "--network", network
47
+ end
48
+
41
49
  def publish_args
42
50
  argumentize "--publish", port if port
43
51
  end
@@ -100,6 +108,17 @@ class Kamal::Configuration::Accessory
100
108
  accessory_config["cmd"]
101
109
  end
102
110
 
111
+ def running_proxy?
112
+ @accessory_config["proxy"].present?
113
+ end
114
+
115
+ def initialize_proxy
116
+ @proxy = Kamal::Configuration::Proxy.new \
117
+ config: config,
118
+ proxy_config: accessory_config["proxy"],
119
+ context: "accessories/#{name}/proxy"
120
+ end
121
+
103
122
  private
104
123
  attr_accessor :config
105
124
 
@@ -123,7 +142,7 @@ class Kamal::Configuration::Accessory
123
142
  end
124
143
 
125
144
  def read_dynamic_file(local_file)
126
- StringIO.new(ERB.new(IO.read(local_file)).result)
145
+ StringIO.new(ERB.new(File.read(local_file)).result)
127
146
  end
128
147
 
129
148
  def expand_remote_file(remote_file)
@@ -170,7 +189,13 @@ class Kamal::Configuration::Accessory
170
189
 
171
190
  def hosts_from_roles
172
191
  if accessory_config.key?("roles")
173
- accessory_config["roles"].flat_map { |role| config.role(role).hosts }
192
+ accessory_config["roles"].flat_map do |role|
193
+ config.role(role)&.hosts || raise(Kamal::ConfigurationError, "Unknown role in accessories config: '#{role}'")
194
+ end
174
195
  end
175
196
  end
197
+
198
+ def network
199
+ accessory_config["network"] || DEFAULT_NETWORK
200
+ end
176
201
  end
@@ -111,6 +111,14 @@ class Kamal::Configuration::Builder
111
111
  builder_config["ssh"]
112
112
  end
113
113
 
114
+ def provenance
115
+ builder_config["provenance"]
116
+ end
117
+
118
+ def sbom
119
+ builder_config["sbom"]
120
+ end
121
+
114
122
  def git_clone?
115
123
  Kamal::Git.used? && builder_config["context"].nil?
116
124
  end
@@ -166,7 +174,7 @@ class Kamal::Configuration::Builder
166
174
  end
167
175
 
168
176
  def cache_to_config_for_registry
169
- [ "type=registry", builder_config["cache"]&.fetch("options", nil), "ref=#{cache_image_ref}" ].compact.join(",")
177
+ [ "type=registry", "ref=#{cache_image_ref}", builder_config["cache"]&.fetch("options", nil) ].compact.join(",")
170
178
  end
171
179
 
172
180
  def repo_basename
@@ -43,8 +43,8 @@ accessories:
43
43
 
44
44
  # Port mappings
45
45
  #
46
- # See https://docs.docker.com/network/, and especially note the warning about the security
47
- # implications of exposing ports publicly.
46
+ # See [https://docs.docker.com/network/](https://docs.docker.com/network/), and
47
+ # especially note the warning about the security implications of exposing ports publicly.
48
48
  port: "127.0.0.1:3306:3306"
49
49
 
50
50
  # Labels
@@ -90,3 +90,15 @@ accessories:
90
90
  # They are not created or copied before mounting:
91
91
  volumes:
92
92
  - /path/to/mysql-logs:/var/log/mysql
93
+
94
+ # Network
95
+ #
96
+ # The network the accessory will be attached to.
97
+ #
98
+ # Defaults to kamal:
99
+ network: custom
100
+
101
+ # Proxy
102
+ #
103
+ proxy:
104
+ ...
@@ -5,12 +5,12 @@
5
5
  # For example, for a Rails app, you might open a console with:
6
6
  #
7
7
  # ```shell
8
- # kamal app exec -i -r console "rails console"
8
+ # kamal app exec -i --reuse "bin/rails console"
9
9
  # ```
10
10
  #
11
11
  # By defining an alias, like this:
12
12
  aliases:
13
- console: app exec -r console -i "rails console"
13
+ console: app exec -i --reuse "bin/rails console"
14
14
  # You can now open the console with:
15
15
  #
16
16
  # ```shell
@@ -102,3 +102,15 @@ builder:
102
102
  #
103
103
  # The build driver to use, defaults to `docker-container`:
104
104
  driver: docker
105
+
106
+ # Provenance
107
+ #
108
+ # It is used to configure provenance attestations for the build result.
109
+ # The value can also be a boolean to enable or disable provenance attestations.
110
+ provenance: mode=max
111
+
112
+ # SBOM (Software Bill of Materials)
113
+ #
114
+ # It is used to configure SBOM generation for the build result.
115
+ # The value can also be a boolean to enable or disable SBOM generation.
116
+ sbom: true
@@ -46,9 +46,22 @@ proxy:
46
46
  # The host value must point to the server we are deploying to, and port 443 must be
47
47
  # open for the Let's Encrypt challenge to succeed.
48
48
  #
49
+ # If you set `ssl` to `true`, `kamal-proxy` will stop forwarding headers to your app,
50
+ # unless you explicitly set `forward_headers: true`
51
+ #
49
52
  # Defaults to `false`:
50
53
  ssl: true
51
54
 
55
+ # Forward headers
56
+ #
57
+ # Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
58
+ #
59
+ # If you are behind a trusted proxy, you can set this to `true` to forward the headers.
60
+ #
61
+ # By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
62
+ # will forward them if it is set to `false`.
63
+ forward_headers: true
64
+
52
65
  # Response timeout
53
66
  #
54
67
  # How long to wait for requests to complete before timing out, defaults to 30 seconds:
@@ -93,13 +106,3 @@ proxy:
93
106
  response_headers:
94
107
  - X-Request-ID
95
108
  - X-Request-Start
96
-
97
- # Forward headers
98
- #
99
- # Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
100
- #
101
- # If you are behind a trusted proxy, you can set this to `true` to forward the headers.
102
- #
103
- # By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
104
- # will forward them if it is set to `false`.
105
- forward_headers: true
@@ -2,6 +2,10 @@
2
2
  #
3
3
  # The default registry is Docker Hub, but you can change it using `registry/server`.
4
4
  #
5
+ # By default, Docker Hub creates public repositories. To avoid making your images public,
6
+ # set up a private repository before deploying, or change the default repository privacy
7
+ # settings to private in your [Docker Hub settings](https://hub.docker.com/repository-settings/default-privacy).
8
+ #
5
9
  # A reference to a secret (in this case, `DOCKER_REGISTRY_TOKEN`) will look up the secret
6
10
  # in the local environment:
7
11
  registry:
@@ -14,7 +14,7 @@ class Kamal::Configuration
14
14
 
15
15
  include Validation
16
16
 
17
- PROXY_MINIMUM_VERSION = "v0.8.1"
17
+ PROXY_MINIMUM_VERSION = "v0.8.4"
18
18
  PROXY_HTTP_PORT = 80
19
19
  PROXY_HTTPS_PORT = 443
20
20
  PROXY_LOG_MAX_SIZE = "10m"
@@ -37,7 +37,7 @@ class Kamal::Configuration
37
37
  if file.exist?
38
38
  # Newer Psych doesn't load aliases by default
39
39
  load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
40
- YAML.send(load_method, ERB.new(IO.read(file)).result).symbolize_keys
40
+ YAML.send(load_method, ERB.new(File.read(file)).result).symbolize_keys
41
41
  else
42
42
  raise "Configuration file not found in #{file}"
43
43
  end
@@ -254,7 +254,7 @@ class Kamal::Configuration
254
254
  end
255
255
 
256
256
  def proxy_logging_args(max_size)
257
- argumentize "--log-opt", "max-size=#{max_size}"
257
+ argumentize "--log-opt", "max-size=#{max_size}" if max_size.present?
258
258
  end
259
259
 
260
260
  def proxy_options_default
@@ -37,6 +37,8 @@ class Kamal::EnvFile
37
37
  def escape_docker_env_file_ascii_value(value)
38
38
  # Doublequotes are treated literally in docker env files
39
39
  # so remove leading and trailing ones and unescape any others
40
- value.to_s.dump[1..-2].gsub(/\\"/, "\"")
40
+ value.to_s.dump[1..-2]
41
+ .gsub(/\\"/, "\"")
42
+ .gsub(/\\#/, "#")
41
43
  end
42
44
  end
@@ -0,0 +1,42 @@
1
+ class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Base
2
+ private
3
+ def login(_account)
4
+ nil
5
+ end
6
+
7
+ def fetch_secrets(secrets, account:, session:)
8
+ {}.tap do |results|
9
+ get_from_secrets_manager(secrets, account: account).each do |secret|
10
+ secret_name = secret["Name"]
11
+ secret_string = JSON.parse(secret["SecretString"])
12
+
13
+ secret_string.each do |key, value|
14
+ results["#{secret_name}/#{key}"] = value
15
+ end
16
+ rescue JSON::ParserError
17
+ results["#{secret_name}"] = secret["SecretString"]
18
+ end
19
+ end
20
+ end
21
+
22
+ def get_from_secrets_manager(secrets, account:)
23
+ `aws secretsmanager batch-get-secret-value --secret-id-list #{secrets.map(&:shellescape).join(" ")} --profile #{account.shellescape}`.tap do |secrets|
24
+ raise RuntimeError, "Could not read #{secrets} from AWS Secrets Manager" unless $?.success?
25
+
26
+ secrets = JSON.parse(secrets)
27
+
28
+ return secrets["SecretValues"] unless secrets["Errors"].present?
29
+
30
+ raise RuntimeError, secrets["Errors"].map { |error| "#{error['SecretId']}: #{error['Message']}" }.join(" ")
31
+ end
32
+ end
33
+
34
+ def check_dependencies!
35
+ raise RuntimeError, "AWS CLI is not installed" unless cli_installed?
36
+ end
37
+
38
+ def cli_installed?
39
+ `aws --version 2> /dev/null`
40
+ $?.success?
41
+ end
42
+ end
@@ -1,12 +1,20 @@
1
1
  class Kamal::Secrets::Adapters::Base
2
2
  delegate :optionize, to: Kamal::Utils
3
3
 
4
- def fetch(secrets, account:, from: nil)
4
+ def fetch(secrets, account: nil, from: nil)
5
+ raise RuntimeError, "Missing required option '--account'" if requires_account? && account.blank?
6
+
7
+ check_dependencies!
8
+
5
9
  session = login(account)
6
10
  full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
7
11
  fetch_secrets(full_secrets, account: account, session: session)
8
12
  end
9
13
 
14
+ def requires_account?
15
+ true
16
+ end
17
+
10
18
  private
11
19
  def login(...)
12
20
  raise NotImplementedError
@@ -15,4 +23,8 @@ class Kamal::Secrets::Adapters::Base
15
23
  def fetch_secrets(...)
16
24
  raise NotImplementedError
17
25
  end
26
+
27
+ def check_dependencies!
28
+ raise NotImplementedError
29
+ end
18
30
  end
@@ -25,18 +25,15 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
25
25
  {}.tap do |results|
26
26
  items_fields(secrets).each do |item, fields|
27
27
  item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
28
- raise RuntimeError, "Could not read #{secret} from Bitwarden" unless $?.success?
28
+ raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success?
29
29
  item_json = JSON.parse(item_json)
30
-
31
30
  if fields.any?
32
- fields.each do |field|
33
- item_field = item_json["fields"].find { |f| f["name"] == field }
34
- raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
35
- value = item_field["value"]
36
- results["#{item}/#{field}"] = value
37
- end
31
+ results.merge! fetch_secrets_from_fields(fields, item, item_json)
38
32
  elsif item_json.dig("login", "password")
39
33
  results[item] = item_json.dig("login", "password")
34
+ elsif item_json["fields"]&.any?
35
+ fields = item_json["fields"].pluck("name")
36
+ results.merge! fetch_secrets_from_fields(fields, item, item_json)
40
37
  else
41
38
  raise RuntimeError, "Item #{item} is not a login type item and no fields were specified"
42
39
  end
@@ -44,6 +41,15 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
44
41
  end
45
42
  end
46
43
 
44
+ def fetch_secrets_from_fields(fields, item, item_json)
45
+ fields.to_h do |field|
46
+ item_field = item_json["fields"].find { |f| f["name"] == field }
47
+ raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
48
+ value = item_field["value"]
49
+ [ "#{item}/#{field}", value ]
50
+ end
51
+ end
52
+
47
53
  def items_fields(secrets)
48
54
  {}.tap do |items|
49
55
  secrets.each do |secret|
@@ -63,4 +69,13 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
63
69
  result = `#{full_command}`.strip
64
70
  raw ? result : JSON.parse(result)
65
71
  end
72
+
73
+ def check_dependencies!
74
+ raise RuntimeError, "Bitwarden CLI is not installed" unless cli_installed?
75
+ end
76
+
77
+ def cli_installed?
78
+ `bw --version 2> /dev/null`
79
+ $?.success?
80
+ end
66
81
  end
@@ -0,0 +1,53 @@
1
+ class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base
2
+ def requires_account?
3
+ false
4
+ end
5
+
6
+ private
7
+ def login(*)
8
+ unless loggedin?
9
+ `doppler login -y`
10
+ raise RuntimeError, "Failed to login to Doppler" unless $?.success?
11
+ end
12
+ end
13
+
14
+ def loggedin?
15
+ `doppler me --json 2> /dev/null`
16
+ $?.success?
17
+ end
18
+
19
+ def fetch_secrets(secrets, **)
20
+ project_and_config_flags = ""
21
+ unless service_token_set?
22
+ project, config, _ = secrets.first.split("/")
23
+
24
+ unless project && config
25
+ raise RuntimeError, "Missing project or config from '--from=project/config' option"
26
+ end
27
+
28
+ project_and_config_flags = "-p #{project.shellescape} -c #{config.shellescape}"
29
+ end
30
+
31
+ secret_names = secrets.collect { |s| s.split("/").last }
32
+
33
+ items = `doppler secrets get #{secret_names.map(&:shellescape).join(" ")} --json #{project_and_config_flags}`
34
+ raise RuntimeError, "Could not read #{secrets} from Doppler" unless $?.success?
35
+
36
+ items = JSON.parse(items)
37
+
38
+ items.transform_values { |value| value["computed"] }
39
+ end
40
+
41
+ def service_token_set?
42
+ ENV["DOPPLER_TOKEN"] && ENV["DOPPLER_TOKEN"][0, 5] == "dp.st"
43
+ end
44
+
45
+ def check_dependencies!
46
+ raise RuntimeError, "Doppler CLI is not installed" unless cli_installed?
47
+ end
48
+
49
+ def cli_installed?
50
+ `doppler --version 2> /dev/null`
51
+ $?.success?
52
+ end
53
+ end
@@ -27,4 +27,13 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
27
27
  end
28
28
  end
29
29
  end
30
+
31
+ def check_dependencies!
32
+ raise RuntimeError, "LastPass CLI is not installed" unless cli_installed?
33
+ end
34
+
35
+ def cli_installed?
36
+ `lpass --version 2> /dev/null`
37
+ $?.success?
38
+ end
30
39
  end
@@ -58,4 +58,13 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
58
58
  raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success?
59
59
  end
60
60
  end
61
+
62
+ def check_dependencies!
63
+ raise RuntimeError, "1Password CLI is not installed" unless cli_installed?
64
+ end
65
+
66
+ def cli_installed?
67
+ `op --version 2> /dev/null`
68
+ $?.success?
69
+ end
61
70
  end
@@ -7,4 +7,8 @@ class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
7
7
  def fetch_secrets(secrets, account:, session:)
8
8
  secrets.to_h { |secret| [ secret, secret.reverse ] }
9
9
  end
10
+
11
+ def check_dependencies!
12
+ # no op
13
+ end
10
14
  end
@@ -0,0 +1,5 @@
1
+ class Kamal::Secrets::Adapters::TestOptionalAccount < Kamal::Secrets::Adapters::Test
2
+ def requires_account?
3
+ false
4
+ end
5
+ end
data/lib/kamal/secrets.rb CHANGED
@@ -1,13 +1,10 @@
1
1
  require "dotenv"
2
2
 
3
3
  class Kamal::Secrets
4
- attr_reader :secrets_files
5
-
6
4
  Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!
7
5
 
8
6
  def initialize(destination: nil)
9
- @secrets_files = \
10
- [ ".kamal/secrets-common", ".kamal/secrets#{(".#{destination}" if destination)}" ].select { |f| File.exist?(f) }
7
+ @destination = destination
11
8
  @mutex = Mutex.new
12
9
  end
13
10
 
@@ -17,10 +14,10 @@ class Kamal::Secrets
17
14
  secrets.fetch(key)
18
15
  end
19
16
  rescue KeyError
20
- if secrets_files
17
+ if secrets_files.present?
21
18
  raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
22
19
  else
23
- raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files provided"
20
+ raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files (#{secrets_filenames.join(", ")}) provided"
24
21
  end
25
22
  end
26
23
 
@@ -28,10 +25,18 @@ class Kamal::Secrets
28
25
  secrets
29
26
  end
30
27
 
28
+ def secrets_files
29
+ @secrets_files ||= secrets_filenames.select { |f| File.exist?(f) }
30
+ end
31
+
31
32
  private
32
33
  def secrets
33
34
  @secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
34
- secrets.merge!(::Dotenv.parse(secrets_file))
35
+ secrets.merge!(::Dotenv.parse(secrets_file, overwrite: true))
35
36
  end
36
37
  end
38
+
39
+ def secrets_filenames
40
+ [ ".kamal/secrets-common", ".kamal/secrets#{(".#{@destination}" if @destination)}" ]
41
+ end
37
42
  end
data/lib/kamal/utils.rb CHANGED
@@ -12,6 +12,8 @@ module Kamal::Utils
12
12
  attr = "#{key}=#{escape_shell_value(value)}"
13
13
  attr = self.sensitive(attr, redaction: "#{key}=[REDACTED]") if sensitive
14
14
  [ argument, attr ]
15
+ elsif value == false
16
+ [ argument, "#{key}=false" ]
15
17
  else
16
18
  [ argument, key ]
17
19
  end
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "2.2.2"
2
+ VERSION = "2.4.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.2
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-10 00:00:00.000000000 Z
11
+ date: 2024-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -50,14 +50,14 @@ dependencies:
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '7.0'
53
+ version: '7.3'
54
54
  type: :runtime
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '7.0'
60
+ version: '7.3'
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: thor
63
63
  requirement: !ruby/object:Gem::Requirement
@@ -90,16 +90,22 @@ dependencies:
90
90
  name: zeitwerk
91
91
  requirement: !ruby/object:Gem::Requirement
92
92
  requirements:
93
- - - "~>"
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 2.6.18
96
+ - - "<"
94
97
  - !ruby/object:Gem::Version
95
- version: '2.5'
98
+ version: '3.0'
96
99
  type: :runtime
97
100
  prerelease: false
98
101
  version_requirements: !ruby/object:Gem::Requirement
99
102
  requirements:
100
- - - "~>"
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: 2.6.18
106
+ - - "<"
101
107
  - !ruby/object:Gem::Version
102
- version: '2.5'
108
+ version: '3.0'
103
109
  - !ruby/object:Gem::Dependency
104
110
  name: ed25519
105
111
  requirement: !ruby/object:Gem::Requirement
@@ -241,6 +247,7 @@ files:
241
247
  - lib/kamal/commander/specifics.rb
242
248
  - lib/kamal/commands.rb
243
249
  - lib/kamal/commands/accessory.rb
250
+ - lib/kamal/commands/accessory/proxy.rb
244
251
  - lib/kamal/commands/app.rb
245
252
  - lib/kamal/commands/app/assets.rb
246
253
  - lib/kamal/commands/app/containers.rb
@@ -306,11 +313,14 @@ files:
306
313
  - lib/kamal/git.rb
307
314
  - lib/kamal/secrets.rb
308
315
  - lib/kamal/secrets/adapters.rb
316
+ - lib/kamal/secrets/adapters/aws_secrets_manager.rb
309
317
  - lib/kamal/secrets/adapters/base.rb
310
318
  - lib/kamal/secrets/adapters/bitwarden.rb
319
+ - lib/kamal/secrets/adapters/doppler.rb
311
320
  - lib/kamal/secrets/adapters/last_pass.rb
312
321
  - lib/kamal/secrets/adapters/one_password.rb
313
322
  - lib/kamal/secrets/adapters/test.rb
323
+ - lib/kamal/secrets/adapters/test_optional_account.rb
314
324
  - lib/kamal/secrets/dotenv/inline_command_substitution.rb
315
325
  - lib/kamal/sshkit_with_ext.rb
316
326
  - lib/kamal/tags.rb