mirror of
https://github.com/umputun/reproxy.git
synced 2024-11-24 08:12:31 +02:00
Docker multi routes (#80)
* support reproxy.N.something labels for docker #78 * lint: suppress false positive * update linter in ci * update readme * add test for failed container parse
This commit is contained in:
parent
bc9ec9184a
commit
1c1f9d1c3c
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
- name: install golangci-lint and goveralls
|
||||
run: |
|
||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.29.0
|
||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.40.0
|
||||
GO111MODULE=off go get -u github.com/mattn/goveralls
|
||||
|
||||
- name: run linters
|
||||
|
@ -37,7 +37,7 @@ linters-settings:
|
||||
linters:
|
||||
enable:
|
||||
- megacheck
|
||||
- golint
|
||||
- revive
|
||||
- govet
|
||||
- unconvert
|
||||
- megacheck
|
||||
|
@ -102,6 +102,8 @@ With `--docker.auto`, all containers with exposed port will be considered as rou
|
||||
|
||||
If no `reproxy.route` defined, the default is `http://<container_name>:<container_port>/(.*)`. In case if all proxied source have the same pattern, for example `/api/(.*)` user can define the common prefix (in this case `/api`) for all container-based routes. This can be done with `--docker.prefix` parameter.
|
||||
|
||||
Docker provider also allows to define multiple set of `reproxy.N.something` labels to match multiple distinct routes on the same container. This is useful as inn some cases, a container may expose multiple endpoints, for example, public API and some admin API. All the labels above can be used with "N-index", i.e. `reproxy.1.server`, `reproxy.1.port` and so on. N should be in 0 to 9 range.
|
||||
|
||||
This is a dynamic provider and any change in container's status will be applied automatically.
|
||||
|
||||
### Consul Catalog
|
||||
|
@ -25,6 +25,8 @@ import (
|
||||
// in the Dockerfile.
|
||||
// Alternatively labels can alter this. reproxy.route sets source route, and reproxy.dest sets the destination.
|
||||
// Optional reproxy.server enforces match by server name (hostname) and reproxy.ping sets the health check url
|
||||
// Labels can be presented multiple times with a numeric suffix to provide multiple matches for a single container
|
||||
// i.e. reproxy.1.server=example.com, reproxy.1.port=12345 and so on
|
||||
type Docker struct {
|
||||
DockerClient DockerClient
|
||||
Excludes []string
|
||||
@ -68,57 +70,66 @@ func (d *Docker) List() ([]discovery.URLMapper, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]discovery.URLMapper, 0, len(containers))
|
||||
var res []discovery.URLMapper //nolint:prealloc // we don't know the final size
|
||||
for _, c := range containers {
|
||||
res = append(res, d.parseContainerInfo(c)...)
|
||||
}
|
||||
|
||||
// sort by len(SrcMatch) to have shorter matches after longer
|
||||
// this way we can handle possible conflicts with more detailed match triggered before less detailed
|
||||
sort.Slice(res, func(i, j int) bool {
|
||||
return len(res[i].SrcMatch.String()) > len(res[j].SrcMatch.String())
|
||||
})
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// parseContainerInfo getting URLMappers for up to 10 routes for 0..9 N (reproxy.N.something)
|
||||
func (d *Docker) parseContainerInfo(c containerInfo) (res []discovery.URLMapper) {
|
||||
|
||||
for n := 0; n < 9; n++ {
|
||||
enabled, explicit := false, false
|
||||
srcURL := fmt.Sprintf("^/%s/(.*)", c.Name) // default destination
|
||||
srcURL := fmt.Sprintf("^/%s/(.*)", c.Name) // default src is /container-name/(.*)
|
||||
if d.APIPrefix != "" {
|
||||
prefix := strings.TrimLeft(d.APIPrefix, "/")
|
||||
prefix = strings.TrimRight(prefix, "/")
|
||||
srcURL = fmt.Sprintf("^/%s/%s/(.*)", prefix, c.Name) // default destination with api prefix
|
||||
prefix := strings.TrimSuffix(strings.TrimPrefix(d.APIPrefix, "/"), "/")
|
||||
srcURL = fmt.Sprintf("^/%s/%s/(.*)", prefix, c.Name) // default src with api prefix is /api-prefix/container-name/(.*)
|
||||
}
|
||||
|
||||
if d.AutoAPI {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
port, err := d.matchedPort(c)
|
||||
port, err := d.matchedPort(c, n)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] container %s disabled, %v", c.Name, err)
|
||||
log.Printf("[DEBUG] container %s (route: %d) disabled, %v", c.Name, n, err)
|
||||
continue
|
||||
}
|
||||
|
||||
destURL := fmt.Sprintf("http://%s:%d/$1", c.IP, port)
|
||||
pingURL := fmt.Sprintf("http://%s:%d/ping", c.IP, port)
|
||||
server := "*"
|
||||
// defaults
|
||||
destURL, pingURL, server := fmt.Sprintf("http://%s:%d/$1", c.IP, port), fmt.Sprintf("http://%s:%d/ping", c.IP, port), "*"
|
||||
assetsWebRoot, assetsLocation := "", ""
|
||||
|
||||
// we don't care about value because disabled will be filtered before
|
||||
if _, ok := c.Labels["reproxy.enabled"]; ok {
|
||||
if d.AutoAPI && n == 0 {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
if _, ok := d.labelN(c.Labels, n, "enabled"); ok {
|
||||
enabled, explicit = true, true
|
||||
}
|
||||
|
||||
if v, ok := c.Labels["reproxy.route"]; ok {
|
||||
if v, ok := d.labelN(c.Labels, n, "route"); ok {
|
||||
enabled, explicit = true, true
|
||||
srcURL = v
|
||||
}
|
||||
|
||||
if v, ok := c.Labels["reproxy.dest"]; ok {
|
||||
if v, ok := d.labelN(c.Labels, n, "dest"); ok {
|
||||
enabled, explicit = true, true
|
||||
destURL = fmt.Sprintf("http://%s:%d%s", c.IP, port, v)
|
||||
}
|
||||
|
||||
if v, ok := c.Labels["reproxy.server"]; ok {
|
||||
if v, ok := d.labelN(c.Labels, n, "server"); ok {
|
||||
enabled = true
|
||||
server = v
|
||||
}
|
||||
|
||||
if v, ok := c.Labels["reproxy.ping"]; ok {
|
||||
if v, ok := d.labelN(c.Labels, n, "ping"); ok {
|
||||
enabled = true
|
||||
pingURL = fmt.Sprintf("http://%s:%d%s", c.IP, port, v)
|
||||
}
|
||||
|
||||
if v, ok := c.Labels["reproxy.assets"]; ok {
|
||||
if v, ok := d.labelN(c.Labels, n, "assets"); ok {
|
||||
if ae := strings.Split(v, ":"); len(ae) == 2 {
|
||||
enabled = true
|
||||
assetsWebRoot = ae[0]
|
||||
@ -127,18 +138,19 @@ func (d *Docker) List() ([]discovery.URLMapper, error) {
|
||||
}
|
||||
|
||||
// should not set anything, handled on matchedPort level. just use to enable implicitly
|
||||
if _, ok := c.Labels["reproxy.port"]; ok {
|
||||
if _, ok := d.labelN(c.Labels, n, "port"); ok {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
log.Printf("[DEBUG] container %s disabled", c.Name)
|
||||
log.Printf("[DEBUG] container %s (route: %d) disabled", c.Name, n)
|
||||
continue
|
||||
}
|
||||
|
||||
srcRegex, err := regexp.Compile(srcURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid src regex: %w", err)
|
||||
log.Printf("[DEBUG] container %s (route: %d) disabled, invalid src regex: %v", c.Name, n, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// docker server label may have multiple, comma separated servers
|
||||
@ -158,37 +170,47 @@ func (d *Docker) List() ([]discovery.URLMapper, error) {
|
||||
mp.AssetsLocation = assetsLocation
|
||||
}
|
||||
res = append(res, mp)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// sort by len(SrcMatch) to have shorter matches after longer
|
||||
// this way we can handle possible conflicts with more detailed match triggered before less detailed
|
||||
|
||||
sort.Slice(res, func(i, j int) bool {
|
||||
return len(res[i].SrcMatch.String()) > len(res[j].SrcMatch.String())
|
||||
})
|
||||
return res, nil
|
||||
return res
|
||||
}
|
||||
|
||||
func (d *Docker) matchedPort(c containerInfo) (port int, err error) {
|
||||
// matchedPort gets port for route match, default the first exposed port
|
||||
// if reproxy.N.label found reruns this port only if it is one of exposed by the container
|
||||
func (d *Docker) matchedPort(c containerInfo, n int) (port int, err error) {
|
||||
port = c.Ports[0] // by default use the first exposed port
|
||||
if v, ok := c.Labels["reproxy.port"]; ok {
|
||||
rp, err := strconv.Atoi(v)
|
||||
|
||||
if portLabel, ok := d.labelN(c.Labels, n, "port"); ok {
|
||||
rp, err := strconv.Atoi(portLabel)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid reproxy port %s: %w", v, err)
|
||||
return 0, fmt.Errorf("invalid reproxy port %s: %w", portLabel, err)
|
||||
}
|
||||
for _, p := range c.Ports {
|
||||
// set port to reproxy.port if matched with one of exposed
|
||||
// set port to reproxy.N.port if matched with one of exposed
|
||||
if p == rp {
|
||||
port = rp
|
||||
break
|
||||
return rp, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("reproxy port %s not exposed", portLabel)
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
|
||||
// labelN returns label value from reproxy.N.suffix, i.e. reproxy.1.server
|
||||
func (d *Docker) labelN(labels map[string]string, n int, suffix string) (result string, ok bool) {
|
||||
switch n {
|
||||
case 0:
|
||||
result, ok = labels["reproxy."+suffix]
|
||||
if !ok {
|
||||
result, ok = labels["reproxy.0."+suffix]
|
||||
}
|
||||
default:
|
||||
result, ok = labels[fmt.Sprintf("reproxy.%d.%s", n, suffix)]
|
||||
}
|
||||
return result, ok
|
||||
}
|
||||
|
||||
// events starts monitoring changes in running containers and sends refresh
|
||||
// notification to eventsCh when change(s) are detected. Blocks caller
|
||||
func (d *Docker) events(ctx context.Context, eventsCh chan<- discovery.ProviderID) error {
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@ -39,10 +40,10 @@ func TestDocker_List(t *testing.T) {
|
||||
Name: "c3", State: "stopped",
|
||||
},
|
||||
{
|
||||
Name: "c4", State: "running", IP: "127.0.0.2", Ports: []int{12345},
|
||||
Name: "c4", State: "running", IP: "127.0.0.2", Ports: []int{12345}, // not enabled
|
||||
},
|
||||
{
|
||||
Name: "c5", State: "running", IP: "127.0.0.122", Ports: []int{2345},
|
||||
Name: "c5", State: "running", IP: "127.0.0.122", Ports: []int{2345}, // not enabled
|
||||
Labels: map[string]string{"reproxy.enabled": "false"},
|
||||
},
|
||||
}, nil
|
||||
@ -70,6 +71,98 @@ func TestDocker_List(t *testing.T) {
|
||||
assert.Equal(t, "example.com", res[2].Server)
|
||||
}
|
||||
|
||||
func TestDocker_ListMulti(t *testing.T) {
|
||||
dclient := &DockerClientMock{
|
||||
ListContainersFunc: func() ([]containerInfo, error) {
|
||||
return []containerInfo{
|
||||
{
|
||||
Name: "c0", State: "running", IP: "127.0.0.2", Ports: []int{12348},
|
||||
Labels: map[string]string{"reproxy.route": "^/a/(.*)", "reproxy.dest": "/a/$1",
|
||||
"reproxy.server": "example.com", "reproxy.ping": "/ping"},
|
||||
},
|
||||
{
|
||||
Name: "c1", State: "running", IP: "127.0.0.2", Ports: []int{12345},
|
||||
Labels: map[string]string{"reproxy.route": "^/api/123/(.*)", "reproxy.dest": "/blah/$1",
|
||||
"reproxy.server": "example.com", "reproxy.ping": "/ping"},
|
||||
},
|
||||
{
|
||||
Name: "c2", State: "running", IP: "127.0.0.3", Ports: []int{12346},
|
||||
Labels: map[string]string{"reproxy.enabled": "y"},
|
||||
},
|
||||
{
|
||||
Name: "c2m", State: "running", IP: "127.0.0.3", Ports: []int{7890, 56789},
|
||||
Labels: map[string]string{
|
||||
"reproxy.enabled": "y",
|
||||
"reproxy.1.route": "/api/1/(.*)",
|
||||
"reproxy.1.dest": "/blah/1/$1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "c4", State: "running", IP: "127.0.0.12", Ports: []int{12345},
|
||||
Labels: map[string]string{"reproxy.port": "12345"},
|
||||
},
|
||||
{
|
||||
Name: "c4", State: "running", IP: "127.0.0.12", Ports: []int{12345},
|
||||
Labels: map[string]string{"reproxy.port": "12xx345"}, // bad port
|
||||
},
|
||||
{
|
||||
Name: "c23", State: "running", IP: "127.0.0.3", Ports: []int{12346},
|
||||
Labels: map[string]string{"reproxy.enabled": "y", "reproxy.route": "^/api/123/(.***)"}, // bad regex
|
||||
},
|
||||
{
|
||||
Name: "c3", State: "stopped",
|
||||
},
|
||||
{
|
||||
Name: "c4", State: "running", IP: "127.0.0.2", Ports: []int{12345}, // not enabled
|
||||
},
|
||||
{
|
||||
Name: "c4", State: "running", IP: "127.0.0.2", Ports: []int{12345}, // not enabled, port mismatch
|
||||
Labels: map[string]string{"reproxy.port": "9999"},
|
||||
},
|
||||
{
|
||||
Name: "c5", State: "running", IP: "127.0.0.122", Ports: []int{2345}, // not enabled
|
||||
Labels: map[string]string{"reproxy.enabled": "false"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
d := Docker{DockerClient: dclient}
|
||||
res, err := d.List()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 6, len(res))
|
||||
|
||||
assert.Equal(t, "^/api/123/(.*)", res[0].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.2:12345/blah/$1", res[0].Dst)
|
||||
assert.Equal(t, "example.com", res[0].Server)
|
||||
assert.Equal(t, "http://127.0.0.2:12345/ping", res[0].PingURL)
|
||||
|
||||
assert.Equal(t, "/api/1/(.*)", res[1].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.3:7890/blah/1/$1", res[1].Dst)
|
||||
assert.Equal(t, "http://127.0.0.3:7890/ping", res[1].PingURL)
|
||||
assert.Equal(t, "*", res[1].Server)
|
||||
|
||||
assert.Equal(t, "^/c2m/(.*)", res[2].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.3:7890/$1", res[2].Dst)
|
||||
assert.Equal(t, "http://127.0.0.3:7890/ping", res[2].PingURL)
|
||||
assert.Equal(t, "*", res[2].Server)
|
||||
|
||||
assert.Equal(t, "^/c2/(.*)", res[3].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.3:12346/$1", res[3].Dst)
|
||||
assert.Equal(t, "http://127.0.0.3:12346/ping", res[3].PingURL)
|
||||
assert.Equal(t, "*", res[3].Server)
|
||||
|
||||
assert.Equal(t, "^/c4/(.*)", res[4].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.12:12345/$1", res[4].Dst)
|
||||
assert.Equal(t, "http://127.0.0.12:12345/ping", res[4].PingURL)
|
||||
assert.Equal(t, "*", res[4].Server)
|
||||
|
||||
assert.Equal(t, "^/a/(.*)", res[5].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.2:12348/a/$1", res[5].Dst)
|
||||
assert.Equal(t, "http://127.0.0.2:12348/ping", res[5].PingURL)
|
||||
assert.Equal(t, "example.com", res[5].Server)
|
||||
}
|
||||
|
||||
func TestDocker_ListWithAutoAPI(t *testing.T) {
|
||||
dclient := &DockerClientMock{
|
||||
ListContainersFunc: func() ([]containerInfo, error) {
|
||||
@ -235,3 +328,35 @@ func TestDockerClient_error(t *testing.T) {
|
||||
_, err := client.ListContainers()
|
||||
require.EqualError(t, err, "unexpected error from docker daemon: bruh")
|
||||
}
|
||||
|
||||
func TestDocker_labelN(t *testing.T) {
|
||||
|
||||
tbl := []struct {
|
||||
labels map[string]string
|
||||
n int
|
||||
suffix string
|
||||
res string
|
||||
ok bool
|
||||
}{
|
||||
{map[string]string{}, 0, "port", "", false},
|
||||
{map[string]string{"a": "123", "reproxy.port": "9999"}, 0, "port", "9999", true},
|
||||
{map[string]string{"a": "123", "reproxy.0.port": "9999"}, 0, "port", "9999", true},
|
||||
{map[string]string{"a": "123", "reproxy.1.port": "9999"}, 0, "port", "", false},
|
||||
{map[string]string{"a": "123", "reproxy.1.port": "9999"}, 1, "port", "9999", true},
|
||||
{map[string]string{"a": "123", "reproxy.1.port": "9999", "reproxy.0.port": "7777"}, 1, "port", "9999", true},
|
||||
{map[string]string{"a": "123", "reproxy.1.port": "9999", "reproxy.0.port": "7777"}, 0, "port", "7777", true},
|
||||
}
|
||||
|
||||
d := Docker{}
|
||||
for i, tt := range tbl {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
res, ok := d.labelN(tt.labels, tt.n, tt.suffix)
|
||||
require.Equal(t, tt.ok, ok)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.res, res)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user