kamal 2.7.0 → 2.8.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: 4e1cf57d731a8b129a8ccbb86faddd3e813bc4d17895e6e538fe904f5bb65d27
4
- data.tar.gz: 2d7d81b3a34f42fb427bfed18f9cf0ed1955d38e4b4783c1407bc1db6de5cef6
3
+ metadata.gz: 80ba6d51041312c99d659a1bfaac13bfa0ed7d1e758bde65e5475c0e51d88238
4
+ data.tar.gz: f0cc94a905da2cfb5dcf4f5d9ead2194df1b0ed46fd3b8671a59bfc333b10fbf
5
5
  SHA512:
6
- metadata.gz: f3144c40082cfa24c78e2a1ebf2f433e491be3f5966e45cfc5488c3a11f5a3f513f097f7818cbc9e34d20b79986e64212055a87030fd4aa386a1d82bbb7d0efe
7
- data.tar.gz: c6b796497a6f7a68815d340664b34fb9943f55ea7121122994aa3fdadaa5b12a18fb4078ff194cffb6060917a74611fdf31017afa75f2515d94ec259e0809251
6
+ metadata.gz: e33ddb40f46e587364d9121bbd2f2d829beddeb956f4bf1d8399e5ac835285180914f51e1365bee96ec4b82a02c0e88df8ff513a68bf95e44a8dc7e0ce081509
7
+ data.tar.gz: 8a0b1a90bbacd96d072cbbe5cec3371e5f7fe6d213cfa5dac3203f6d4ef02fadbddd1e3458909f3051adcd9218d69d6edb5b193f86fca6fac28b49b00bd2dd91
@@ -77,6 +77,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
77
77
  KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
78
78
  else
79
79
  prepare(name)
80
+ pull_image(name)
80
81
  stop(name)
81
82
  remove_container(name)
82
83
  boot(name, prepare: false)
@@ -203,6 +204,18 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
203
204
  end
204
205
  end
205
206
 
207
+ desc "pull_image [NAME]", "Pull accessory image on host", hide: true
208
+ def pull_image(name)
209
+ with_lock do
210
+ with_accessory(name) do |accessory, hosts|
211
+ on(hosts) do
212
+ execute *KAMAL.auditor.record("Pull #{name} accessory image"), verbosity: :debug
213
+ execute *accessory.pull_image
214
+ end
215
+ end
216
+ end
217
+ end
218
+
206
219
  desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
207
220
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
208
221
  def remove(name)
@@ -11,6 +11,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
11
11
 
12
12
  desc "push", "Build and push app image to registry"
13
13
  option :output, type: :string, default: "registry", banner: "export_type", desc: "Exported type for the build result, and may be any exported type supported by 'buildx --output'."
14
+ option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
14
15
  def push
15
16
  cli = self
16
17
 
@@ -19,7 +20,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
19
20
  pre_connect_if_required
20
21
 
21
22
  ensure_docker_installed
22
- login_to_registry_locally
23
+ login_to_registry_locally if KAMAL.builder.login_to_registry_locally?
23
24
 
24
25
  run_hook "pre-build"
25
26
 
@@ -56,10 +57,10 @@ class Kamal::Cli::Build < Kamal::Cli::Base
56
57
  end
57
58
 
58
59
  # Get the command here to ensure the Dir.chdir doesn't interfere with it
59
- push = KAMAL.builder.push(cli.options[:output])
60
+ push = KAMAL.builder.push(cli.options[:output], no_cache: cli.options[:no_cache])
60
61
 
61
62
  KAMAL.with_verbosity(:debug) do
62
- Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
63
+ Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.builder.push_env }
63
64
  end
64
65
  end
65
66
  end
@@ -67,16 +68,18 @@ class Kamal::Cli::Build < Kamal::Cli::Base
67
68
 
68
69
  desc "pull", "Pull app image from registry onto servers"
69
70
  def pull
70
- login_to_registry_remotely
71
-
72
- if (first_hosts = mirror_hosts).any?
73
- #  Pull on a single host per mirror first to seed them
74
- say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
75
- pull_on_hosts(first_hosts)
76
- say "Pulling image on remaining hosts...", :magenta
77
- pull_on_hosts(KAMAL.app_hosts - first_hosts)
78
- else
79
- pull_on_hosts(KAMAL.app_hosts)
71
+ login_to_registry_remotely unless KAMAL.registry.local?
72
+
73
+ forward_local_registry_port do
74
+ if (first_hosts = mirror_hosts).any?
75
+ #  Pull on a single host per mirror first to seed them
76
+ say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
77
+ pull_on_hosts(first_hosts)
78
+ say "Pulling image on remaining hosts...", :magenta
79
+ pull_on_hosts(KAMAL.app_hosts - first_hosts)
80
+ else
81
+ pull_on_hosts(KAMAL.app_hosts)
82
+ end
80
83
  end
81
84
  end
82
85
 
@@ -119,6 +122,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
119
122
 
120
123
  desc "dev", "Build using the working directory, tag it as dirty, and push to local image store."
121
124
  option :output, type: :string, default: "docker", banner: "export_type", desc: "Exported type for the build result, and may be any exported type supported by 'buildx --output'."
125
+ option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
122
126
  def dev
123
127
  cli = self
124
128
 
@@ -144,7 +148,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
144
148
 
145
149
  with_env(KAMAL.config.builder.secrets) do
146
150
  run_locally do
147
- build = KAMAL.builder.push(cli.options[:output], tag_as_dirty: true)
151
+ build = KAMAL.builder.push(cli.options[:output], tag_as_dirty: true, no_cache: cli.options[:no_cache])
148
152
  KAMAL.with_verbosity(:debug) do
149
153
  execute(*build)
150
154
  end
@@ -192,7 +196,11 @@ class Kamal::Cli::Build < Kamal::Cli::Base
192
196
 
193
197
  def login_to_registry_locally
194
198
  run_locally do
195
- execute *KAMAL.registry.login
199
+ if KAMAL.registry.local?
200
+ execute *KAMAL.registry.setup
201
+ else
202
+ execute *KAMAL.registry.login
203
+ end
196
204
  end
197
205
  end
198
206
 
@@ -201,4 +209,14 @@ class Kamal::Cli::Build < Kamal::Cli::Base
201
209
  execute *KAMAL.registry.login
202
210
  end
203
211
  end
212
+
213
+ def forward_local_registry_port(&block)
214
+ if KAMAL.config.registry.local?
215
+ Kamal::Cli::PortForwarding.
216
+ new(KAMAL.hosts, KAMAL.config.registry.local_port).
217
+ forward(&block)
218
+ else
219
+ yield
220
+ end
221
+ end
204
222
  end
@@ -1,6 +1,7 @@
1
1
  class Kamal::Cli::Main < Kamal::Cli::Base
2
2
  desc "setup", "Setup all accessories, push the env, and deploy app to servers"
3
3
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
4
+ option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
4
5
  def setup
5
6
  print_runtime do
6
7
  with_lock do
@@ -16,6 +17,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
16
17
 
17
18
  desc "deploy", "Deploy app to servers"
18
19
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
20
+ option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
19
21
  def deploy(boot_accessories: false)
20
22
  runtime = print_runtime do
21
23
  invoke_options = deploy_options
@@ -51,6 +53,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
51
53
 
52
54
  desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy and pruning"
53
55
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
56
+ option :no_cache, type: :boolean, default: false, desc: "Build without using Docker's build cache"
54
57
  def redeploy
55
58
  runtime = print_runtime do
56
59
  invoke_options = deploy_options
@@ -182,7 +185,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
182
185
  invoke "kamal:cli:app:remove", [], options.without(:confirmed)
183
186
  invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
184
187
  invoke "kamal:cli:accessory:remove", [ "all" ], options
185
- invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true)
188
+ invoke "kamal:cli:registry:remove", [], options.without(:confirmed).merge(skip_local: true)
186
189
  end
187
190
  end
188
191
  end
@@ -272,6 +275,8 @@ class Kamal::Cli::Main < Kamal::Cli::Base
272
275
  end
273
276
 
274
277
  def deploy_options
275
- { "version" => KAMAL.config.version }.merge(options.without("skip_push"))
278
+ base_options = options.without("skip_push")
279
+ base_options = base_options.except("no_cache") unless base_options["no_cache"]
280
+ { "version" => KAMAL.config.version }.merge(base_options)
276
281
  end
277
282
  end
@@ -0,0 +1,42 @@
1
+ class Kamal::Cli::PortForwarding
2
+ attr_reader :hosts, :port
3
+
4
+ def initialize(hosts, port)
5
+ @hosts = hosts
6
+ @port = port
7
+ end
8
+
9
+ def forward
10
+ @done = false
11
+ forward_ports
12
+
13
+ yield
14
+ ensure
15
+ stop
16
+ end
17
+
18
+ private
19
+
20
+ def stop
21
+ @done = true
22
+ @threads.to_a.each(&:join)
23
+ end
24
+
25
+ def forward_ports
26
+ @threads = hosts.map do |host|
27
+ Thread.new do
28
+ Net::SSH.start(host, KAMAL.config.ssh.user, **{ proxy: KAMAL.config.ssh.proxy }.compact) do |ssh|
29
+ ssh.forward.remote(port, "localhost", port, "localhost")
30
+ ssh.loop(0.1) do
31
+ if @done
32
+ ssh.forward.cancel_remote(port, "localhost")
33
+ break
34
+ else
35
+ true
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,19 +1,27 @@
1
1
  class Kamal::Cli::Registry < Kamal::Cli::Base
2
- desc "login", "Log in to registry locally and remotely"
2
+ desc "setup", "Setup local registry or log in to remote registry locally and remotely"
3
3
  option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
4
4
  option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
5
- def login
5
+ def setup
6
6
  ensure_docker_installed unless options[:skip_local]
7
7
 
8
- run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
9
- on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
8
+ if KAMAL.registry.local?
9
+ run_locally { execute *KAMAL.registry.setup } unless options[:skip_local]
10
+ else
11
+ run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
12
+ on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
13
+ end
10
14
  end
11
15
 
12
- desc "logout", "Log out of registry locally and remotely"
16
+ desc "remove", "Remove local registry or log out of remote registry locally and remotely"
13
17
  option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
14
18
  option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
15
- def logout
16
- run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
17
- on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
19
+ def remove
20
+ if KAMAL.registry.local?
21
+ run_locally { execute *KAMAL.registry.remove, raise_on_non_zero_exit: false } unless options[:skip_local]
22
+ else
23
+ run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
24
+ on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
25
+ end
18
26
  end
19
27
  end
@@ -25,13 +25,14 @@ proxy:
25
25
 
26
26
  # Credentials for your image host.
27
27
  registry:
28
+ server: localhost:5555
28
29
  # Specify the registry server, if you're not using Docker Hub
29
30
  # server: registry.digitalocean.com / ghcr.io / ...
30
- username: my-user
31
+ # username: my-user
31
32
 
32
33
  # Always use an access token rather than real password (pulled from .kamal/secrets).
33
- password:
34
- - KAMAL_REGISTRY_PASSWORD
34
+ # password:
35
+ # - KAMAL_REGISTRY_PASSWORD
35
36
 
36
37
  # Configure builder setup.
37
38
  builder:
@@ -3,7 +3,7 @@
3
3
  # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
4
4
 
5
5
  # Option 1: Read secrets from the environment
6
- KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
6
+ # KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
7
7
 
8
8
  # Option 2: Read secrets via a command
9
9
  # RAILS_MASTER_KEY=$(cat config/master.key)
@@ -21,7 +21,7 @@ class Kamal::Commander
21
21
  end
22
22
 
23
23
  def config
24
- @config ||= Kamal::Configuration.create_from(**@config_kwargs).tap do |config|
24
+ @config ||= Kamal::Configuration.create_from(**@config_kwargs.to_h).tap do |config|
25
25
  @config_kwargs = nil
26
26
  configure_sshkit_with(config)
27
27
  end
@@ -90,6 +90,10 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
90
90
  end
91
91
  end
92
92
 
93
+ def pull_image
94
+ docker :image, :pull, image
95
+ end
96
+
93
97
  def remove_service_directory
94
98
  [ :rm, "-rf", service_name ]
95
99
  end
@@ -23,6 +23,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
23
23
  "--env", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
24
24
  "--env", "KAMAL_VERSION=\"#{config.version}\"",
25
25
  "--env", "KAMAL_HOST=\"#{host}\"",
26
+ "--env", "KAMAL_DESTINATION=\"#{config.destination}\"",
26
27
  *role.env_args(host),
27
28
  *role.logging_args,
28
29
  *config.volume_args,
@@ -14,13 +14,14 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
14
14
  docker :image, :rm, "--force", config.absolute_image
15
15
  end
16
16
 
17
- def push(export_action = "registry", tag_as_dirty: false)
17
+ def push(export_action = "registry", tag_as_dirty: false, no_cache: false)
18
18
  docker :buildx, :build,
19
19
  "--output=type=#{export_action}",
20
20
  *platform_options(arches),
21
21
  *([ "--builder", builder_name ] unless docker_driver?),
22
22
  *build_tag_options(tag_as_dirty: tag_as_dirty),
23
23
  *build_options,
24
+ *([ "--no-cache" ] if no_cache),
24
25
  build_context,
25
26
  "2>&1"
26
27
  end
@@ -60,6 +61,14 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
60
61
  docker(:info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
61
62
  end
62
63
 
64
+ def login_to_registry_locally?
65
+ true
66
+ end
67
+
68
+ def push_env
69
+ {}
70
+ end
71
+
63
72
  private
64
73
  def build_tag_names(tag_as_dirty: false)
65
74
  tag_names = [ config.absolute_image, config.latest_image ]
@@ -1,6 +1,15 @@
1
1
  class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
2
2
  def create
3
- docker :buildx, :create, "--name", builder_name, "--driver=#{driver}" unless docker_driver?
3
+ return if docker_driver?
4
+
5
+ options =
6
+ if KAMAL.registry.local?
7
+ "--driver=#{driver} --driver-opt network=host"
8
+ else
9
+ "--driver=#{driver}"
10
+ end
11
+
12
+ docker :buildx, :create, "--name", builder_name, options
4
13
  end
5
14
 
6
15
  def remove
@@ -9,6 +18,10 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
9
18
 
10
19
  private
11
20
  def builder_name
12
- "kamal-local-#{driver}"
21
+ if KAMAL.registry.local?
22
+ "kamal-local-registry-#{driver}"
23
+ else
24
+ "kamal-local-#{driver}"
25
+ end
13
26
  end
14
27
  end
@@ -1,7 +1,7 @@
1
1
  class Kamal::Commands::Builder::Pack < Kamal::Commands::Builder::Base
2
- def push(export_action = "registry")
2
+ def push(export_action = "registry", tag_as_dirty: false, no_cache: false)
3
3
  combine \
4
- build,
4
+ build(tag_as_dirty: tag_as_dirty, no_cache: no_cache),
5
5
  export(export_action)
6
6
  end
7
7
 
@@ -13,15 +13,15 @@ class Kamal::Commands::Builder::Pack < Kamal::Commands::Builder::Base
13
13
  alias_method :inspect_builder, :info
14
14
 
15
15
  private
16
- def build
16
+ def build(tag_as_dirty: false, no_cache: false)
17
17
  pack(:build,
18
18
  config.repository,
19
19
  "--platform", platform,
20
20
  "--creation-time", "now",
21
21
  "--builder", pack_builder,
22
22
  buildpacks,
23
- "-t", config.absolute_image,
24
- "-t", config.latest_image,
23
+ *build_tag_options(tag_as_dirty: tag_as_dirty),
24
+ *([ "--clear-cache" ] if no_cache),
25
25
  "--env", "BP_IMAGE_LABELS=service=#{config.service}",
26
26
  *argumentize("--env", args),
27
27
  *argumentize("--env", secrets, sensitive: true),
@@ -19,11 +19,19 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base
19
19
 
20
20
  def inspect_builder
21
21
  combine \
22
- combine inspect_buildx, inspect_remote_context,
22
+ combine(inspect_buildx, inspect_remote_context),
23
23
  [ "(echo no compatible builder && exit 1)" ],
24
24
  by: "||"
25
25
  end
26
26
 
27
+ def login_to_registry_locally?
28
+ false
29
+ end
30
+
31
+ def push_env
32
+ { "BUILDKIT_NO_CLIENT_TOKEN" => "1" }
33
+ end
34
+
27
35
  private
28
36
  def builder_name
29
37
  "kamal-remote-#{remote.gsub(/[^a-z0-9_-]/, "-")}"
@@ -1,8 +1,14 @@
1
1
  require "active_support/core_ext/string/filters"
2
2
 
3
3
  class Kamal::Commands::Builder < Kamal::Commands::Base
4
- delegate :create, :remove, :dev, :push, :clean, :pull, :info, :inspect_builder, :validate_image, :first_mirror, to: :target
5
- delegate :local?, :remote?, :pack?, :cloud?, to: "config.builder"
4
+ delegate \
5
+ :create, :remove, :dev, :push, :clean, :pull, :info, :inspect_builder,
6
+ :validate_image, :first_mirror, :login_to_registry_locally?, :push_env,
7
+ to: :target
8
+
9
+ delegate \
10
+ :local?, :remote?, :pack?, :cloud?,
11
+ to: "config.builder"
6
12
 
7
13
  include Clone
8
14
 
@@ -2,6 +2,8 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
2
2
  def login(registry_config: nil)
3
3
  registry_config ||= config.registry
4
4
 
5
+ return if registry_config.local?
6
+
5
7
  docker :login,
6
8
  registry_config.server,
7
9
  "-u", sensitive(Kamal::Utils.escape_shell_value(registry_config.username)),
@@ -13,4 +15,24 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
13
15
 
14
16
  docker :logout, registry_config.server
15
17
  end
18
+
19
+ def setup(registry_config: nil)
20
+ registry_config ||= config.registry
21
+
22
+ combine \
23
+ docker(:start, "kamal-docker-registry"),
24
+ docker(:run, "--detach", "-p", "127.0.0.1:#{registry_config.local_port}:5000", "--name", "kamal-docker-registry", "registry:3"),
25
+ by: "||"
26
+ end
27
+
28
+ def remove
29
+ combine \
30
+ docker(:stop, "kamal-docker-registry"),
31
+ docker(:rm, "kamal-docker-registry"),
32
+ by: "&&"
33
+ end
34
+
35
+ def local?
36
+ config.registry.local?
37
+ end
16
38
  end
@@ -45,27 +45,23 @@ proxy:
45
45
  # unless you explicitly set `forward_headers: true`
46
46
  #
47
47
  # Defaults to `false`:
48
- ssl: ...
48
+ ssl: true
49
49
 
50
50
  # Custom SSL certificate
51
51
  #
52
52
  # In some cases, using Let's Encrypt for automatic certificate management is not an
53
- # option, for example if you are running from host than one host. Or you may already
54
- # have SSL certificates issued by a different Certificate Authority (CA).
55
- # Kamal supports loading custom SSL certificates
56
- # directly from secrets.
57
- #
58
- # Examples:
59
- # ssl: true # Enable SSL with Let's Encrypt
60
- # ssl: false # Disable SSL
61
- # ssl: # Enable custom SSL
62
- # certificate_pem: CERTIFICATE_PEM
63
- # private_key_pem: PRIVATE_KEY_PEM
53
+ # option, for example if you are running from more than one host.
64
54
  #
55
+ # Or you may already have SSL certificates issued by a different Certificate Authority (CA).
56
+ #
57
+ # Kamal supports loading custom SSL certificates directly from secrets. You should
58
+ # pass a hash mapping the `certificate_pem` and `private_key_pem` to the secret names.
59
+ ssl:
60
+ certificate_pem: CERTIFICATE_PEM
61
+ private_key_pem: PRIVATE_KEY_PEM
65
62
  # ### Notes
66
- # - If the certificate or key is missing or invalid, kamal-proxy will fail to start.
67
- # - Always handle SSL certificates and private keys securely. Avoid hard-coding them in deploy.yml files or source control.
68
- # - For automated certificate management, consider using the built-in Let's Encrypt integration instead.
63
+ # - If the certificate or key is missing or invalid, deployments will fail.
64
+ # - Always handle SSL certificates and private keys securely. Avoid hard-coding them in source control.
69
65
 
70
66
  # SSL redirect
71
67
  #
@@ -93,9 +89,21 @@ proxy:
93
89
  #
94
90
  # For applications that split their traffic to different services based on the request path,
95
91
  # you can use path-based routing to mount services under different path prefixes.
96
- path_prefix: '/api'
92
+ # Usage sample: path_prefix: '/api'
93
+ #
94
+ # You can also specify multiple paths in two ways.
95
+ #
96
+ # When using path_prefix you can supply multiple routes separated by commas.
97
+ path_prefix: "/api,/oauth_callback"
98
+ # You can also specify paths as a list of paths, the configuration will be
99
+ # rolled together into a comma separated string.
100
+ path_prefixes:
101
+ - "/api"
102
+ - "/oauth_callback"
97
103
  # By default, the path prefix will be stripped from the request before it is forwarded upstream.
104
+ #
98
105
  # So in the example above, a request to /api/users/123 will be forwarded to web-1 as /users/123.
106
+ #
99
107
  # To instead forward the request with the original path (including the prefix),
100
108
  # specify --strip-path-prefix=false
101
109
  strip_path_prefix: false
@@ -63,6 +63,10 @@ class Kamal::Configuration::Proxy
63
63
  tls_path(config.proxy_boot.tls_container_directory, "key.pem") if custom_ssl_certificate?
64
64
  end
65
65
 
66
+ def path_prefixes
67
+ proxy_config["path_prefixes"] || proxy_config["path_prefix"]&.split(",") || []
68
+ end
69
+
66
70
  def deploy_options
67
71
  {
68
72
  host: hosts,
@@ -80,7 +84,7 @@ class Kamal::Configuration::Proxy
80
84
  "buffer-memory": proxy_config.dig("buffering", "memory"),
81
85
  "max-request-body": proxy_config.dig("buffering", "max_request_body"),
82
86
  "max-response-body": proxy_config.dig("buffering", "max_response_body"),
83
- "path-prefix": proxy_config.dig("path_prefix"),
87
+ "path-prefix": path_prefixes,
84
88
  "strip-path-prefix": proxy_config.dig("strip_path_prefix"),
85
89
  "forward-headers": proxy_config.dig("forward_headers"),
86
90
  "tls-redirect": proxy_config.dig("ssl_redirect"),
@@ -19,6 +19,14 @@ class Kamal::Configuration::Registry
19
19
  lookup("password")
20
20
  end
21
21
 
22
+ def local?
23
+ server.to_s.match?("^localhost[:$]")
24
+ end
25
+
26
+ def local_port
27
+ local? ? (server.split(":").last.to_i || 80) : nil
28
+ end
29
+
22
30
  private
23
31
  attr_reader :registry_config, :secrets
24
32
 
@@ -15,10 +15,12 @@ class Kamal::Configuration::Validator::Registry < Kamal::Configuration::Validato
15
15
  with_context(key) do
16
16
  value = config[key]
17
17
 
18
- error "is required" unless value.present?
18
+ unless config["server"]&.match?("^localhost[:$]")
19
+ error "is required" unless value.present?
19
20
 
20
- unless value.is_a?(String) || (value.is_a?(Array) && value.size == 1 && value.first.is_a?(String))
21
- error "should be a string or an array with one string (for secret lookup)"
21
+ unless value.is_a?(String) || (value.is_a?(Array) && value.size == 1 && value.first.is_a?(String))
22
+ error "should be a string or an array with one string (for secret lookup)"
23
+ end
22
24
  end
23
25
  end
24
26
  end
@@ -24,11 +24,11 @@ class Kamal::Configuration::Validator
24
24
  example_value = example[key]
25
25
 
26
26
  if example_value == "..."
27
- if key.to_s == "ssl"
28
- validate_type! value, TrueClass, FalseClass, Hash
29
- elsif key.to_s != "proxy" || !boolean?(value.class)
27
+ unless key.to_s == "proxy" && boolean?(value.class)
30
28
  validate_type! value, *(Array if key == :servers), Hash
31
29
  end
30
+ elsif key.to_s == "ssl"
31
+ validate_type! value, TrueClass, FalseClass, Hash
32
32
  elsif key == "hosts"
33
33
  validate_servers! value
34
34
  elsif example_value.is_a?(Array)
@@ -6,7 +6,7 @@ require "erb"
6
6
  require "net/ssh/proxy/jump"
7
7
 
8
8
  class Kamal::Configuration
9
- delegate :service, :image, :labels, :hooks_path, to: :raw_config, allow_nil: true
9
+ delegate :service, :labels, :hooks_path, to: :raw_config, allow_nil: true
10
10
  delegate :argumentize, :optionize, to: Kamal::Utils
11
11
 
12
12
  attr_reader :destination, :raw_config, :secrets
@@ -157,6 +157,13 @@ class Kamal::Configuration
157
157
  (proxy_roles.flat_map(&:hosts) + proxy_accessories.flat_map(&:hosts)).uniq
158
158
  end
159
159
 
160
+ def image
161
+ name = raw_config&.image.presence
162
+ name ||= raw_config&.service if registry.local?
163
+
164
+ name
165
+ end
166
+
160
167
  def repository
161
168
  [ registry.server, image ].compact.join("/")
162
169
  end
@@ -282,10 +289,12 @@ class Kamal::Configuration
282
289
  end
283
290
 
284
291
  def ensure_required_keys_present
285
- %i[ service image registry ].each do |key|
292
+ %i[ service registry ].each do |key|
286
293
  raise Kamal::ConfigurationError, "Missing required configuration for #{key}" unless raw_config[key].present?
287
294
  end
288
295
 
296
+ raise Kamal::ConfigurationError, "Missing required configuration for image" if image.blank?
297
+
289
298
  if raw_config.servers.nil?
290
299
  raise Kamal::ConfigurationError, "No servers or accessories specified" unless raw_config.accessories.present?
291
300
  else
@@ -17,7 +17,7 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
17
17
 
18
18
  def fetch_secrets(secrets, from:, account:, session:)
19
19
  if secrets.blank?
20
- fetch_all_secrets(from: from, account: account, session: session) if secrets.blank?
20
+ fetch_all_secrets(from: from, account: account, session: session)
21
21
  else
22
22
  fetch_specified_secrets(secrets, from: from, account: account, session: session)
23
23
  end
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "2.7.0"
2
+ VERSION = "2.8.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.0
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
@@ -229,6 +229,7 @@ files:
229
229
  - lib/kamal/cli/healthcheck/poller.rb
230
230
  - lib/kamal/cli/lock.rb
231
231
  - lib/kamal/cli/main.rb
232
+ - lib/kamal/cli/port_forwarding.rb
232
233
  - lib/kamal/cli/proxy.rb
233
234
  - lib/kamal/cli/prune.rb
234
235
  - lib/kamal/cli/registry.rb
@@ -355,7 +356,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
355
356
  - !ruby/object:Gem::Version
356
357
  version: '0'
357
358
  requirements: []
358
- rubygems_version: 3.6.7
359
+ rubygems_version: 3.6.9
359
360
  specification_version: 4
360
361
  summary: Deploy web apps in containers to servers running Docker with zero downtime.
361
362
  test_files: []