kamal 2.2.1 → 2.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69b742751668ecf7877e0afc8e71a111e3faad501fe5ea01ff773793bf599df7
4
- data.tar.gz: f5e3738aac9cdbc2f9e493613c0c975ef7c7939ba06f9982a1508f5440c02213
3
+ metadata.gz: 0d5e6961984a3361505ebf35dfc52920c49af92085dd99c923dbfa801c668a95
4
+ data.tar.gz: adddf71abb26f58e5f7bb16c62553f0d3358499ac4c09f333df75b650cc37b54
5
5
  SHA512:
6
- metadata.gz: 6466a0ffb55dacfe8e2f411341ecdd6a71f4f00dc0f995789d8225ab39d0c9cbd9f5b1e88016e5ab06e88cea4bba6fac48d847d8ee84f37cf2f251d021bba1b1
7
- data.tar.gz: c4efb73c259cd4ae0d5025c6eccecb3aaa622b9ce93d5cf11d0794f3673a36b8decfb61f1092449e2a293562ec3fb1df45eca7e5158179373a48734b99c4a46c
6
+ metadata.gz: fdbd4d88c6fe8001def4c53a9f3ee058e871e58ce99f6697a478cbbac48f646947aab415d08074668e2c41147f8fd15fa1cb76f01919d9411aa0e85be6767aba
7
+ data.tar.gz: b1716d147e84b386f8bac27deb60f70fc6a7fdfad18fc376dca1391d400de388085b654d5ec8e8e9b3b83724e0a22599bc5626d07cd3812330389add2ed915f9
@@ -14,14 +14,14 @@ 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
21
21
  end
22
22
  end
23
23
 
24
- desc "boot_config <set|get|reset>", "Mange kamal-proxy boot configuration"
24
+ desc "boot_config <set|get|reset>", "Manage kamal-proxy boot configuration"
25
25
  option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
26
26
  option :http_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTP_PORT, desc: "HTTP port to publish on the host"
27
27
  option :https_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTPS_PORT, desc: "HTTPS port to publish on the host"
@@ -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.
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.
19
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.
@@ -90,7 +91,7 @@ builder:
90
91
  # directories:
91
92
  # - data:/var/lib/mysql
92
93
  # redis:
93
- # image: redis:7.0
94
+ # image: valkey/valkey:8
94
95
  # host: 192.168.0.2
95
96
  # port: 6379
96
97
  # directories:
@@ -1,7 +1,7 @@
1
1
  class Kamal::Commands::Accessory < Kamal::Commands::Base
2
2
  attr_reader :accessory_config
3
3
  delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
4
- :publish_args, :env_args, :volume_args, :label_args, :option_args,
4
+ :network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
5
5
  :secrets_io, :secrets_path, :env_directory,
6
6
  to: :accessory_config
7
7
 
@@ -15,7 +15,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
15
15
  "--name", service_name,
16
16
  "--detach",
17
17
  "--restart", "unless-stopped",
18
- "--network", "kamal",
18
+ *network_args,
19
19
  *config.logging_args,
20
20
  *publish_args,
21
21
  *env_args,
@@ -64,7 +64,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
64
64
  docker :run,
65
65
  ("-it" if interactive),
66
66
  "--rm",
67
- "--network", "kamal",
67
+ *network_args,
68
68
  *env_args,
69
69
  *volume_args,
70
70
  image,
@@ -11,14 +11,7 @@ module Kamal::Commands
11
11
  end
12
12
 
13
13
  def run_over_ssh(*command, host:)
14
- "ssh".tap do |cmd|
15
- if config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Jump)
16
- cmd << " -J #{config.ssh.proxy.jump_proxies}"
17
- elsif config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Command)
18
- cmd << " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
19
- end
20
- cmd << " -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
21
- end
14
+ "ssh#{ssh_proxy_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
22
15
  end
23
16
 
24
17
  def container_id_for(container_name:, only_running: false)
@@ -92,5 +85,14 @@ module Kamal::Commands
92
85
  def tags(**details)
93
86
  Kamal::Tags.from_config(config, **details)
94
87
  end
88
+
89
+ def ssh_proxy_args
90
+ case config.ssh.proxy
91
+ when Net::SSH::Proxy::Jump
92
+ " -J #{config.ssh.proxy.jump_proxies}"
93
+ when Net::SSH::Proxy::Command
94
+ " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
95
+ end
96
+ end
95
97
  end
96
98
  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, :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 ]
41
41
  end
42
42
 
43
43
  def build_context
@@ -97,6 +97,10 @@ 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
+
100
104
  def builder_config
101
105
  config.builder
102
106
  end
@@ -1,29 +1,31 @@
1
1
  module Kamal::Commands::Builder::Clone
2
- extend ActiveSupport::Concern
3
-
4
- included do
5
- delegate :clone_directory, :build_directory, to: :"config.builder"
6
- end
7
-
8
2
  def clone
9
- git :clone, Kamal::Git.root, "--recurse-submodules", path: clone_directory
3
+ git :clone, escaped_root, "--recurse-submodules", path: config.builder.clone_directory.shellescape
10
4
  end
11
5
 
12
6
  def clone_reset_steps
13
7
  [
14
- git(:remote, "set-url", :origin, Kamal::Git.root, path: build_directory),
15
- git(:fetch, :origin, path: build_directory),
16
- git(:reset, "--hard", Kamal::Git.revision, path: build_directory),
17
- git(:clean, "-fdx", path: build_directory),
18
- git(:submodule, :update, "--init", path: build_directory)
8
+ git(:remote, "set-url", :origin, escaped_root, path: escaped_build_directory),
9
+ git(:fetch, :origin, path: escaped_build_directory),
10
+ git(:reset, "--hard", Kamal::Git.revision, path: escaped_build_directory),
11
+ git(:clean, "-fdx", path: escaped_build_directory),
12
+ git(:submodule, :update, "--init", path: escaped_build_directory)
19
13
  ]
20
14
  end
21
15
 
22
16
  def clone_status
23
- git :status, "--porcelain", path: build_directory
17
+ git :status, "--porcelain", path: escaped_build_directory
24
18
  end
25
19
 
26
20
  def clone_revision
27
- git :"rev-parse", :HEAD, path: build_directory
21
+ git :"rev-parse", :HEAD, path: escaped_build_directory
22
+ end
23
+
24
+ def escaped_root
25
+ Kamal::Git.root.shellescape
26
+ end
27
+
28
+ def escaped_build_directory
29
+ config.builder.build_directory.shellescape
28
30
  end
29
31
  end
@@ -1,6 +1,8 @@
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
8
  attr_reader :name, :accessory_config, :env
@@ -38,6 +40,10 @@ class Kamal::Configuration::Accessory
38
40
  end
39
41
  end
40
42
 
43
+ def network_args
44
+ argumentize "--network", network
45
+ end
46
+
41
47
  def publish_args
42
48
  argumentize "--publish", port if port
43
49
  end
@@ -173,4 +179,8 @@ class Kamal::Configuration::Accessory
173
179
  accessory_config["roles"].flat_map { |role| config.role(role).hosts }
174
180
  end
175
181
  end
182
+
183
+ def network
184
+ accessory_config["network"] || DEFAULT_NETWORK
185
+ end
176
186
  end
@@ -111,6 +111,10 @@ class Kamal::Configuration::Builder
111
111
  builder_config["ssh"]
112
112
  end
113
113
 
114
+ def provenance
115
+ builder_config["provenance"]
116
+ end
117
+
114
118
  def git_clone?
115
119
  Kamal::Git.used? && builder_config["context"].nil?
116
120
  end
@@ -166,7 +170,7 @@ class Kamal::Configuration::Builder
166
170
  end
167
171
 
168
172
  def cache_to_config_for_registry
169
- [ "type=registry", builder_config["cache"]&.fetch("options", nil), "ref=#{cache_image_ref}" ].compact.join(",")
173
+ [ "type=registry", "ref=#{cache_image_ref}", builder_config["cache"]&.fetch("options", nil) ].compact.join(",")
170
174
  end
171
175
 
172
176
  def repo_basename
@@ -90,3 +90,11 @@ 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
+
@@ -102,3 +102,9 @@ 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
@@ -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.2"
18
18
  PROXY_HTTP_PORT = 80
19
19
  PROXY_HTTPS_PORT = 443
20
20
  PROXY_LOG_MAX_SIZE = "10m"
@@ -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
@@ -2,6 +2,7 @@ class Kamal::Secrets::Adapters::Base
2
2
  delegate :optionize, to: Kamal::Utils
3
3
 
4
4
  def fetch(secrets, account:, from: nil)
5
+ check_dependencies!
5
6
  session = login(account)
6
7
  full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
7
8
  fetch_secrets(full_secrets, account: account, session: session)
@@ -15,4 +16,8 @@ class Kamal::Secrets::Adapters::Base
15
16
  def fetch_secrets(...)
16
17
  raise NotImplementedError
17
18
  end
19
+
20
+ def check_dependencies!
21
+ raise NotImplementedError
22
+ end
18
23
  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
@@ -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
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
35
  secrets.merge!(::Dotenv.parse(secrets_file))
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.1"
2
+ VERSION = "2.3.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.1
4
+ version: 2.3.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-09 00:00:00.000000000 Z
11
+ date: 2024-10-31 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