1
0
Fork 0

API: expose runtime representation

Co-authored-by: Julien Salleyron <julien.salleyron@gmail.com>
Co-authored-by: Jean-Baptiste Doumenjou <jb.doumenjou@gmail.com>
This commit is contained in:
mpl 2019-05-16 10:58:06 +02:00 committed by Traefiker Bot
parent 5cd9396dae
commit f6df556eb0
50 changed files with 2250 additions and 1158 deletions

210
pkg/config/runtime.go Normal file
View file

@ -0,0 +1,210 @@
package config
import (
"sort"
"strings"
"sync"
"github.com/containous/traefik/pkg/log"
)
// RuntimeConfiguration holds the information about the currently running traefik instance.
type RuntimeConfiguration struct {
Routers map[string]*RouterInfo `json:"routers,omitempty"`
Middlewares map[string]*MiddlewareInfo `json:"middlewares,omitempty"`
Services map[string]*ServiceInfo `json:"services,omitempty"`
TCPRouters map[string]*TCPRouterInfo `json:"tcpRouters,omitempty"`
TCPServices map[string]*TCPServiceInfo `json:"tcpServices,omitempty"`
}
// NewRuntimeConfig returns a RuntimeConfiguration initialized with the given conf. It never returns nil.
func NewRuntimeConfig(conf Configuration) *RuntimeConfiguration {
if conf.HTTP == nil && conf.TCP == nil {
return &RuntimeConfiguration{}
}
runtimeConfig := &RuntimeConfiguration{}
if conf.HTTP != nil {
routers := conf.HTTP.Routers
if len(routers) > 0 {
runtimeConfig.Routers = make(map[string]*RouterInfo, len(routers))
for k, v := range routers {
runtimeConfig.Routers[k] = &RouterInfo{Router: v}
}
}
services := conf.HTTP.Services
if len(services) > 0 {
runtimeConfig.Services = make(map[string]*ServiceInfo, len(services))
for k, v := range services {
runtimeConfig.Services[k] = &ServiceInfo{Service: v}
}
}
middlewares := conf.HTTP.Middlewares
if len(middlewares) > 0 {
runtimeConfig.Middlewares = make(map[string]*MiddlewareInfo, len(middlewares))
for k, v := range middlewares {
runtimeConfig.Middlewares[k] = &MiddlewareInfo{Middleware: v}
}
}
}
if conf.TCP != nil {
if len(conf.TCP.Routers) > 0 {
runtimeConfig.TCPRouters = make(map[string]*TCPRouterInfo, len(conf.TCP.Routers))
for k, v := range conf.TCP.Routers {
runtimeConfig.TCPRouters[k] = &TCPRouterInfo{TCPRouter: v}
}
}
if len(conf.TCP.Services) > 0 {
runtimeConfig.TCPServices = make(map[string]*TCPServiceInfo, len(conf.TCP.Services))
for k, v := range conf.TCP.Services {
runtimeConfig.TCPServices[k] = &TCPServiceInfo{TCPService: v}
}
}
}
return runtimeConfig
}
// PopulateUsedBy populates all the UsedBy lists of the underlying fields of r,
// based on the relations between the included services, routers, and middlewares.
func (r *RuntimeConfiguration) PopulateUsedBy() {
if r == nil {
return
}
logger := log.WithoutContext()
for routerName, routerInfo := range r.Routers {
providerName := getProviderName(routerName)
if providerName == "" {
logger.WithField(log.RouterName, routerName).Error("router name is not fully qualified")
continue
}
for _, midName := range routerInfo.Router.Middlewares {
fullMidName := getQualifiedName(providerName, midName)
if _, ok := r.Middlewares[fullMidName]; !ok {
continue
}
r.Middlewares[fullMidName].UsedBy = append(r.Middlewares[fullMidName].UsedBy, routerName)
}
serviceName := getQualifiedName(providerName, routerInfo.Router.Service)
if _, ok := r.Services[serviceName]; !ok {
continue
}
r.Services[serviceName].UsedBy = append(r.Services[serviceName].UsedBy, routerName)
}
for k := range r.Services {
sort.Strings(r.Services[k].UsedBy)
}
for k := range r.Middlewares {
sort.Strings(r.Middlewares[k].UsedBy)
}
for routerName, routerInfo := range r.TCPRouters {
providerName := getProviderName(routerName)
if providerName == "" {
logger.WithField(log.RouterName, routerName).Error("tcp router name is not fully qualified")
continue
}
serviceName := getQualifiedName(providerName, routerInfo.TCPRouter.Service)
if _, ok := r.TCPServices[serviceName]; !ok {
continue
}
r.TCPServices[serviceName].UsedBy = append(r.TCPServices[serviceName].UsedBy, routerName)
}
for k := range r.TCPServices {
sort.Strings(r.TCPServices[k].UsedBy)
}
}
// RouterInfo holds information about a currently running HTTP router
type RouterInfo struct {
*Router // dynamic configuration
Err string `json:"error,omitempty"` // initialization error
}
// TCPRouterInfo holds information about a currently running TCP router
type TCPRouterInfo struct {
*TCPRouter // dynamic configuration
Err string `json:"error,omitempty"` // initialization error
}
// MiddlewareInfo holds information about a currently running middleware
type MiddlewareInfo struct {
*Middleware // dynamic configuration
Err error `json:"error,omitempty"` // initialization error
UsedBy []string `json:"usedBy,omitempty"` // list of routers and services using that middleware
}
// ServiceInfo holds information about a currently running service
type ServiceInfo struct {
*Service // dynamic configuration
Err error `json:"error,omitempty"` // initialization error
UsedBy []string `json:"usedBy,omitempty"` // list of routers using that service
statusMu sync.RWMutex
status map[string]string // keyed by server URL
}
// UpdateStatus sets the status of the server in the ServiceInfo.
// It is the responsibility of the caller to check that s is not nil.
func (s *ServiceInfo) UpdateStatus(server string, status string) {
s.statusMu.Lock()
defer s.statusMu.Unlock()
if s.status == nil {
s.status = make(map[string]string)
}
s.status[server] = status
}
// GetAllStatus returns all the statuses of all the servers in ServiceInfo.
// It is the responsibility of the caller to check that s is not nil
func (s *ServiceInfo) GetAllStatus() map[string]string {
s.statusMu.RLock()
defer s.statusMu.RUnlock()
if len(s.status) == 0 {
return nil
}
allStatus := make(map[string]string, len(s.status))
for k, v := range s.status {
allStatus[k] = v
}
return allStatus
}
// TCPServiceInfo holds information about a currently running TCP service
type TCPServiceInfo struct {
*TCPService // dynamic configuration
Err error `json:"error,omitempty"` // initialization error
UsedBy []string `json:"usedBy,omitempty"` // list of routers using that service
}
func getProviderName(elementName string) string {
parts := strings.Split(elementName, ".")
if len(parts) > 1 {
return parts[0]
}
return ""
}
func getQualifiedName(provider, elementName string) string {
parts := strings.Split(elementName, ".")
if len(parts) == 1 {
return provider + "." + elementName
}
return elementName
}

726
pkg/config/runtime_test.go Normal file
View file

@ -0,0 +1,726 @@
package config_test
import (
"testing"
"github.com/containous/traefik/pkg/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// all the Routers/Middlewares/Services are considered fully qualified
func TestPopulateUsedby(t *testing.T) {
testCases := []struct {
desc string
conf *config.RuntimeConfiguration
expected config.RuntimeConfiguration
}{
{
desc: "nil config",
conf: nil,
expected: config.RuntimeConfiguration{},
},
{
desc: "One service used by two routers",
conf: &config.RuntimeConfiguration{
Routers: map[string]*config.RouterInfo{
"myprovider.foo": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`bar.foo`)",
},
},
"myprovider.bar": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
},
},
},
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1:8085",
Weight: 1,
},
{
URL: "http://127.0.0.1:8086",
Weight: 1,
},
},
Method: "wrr",
HealthCheck: &config.HealthCheck{
Interval: "500ms",
Path: "/health",
},
},
},
},
},
},
expected: config.RuntimeConfiguration{
Routers: map[string]*config.RouterInfo{
"myprovider.foo": {},
"myprovider.bar": {},
},
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
UsedBy: []string{"myprovider.bar", "myprovider.foo"},
},
},
},
},
{
desc: "One service used by two routers, but one router with wrong rule",
conf: &config.RuntimeConfiguration{
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
Routers: map[string]*config.RouterInfo{
"myprovider.foo": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "WrongRule(`bar.foo`)",
},
},
"myprovider.bar": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
},
},
},
},
expected: config.RuntimeConfiguration{
Routers: map[string]*config.RouterInfo{
"myprovider.foo": {},
"myprovider.bar": {},
},
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
UsedBy: []string{"myprovider.bar", "myprovider.foo"},
},
},
},
},
{
desc: "Broken Service used by one Router",
conf: &config.RuntimeConfiguration{
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
Service: &config.Service{
LoadBalancer: nil,
},
},
},
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
},
},
},
},
expected: config.RuntimeConfiguration{
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {},
},
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
UsedBy: []string{"myprovider.bar"},
},
},
},
},
{
desc: "2 different Services each used by a disctinct router.",
conf: &config.RuntimeConfiguration{
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1:8085",
Weight: 1,
},
{
URL: "http://127.0.0.1:8086",
Weight: 1,
},
},
Method: "wrr",
HealthCheck: &config.HealthCheck{
Interval: "500ms",
Path: "/health",
},
},
},
},
"myprovider.bar-service": {
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1:8087",
Weight: 1,
},
{
URL: "http://127.0.0.1:8088",
Weight: 1,
},
},
Method: "wrr",
HealthCheck: &config.HealthCheck{
Interval: "500ms",
Path: "/health",
},
},
},
},
},
Routers: map[string]*config.RouterInfo{
"myprovider.foo": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`bar.foo`)",
},
},
"myprovider.bar": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.bar-service",
Rule: "Host(`foo.bar`)",
},
},
},
},
expected: config.RuntimeConfiguration{
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {},
"myprovider.foo": {},
},
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
UsedBy: []string{"myprovider.foo"},
},
"myprovider.bar-service": {
UsedBy: []string{"myprovider.bar"},
},
},
},
},
{
desc: "2 middlewares both used by 2 Routers",
conf: &config.RuntimeConfiguration{
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
Middlewares: map[string]*config.MiddlewareInfo{
"myprovider.auth": {
Middleware: &config.Middleware{
BasicAuth: &config.BasicAuth{
Users: []string{"admin:admin"},
},
},
},
"myprovider.addPrefixTest": {
Middleware: &config.Middleware{
AddPrefix: &config.AddPrefix{
Prefix: "/toto",
},
},
},
},
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"auth", "addPrefixTest"},
},
},
"myprovider.test": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar.other`)",
Middlewares: []string{"addPrefixTest", "auth"},
},
},
},
},
expected: config.RuntimeConfiguration{
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {},
"myprovider.test": {},
},
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
UsedBy: []string{"myprovider.bar", "myprovider.test"},
},
},
Middlewares: map[string]*config.MiddlewareInfo{
"myprovider.auth": {
UsedBy: []string{"myprovider.bar", "myprovider.test"},
},
"myprovider.addPrefixTest": {
UsedBy: []string{"myprovider.bar", "myprovider.test"},
},
},
},
},
{
desc: "Unknown middleware is not used by the Router",
conf: &config.RuntimeConfiguration{
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
Middlewares: map[string]*config.MiddlewareInfo{
"myprovider.auth": {
Middleware: &config.Middleware{
BasicAuth: &config.BasicAuth{
Users: []string{"admin:admin"},
},
},
},
},
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"unknown"},
},
},
},
},
expected: config.RuntimeConfiguration{
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
UsedBy: []string{"myprovider.bar"},
},
},
},
},
{
desc: "Broken middleware is used by Router",
conf: &config.RuntimeConfiguration{
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
Middlewares: map[string]*config.MiddlewareInfo{
"myprovider.auth": {
Middleware: &config.Middleware{
BasicAuth: &config.BasicAuth{
Users: []string{"badConf"},
},
},
},
},
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"myprovider.auth"},
},
},
},
},
expected: config.RuntimeConfiguration{
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {},
},
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
UsedBy: []string{"myprovider.bar"},
},
},
Middlewares: map[string]*config.MiddlewareInfo{
"myprovider.auth": {
UsedBy: []string{"myprovider.bar"},
},
},
},
},
{
desc: "2 middlewares from 2 disctinct providers both used by 2 Routers",
conf: &config.RuntimeConfiguration{
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
Middlewares: map[string]*config.MiddlewareInfo{
"myprovider.auth": {
Middleware: &config.Middleware{
BasicAuth: &config.BasicAuth{
Users: []string{"admin:admin"},
},
},
},
"myprovider.addPrefixTest": {
Middleware: &config.Middleware{
AddPrefix: &config.AddPrefix{
Prefix: "/titi",
},
},
},
"anotherprovider.addPrefixTest": {
Middleware: &config.Middleware{
AddPrefix: &config.AddPrefix{
Prefix: "/toto",
},
},
},
},
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"auth", "anotherprovider.addPrefixTest"},
},
},
"myprovider.test": {
Router: &config.Router{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar.other`)",
Middlewares: []string{"addPrefixTest", "auth"},
},
},
},
},
expected: config.RuntimeConfiguration{
Routers: map[string]*config.RouterInfo{
"myprovider.bar": {},
"myprovider.test": {},
},
Services: map[string]*config.ServiceInfo{
"myprovider.foo-service": {
UsedBy: []string{"myprovider.bar", "myprovider.test"},
},
},
Middlewares: map[string]*config.MiddlewareInfo{
"myprovider.auth": {
UsedBy: []string{"myprovider.bar", "myprovider.test"},
},
"myprovider.addPrefixTest": {
UsedBy: []string{"myprovider.test"},
},
"anotherprovider.addPrefixTest": {
UsedBy: []string{"myprovider.bar"},
},
},
},
},
// TCP tests from hereon
{
desc: "TCP, One service used by two routers",
conf: &config.RuntimeConfiguration{
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.foo": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`bar.foo`)",
},
},
"myprovider.bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
},
},
},
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.foo-service": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1",
Port: "8085",
Weight: 1,
},
{
Address: "127.0.0.1",
Port: "8086",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
},
expected: config.RuntimeConfiguration{
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.foo": {},
"myprovider.bar": {},
},
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.foo-service": {
UsedBy: []string{"myprovider.bar", "myprovider.foo"},
},
},
},
},
{
desc: "TCP, One service used by two routers, but one router with wrong rule",
conf: &config.RuntimeConfiguration{
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.foo-service": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.foo": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "WrongRule(`bar.foo`)",
},
},
"myprovider.bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
},
},
},
},
expected: config.RuntimeConfiguration{
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.foo": {},
"myprovider.bar": {},
},
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.foo-service": {
UsedBy: []string{"myprovider.bar", "myprovider.foo"},
},
},
},
},
{
desc: "TCP, Broken Service used by one Router",
conf: &config.RuntimeConfiguration{
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.foo-service": {
TCPService: &config.TCPService{
LoadBalancer: nil,
},
},
},
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`foo.bar`)",
},
},
},
},
expected: config.RuntimeConfiguration{
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.bar": {},
},
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.foo-service": {
UsedBy: []string{"myprovider.bar"},
},
},
},
},
{
desc: "TCP, 2 different Services each used by a disctinct router.",
conf: &config.RuntimeConfiguration{
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.foo-service": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1",
Port: "8085",
Weight: 1,
},
{
Address: "127.0.0.1",
Port: "8086",
Weight: 1,
},
},
Method: "wrr",
},
},
},
"myprovider.bar-service": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1",
Port: "8087",
Weight: 1,
},
{
Address: "127.0.0.1",
Port: "8088",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.foo": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.foo-service",
Rule: "Host(`bar.foo`)",
},
},
"myprovider.bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.bar-service",
Rule: "Host(`foo.bar`)",
},
},
},
},
expected: config.RuntimeConfiguration{
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.bar": {},
"myprovider.foo": {},
},
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.foo-service": {
UsedBy: []string{"myprovider.foo"},
},
"myprovider.bar-service": {
UsedBy: []string{"myprovider.bar"},
},
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
runtimeConf := test.conf
runtimeConf.PopulateUsedBy()
for key, expectedService := range test.expected.Services {
require.NotNil(t, runtimeConf.Services[key])
assert.Equal(t, expectedService.UsedBy, runtimeConf.Services[key].UsedBy)
}
for key, expectedMiddleware := range test.expected.Middlewares {
require.NotNil(t, runtimeConf.Middlewares[key])
assert.Equal(t, expectedMiddleware.UsedBy, runtimeConf.Middlewares[key].UsedBy)
}
for key, expectedTCPService := range test.expected.TCPServices {
require.NotNil(t, runtimeConf.TCPServices[key])
assert.Equal(t, expectedTCPService.UsedBy, runtimeConf.TCPServices[key].UsedBy)
}
})
}
}