diff --git a/CHANGELOG.md b/CHANGELOG.md index b07b3bd61..bde58197a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. * docker: Official Docker image for `hashicorp/boundary` ([PR](https://github.com/hashicorp/boundary/pull/755)) +* controller: Add ability to set public address for cluster purposes + ([Issue](https://github.com/hashicorp/boundary/pull/758)) + ([PR](https://github.com/hashicorp/boundary/pull/761)) ### Bug Fixes diff --git a/internal/cmd/base/listener.go b/internal/cmd/base/listener.go index bd8b4982a..a69e3392c 100644 --- a/internal/cmd/base/listener.go +++ b/internal/cmd/base/listener.go @@ -171,6 +171,11 @@ func tcpListenerFactory(purpose string, l *configutil.Listener, logger hclog.Log return "", nil, err } + // If we used a random port, for a test, we need to save it back so we can set the public address appropriately + if l.RandomPort { + l.Address = ln.Addr().String() + } + ln = TCPKeepAliveListener{ln.(*net.TCPListener)} return finalListenAddr, ln, nil diff --git a/internal/cmd/base/servers.go b/internal/cmd/base/servers.go index fd2b0c622..4b8d16ded 100644 --- a/internal/cmd/base/servers.go +++ b/internal/cmd/base/servers.go @@ -596,6 +596,37 @@ func (b *Server) DestroyDevDatabase() error { return nil } +func (b *Server) SetupControllerPublicClusterAddress(conf *config.Config, flagValue string) error { + if conf.Controller == nil { + conf.Controller = new(config.Controller) + } + if flagValue != "" { + conf.Controller.PublicClusterAddr = flagValue + } + if conf.Controller.PublicClusterAddr == "" { + FindAddr: + for _, listener := range conf.Listeners { + for _, purpose := range listener.Purpose { + if purpose == "cluster" { + conf.Controller.PublicClusterAddr = listener.Address + break FindAddr + } + } + } + } + host, port, err := net.SplitHostPort(conf.Controller.PublicClusterAddr) + if err != nil { + if strings.Contains(err.Error(), "missing port") { + port = "9201" + host = conf.Controller.PublicClusterAddr + } else { + return fmt.Errorf("Error splitting public cluster adddress host/port: %w", err) + } + } + conf.Controller.PublicClusterAddr = net.JoinHostPort(host, port) + return nil +} + func (b *Server) SetupWorkerPublicAddress(conf *config.Config, flagValue string) error { if conf.Worker == nil { conf.Worker = new(config.Worker) diff --git a/internal/cmd/commands/dev/dev.go b/internal/cmd/commands/dev/dev.go index 772dd1fcf..b934161c5 100644 --- a/internal/cmd/commands/dev/dev.go +++ b/internal/cmd/commands/dev/dev.go @@ -48,6 +48,7 @@ type Command struct { flagTargetSessionConnectionLimit int flagControllerAPIListenAddr string flagControllerClusterListenAddr string + flagControllerPublicClusterAddr string flagWorkerProxyListenAddr string flagWorkerPublicAddr string flagPassthroughDirectory string @@ -163,6 +164,13 @@ func (c *Command) Flags() *base.FlagSets { Usage: "Address to bind to for controller \"cluster\" purpose. If this begins with a forward slash, it will be assumed to be a Unix domain socket path.", }) + f.StringVar(&base.StringVar{ + Name: "controller-public-cluster-address", + Target: &c.flagControllerPublicClusterAddr, + EnvVar: "BOUNDARY_DEV_CONTROLLER_PUBLIC_CLUSTER_ADDRESS", + Usage: "Public address at which the controller is reachable for cluster tasks (like worker connections).", + }) + f.StringVar(&base.StringVar{ Name: "proxy-listen-address", Target: &c.flagWorkerProxyListenAddr, @@ -356,6 +364,13 @@ func (c *Command) Run(args []string) int { return 1 } + if err := c.SetupControllerPublicClusterAddress(c.Config, c.flagControllerPublicClusterAddr); err != nil { + c.UI.Error(err.Error()) + return 1 + } + c.InfoKeys = append(c.InfoKeys, "controller public cluster addr") + c.Info["controller public cluster addr"] = c.Config.Controller.PublicClusterAddr + if err := c.SetupWorkerPublicAddress(c.Config, c.flagWorkerPublicAddr); err != nil { c.UI.Error(err.Error()) return 1 diff --git a/internal/cmd/commands/server/server.go b/internal/cmd/commands/server/server.go index ebe4935c3..e8791c0c5 100644 --- a/internal/cmd/commands/server/server.go +++ b/internal/cmd/commands/server/server.go @@ -246,6 +246,14 @@ func (c *Command) Run(args []string) int { c.InfoKeys = append(c.InfoKeys, "public addr") c.Info["public addr"] = c.Config.Worker.PublicAddr } + if c.Config.Controller != nil { + if err := c.SetupControllerPublicClusterAddress(c.Config, ""); err != nil { + c.UI.Error(err.Error()) + return 1 + } + c.InfoKeys = append(c.InfoKeys, "public cluster addr") + c.Info["public cluster addr"] = c.Config.Controller.PublicClusterAddr + } // Write out the PID to the file now that server has successfully started if err := c.StorePidFile(c.Config.PidFile); err != nil { diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index f402ff040..9e19e21d8 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -95,9 +95,10 @@ type Config struct { } type Controller struct { - Name string `hcl:"name"` - Description string `hcl:"description"` - Database *Database `hcl:"database"` + Name string `hcl:"name"` + Description string `hcl:"description"` + Database *Database `hcl:"database"` + PublicClusterAddr string `hcl:"public_cluster_addr"` } type Worker struct { diff --git a/internal/servers/controller/controller.go b/internal/servers/controller/controller.go index e44beb0e1..116d69ccf 100644 --- a/internal/servers/controller/controller.go +++ b/internal/servers/controller/controller.go @@ -47,8 +47,6 @@ type Controller struct { TargetRepoFn common.TargetRepoFactory kms *kms.Kms - - clusterAddress string } func New(conf *Config) (*Controller, error) { @@ -162,7 +160,6 @@ func (c *Controller) Shutdown(serversOnly bool) error { if err := c.stopListeners(serversOnly); err != nil { return fmt.Errorf("error stopping controller listeners: %w", err) } - c.clusterAddress = "" c.started.Store(false) return nil } diff --git a/internal/servers/controller/listeners.go b/internal/servers/controller/listeners.go index 267587b7c..5442a0b58 100644 --- a/internal/servers/controller/listeners.go +++ b/internal/servers/controller/listeners.go @@ -114,8 +114,6 @@ func (c *Controller) startListeners() error { if err != nil { return fmt.Errorf("error getting sub-listener for worker proto: %w", err) } - c.clusterAddress = l.Addr().String() - c.logger.Info("cluster address", "addr", c.clusterAddress) workerServer := grpc.NewServer( grpc.MaxRecvMsgSize(math.MaxInt32), @@ -142,11 +140,7 @@ func (c *Controller) startListeners() error { case "api": err = configureForAPI(ln) case "cluster": - if c.clusterAddress != "" { - err = errors.New("more than one cluster listener found") - } else { - err = configureForCluster(ln) - } + err = configureForCluster(ln) case "proxy": // Do nothing, in a dev mode we might see it here default: diff --git a/internal/servers/controller/testing.go b/internal/servers/controller/testing.go index 997d3023b..751eb8f35 100644 --- a/internal/servers/controller/testing.go +++ b/internal/servers/controller/testing.go @@ -406,6 +406,9 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { if err := tc.b.SetupListeners(nil, opts.Config.SharedConfig, []string{"api", "cluster"}); err != nil { t.Fatal(err) } + if err := tc.b.SetupControllerPublicClusterAddress(opts.Config, ""); err != nil { + t.Fatal(err) + } if opts.DatabaseUrl != "" { tc.b.DatabaseUrl = opts.DatabaseUrl diff --git a/internal/servers/controller/tickers.go b/internal/servers/controller/tickers.go index 5168fc5d7..883558e4f 100644 --- a/internal/servers/controller/tickers.go +++ b/internal/servers/controller/tickers.go @@ -33,7 +33,7 @@ func (c *Controller) startStatusTicking(cancelCtx context.Context) { Name: c.conf.RawConfig.Controller.Name, Type: resource.Controller.String(), Description: c.conf.RawConfig.Controller.Description, - Address: c.clusterAddress, + Address: c.conf.RawConfig.Controller.PublicClusterAddr, } repo, err := c.ServersRepoFn() if err != nil { diff --git a/internal/servers/worker/testing.go b/internal/servers/worker/testing.go index 1c7f6e276..19b5755e6 100644 --- a/internal/servers/worker/testing.go +++ b/internal/servers/worker/testing.go @@ -188,6 +188,9 @@ func NewTestWorker(t *testing.T, opts *TestWorkerOpts) *TestWorker { if err := tw.b.SetupListeners(nil, opts.Config.SharedConfig, []string{"proxy"}); err != nil { t.Fatal(err) } + if err := tw.b.SetupWorkerPublicAddress(opts.Config, ""); err != nil { + t.Fatal(err) + } conf := &Config{ RawConfig: opts.Config, diff --git a/internal/tests/cluster/ipv6_listener_test.go b/internal/tests/cluster/ipv6_listener_test.go index 12828add6..936f1103d 100644 --- a/internal/tests/cluster/ipv6_listener_test.go +++ b/internal/tests/cluster/ipv6_listener_test.go @@ -60,7 +60,7 @@ func TestIPv6Listener(t *testing.T) { // expecting it we'll see an out-of-date entry return true } - assert.WithinDuration(time.Now(), v.(time.Time), 35*time.Second) + assert.WithinDuration(time.Now(), v.(time.Time), 60*time.Second) delete(workerMap, k.(string)) return true }) diff --git a/internal/tests/cluster/unix_listener_test.go b/internal/tests/cluster/unix_listener_test.go index 735a99c1f..2885a740c 100644 --- a/internal/tests/cluster/unix_listener_test.go +++ b/internal/tests/cluster/unix_listener_test.go @@ -72,7 +72,7 @@ func TestUnixListener(t *testing.T) { // expecting it we'll see an out-of-date entry return true } - assert.WithinDuration(time.Now(), v.(time.Time), 35*time.Second) + assert.WithinDuration(time.Now(), v.(time.Time), 60*time.Second) delete(workerMap, k.(string)) return true }) diff --git a/website/content/docs/configuration/controller.mdx b/website/content/docs/configuration/controller.mdx index 4188fb0c1..a8a7a3adc 100644 --- a/website/content/docs/configuration/controller.mdx +++ b/website/content/docs/configuration/controller.mdx @@ -32,6 +32,14 @@ controller { Either can refer to a file on disk (file://) from which a URL will be read; an env var (env://) from which the URL will be read; or a direct database URL (postgres://). +- `public_cluster_addr` - Specifies the public host or IP address (and +optionally port) at which the worker can be reached _by workers_. This will be +used by workers after initial connection to controllers via the worker's +`controllers` block. This defaults to the address of the listener marked for +`cluster` purpose. This is especially useful for cloud environments that do not +bind a publicly accessible IP to a NIC on the host directly, such as an Amazon +EIP. + ## KMS Configuration The controller requires two KMS stanzas for `root` and `worker-auth` purposes: