Skip to content

Commit dd1adc7

Browse files
Support multiple path prefixes per service
Adds the ability to route multiple paths to a service. As with the multiple host routing, specify multiple paths either by comma-delimiting them or by using the flag multiple times: kamal-proxy deploy myservice --path-prefix=/app,/api ...
1 parent d46f256 commit dd1adc7

File tree

9 files changed

+171
-134
lines changed

9 files changed

+171
-134
lines changed

internal/cmd/deploy.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"fmt"
55
"net/rpc"
6+
"slices"
67

78
"github.com/spf13/cobra"
89

@@ -28,7 +29,7 @@ func newDeployCommand() *deployCommand {
2829

2930
deployCommand.cmd.Flags().StringVar(&deployCommand.args.TargetURL, "target", "", "Target host to deploy")
3031
deployCommand.cmd.Flags().StringSliceVar(&deployCommand.args.Hosts, "host", []string{}, "Host(s) to serve this target on (empty for wildcard)")
31-
deployCommand.cmd.Flags().StringVar(&deployCommand.args.PathPrefix, "path-prefix", "", "Deploy the service below the specified path")
32+
deployCommand.cmd.Flags().StringSliceVar(&deployCommand.args.PathPrefixes, "path-prefix", []string{}, "Deploy the service below the specified path(s)")
3233
deployCommand.cmd.Flags().BoolVar(&deployCommand.args.ServiceOptions.StripPrefix, "strip-path-prefix", true, "With --path-prefix, strip prefix from request before forwarding")
3334

3435
deployCommand.cmd.Flags().BoolVar(&deployCommand.args.ServiceOptions.TLSEnabled, "tls", false, "Configure TLS for this target (requires a non-empty host)")
@@ -81,7 +82,7 @@ func (c *deployCommand) run(cmd *cobra.Command, args []string) error {
8182
}
8283

8384
func (c *deployCommand) preRun(cmd *cobra.Command, args []string) error {
84-
c.args.PathPrefix = server.NormalizePath(c.args.PathPrefix)
85+
c.args.PathPrefixes = server.NormalizePathPrefixes(c.args.PathPrefixes)
8586

8687
if cmd.Flags().Changed("max-request-body") && !cmd.Flags().Changed("buffer-requests") {
8788
return fmt.Errorf("max-request-body can only be set when request buffering is enabled")
@@ -100,7 +101,7 @@ func (c *deployCommand) preRun(cmd *cobra.Command, args []string) error {
100101
return fmt.Errorf("host must be set when using TLS")
101102
}
102103

103-
if c.args.PathPrefix != "/" {
104+
if !slices.Contains(c.args.PathPrefixes, "/") {
104105
return fmt.Errorf("TLS settings must be specified on the root path service")
105106
}
106107
}

internal/server/commands.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type DeployArgs struct {
2020
Service string
2121
TargetURL string
2222
Hosts []string
23-
PathPrefix string
23+
PathPrefixes []string
2424
DeployTimeout time.Duration
2525
DrainTimeout time.Duration
2626
ServiceOptions ServiceOptions
@@ -115,7 +115,7 @@ func (h *CommandHandler) Close() error {
115115
}
116116

117117
func (h *CommandHandler) Deploy(args DeployArgs, reply *bool) error {
118-
return h.router.SetServiceTarget(args.Service, args.Hosts, args.PathPrefix, args.TargetURL, args.ServiceOptions, args.TargetOptions, args.DeployTimeout, args.DrainTimeout)
118+
return h.router.SetServiceTarget(args.Service, args.Hosts, args.PathPrefixes, args.TargetURL, args.ServiceOptions, args.TargetOptions, args.DeployTimeout, args.DrainTimeout)
119119
}
120120

121121
func (h *CommandHandler) Pause(args PauseArgs, reply *bool) error {

internal/server/router.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ func (r *Router) RestoreLastSavedState() error {
9393

9494
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
9595
service, prefix := r.serviceForRequest(req)
96-
slog.Info("Matched", "service", service.name, "prefix", prefix)
9796
if service == nil {
9897
SetErrorResponse(w, req, http.StatusNotFound, nil)
9998
return
@@ -107,20 +106,20 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
107106
service.ServeHTTP(w, req)
108107
}
109108

110-
func (r *Router) SetServiceTarget(name string, hosts []string, pathPrefix string, targetURL string,
109+
func (r *Router) SetServiceTarget(name string, hosts []string, pathPrefixes []string, targetURL string,
111110
options ServiceOptions, targetOptions TargetOptions,
112111
deployTimeout time.Duration, drainTimeout time.Duration,
113112
) error {
114113
defer r.saveStateSnapshot()
115114

116-
slog.Info("Deploying", "service", name, "hosts", hosts, "path", pathPrefix, "target", targetURL, "tls", options.TLSEnabled, "strip", options.StripPrefix)
115+
slog.Info("Deploying", "service", name, "hosts", hosts, "paths", pathPrefixes, "target", targetURL, "tls", options.TLSEnabled, "strip", options.StripPrefix)
117116

118117
target, err := r.deployNewTargetWithOptions(targetURL, targetOptions, deployTimeout)
119118
if err != nil {
120119
return err
121120
}
122121

123-
err = r.setActiveTarget(name, hosts, pathPrefix, target, options, drainTimeout)
122+
err = r.setActiveTarget(name, hosts, pathPrefixes, target, options, drainTimeout)
124123
if err != nil {
125124
return err
126125
}
@@ -241,14 +240,17 @@ func (r *Router) ListActiveServices() ServiceDescriptionMap {
241240

242241
r.withReadLock(func() error {
243242
for name, service := range r.services.All() {
244-
host := strings.Join(service.hosts, ",")
245-
if host == "" {
246-
host = "*"
247-
}
248243
if service.active != nil {
244+
host := strings.Join(service.hosts, ",")
245+
if host == "" {
246+
host = "*"
247+
}
248+
249+
path := strings.Join(service.pathPrefixes, ",")
250+
249251
result[name] = ServiceDescription{
250252
Host: host,
251-
Path: service.pathPrefix,
253+
Path: path,
252254
Target: service.active.Target(),
253255
TLS: service.options.TLSEnabled,
254256
State: service.pauseController.GetState().String(),
@@ -337,11 +339,11 @@ func (r *Router) serviceForHost(host string) *Service {
337339
return r.services.ServiceForHost(host)
338340
}
339341

340-
func (r *Router) setActiveTarget(name string, hosts []string, pathPrefix string, target *Target, options ServiceOptions, drainTimeout time.Duration) error {
342+
func (r *Router) setActiveTarget(name string, hosts []string, pathPrefixes []string, target *Target, options ServiceOptions, drainTimeout time.Duration) error {
341343
var replacedTarget *Target
342344

343345
err := r.withWriteLock(func() error {
344-
conflict := r.services.CheckAvailability(name, hosts, pathPrefix)
346+
conflict := r.services.CheckAvailability(name, hosts, pathPrefixes)
345347
if conflict != nil {
346348
slog.Error("Host settings conflict with another service", "service", conflict.name)
347349
return ErrorHostInUse
@@ -350,9 +352,9 @@ func (r *Router) setActiveTarget(name string, hosts []string, pathPrefix string,
350352
var err error
351353
service := r.services.Get(name)
352354
if service == nil {
353-
service, err = NewService(name, hosts, pathPrefix, options)
355+
service, err = NewService(name, hosts, pathPrefixes, options)
354356
} else {
355-
err = service.UpdateOptions(hosts, pathPrefix, options)
357+
err = service.UpdateOptions(hosts, pathPrefixes, options)
356358
}
357359
if err != nil {
358360
return err

0 commit comments

Comments
 (0)