Skip to content

Commit

Permalink
feat: cache calls to docker api
Browse files Browse the repository at this point in the history
  • Loading branch information
chetan committed Jul 4, 2024
1 parent 3830c21 commit c3e86f5
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 40 deletions.
33 changes: 25 additions & 8 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import (

// Copied from traefik. See docker provider package for original impl

type dockerCache struct {
client client.APIClient
list []types.Container
details map[string]types.ContainerJSON
}

// Must be 0 for unix socket?
// Non-zero throws an error
const defaultTimeout = time.Duration(0)
Expand Down Expand Up @@ -51,19 +57,30 @@ func getClientOpts(endpoint string) ([]client.Opt, error) {
}

// looks up the docker container by finding the matching service or router traefik label
func findContainerByServiceName(dc client.APIClient, svcType string, svcName string, routerName string) (types.ContainerJSON, error) {
func (dc *dockerCache) findContainerByServiceName(svcType string, svcName string, routerName string) (types.ContainerJSON, error) {
svcName = strings.TrimSuffix(svcName, "@docker")
routerName = strings.TrimSuffix(routerName, "@docker")

list, err := dc.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
return types.ContainerJSON{}, errors.Wrap(err, "failed to list containers")
}
for _, c := range list {
container, err := dc.ContainerInspect(context.Background(), c.ID)
if dc.list == nil {
var err error
dc.list, err = dc.client.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
return types.ContainerJSON{}, errors.Wrapf(err, "failed to inspect container %s", c.ID)
return types.ContainerJSON{}, errors.Wrap(err, "failed to list containers")
}
}

for _, c := range dc.list {
var container types.ContainerJSON
var ok bool
if container, ok = dc.details[c.ID]; !ok {
var err error
container, err = dc.client.ContainerInspect(context.Background(), c.ID)
if err != nil {
return types.ContainerJSON{}, errors.Wrapf(err, "failed to inspect container %s", c.ID)
}
dc.details[c.ID] = container
}

// check labels
svcNeedle := fmt.Sprintf("traefik.%s.services.%s", svcType, svcName)
routerNeedle := fmt.Sprintf("traefik.%s.routers.%s", svcType, routerName)
Expand Down
54 changes: 30 additions & 24 deletions traefik_kop.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,17 @@ func createConfigHandler(config Config, store TraefikStore, dp *docker.Provider,
// fmt.Printf("%s\n", dumpJson(conf))
logrus.Infoln("refreshing traefik-kop configuration")

filterServices(dockerClient, &conf, config.Namespace)
dc := &dockerCache{
client: dockerClient,
list: nil,
details: make(map[string]types.ContainerJSON),
}

filterServices(dc, &conf, config.Namespace)

if !dp.UseBindPortIP {
// if not using traefik's built in IP/Port detection, use our own
replaceIPs(dockerClient, &conf, config.BindIP)
replaceIPs(dc, &conf, config.BindIP)
}
err := store.Store(conf)
if err != nil {
Expand Down Expand Up @@ -133,10 +139,10 @@ func keepContainer(ns string, container types.ContainerJSON) bool {

// filter out services by namespace
// ns is traefik-kop's configured namespace to match against.
func filterServices(dockerClient client.APIClient, conf *dynamic.Configuration, ns string) {
func filterServices(dc *dockerCache, conf *dynamic.Configuration, ns string) {
if conf.HTTP != nil && conf.HTTP.Services != nil {
for svcName := range conf.HTTP.Services {
container, err := findContainerByServiceName(dockerClient, "http", svcName, getRouterOfService(conf, svcName, "http"))
container, err := dc.findContainerByServiceName("http", svcName, getRouterOfService(conf, svcName, "http"))
if err != nil {
logrus.Warnf("failed to find container for service '%s': %s", svcName, err)
continue
Expand All @@ -151,7 +157,7 @@ func filterServices(dockerClient client.APIClient, conf *dynamic.Configuration,
if conf.HTTP != nil && conf.HTTP.Routers != nil {
for routerName, router := range conf.HTTP.Routers {
svcName := router.Service
container, err := findContainerByServiceName(dockerClient, "http", svcName, routerName)
container, err := dc.findContainerByServiceName("http", svcName, routerName)
if err != nil {
logrus.Warnf("failed to find container for service '%s': %s", svcName, err)
continue
Expand All @@ -165,7 +171,7 @@ func filterServices(dockerClient client.APIClient, conf *dynamic.Configuration,

if conf.TCP != nil && conf.TCP.Services != nil {
for svcName := range conf.TCP.Services {
container, err := findContainerByServiceName(dockerClient, "tcp", svcName, getRouterOfService(conf, svcName, "tcp"))
container, err := dc.findContainerByServiceName("tcp", svcName, getRouterOfService(conf, svcName, "tcp"))
if err != nil {
logrus.Warnf("failed to find container for service '%s': %s", svcName, err)
continue
Expand All @@ -180,7 +186,7 @@ func filterServices(dockerClient client.APIClient, conf *dynamic.Configuration,
if conf.TCP != nil && conf.TCP.Routers != nil {
for routerName, router := range conf.TCP.Routers {
svcName := router.Service
container, err := findContainerByServiceName(dockerClient, "tcp", svcName, routerName)
container, err := dc.findContainerByServiceName("tcp", svcName, routerName)
if err != nil {
logrus.Warnf("failed to find container for service '%s': %s", svcName, err)
continue
Expand All @@ -194,7 +200,7 @@ func filterServices(dockerClient client.APIClient, conf *dynamic.Configuration,

if conf.UDP != nil && conf.UDP.Services != nil {
for svcName := range conf.UDP.Services {
container, err := findContainerByServiceName(dockerClient, "udp", svcName, getRouterOfService(conf, svcName, "udp"))
container, err := dc.findContainerByServiceName("udp", svcName, getRouterOfService(conf, svcName, "udp"))
if err != nil {
logrus.Warnf("failed to find container for service '%s': %s", svcName, err)
continue
Expand All @@ -209,7 +215,7 @@ func filterServices(dockerClient client.APIClient, conf *dynamic.Configuration,
if conf.UDP != nil && conf.UDP.Routers != nil {
for routerName, router := range conf.UDP.Routers {
svcName := router.Service
container, err := findContainerByServiceName(dockerClient, "udp", svcName, routerName)
container, err := dc.findContainerByServiceName("udp", svcName, routerName)
if err != nil {
logrus.Warnf("failed to find container for service '%s': %s", svcName, err)
continue
Expand All @@ -230,17 +236,17 @@ func filterServices(dockerClient client.APIClient, conf *dynamic.Configuration,
//
// When using CNI, as indicated by the container label `traefik.docker.network`,
// we will stick with the container IP.
func replaceIPs(dockerClient client.APIClient, conf *dynamic.Configuration, ip string) {
func replaceIPs(dc *dockerCache, conf *dynamic.Configuration, ip string) {
// modify HTTP URLs
if conf.HTTP != nil && conf.HTTP.Services != nil {
for svcName, svc := range conf.HTTP.Services {
log := logrus.WithFields(logrus.Fields{"service": svcName, "service-type": "http"})
log.Debugf("found http service: %s", svcName)
for i := range svc.LoadBalancer.Servers {
ip, changed := getKopOverrideBinding(dockerClient, conf, "http", svcName, ip)
ip, changed := getKopOverrideBinding(dc, conf, "http", svcName, ip)
if !changed {
// override with container IP if we have a routable IP
ip = getContainerNetworkIP(dockerClient, conf, "http", svcName, ip)
ip = getContainerNetworkIP(dc, conf, "http", svcName, ip)
}

// replace ip into URLs
Expand All @@ -254,7 +260,7 @@ func replaceIPs(dockerClient client.APIClient, conf *dynamic.Configuration, ip s
// labels ourselves.
log.Debugf("using load balancer URL for port detection: %s", server.URL)
u, _ := url.Parse(server.URL)
p := getContainerPort(dockerClient, conf, "http", svcName, u.Port())
p := getContainerPort(dc, conf, "http", svcName, u.Port())
if p != "" {
u.Host = ip + ":" + p
} else {
Expand All @@ -267,7 +273,7 @@ func replaceIPs(dockerClient client.APIClient, conf *dynamic.Configuration, ip s
scheme = server.Scheme
}
server.URL = fmt.Sprintf("%s://%s", scheme, ip)
port := getContainerPort(dockerClient, conf, "http", svcName, server.Port)
port := getContainerPort(dc, conf, "http", svcName, server.Port)
if port != "" {
server.URL += ":" + server.Port
}
Expand All @@ -292,10 +298,10 @@ func replaceIPs(dockerClient client.APIClient, conf *dynamic.Configuration, ip s
log.Debugf("found tcp service: %s", svcName)
for i := range svc.LoadBalancer.Servers {
// override with container IP if we have a routable IP
ip = getContainerNetworkIP(dockerClient, conf, "tcp", svcName, ip)
ip = getContainerNetworkIP(dc, conf, "tcp", svcName, ip)

server := &svc.LoadBalancer.Servers[i]
server.Port = getContainerPort(dockerClient, conf, "tcp", svcName, server.Port)
server.Port = getContainerPort(dc, conf, "tcp", svcName, server.Port)
log.Debugf("using ip '%s' and port '%s' for %s", ip, server.Port, svcName)
server.Address = ip
if server.Port != "" {
Expand All @@ -313,10 +319,10 @@ func replaceIPs(dockerClient client.APIClient, conf *dynamic.Configuration, ip s
log.Debugf("found udp service: %s", svcName)
for i := range svc.LoadBalancer.Servers {
// override with container IP if we have a routable IP
ip = getContainerNetworkIP(dockerClient, conf, "udp", svcName, ip)
ip = getContainerNetworkIP(dc, conf, "udp", svcName, ip)

server := &svc.LoadBalancer.Servers[i]
server.Port = getContainerPort(dockerClient, conf, "udp", svcName, server.Port)
server.Port = getContainerPort(dc, conf, "udp", svcName, server.Port)
log.Debugf("using ip '%s' and port '%s' for %s", ip, server.Port, svcName)
server.Address = ip
if server.Port != "" {
Expand Down Expand Up @@ -370,9 +376,9 @@ func getRouterOfService(conf *dynamic.Configuration, svcName string, svcType str
// traefik during its config parsing (possibly an container-internal port). The
// purpose of this method is to see if we can find a better match, specifically
// by looking at the host-port bindings in the docker config.
func getContainerPort(dockerClient client.APIClient, conf *dynamic.Configuration, svcType string, svcName string, port string) string {
func getContainerPort(dc *dockerCache, conf *dynamic.Configuration, svcType string, svcName string, port string) string {
log := logrus.WithFields(logrus.Fields{"service": svcName, "service-type": svcType})
container, err := findContainerByServiceName(dockerClient, svcType, svcName, getRouterOfService(conf, svcName, svcType))
container, err := dc.findContainerByServiceName(svcType, svcName, getRouterOfService(conf, svcName, svcType))
if err != nil {
log.Warnf("failed to find host-port: %s", err)
return port
Expand Down Expand Up @@ -403,8 +409,8 @@ func getContainerPort(dockerClient client.APIClient, conf *dynamic.Configuration
// (i.e., via CNI plugins such as calico or weave)
//
// If not configured, returns the globally bound hostIP
func getContainerNetworkIP(dockerClient client.APIClient, conf *dynamic.Configuration, svcType string, svcName string, hostIP string) string {
container, err := findContainerByServiceName(dockerClient, svcType, svcName, getRouterOfService(conf, svcName, svcType))
func getContainerNetworkIP(dc *dockerCache, conf *dynamic.Configuration, svcType string, svcName string, hostIP string) string {
container, err := dc.findContainerByServiceName(svcType, svcName, getRouterOfService(conf, svcName, svcType))
if err != nil {
logrus.Debugf("failed to find container for service '%s': %s", svcName, err)
return hostIP
Expand Down Expand Up @@ -437,8 +443,8 @@ func getContainerNetworkIP(dockerClient client.APIClient, conf *dynamic.Configur
//
// For a container with only a single exposed service, or where all services use
// the same IP, the latter is sufficient.
func getKopOverrideBinding(dockerClient client.APIClient, conf *dynamic.Configuration, svcType string, svcName string, hostIP string) (string, bool) {
container, err := findContainerByServiceName(dockerClient, svcType, svcName, getRouterOfService(conf, svcName, svcType))
func getKopOverrideBinding(dc *dockerCache, conf *dynamic.Configuration, svcType string, svcName string, hostIP string) (string, bool) {
container, err := dc.findContainerByServiceName(svcType, svcName, getRouterOfService(conf, svcName, svcType))
if err != nil {
logrus.Debugf("failed to find container for service '%s': %s", svcName, err)
return hostIP, false
Expand Down
21 changes: 13 additions & 8 deletions traefik_kop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ func Test_replaceIPs(t *testing.T) {
require.NoError(t, err)
require.Contains(t, cfg.HTTP.Services["nginx@docker"].LoadBalancer.Servers[0].URL, "172.20.0.2")

fc := &dockerCache{client: &fakeDockerClient{}, list: nil, details: make(map[string]types.ContainerJSON)}

// replace and test check again
replaceIPs(&fakeDockerClient{}, cfg, "7.7.7.7")
replaceIPs(fc, cfg, "7.7.7.7")
require.NotContains(t, cfg.HTTP.Services["nginx@docker"].LoadBalancer.Servers[0].URL, "172.20.0.2")

// full url
Expand All @@ -56,7 +58,7 @@ func Test_replaceIPs(t *testing.T) {
_, err = toml.DecodeFile("./fixtures/sample.toml", &cfg)
require.NoError(t, err)
require.Equal(t, "foobar", cfg.TCP.Services["TCPService0"].LoadBalancer.Servers[0].Address)
replaceIPs(&fakeDockerClient{}, cfg, "7.7.7.7")
replaceIPs(fc, cfg, "7.7.7.7")
require.Equal(t, "7.7.7.7", cfg.TCP.Services["TCPService0"].LoadBalancer.Servers[0].Address)
}

Expand Down Expand Up @@ -94,26 +96,28 @@ func Test_replacePorts(t *testing.T) {
portLabel: "8888",
})

fc := &dockerCache{client: dc, list: nil, details: make(map[string]types.ContainerJSON)}

cfg := &dynamic.Configuration{}
err := json.Unmarshal([]byte(NGINX_CONF_JSON), cfg)
require.NoError(t, err)

require.True(t, strings.HasSuffix(cfg.HTTP.Services["nginx@docker"].LoadBalancer.Servers[0].URL, "172.20.0.2:80"))

// explicit label present
replaceIPs(dc, cfg, "4.4.4.4")
replaceIPs(fc, cfg, "4.4.4.4")
require.True(t, strings.HasSuffix(cfg.HTTP.Services["nginx@docker"].LoadBalancer.Servers[0].URL, "4.4.4.4:8888"), "URL '%s' should end with '%s'", cfg.HTTP.Services["nginx@docker"].LoadBalancer.Servers[0].URL, "4.4.4.4:8888")

// without label but no port binding
delete(dc.container.Config.Labels, portLabel)
json.Unmarshal([]byte(NGINX_CONF_JSON), cfg)
replaceIPs(dc, cfg, "4.4.4.4")
replaceIPs(fc, cfg, "4.4.4.4")
require.True(t, strings.HasSuffix(cfg.HTTP.Services["nginx@docker"].LoadBalancer.Servers[0].URL, "4.4.4.4:80"))

// with port binding
dc.container.HostConfig.PortBindings = portMap
json.Unmarshal([]byte(NGINX_CONF_JSON), cfg)
replaceIPs(dc, cfg, "4.4.4.4")
replaceIPs(fc, cfg, "4.4.4.4")
require.False(t, strings.HasSuffix(cfg.HTTP.Services["nginx@docker"].LoadBalancer.Servers[0].URL, "4.4.4.4:80"))
require.True(t, strings.HasSuffix(cfg.HTTP.Services["nginx@docker"].LoadBalancer.Servers[0].URL, "4.4.4.4:8888"))
}
Expand All @@ -129,6 +133,7 @@ func Test_replacePortsNoService(t *testing.T) {
dc := createTestClient(map[string]string{
"traefik.http.routers.nginx.entrypoints": "web-secure",
})
fc := &dockerCache{client: dc, list: nil, details: make(map[string]types.ContainerJSON)}

cfg := &dynamic.Configuration{}
err := json.Unmarshal([]byte(NGINX_CONF_JSON_DIFFRENT_SERVICE_NAME), cfg)
Expand All @@ -137,18 +142,18 @@ func Test_replacePortsNoService(t *testing.T) {
require.True(t, strings.HasSuffix(cfg.HTTP.Services["nginx-nginx@docker"].LoadBalancer.Servers[0].URL, "172.20.0.2:80"))

// explicit label present
replaceIPs(dc, cfg, "4.4.4.4")
replaceIPs(fc, cfg, "4.4.4.4")
require.True(t, strings.HasSuffix(cfg.HTTP.Services["nginx-nginx@docker"].LoadBalancer.Servers[0].URL, "4.4.4.4:80"))

// without label but no port binding
json.Unmarshal([]byte(NGINX_CONF_JSON_DIFFRENT_SERVICE_NAME), cfg)
replaceIPs(dc, cfg, "4.4.4.4")
replaceIPs(fc, cfg, "4.4.4.4")
require.True(t, strings.HasSuffix(cfg.HTTP.Services["nginx-nginx@docker"].LoadBalancer.Servers[0].URL, "4.4.4.4:80"))

// with port binding
dc.container.HostConfig.PortBindings = portMap
json.Unmarshal([]byte(NGINX_CONF_JSON_DIFFRENT_SERVICE_NAME), cfg)
replaceIPs(dc, cfg, "4.4.4.4")
replaceIPs(fc, cfg, "4.4.4.4")
require.False(t, strings.HasSuffix(cfg.HTTP.Services["nginx-nginx@docker"].LoadBalancer.Servers[0].URL, "4.4.4.4:80"))
require.True(t, strings.HasSuffix(cfg.HTTP.Services["nginx-nginx@docker"].LoadBalancer.Servers[0].URL, "4.4.4.4:8888"))
}

0 comments on commit c3e86f5

Please sign in to comment.