diff --git a/docker.go b/docker.go index 913f08f..30b78c9 100644 --- a/docker.go +++ b/docker.go @@ -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) @@ -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) diff --git a/traefik_kop.go b/traefik_kop.go index 8d10538..9267dc7 100644 --- a/traefik_kop.go +++ b/traefik_kop.go @@ -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 { @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 { @@ -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 } @@ -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 != "" { @@ -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 != "" { @@ -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 @@ -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 @@ -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 diff --git a/traefik_kop_test.go b/traefik_kop_test.go index 2b7fa6b..780fd09 100644 --- a/traefik_kop_test.go +++ b/traefik_kop_test.go @@ -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 @@ -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) } @@ -94,6 +96,8 @@ 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) @@ -101,19 +105,19 @@ func Test_replacePorts(t *testing.T) { 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")) } @@ -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) @@ -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")) }