2021-04-01 02:37:28 -05:00
|
|
|
package provider
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"regexp"
|
2021-04-13 01:52:12 -05:00
|
|
|
"sort"
|
2021-04-01 02:37:28 -05:00
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
2021-04-03 01:38:05 -05:00
|
|
|
dc "github.com/fsouza/go-dockerclient"
|
2021-04-01 02:37:28 -05:00
|
|
|
log "github.com/go-pkgz/lgr"
|
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
|
2021-04-03 14:23:23 -05:00
|
|
|
"github.com/umputun/reproxy/app/discovery"
|
2021-04-01 02:37:28 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//go:generate moq -out docker_client_mock.go -skip-ensure -fmt goimports . DockerClient
|
|
|
|
|
|
2021-04-07 21:52:14 -05:00
|
|
|
// Docker provider watches compatible for stop/start changes from containers and maps by
|
|
|
|
|
// default from ^/api/%s/(.*) to http://%s:%d/$1, i.e. http://example.com/api/my_container/something
|
|
|
|
|
// will be mapped to http://172.17.42.1:8080/something. Ip will be the internal ip of the container and port exposed
|
|
|
|
|
// in the Dockerfile.
|
2021-04-06 23:17:50 -05:00
|
|
|
// Alternatively labels can alter this. reproxy.route sets source route, and reproxy.dest sets the destination.
|
2021-04-07 21:52:14 -05:00
|
|
|
// Optional reproxy.server enforces match by server name (hostname) and reproxy.ping sets the health check url
|
2021-04-01 02:37:28 -05:00
|
|
|
type Docker struct {
|
|
|
|
|
DockerClient DockerClient
|
|
|
|
|
Excludes []string
|
2021-04-02 00:50:16 -05:00
|
|
|
Network string
|
2021-04-12 02:57:13 -05:00
|
|
|
AutoAPI bool
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DockerClient defines interface listing containers and subscribing to events
|
|
|
|
|
type DockerClient interface {
|
2021-04-03 01:38:05 -05:00
|
|
|
ListContainers(opts dc.ListContainersOptions) ([]dc.APIContainers, error)
|
2021-04-08 22:06:16 -05:00
|
|
|
AddEventListenerWithOptions(options dc.EventsOptions, listener chan<- *dc.APIEvents) error
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// containerInfo is simplified docker.APIEvents for containers only
|
|
|
|
|
type containerInfo struct {
|
|
|
|
|
ID string
|
|
|
|
|
Name string
|
|
|
|
|
TS time.Time
|
|
|
|
|
Labels map[string]string
|
2021-04-02 00:50:16 -05:00
|
|
|
IP string
|
|
|
|
|
Port int
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
|
|
|
|
|
2021-04-08 22:06:16 -05:00
|
|
|
// Events gets eventsCh with all containers-related docker events events
|
2021-04-12 02:09:34 -05:00
|
|
|
func (d *Docker) Events(ctx context.Context) (res <-chan discovery.ProviderID) {
|
|
|
|
|
eventsCh := make(chan discovery.ProviderID)
|
2021-04-01 02:37:28 -05:00
|
|
|
go func() {
|
2021-04-08 22:06:16 -05:00
|
|
|
defer close(eventsCh)
|
2021-04-03 01:00:09 -05:00
|
|
|
// loop over to recover from failed events call
|
2021-04-01 02:37:28 -05:00
|
|
|
for {
|
2021-04-08 22:06:16 -05:00
|
|
|
err := d.events(ctx, d.DockerClient, eventsCh) // publish events to eventsCh in a blocking loop
|
2021-04-01 02:37:28 -05:00
|
|
|
if err == context.Canceled || err == context.DeadlineExceeded {
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-04-08 22:06:16 -05:00
|
|
|
log.Printf("[WARN] docker events listener failed (restarted), %v", err)
|
|
|
|
|
time.Sleep(1 * time.Second) // prevent busy loop on restart of event listener
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
return eventsCh
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// List all containers and make url mappers
|
2021-04-12 21:26:31 -05:00
|
|
|
// If AutoAPI enabled all each container and set all params, if not - allow only container with reproxy.* labels
|
2021-04-09 15:05:22 -05:00
|
|
|
func (d *Docker) List() ([]discovery.URLMapper, error) {
|
2021-04-02 00:07:36 -05:00
|
|
|
containers, err := d.listContainers()
|
2021-04-01 02:37:28 -05:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-09 15:05:22 -05:00
|
|
|
res := make([]discovery.URLMapper, 0, len(containers))
|
2021-04-01 02:37:28 -05:00
|
|
|
for _, c := range containers {
|
2021-04-12 21:26:31 -05:00
|
|
|
enabled := false
|
2021-04-12 02:57:13 -05:00
|
|
|
srcURL := "^/(.*)"
|
|
|
|
|
if d.AutoAPI {
|
2021-04-12 21:26:31 -05:00
|
|
|
enabled = true
|
2021-04-12 02:57:13 -05:00
|
|
|
srcURL = fmt.Sprintf("^/api/%s/(.*)", c.Name)
|
2021-04-12 21:26:31 -05:00
|
|
|
|
2021-04-12 02:57:13 -05:00
|
|
|
}
|
2021-04-02 00:50:16 -05:00
|
|
|
destURL := fmt.Sprintf("http://%s:%d/$1", c.IP, c.Port)
|
2021-04-05 03:37:28 -05:00
|
|
|
pingURL := fmt.Sprintf("http://%s:%d/ping", c.IP, c.Port)
|
2021-04-02 00:07:36 -05:00
|
|
|
server := "*"
|
2021-04-05 03:37:28 -05:00
|
|
|
|
2021-04-12 21:26:31 -05:00
|
|
|
// we don't care about value because disabled will be filtered before
|
|
|
|
|
if _, ok := c.Labels["reproxy.enabled"]; ok {
|
|
|
|
|
enabled = true
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-05 03:37:28 -05:00
|
|
|
if v, ok := c.Labels["reproxy.route"]; ok {
|
2021-04-12 21:26:31 -05:00
|
|
|
enabled = true
|
2021-04-01 02:37:28 -05:00
|
|
|
srcURL = v
|
|
|
|
|
}
|
2021-04-12 21:26:31 -05:00
|
|
|
|
2021-04-05 03:37:28 -05:00
|
|
|
if v, ok := c.Labels["reproxy.dest"]; ok {
|
2021-04-12 21:26:31 -05:00
|
|
|
enabled = true
|
2021-04-02 00:50:16 -05:00
|
|
|
destURL = fmt.Sprintf("http://%s:%d%s", c.IP, c.Port, v)
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
2021-04-12 21:26:31 -05:00
|
|
|
|
2021-04-05 03:37:28 -05:00
|
|
|
if v, ok := c.Labels["reproxy.server"]; ok {
|
2021-04-12 21:26:31 -05:00
|
|
|
enabled = true
|
2021-04-02 00:07:36 -05:00
|
|
|
server = v
|
|
|
|
|
}
|
2021-04-12 21:26:31 -05:00
|
|
|
|
2021-04-05 03:37:28 -05:00
|
|
|
if v, ok := c.Labels["reproxy.ping"]; ok {
|
2021-04-12 21:26:31 -05:00
|
|
|
enabled = true
|
2021-04-10 02:44:40 -05:00
|
|
|
pingURL = fmt.Sprintf("http://%s:%d%s", c.IP, c.Port, v)
|
2021-04-05 03:37:28 -05:00
|
|
|
}
|
|
|
|
|
|
2021-04-12 21:26:31 -05:00
|
|
|
if !enabled {
|
|
|
|
|
log.Printf("[DEBUG] container %s disabled", c.Name)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-12 03:13:25 -05:00
|
|
|
srcRegex, err := regexp.Compile(srcURL)
|
2021-04-01 02:37:28 -05:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.Wrapf(err, "invalid src regex %s", srcURL)
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-12 03:13:25 -05:00
|
|
|
// docker server label may have multiple, comma separated servers
|
|
|
|
|
for _, srv := range strings.Split(server, ",") {
|
|
|
|
|
res = append(res, discovery.URLMapper{Server: strings.TrimSpace(srv), SrcMatch: *srcRegex, Dst: destURL,
|
|
|
|
|
PingURL: pingURL, ProviderID: discovery.PIDocker})
|
|
|
|
|
}
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
2021-04-13 01:52:12 -05:00
|
|
|
|
|
|
|
|
// 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())
|
|
|
|
|
})
|
2021-04-01 02:37:28 -05:00
|
|
|
return res, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// activate starts blocking listener for all docker events
|
|
|
|
|
// filters everything except "container" type, detects stop/start events and publishes signals to eventsCh
|
2021-04-12 02:09:34 -05:00
|
|
|
func (d *Docker) events(ctx context.Context, client DockerClient, eventsCh chan discovery.ProviderID) error {
|
2021-04-03 01:38:05 -05:00
|
|
|
dockerEventsCh := make(chan *dc.APIEvents)
|
2021-04-08 22:06:16 -05:00
|
|
|
err := client.AddEventListenerWithOptions(dc.EventsOptions{
|
|
|
|
|
Filters: map[string][]string{"type": {"container"}, "event": {"start", "die", "destroy", "restart", "pause"}}},
|
|
|
|
|
dockerEventsCh)
|
|
|
|
|
if err != nil {
|
2021-04-01 02:37:28 -05:00
|
|
|
return errors.Wrap(err, "can't add even listener")
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-12 02:09:34 -05:00
|
|
|
eventsCh <- discovery.PIDocker // initial emmit
|
2021-04-01 02:37:28 -05:00
|
|
|
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"], "/")
|
|
|
|
|
|
2021-04-02 00:07:36 -05:00
|
|
|
if contains(containerName, d.Excludes) {
|
2021-04-01 02:37:28 -05:00
|
|
|
log.Printf("[DEBUG] container %s excluded", containerName)
|
|
|
|
|
continue
|
|
|
|
|
}
|
2021-04-12 02:09:34 -05:00
|
|
|
log.Printf("[INFO] new docker event: container %s, status %s", containerName, ev.Status)
|
|
|
|
|
eventsCh <- discovery.PIDocker
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-02 00:07:36 -05:00
|
|
|
func (d *Docker) listContainers() (res []containerInfo, err error) {
|
2021-04-01 02:37:28 -05:00
|
|
|
|
2021-04-03 01:38:05 -05:00
|
|
|
portExposed := func(c dc.APIContainers) (int, bool) {
|
2021-04-02 00:50:16 -05:00
|
|
|
if len(c.Ports) == 0 {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
return int(c.Ports[0].PrivatePort), true
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-03 01:38:05 -05:00
|
|
|
containers, err := d.DockerClient.ListContainers(dc.ListContainersOptions{All: false})
|
2021-04-01 02:37:28 -05:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.Wrap(err, "can't list containers")
|
|
|
|
|
}
|
|
|
|
|
log.Printf("[DEBUG] total containers = %d", len(containers))
|
|
|
|
|
|
|
|
|
|
for _, c := range containers {
|
2021-04-08 22:06:16 -05:00
|
|
|
if !contains(c.State, []string{"running"}) {
|
2021-04-10 02:07:42 -05:00
|
|
|
log.Printf("[DEBUG] skip container %s due to state %s", c.Names[0], c.State)
|
2021-04-01 02:37:28 -05:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
containerName := strings.TrimPrefix(c.Names[0], "/")
|
2021-04-02 00:07:36 -05:00
|
|
|
if contains(containerName, d.Excludes) {
|
2021-04-01 02:37:28 -05:00
|
|
|
log.Printf("[DEBUG] container %s excluded", containerName)
|
|
|
|
|
continue
|
|
|
|
|
}
|
2021-04-02 00:50:16 -05:00
|
|
|
|
2021-04-11 01:37:45 -05:00
|
|
|
if v, ok := c.Labels["reproxy.enabled"]; ok {
|
2021-04-12 21:26:31 -05:00
|
|
|
if strings.EqualFold(v, "false") || strings.EqualFold(v, "no") || v == "0" {
|
2021-04-11 01:37:45 -05:00
|
|
|
log.Printf("[DEBUG] skip container %s due to reproxy.enabled=%s", containerName, v)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-02 00:50:16 -05:00
|
|
|
var ip string
|
|
|
|
|
for k, v := range c.Networks.Networks {
|
2021-04-10 02:07:42 -05:00
|
|
|
if d.Network == "" || k == d.Network { // match on network name if defined
|
2021-04-02 00:50:16 -05:00
|
|
|
ip = v.IPAddress
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ip == "" {
|
2021-04-10 02:07:42 -05:00
|
|
|
log.Printf("[DEBUG] skip container %s, no ip on %+v", c.Names[0], c.Networks.Networks)
|
2021-04-02 00:50:16 -05:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
port, ok := portExposed(c)
|
|
|
|
|
if !ok {
|
2021-04-10 02:07:42 -05:00
|
|
|
log.Printf("[DEBUG] skip container %s, no exposed ports", c.Names[0])
|
2021-04-02 00:50:16 -05:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ci := containerInfo{
|
2021-04-01 02:37:28 -05:00
|
|
|
Name: containerName,
|
|
|
|
|
ID: c.ID,
|
|
|
|
|
TS: time.Unix(c.Created/1000, 0),
|
|
|
|
|
Labels: c.Labels,
|
2021-04-02 00:50:16 -05:00
|
|
|
IP: ip,
|
|
|
|
|
Port: port,
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
2021-04-02 00:50:16 -05:00
|
|
|
|
|
|
|
|
log.Printf("[DEBUG] running container added, %+v", ci)
|
|
|
|
|
res = append(res, ci)
|
2021-04-01 02:37:28 -05:00
|
|
|
}
|
|
|
|
|
log.Print("[DEBUG] completed list")
|
|
|
|
|
return res, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func contains(e string, s []string) bool {
|
|
|
|
|
for _, a := range s {
|
|
|
|
|
if a == e {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|