diff --git a/docs/configuration-raft.md b/docs/configuration-raft.md index 7b8550afc..31ae6b81f 100644 --- a/docs/configuration-raft.md +++ b/docs/configuration-raft.md @@ -51,7 +51,19 @@ as well as this: ], ``` -If your orchestrator/raft nodes need to communicate via NAT gateways, you can additionally set "RaftAdvertise" to IP or hostname which other nodes should contact. Otherwise other nodes would try to talk to the "RaftBind" address and fail. +### NAT, firewalls, routing + +If your orchestrator/raft nodes need to communicate via NAT gateways, you can additionally set: + +- `"RaftAdvertise": ""` + +to IP or hostname which other nodes should contact. Otherwise other nodes would try to talk to the "RaftBind" address and fail. + +Raft nodes will reverse proxy HTTP requests to the leader. `orchestrator` will attempt to heuristically compute the leader's URL to which redirect requests. If behind NAT, rerouting ports etc., `orchestrator` may not be able to compute that URL. You may configure: + +- `"HTTPAdvertise": "scheme://hostname:port"` + +to explicitly specify where a node, assuming it were the leader, would be accessed through HTTP API. As example, you would: `"HTTPAdvertise": "http://my.public.hostname:3000"` ### Backend DB diff --git a/go/config/config.go b/go/config/config.go index f2a71cfa5..b0720d163 100644 --- a/go/config/config.go +++ b/go/config/config.go @@ -19,6 +19,7 @@ package config import ( "encoding/json" "fmt" + "net/url" "os" "regexp" "strings" @@ -86,6 +87,7 @@ type Configuration struct { EnableSyslog bool // Should logs be directed (in addition) to syslog daemon? ListenAddress string // Where orchestrator HTTP should listen for TCP ListenSocket string // Where orchestrator HTTP should listen for unix socket (default: empty; when given, TCP is disabled) + HTTPAdvertise string // optional, for raft setups, what is the HTTP address this node will advertise to its peers (potentially use where behind NAT or when rerouting ports; example: "http://11.22.33.44:3030") AgentsServerPort string // port orchestrator agents talk back to MySQLTopologyUser string MySQLTopologyPassword string // my.cnf style configuration file from where to pick credentials. Expecting `user`, `password` under `[client]` section @@ -266,6 +268,7 @@ func newConfiguration() *Configuration { EnableSyslog: false, ListenAddress: ":3000", ListenSocket: "", + HTTPAdvertise: "", AgentsServerPort: ":3001", StatusEndpoint: "/api/status", StatusOUVerify: false, @@ -519,12 +522,15 @@ func (this *Configuration) postReadAdjustments() error { if this.RemoteSSHForMasterFailover && this.RemoteSSHCommand == "" { return fmt.Errorf("RemoteSSHCommand is required when RemoteSSHForMasterFailover is set") } - if this.RaftAdvertise == "" { - this.RaftAdvertise = this.RaftBind - } if this.RaftEnabled && this.RaftDataDir == "" { return fmt.Errorf("RaftDataDir must be defined since raft is enabled (RaftEnabled)") } + if this.RaftEnabled && this.RaftBind == "" { + return fmt.Errorf("RaftBind must be defined since raft is enabled (RaftEnabled)") + } + if this.RaftAdvertise == "" { + this.RaftAdvertise = this.RaftBind + } if this.KVClusterMasterPrefix != "/" { // "/" remains "/" // "prefix" turns to "prefix/" @@ -542,6 +548,24 @@ func (this *Configuration) postReadAdjustments() error { this.DetectPseudoGTIDQuery = SelectTrueQuery this.PseudoGTIDPreferIndependentMultiMatch = true } + if this.HTTPAdvertise != "" { + u, err := url.Parse(this.HTTPAdvertise) + if err != nil { + return fmt.Errorf("Failed parsing HTTPAdvertise %s: %s", this.HTTPAdvertise, err.Error) + } + if u.Scheme == "" { + return fmt.Errorf("If specified, HTTPAdvertise must include scheme (http:// or https://)") + } + if u.Hostname() == "" { + return fmt.Errorf("If specified, HTTPAdvertise must include host name") + } + if u.Port() == "" { + return fmt.Errorf("If specified, HTTPAdvertise must include port number") + } + if u.Path != "" { + return fmt.Errorf("If specified, HTTPAdvertise must not specify a path") + } + } return nil } diff --git a/go/config/config_test.go b/go/config/config_test.go index 680c17a15..6fe01b5d7 100644 --- a/go/config/config_test.go +++ b/go/config/config_test.go @@ -150,3 +150,68 @@ func TestRecoveryPeriodBlock(t *testing.T) { test.S(t).ExpectEquals(c.RecoveryPeriodBlockSeconds, 15) } } + +func TestRaft(t *testing.T) { + { + c := newConfiguration() + c.RaftBind = "1.2.3.4:1008" + c.RaftDataDir = "/path/to/somewhere" + err := c.postReadAdjustments() + test.S(t).ExpectNil(err) + test.S(t).ExpectEquals(c.RaftAdvertise, c.RaftBind) + } + { + c := newConfiguration() + c.RaftEnabled = true + err := c.postReadAdjustments() + test.S(t).ExpectNotNil(err) + } + { + c := newConfiguration() + c.RaftEnabled = true + c.RaftDataDir = "/path/to/somewhere" + err := c.postReadAdjustments() + test.S(t).ExpectNil(err) + } + { + c := newConfiguration() + c.RaftEnabled = true + c.RaftDataDir = "/path/to/somewhere" + c.RaftBind = "" + err := c.postReadAdjustments() + test.S(t).ExpectNotNil(err) + } +} + +func TestHttpAdvertise(t *testing.T) { + { + c := newConfiguration() + c.HTTPAdvertise = "" + err := c.postReadAdjustments() + test.S(t).ExpectNil(err) + } + { + c := newConfiguration() + c.HTTPAdvertise = "http://127.0.0.1:1234" + err := c.postReadAdjustments() + test.S(t).ExpectNil(err) + } + { + c := newConfiguration() + c.HTTPAdvertise = "http://127.0.0.1" + err := c.postReadAdjustments() + test.S(t).ExpectNotNil(err) + } + { + c := newConfiguration() + c.HTTPAdvertise = "127.0.0.1:1234" + err := c.postReadAdjustments() + test.S(t).ExpectNotNil(err) + } + { + c := newConfiguration() + c.HTTPAdvertise = "http://127.0.0.1:1234/mypath" + err := c.postReadAdjustments() + test.S(t).ExpectNotNil(err) + } +} diff --git a/go/raft/raft.go b/go/raft/raft.go index 57163d9b0..f5c0092de 100644 --- a/go/raft/raft.go +++ b/go/raft/raft.go @@ -82,9 +82,14 @@ func FatalRaftError(err error) error { } func computeLeaderURI() (uri string, err error) { - protocol := "http" + if config.Config.HTTPAdvertise != "" { + // Explicitly given + return config.Config.HTTPAdvertise, nil + } + // Not explicitly given. Let's heuristically compute using RaftAdvertise + scheme := "http" if config.Config.UseSSL { - protocol = "https" + scheme = "https" } hostname := config.Config.RaftAdvertise listenTokens := strings.Split(config.Config.ListenAddress, ":") @@ -92,7 +97,7 @@ func computeLeaderURI() (uri string, err error) { return uri, fmt.Errorf("computeLeaderURI: cannot determine listen port out of config.Config.ListenAddress: %+v", config.Config.ListenAddress) } port := listenTokens[1] - uri = fmt.Sprintf("%s://%s:%s", protocol, hostname, port) + uri = fmt.Sprintf("%s://%s:%s", scheme, hostname, port) return uri, nil }