kamal 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d5e6961984a3361505ebf35dfc52920c49af92085dd99c923dbfa801c668a95
4
- data.tar.gz: adddf71abb26f58e5f7bb16c62553f0d3358499ac4c09f333df75b650cc37b54
3
+ metadata.gz: 246d570a9a6f85698245aa14237986648b39f5e32750bd6f76fd401f96e2398a
4
+ data.tar.gz: 02a6b0c3aff13021ee86381e5c10bd4f5b7708d8f9c13c92e03ddd46ed5fa9d5
5
5
  SHA512:
6
- metadata.gz: fdbd4d88c6fe8001def4c53a9f3ee058e871e58ce99f6697a478cbbac48f646947aab415d08074668e2c41147f8fd15fa1cb76f01919d9411aa0e85be6767aba
7
- data.tar.gz: b1716d147e84b386f8bac27deb60f70fc6a7fdfad18fc376dca1391d400de388085b654d5ec8e8e9b3b83724e0a22599bc5626d07cd3812330389add2ed915f9
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
@@ -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
 
@@ -16,8 +16,8 @@ servers:
16
16
  # Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
17
17
  # Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
18
18
  #
19
- # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
20
- proxy:
19
+ # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
20
+ proxy:
21
21
  ssl: true
22
22
  host: app.example.com
23
23
  # Proxy connects to your container on port 80 by default.
@@ -36,6 +36,9 @@ registry:
36
36
  # Configure builder setup.
37
37
  builder:
38
38
  arch: amd64
39
+ # Pass in additional build args needed for your Dockerfile.
40
+ # args:
41
+ # RUBY_VERSION: <%= File.read('.ruby-version').strip %>
39
42
 
40
43
  # Inject ENV variables into containers (secrets come from .kamal/secrets).
41
44
  #
@@ -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
6
  :network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
5
- :secrets_io, :secrets_path, :env_directory,
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)
@@ -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, :provenance, :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, *builder_provenance ]
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
@@ -101,6 +101,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
101
101
  argumentize "--provenance", provenance unless provenance.nil?
102
102
  end
103
103
 
104
+ def builder_sbom
105
+ argumentize "--sbom", sbom unless sbom.nil?
106
+ end
107
+
104
108
  def builder_config
105
109
  config.builder
106
110
  end
@@ -5,7 +5,7 @@ class Kamal::Configuration::Accessory
5
5
 
6
6
  delegate :argumentize, :optionize, to: Kamal::Utils
7
7
 
8
- attr_reader :name, :accessory_config, :env
8
+ attr_reader :name, :accessory_config, :env, :proxy
9
9
 
10
10
  def initialize(name, config:)
11
11
  @name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]
@@ -20,6 +20,8 @@ class Kamal::Configuration::Accessory
20
20
  config: accessory_config.fetch("env", {}),
21
21
  secrets: config.secrets,
22
22
  context: "accessories/#{name}/env"
23
+
24
+ initialize_proxy if running_proxy?
23
25
  end
24
26
 
25
27
  def service_name
@@ -106,6 +108,17 @@ class Kamal::Configuration::Accessory
106
108
  accessory_config["cmd"]
107
109
  end
108
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
+
109
122
  private
110
123
  attr_accessor :config
111
124
 
@@ -129,7 +142,7 @@ class Kamal::Configuration::Accessory
129
142
  end
130
143
 
131
144
  def read_dynamic_file(local_file)
132
- StringIO.new(ERB.new(IO.read(local_file)).result)
145
+ StringIO.new(ERB.new(File.read(local_file)).result)
133
146
  end
134
147
 
135
148
  def expand_remote_file(remote_file)
@@ -176,7 +189,9 @@ class Kamal::Configuration::Accessory
176
189
 
177
190
  def hosts_from_roles
178
191
  if accessory_config.key?("roles")
179
- 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
180
195
  end
181
196
  end
182
197
 
@@ -115,6 +115,10 @@ class Kamal::Configuration::Builder
115
115
  builder_config["provenance"]
116
116
  end
117
117
 
118
+ def sbom
119
+ builder_config["sbom"]
120
+ end
121
+
118
122
  def git_clone?
119
123
  Kamal::Git.used? && builder_config["context"].nil?
120
124
  end
@@ -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
@@ -98,3 +98,7 @@ accessories:
98
98
  # Defaults to kamal:
99
99
  network: custom
100
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
@@ -108,3 +108,9 @@ builder:
108
108
  # It is used to configure provenance attestations for the build result.
109
109
  # The value can also be a boolean to enable or disable provenance attestations.
110
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.2"
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
@@ -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,13 +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
+
5
7
  check_dependencies!
8
+
6
9
  session = login(account)
7
10
  full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
8
11
  fetch_secrets(full_secrets, account: account, session: session)
9
12
  end
10
13
 
14
+ def requires_account?
15
+ true
16
+ end
17
+
11
18
  private
12
19
  def login(...)
13
20
  raise NotImplementedError
@@ -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
@@ -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
@@ -32,7 +32,7 @@ class Kamal::Secrets
32
32
  private
33
33
  def secrets
34
34
  @secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
35
- secrets.merge!(::Dotenv.parse(secrets_file))
35
+ secrets.merge!(::Dotenv.parse(secrets_file, overwrite: true))
36
36
  end
37
37
  end
38
38
 
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "2.3.0"
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.3.0
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-31 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
@@ -247,6 +247,7 @@ files:
247
247
  - lib/kamal/commander/specifics.rb
248
248
  - lib/kamal/commands.rb
249
249
  - lib/kamal/commands/accessory.rb
250
+ - lib/kamal/commands/accessory/proxy.rb
250
251
  - lib/kamal/commands/app.rb
251
252
  - lib/kamal/commands/app/assets.rb
252
253
  - lib/kamal/commands/app/containers.rb
@@ -312,11 +313,14 @@ files:
312
313
  - lib/kamal/git.rb
313
314
  - lib/kamal/secrets.rb
314
315
  - lib/kamal/secrets/adapters.rb
316
+ - lib/kamal/secrets/adapters/aws_secrets_manager.rb
315
317
  - lib/kamal/secrets/adapters/base.rb
316
318
  - lib/kamal/secrets/adapters/bitwarden.rb
319
+ - lib/kamal/secrets/adapters/doppler.rb
317
320
  - lib/kamal/secrets/adapters/last_pass.rb
318
321
  - lib/kamal/secrets/adapters/one_password.rb
319
322
  - lib/kamal/secrets/adapters/test.rb
323
+ - lib/kamal/secrets/adapters/test_optional_account.rb
320
324
  - lib/kamal/secrets/dotenv/inline_command_substitution.rb
321
325
  - lib/kamal/sshkit_with_ext.rb
322
326
  - lib/kamal/tags.rb