hashicorp / consul

Consul is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure.
https://www.consul.io
Other
28.37k stars 4.42k forks source link

enhance consul/api to allow access of consul behind an HTTP API gateway (e.g. nginx) #10354

Open funkiestj opened 3 years ago

funkiestj commented 3 years ago

Feature Description

I want to access a consul service that is behind an nginx API gateway (see nginx.conf snippet below) i.e. instead of acessing consul directly like this:

curl -v http://10.1.2.3:8500/v1/kv/x/y/z/?recurse=&wait=1800000ms

I want to access consul behind an nginx API gateway, like this:

curl -v http://10.1.2.3/foo/bar/consul/v1/kv/x/y/z/?recurse=&wait=1800000ms

This works fine with curl creating the HTTP requests but the api.Config does not have any provision for adding a prefix path to requests. When I try the following

c := &api.Config{
    Address: "http://10.1.2.3/foo/bar/consul",
}

consul fails because it tries to resolve 10.1.2.3/foo%2Fbar%2Fconsul as a hostname

error: Get "http://10.1.2.3%2Faugtera%2Fconfdb/v1/kv/x/y/z/?recurse=&wait=1800000ms": dial tcp: lookup 10.1.2.3/foo/bar/consul: no such host

Use Case(s)

using consul that is behind an API gateway

Sample diffs

I don't have permission to push a branch for a PR. Here is a diff

$ git diff
diff --git a/api/api.go b/api/api.go
index 5cdd48680..0c74ac878 100644
--- a/api/api.go
+++ b/api/api.go
@@ -285,6 +285,9 @@ type Config struct {
    // Scheme is the URI scheme for the Consul server
    Scheme string

+       // Prefix for URIs for when consul is behind an API gateway
+       PathPrefix string
+
    // Datacenter to use. If not provided, the default agent datacenter is used.
    Datacenter string

@@ -663,6 +666,13 @@ func NewClient(config *Config) (*Client, error) {
        config.Address = parts[1]
    }

+       // extract API gateway PathPrefix, if present
+       parts = strings.SplitN(config.Address, "/", 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.
    // This is because when TokenFile is set it is read into the Token field.
    // We want any derived clients to have to re-read the token file.
@@ -896,7 +906,7 @@ func (c *Client) newRequest(method, path string) *request {
        url: &url.URL{
            Scheme: c.config.Scheme,
            Host:   c.config.Address,
-                       Path:   path,
+                       Path:   c.config.PathPrefix + path,
        },
        params: make(map[string][]string),
        header: c.Headers(),

nginx.conf snippet

location /foo/bar/consul {
    rewrite ^/foo/bar/consul/(.*)  /$1 break;
    proxy_pass http://consul:8500;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
    proxy_no_cache 1;
}
dnephin commented 3 years ago

Thank you for raising this issue! I think this would be a great addition. Would you be willing to open a pull request with these changes?

Our contributing guide has some instructions on how to fork the Consul repo and submit a pull request.

funkiestj commented 3 years ago

ha ha ha, I was hoping a skilled consul developer would do the tedious work of creating a PR and testing.

I will create a PR per the linked to contributing guide but this will have to wait until I can fit it into my work schedule. I'll update this thread when I begin the task. If someone wants it sooner they are welcome to make the PR themself

ekhabarov commented 3 years ago

@dnephin PR submitted. Please, let me know, what I can do to have it in upstream. Thanks!