kamal 2.2.2 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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