api: add the ability to specify a path prefix (#12914)

Specifically meant for when consul is behind a reverse proxy / API gateway

Co-authored-by: Evan Culver <eculver@hashicorp.com>
This commit is contained in:
funkiestj 2022-05-19 16:07:59 -07:00 committed by GitHub
parent 63a9175bd6
commit 386106a139
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 77 additions and 1 deletions

3
.changelog/12914.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:enhancement
api: add the ability to specify a path prefix for when consul is behind a reverse proxy or API gateway
```

View File

@ -320,6 +320,11 @@ type Config struct {
// Scheme is the URI scheme for the Consul server // Scheme is the URI scheme for the Consul server
Scheme string Scheme string
// Prefix for URIs for when consul is behind an API gateway (reverse
// proxy). The API gateway must strip off the PathPrefix before
// passing the request onto consul.
PathPrefix string
// Datacenter to use. If not provided, the default agent datacenter is used. // Datacenter to use. If not provided, the default agent datacenter is used.
Datacenter string Datacenter string
@ -712,6 +717,18 @@ func NewClient(config *Config) (*Client, error) {
return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0]) return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0])
} }
config.Address = parts[1] config.Address = parts[1]
// separate out a reverse proxy prefix, if it is present.
// NOTE: Rewriting this code to use url.Parse() instead of
// strings.SplitN() breaks existing test cases.
switch parts[0] {
case "http", "https":
parts := strings.SplitN(parts[1], "/", 2)
if len(parts) == 2 {
config.Address = parts[0]
config.PathPrefix = "/" + parts[1]
}
}
} }
// If the TokenFile is set, always use that, even if a Token is configured. // If the TokenFile is set, always use that, even if a Token is configured.
@ -953,7 +970,7 @@ func (c *Client) newRequest(method, path string) *request {
url: &url.URL{ url: &url.URL{
Scheme: c.config.Scheme, Scheme: c.config.Scheme,
Host: c.config.Address, Host: c.config.Address,
Path: path, Path: c.config.PathPrefix + path,
}, },
params: make(map[string][]string), params: make(map[string][]string),
header: c.Headers(), header: c.Headers(),

View File

@ -1119,6 +1119,62 @@ func TestAPI_GenerateEnvHTTPS(t *testing.T) {
require.Equal(t, expected, c.GenerateEnv()) require.Equal(t, expected, c.GenerateEnv())
} }
// TestAPI_PrefixPath() validates that Config.Address is split into
// Config.Address and Config.PathPrefix as expected. If we want to add end to
// end testing in the future this will require configuring and running an
// API gateway / reverse proxy (e.g. nginx)
func TestAPI_PrefixPath(t *testing.T) {
t.Parallel()
cases := []struct {
name string
addr string
expectAddr string
expectPrefix string
}{
{
name: "with http and prefix",
addr: "http://reverse.proxy.com/consul/path/prefix",
expectAddr: "reverse.proxy.com",
expectPrefix: "/consul/path/prefix",
},
{
name: "with https and prefix",
addr: "https://reverse.proxy.com/consul/path/prefix",
expectAddr: "reverse.proxy.com",
expectPrefix: "/consul/path/prefix",
},
{
name: "with http and no prefix",
addr: "http://localhost",
expectAddr: "localhost",
expectPrefix: "",
},
{
name: "with https and no prefix",
addr: "https://localhost",
expectAddr: "localhost",
expectPrefix: "",
},
{
name: "no scheme and no prefix",
addr: "localhost",
expectAddr: "localhost",
expectPrefix: "",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := &Config{Address: tc.addr}
client, err := NewClient(c)
require.NoError(t, err)
require.Equal(t, tc.expectAddr, client.config.Address)
require.Equal(t, tc.expectPrefix, client.config.PathPrefix)
})
}
}
func getExpectedCaPoolByDir(t *testing.T) *x509.CertPool { func getExpectedCaPoolByDir(t *testing.T) *x509.CertPool {
pool := x509.NewCertPool() pool := x509.NewCertPool()
entries, err := os.ReadDir("../test/ca_path") entries, err := os.ReadDir("../test/ca_path")