mirror of
https://github.com/umputun/reproxy.git
synced 2025-11-29 22:08:14 +02:00
Invoke Docker API directly
* Remove third-party docker client dependency * Simplify code and tests Might need to run `go mod tidy` and `go mod vendor` afterwards
This commit is contained in:
@@ -2,16 +2,18 @@ package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dc "github.com/fsouza/go-dockerclient"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/umputun/reproxy/app/discovery"
|
||||
)
|
||||
@@ -27,41 +29,29 @@ import (
|
||||
type Docker struct {
|
||||
DockerClient DockerClient
|
||||
Excludes []string
|
||||
Network string
|
||||
AutoAPI bool
|
||||
}
|
||||
|
||||
// DockerClient defines interface listing containers and subscribing to events
|
||||
type DockerClient interface {
|
||||
ListContainers(opts dc.ListContainersOptions) ([]dc.APIContainers, error)
|
||||
AddEventListenerWithOptions(options dc.EventsOptions, listener chan<- *dc.APIEvents) error
|
||||
ListContainers() ([]containerInfo, error)
|
||||
}
|
||||
|
||||
// containerInfo is simplified docker.APIEvents for containers only
|
||||
// containerInfo is simplified view of container metadata
|
||||
type containerInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
TS time.Time
|
||||
Labels map[string]string
|
||||
IP string
|
||||
Ports []int
|
||||
ID string `json:"Id"`
|
||||
Name string `json:"-"`
|
||||
State string `json:"State"`
|
||||
Labels map[string]string `json:"Labels"`
|
||||
TS time.Time `json:"-"`
|
||||
IP string `json:"-"`
|
||||
Ports []int `json:"-"`
|
||||
}
|
||||
|
||||
// Events gets eventsCh with all containers-related docker events events
|
||||
func (d *Docker) Events(ctx context.Context) (res <-chan discovery.ProviderID) {
|
||||
eventsCh := make(chan discovery.ProviderID)
|
||||
go func() {
|
||||
defer close(eventsCh)
|
||||
// loop over to recover from failed events call
|
||||
for {
|
||||
err := d.events(ctx, d.DockerClient, eventsCh) // publish events to eventsCh in a blocking loop
|
||||
if err == context.Canceled || err == context.DeadlineExceeded {
|
||||
return
|
||||
}
|
||||
log.Printf("[WARN] docker events listener failed (restarted), %v", err)
|
||||
time.Sleep(1 * time.Second) // prevent busy loop on restart of event listener
|
||||
}
|
||||
}()
|
||||
go d.events(ctx, eventsCh)
|
||||
return eventsCh
|
||||
}
|
||||
|
||||
@@ -133,7 +123,7 @@ func (d *Docker) List() ([]discovery.URLMapper, error) {
|
||||
|
||||
srcRegex, err := regexp.Compile(srcURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid src regex %s: %w", srcURL, err)
|
||||
return nil, errors.Wrapf(err, "invalid src regex %s", srcURL)
|
||||
}
|
||||
|
||||
// docker server label may have multiple, comma separated servers
|
||||
@@ -165,7 +155,7 @@ func (d *Docker) matchedPort(c containerInfo) (port int, err error) {
|
||||
if v, ok := c.Labels["reproxy.port"]; ok {
|
||||
rp, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid reproxy.port %s: %w", v, err)
|
||||
return 0, errors.Wrapf(err, "invalid reproxy.port %s", v)
|
||||
}
|
||||
for _, p := range c.Ports {
|
||||
// set port to reproxy.port if matched with one of exposed
|
||||
@@ -178,105 +168,140 @@ func (d *Docker) matchedPort(c containerInfo) (port int, err error) {
|
||||
return port, nil
|
||||
}
|
||||
|
||||
// activate starts blocking listener for all docker events
|
||||
// filters everything except "container" type, detects stop/start events and publishes signals to eventsCh
|
||||
func (d *Docker) events(ctx context.Context, client DockerClient, eventsCh chan discovery.ProviderID) error {
|
||||
dockerEventsCh := make(chan *dc.APIEvents)
|
||||
err := client.AddEventListenerWithOptions(dc.EventsOptions{
|
||||
Filters: map[string][]string{"type": {"container"}, "event": {"start", "die", "destroy", "restart", "pause"}}},
|
||||
dockerEventsCh)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't add even listener: %w", err)
|
||||
}
|
||||
// changed in tests
|
||||
var dockerPollingInterval = time.Second * 10
|
||||
|
||||
// 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 {
|
||||
ticker := time.NewTicker(dockerPollingInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
eventsCh <- discovery.PIDocker // initial emmit
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case ev, ok := <-dockerEventsCh:
|
||||
if !ok {
|
||||
return errors.New("events closed")
|
||||
}
|
||||
log.Printf("[DEBUG] api event %+v", ev)
|
||||
containerName := strings.TrimPrefix(ev.Actor.Attributes["name"], "/")
|
||||
|
||||
if discovery.Contains(containerName, d.Excludes) {
|
||||
log.Printf("[DEBUG] container %s excluded", containerName)
|
||||
continue
|
||||
}
|
||||
log.Printf("[INFO] new docker event: container %s, status %s", containerName, ev.Status)
|
||||
case <-ticker.C:
|
||||
eventsCh <- discovery.PIDocker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Docker) listContainers() (res []containerInfo, err error) {
|
||||
|
||||
portsExposed := func(c dc.APIContainers) (ports []int) {
|
||||
if len(c.Ports) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, p := range c.Ports {
|
||||
ports = append(ports, int(p.PrivatePort))
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
containers, err := d.DockerClient.ListContainers(dc.ListContainersOptions{All: false})
|
||||
containers, err := d.DockerClient.ListContainers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't list containers: %w", err)
|
||||
return nil, errors.Wrap(err, "can't list containers")
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] total containers = %d", len(containers))
|
||||
|
||||
for _, c := range containers {
|
||||
if c.State != "running" {
|
||||
log.Printf("[DEBUG] skip container %s due to state %s", c.Names[0], c.State)
|
||||
log.Printf("[DEBUG] skip container %s due to state %s", c.Name, c.State)
|
||||
continue
|
||||
}
|
||||
containerName := strings.TrimPrefix(c.Names[0], "/")
|
||||
if discovery.Contains(containerName, d.Excludes) || strings.EqualFold(containerName, "reproxy") {
|
||||
log.Printf("[DEBUG] container %s excluded", containerName)
|
||||
|
||||
if discovery.Contains(c.Name, d.Excludes) || strings.EqualFold(c.Name, "reproxy") {
|
||||
log.Printf("[DEBUG] container %s excluded", c.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if v, ok := c.Labels["reproxy.enabled"]; ok {
|
||||
if strings.EqualFold(v, "false") || strings.EqualFold(v, "no") || v == "0" {
|
||||
log.Printf("[DEBUG] skip container %s due to reproxy.enabled=%s", containerName, v)
|
||||
log.Printf("[DEBUG] skip container %s due to reproxy.enabled=%s", c.Name, v)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var ip string
|
||||
for k, v := range c.Networks.Networks {
|
||||
if d.Network == "" || k == d.Network { // match on network name if defined
|
||||
ip = v.IPAddress
|
||||
break
|
||||
}
|
||||
}
|
||||
if ip == "" {
|
||||
log.Printf("[DEBUG] skip container %s, no ip on %+v", c.Names[0], c.Networks.Networks)
|
||||
if c.IP == "" {
|
||||
log.Printf("[DEBUG] skip container %s, no ip on defined networks", c.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
ports := portsExposed(c)
|
||||
if len(ports) == 0 {
|
||||
log.Printf("[DEBUG] skip container %s, no exposed ports", c.Names[0])
|
||||
if len(c.Ports) == 0 {
|
||||
log.Printf("[DEBUG] skip container %s, no exposed ports", c.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
ci := containerInfo{
|
||||
Name: containerName,
|
||||
ID: c.ID,
|
||||
TS: time.Unix(c.Created/1000, 0),
|
||||
Labels: c.Labels,
|
||||
IP: ip,
|
||||
Ports: ports,
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] running container added, %+v", ci)
|
||||
res = append(res, ci)
|
||||
log.Printf("[DEBUG] running container added, %+v", c)
|
||||
res = append(res, c)
|
||||
}
|
||||
log.Print("[DEBUG] completed list")
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type dockerClient struct {
|
||||
client http.Client
|
||||
network string // network for IP selection
|
||||
}
|
||||
|
||||
// NewDockerClient constructs docker client for given host and network
|
||||
func NewDockerClient(host, network string) DockerClient {
|
||||
var schemaRegex = regexp.MustCompile("^(?:([a-z0-9]+)://)?(.*)$")
|
||||
parts := schemaRegex.FindStringSubmatch(host)
|
||||
proto, addr := parts[1], parts[2]
|
||||
log.Printf("[DEBUG] Configuring docker client to talk to %s via %s", addr, proto)
|
||||
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return net.Dial(proto, addr)
|
||||
},
|
||||
},
|
||||
Timeout: time.Second * 5,
|
||||
}
|
||||
|
||||
return &dockerClient{client, network}
|
||||
}
|
||||
|
||||
func (d *dockerClient) ListContainers() ([]containerInfo, error) {
|
||||
|
||||
const dockerAPIVersion = "v1.41"
|
||||
|
||||
resp, err := d.client.Get(fmt.Sprintf("http://localhost/%s/containers/json", dockerAPIVersion))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed connection to docker socket")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
response := []struct {
|
||||
containerInfo
|
||||
Created int64
|
||||
NetworkSettings struct {
|
||||
Networks map[string]struct {
|
||||
IPAddress string
|
||||
}
|
||||
}
|
||||
Names []string
|
||||
ExposedPorts []struct{ PrivatePort int } `json:"Ports"`
|
||||
}{}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse response from docker daemon")
|
||||
}
|
||||
|
||||
containers := make([]containerInfo, len(response))
|
||||
|
||||
for i, c := range response {
|
||||
// fill remaining fields
|
||||
c.TS = time.Unix(c.Created, 0)
|
||||
|
||||
for k, v := range c.NetworkSettings.Networks {
|
||||
if d.network == "" || k == d.network { // match on network name if defined
|
||||
c.IP = v.IPAddress
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.Name = strings.TrimPrefix(c.Names[0], "/")
|
||||
|
||||
for _, p := range c.ExposedPorts {
|
||||
c.Ports = append(c.Ports, p.PrivatePort)
|
||||
}
|
||||
|
||||
containers[i] = c.containerInfo
|
||||
}
|
||||
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ package provider
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
dclient "github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
// DockerClientMock is a mock implementation of DockerClient.
|
||||
@@ -15,10 +13,7 @@ import (
|
||||
//
|
||||
// // make and configure a mocked DockerClient
|
||||
// mockedDockerClient := &DockerClientMock{
|
||||
// AddEventListenerWithOptionsFunc: func(options dclient.EventsOptions, listener chan<- *dclient.APIEvents) error {
|
||||
// panic("mock out the AddEventListenerWithOptions method")
|
||||
// },
|
||||
// ListContainersFunc: func(opts dclient.ListContainersOptions) ([]dclient.APIContainers, error) {
|
||||
// ListContainersFunc: func() ([]containerInfo, error) {
|
||||
// panic("mock out the ListContainers method")
|
||||
// },
|
||||
// }
|
||||
@@ -28,90 +23,37 @@ import (
|
||||
//
|
||||
// }
|
||||
type DockerClientMock struct {
|
||||
// AddEventListenerWithOptionsFunc mocks the AddEventListenerWithOptions method.
|
||||
AddEventListenerWithOptionsFunc func(options dclient.EventsOptions, listener chan<- *dclient.APIEvents) error
|
||||
|
||||
// ListContainersFunc mocks the ListContainers method.
|
||||
ListContainersFunc func(opts dclient.ListContainersOptions) ([]dclient.APIContainers, error)
|
||||
ListContainersFunc func() ([]containerInfo, error)
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// AddEventListenerWithOptions holds details about calls to the AddEventListenerWithOptions method.
|
||||
AddEventListenerWithOptions []struct {
|
||||
// Options is the options argument value.
|
||||
Options dclient.EventsOptions
|
||||
// Listener is the listener argument value.
|
||||
Listener chan<- *dclient.APIEvents
|
||||
}
|
||||
// ListContainers holds details about calls to the ListContainers method.
|
||||
ListContainers []struct {
|
||||
// Opts is the opts argument value.
|
||||
Opts dclient.ListContainersOptions
|
||||
}
|
||||
}
|
||||
lockAddEventListenerWithOptions sync.RWMutex
|
||||
lockListContainers sync.RWMutex
|
||||
}
|
||||
|
||||
// AddEventListenerWithOptions calls AddEventListenerWithOptionsFunc.
|
||||
func (mock *DockerClientMock) AddEventListenerWithOptions(options dclient.EventsOptions, listener chan<- *dclient.APIEvents) error {
|
||||
if mock.AddEventListenerWithOptionsFunc == nil {
|
||||
panic("DockerClientMock.AddEventListenerWithOptionsFunc: method is nil but DockerClient.AddEventListenerWithOptions was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Options dclient.EventsOptions
|
||||
Listener chan<- *dclient.APIEvents
|
||||
}{
|
||||
Options: options,
|
||||
Listener: listener,
|
||||
}
|
||||
mock.lockAddEventListenerWithOptions.Lock()
|
||||
mock.calls.AddEventListenerWithOptions = append(mock.calls.AddEventListenerWithOptions, callInfo)
|
||||
mock.lockAddEventListenerWithOptions.Unlock()
|
||||
return mock.AddEventListenerWithOptionsFunc(options, listener)
|
||||
}
|
||||
|
||||
// AddEventListenerWithOptionsCalls gets all the calls that were made to AddEventListenerWithOptions.
|
||||
// Check the length with:
|
||||
// len(mockedDockerClient.AddEventListenerWithOptionsCalls())
|
||||
func (mock *DockerClientMock) AddEventListenerWithOptionsCalls() []struct {
|
||||
Options dclient.EventsOptions
|
||||
Listener chan<- *dclient.APIEvents
|
||||
} {
|
||||
var calls []struct {
|
||||
Options dclient.EventsOptions
|
||||
Listener chan<- *dclient.APIEvents
|
||||
}
|
||||
mock.lockAddEventListenerWithOptions.RLock()
|
||||
calls = mock.calls.AddEventListenerWithOptions
|
||||
mock.lockAddEventListenerWithOptions.RUnlock()
|
||||
return calls
|
||||
lockListContainers sync.RWMutex
|
||||
}
|
||||
|
||||
// ListContainers calls ListContainersFunc.
|
||||
func (mock *DockerClientMock) ListContainers(opts dclient.ListContainersOptions) ([]dclient.APIContainers, error) {
|
||||
func (mock *DockerClientMock) ListContainers() ([]containerInfo, error) {
|
||||
if mock.ListContainersFunc == nil {
|
||||
panic("DockerClientMock.ListContainersFunc: method is nil but DockerClient.ListContainers was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Opts dclient.ListContainersOptions
|
||||
}{
|
||||
Opts: opts,
|
||||
}
|
||||
}{}
|
||||
mock.lockListContainers.Lock()
|
||||
mock.calls.ListContainers = append(mock.calls.ListContainers, callInfo)
|
||||
mock.lockListContainers.Unlock()
|
||||
return mock.ListContainersFunc(opts)
|
||||
return mock.ListContainersFunc()
|
||||
}
|
||||
|
||||
// ListContainersCalls gets all the calls that were made to ListContainers.
|
||||
// Check the length with:
|
||||
// len(mockedDockerClient.ListContainersCalls())
|
||||
func (mock *DockerClientMock) ListContainersCalls() []struct {
|
||||
Opts dclient.ListContainersOptions
|
||||
} {
|
||||
var calls []struct {
|
||||
Opts dclient.ListContainersOptions
|
||||
}
|
||||
mock.lockListContainers.RLock()
|
||||
calls = mock.calls.ListContainers
|
||||
|
||||
@@ -1,71 +1,51 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dc "github.com/fsouza/go-dockerclient"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDocker_List(t *testing.T) {
|
||||
dclient := &DockerClientMock{
|
||||
ListContainersFunc: func(opts dc.ListContainersOptions) ([]dc.APIContainers, error) {
|
||||
return []dc.APIContainers{
|
||||
{Names: []string{"c0"}, State: "running",
|
||||
Networks: dc.NetworkList{
|
||||
Networks: map[string]dc.ContainerNetwork{"bridge": {IPAddress: "127.0.0.2"}},
|
||||
},
|
||||
Ports: []dc.APIPort{
|
||||
{PrivatePort: 12348},
|
||||
},
|
||||
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"},
|
||||
},
|
||||
{Names: []string{"c1"}, State: "running",
|
||||
Networks: dc.NetworkList{
|
||||
Networks: map[string]dc.ContainerNetwork{"bridge": {IPAddress: "127.0.0.2"}},
|
||||
},
|
||||
Ports: []dc.APIPort{
|
||||
{PrivatePort: 12345},
|
||||
},
|
||||
{
|
||||
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"},
|
||||
},
|
||||
{Names: []string{"c2"}, State: "running",
|
||||
Networks: dc.NetworkList{
|
||||
Networks: map[string]dc.ContainerNetwork{"bridge": {IPAddress: "127.0.0.3"}},
|
||||
},
|
||||
Ports: []dc.APIPort{
|
||||
{PrivatePort: 12346},
|
||||
},
|
||||
{
|
||||
Name: "c2", State: "running", IP: "127.0.0.3", Ports: []int{12346},
|
||||
Labels: map[string]string{"reproxy.enabled": "y"},
|
||||
},
|
||||
{Names: []string{"c3"}, State: "stopped"},
|
||||
{Names: []string{"c4"}, State: "running",
|
||||
Networks: dc.NetworkList{
|
||||
Networks: map[string]dc.ContainerNetwork{"other": {IPAddress: "127.0.0.2"}},
|
||||
},
|
||||
Ports: []dc.APIPort{
|
||||
{PrivatePort: 12345},
|
||||
},
|
||||
{
|
||||
Name: "c3", State: "stopped",
|
||||
},
|
||||
{Names: []string{"c5"}, State: "running",
|
||||
Networks: dc.NetworkList{
|
||||
Networks: map[string]dc.ContainerNetwork{"bridge": {IPAddress: "127.0.0.122"}},
|
||||
},
|
||||
Ports: []dc.APIPort{
|
||||
{PrivatePort: 2345},
|
||||
},
|
||||
{
|
||||
Name: "c4", State: "running", IP: "127.0.0.2", Ports: []int{12345},
|
||||
},
|
||||
{
|
||||
Name: "c5", State: "running", IP: "127.0.0.122", Ports: []int{2345},
|
||||
Labels: map[string]string{"reproxy.enabled": "false"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
d := Docker{DockerClient: dclient, Network: "bridge"}
|
||||
d := Docker{DockerClient: dclient}
|
||||
res, err := d.List()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, len(res))
|
||||
@@ -86,97 +66,32 @@ func TestDocker_List(t *testing.T) {
|
||||
assert.Equal(t, "*", res[2].Server)
|
||||
}
|
||||
|
||||
func TestDocker_ListWithAutoAPI(t *testing.T) {
|
||||
dclient := &DockerClientMock{
|
||||
ListContainersFunc: func(opts dc.ListContainersOptions) ([]dc.APIContainers, error) {
|
||||
return []dc.APIContainers{
|
||||
{Names: []string{"c1"}, State: "running",
|
||||
Networks: dc.NetworkList{
|
||||
Networks: map[string]dc.ContainerNetwork{"bridge": {IPAddress: "127.0.0.2"}},
|
||||
},
|
||||
Ports: []dc.APIPort{
|
||||
{PrivatePort: 1345},
|
||||
{PrivatePort: 12345},
|
||||
},
|
||||
Labels: map[string]string{"reproxy.route": "^/api/123/(.*)", "reproxy.dest": "/blah/$1",
|
||||
"reproxy.port": "12345", "reproxy.server": "example.com, example2.com", "reproxy.ping": "/ping"},
|
||||
},
|
||||
{Names: []string{"c2"}, State: "running",
|
||||
Networks: dc.NetworkList{
|
||||
Networks: map[string]dc.ContainerNetwork{"bridge": {IPAddress: "127.0.0.3"}},
|
||||
},
|
||||
Ports: []dc.APIPort{
|
||||
{PrivatePort: 12346},
|
||||
},
|
||||
},
|
||||
{Names: []string{"c3"}, State: "stopped"},
|
||||
{Names: []string{"c4"}, State: "running",
|
||||
Networks: dc.NetworkList{
|
||||
Networks: map[string]dc.ContainerNetwork{"other": {IPAddress: "127.0.0.2"}},
|
||||
},
|
||||
Ports: []dc.APIPort{
|
||||
{PrivatePort: 12345},
|
||||
},
|
||||
},
|
||||
{Names: []string{"c5"}, State: "running",
|
||||
Networks: dc.NetworkList{
|
||||
Networks: map[string]dc.ContainerNetwork{"bridge": {IPAddress: "127.0.0.122"}},
|
||||
},
|
||||
Ports: []dc.APIPort{
|
||||
{PrivatePort: 2345},
|
||||
},
|
||||
Labels: map[string]string{"reproxy.enabled": "false"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
func TestDockerClient(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1.41/containers/json", r.URL.Path)
|
||||
|
||||
d := Docker{DockerClient: dclient, Network: "bridge", AutoAPI: true}
|
||||
res, err := d.List()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, len(res))
|
||||
// obtained using curl --unix-socket /var/run/docker.sock http://localhost/v1.41/containers/json
|
||||
resp, err := ioutil.ReadFile("testdata/containers.json")
|
||||
require.NoError(t, err)
|
||||
w.Write(resp)
|
||||
}))
|
||||
|
||||
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)
|
||||
defer srv.Close()
|
||||
addr := fmt.Sprintf("tcp://%s", strings.TrimPrefix(srv.URL, "http://"))
|
||||
|
||||
assert.Equal(t, "^/api/123/(.*)", res[1].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.2:12345/blah/$1", res[1].Dst)
|
||||
assert.Equal(t, "example2.com", res[1].Server)
|
||||
assert.Equal(t, "http://127.0.0.2:12345/ping", res[1].PingURL)
|
||||
client := NewDockerClient(addr, "bridge")
|
||||
c, err := client.ListContainers()
|
||||
require.NoError(t, err, "unexpected error while listing containers")
|
||||
|
||||
assert.Equal(t, "^/api/c2/(.*)", res[2].SrcMatch.String())
|
||||
assert.Equal(t, "http://127.0.0.3:12346/$1", res[2].Dst)
|
||||
assert.Equal(t, "http://127.0.0.3:12346/ping", res[2].PingURL)
|
||||
assert.Equal(t, "*", res[2].Server)
|
||||
}
|
||||
|
||||
func TestDocker_Events(t *testing.T) {
|
||||
dclient := &DockerClientMock{
|
||||
AddEventListenerWithOptionsFunc: func(options dc.EventsOptions, listener chan<- *dc.APIEvents) error {
|
||||
go func() {
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
listener <- &dc.APIEvents{Type: "container", Status: "start",
|
||||
Actor: dc.APIActor{Attributes: map[string]string{"name": "/c1"}}}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
listener <- &dc.APIEvents{Type: "container", Status: "start",
|
||||
Actor: dc.APIActor{Attributes: map[string]string{"name": "/c2"}}}
|
||||
}()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
d := Docker{DockerClient: dclient}
|
||||
ch := d.Events(ctx)
|
||||
|
||||
events := 0
|
||||
for range ch {
|
||||
t.Log("event")
|
||||
events++
|
||||
}
|
||||
assert.Equal(t, 2+1, events, "initial event plus 2 more")
|
||||
assert.Len(t, c, 2)
|
||||
|
||||
assert.NotEmpty(t, c[0].ID)
|
||||
assert.Equal(t, "nginx", c[0].Name)
|
||||
assert.Equal(t, "running", c[0].State)
|
||||
assert.Equal(t, "172.17.0.3", c[0].IP)
|
||||
assert.Equal(t, "y", c[0].Labels["reproxy.enabled"])
|
||||
assert.Equal(t, []int{80}, c[0].Ports)
|
||||
assert.Equal(t, time.Unix(1618417435, 0), c[0].TS)
|
||||
|
||||
assert.Equal(t, "", c[1].IP)
|
||||
}
|
||||
|
||||
91
app/discovery/provider/testdata/containers.json
vendored
Normal file
91
app/discovery/provider/testdata/containers.json
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
[
|
||||
{
|
||||
"Id": "6f3c99ca4d83811ae1bf1c3e288ac229288028a8b097565cb93eed85ccc4c9b4",
|
||||
"Names": [
|
||||
"/nginx"
|
||||
],
|
||||
"Image": "nginx",
|
||||
"ImageID": "sha256:62d49f9bab67f7c70ac3395855bf01389eb3175b374e621f6f191bf31b54cd5b",
|
||||
"Command": "/docker-entrypoint.sh nginx -g 'daemon off;'",
|
||||
"Created": 1618417435,
|
||||
"Ports": [
|
||||
{
|
||||
"PrivatePort": 80,
|
||||
"Type": "tcp"
|
||||
}
|
||||
],
|
||||
"Labels": {
|
||||
"maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>",
|
||||
"reproxy.enabled": "y"
|
||||
},
|
||||
"State": "running",
|
||||
"Status": "Up 2 seconds",
|
||||
"HostConfig": {
|
||||
"NetworkMode": "default"
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"NetworkID": "7cf54d5b5b0cea56bcd4b5a8d7fd00ec6da9283a77e3cc0bd1252ebab60eca67",
|
||||
"EndpointID": "481acb2624a95a12826357b1d6e9560ff0225b102ef932bb4dc484770f5b632b",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.3",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"MacAddress": "02:42:42:42:42:03",
|
||||
"DriverOpts": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"Mounts": []
|
||||
},
|
||||
{
|
||||
"Id": "d5503b81f356718bb01357daab63acb88eaae682bcb638383fadbac66e0bb830",
|
||||
"Names": [
|
||||
"/weather"
|
||||
],
|
||||
"Image": "weather:latest",
|
||||
"ImageID": "sha256:28ceada083a985a36f794904261b0a82d2c3be9de57076acab90ac304f77952e",
|
||||
"Command": "/usr/local/bin/weather",
|
||||
"Created": 1618410445,
|
||||
"Ports": [
|
||||
{
|
||||
"PrivatePort": 8000,
|
||||
"Type": "tcp"
|
||||
}
|
||||
],
|
||||
"Labels": {
|
||||
"hello": "world"
|
||||
},
|
||||
"State": "running",
|
||||
"Status": "Up 2 hours (healthy)",
|
||||
"HostConfig": {
|
||||
"NetworkMode": "default"
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Networks": {
|
||||
"someOtherNetwork": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"NetworkID": "7cf54d5b5b0cea56bcd4b5a8d7fd00ec6da9283a77e3cc0bd1252ebab60eca67",
|
||||
"EndpointID": "b4a14144a7300b37d4e907fdffa68b5234f67d2e7bb5db869198cf2db78d6280",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"MacAddress": "02:42:42:42:42:02",
|
||||
"DriverOpts": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"Mounts": []
|
||||
}
|
||||
]
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
docker "github.com/fsouza/go-dockerclient"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/umputun/go-flags"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
@@ -203,15 +202,13 @@ func makeProviders() ([]discovery.Provider, error) {
|
||||
}
|
||||
|
||||
if opts.Docker.Enabled {
|
||||
client, err := docker.NewClient(opts.Docker.Host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make docker client %w", err)
|
||||
}
|
||||
client := provider.NewDockerClient(opts.Docker.Host, opts.Docker.Network)
|
||||
|
||||
if opts.Docker.AutoAPI {
|
||||
log.Printf("[INFO] auto-api enabled for docker")
|
||||
}
|
||||
res = append(res, &provider.Docker{DockerClient: client, Excludes: opts.Docker.Excluded,
|
||||
Network: opts.Docker.Network, AutoAPI: opts.Docker.AutoAPI})
|
||||
AutoAPI: opts.Docker.AutoAPI})
|
||||
}
|
||||
|
||||
if len(res) == 0 && opts.Assets.Location == "" {
|
||||
|
||||
Reference in New Issue
Block a user