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

View file

@ -6,79 +6,70 @@ import (
"github.com/containous/mux"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/config/static"
"github.com/containous/traefik/pkg/log"
"github.com/containous/traefik/pkg/safe"
"github.com/containous/traefik/pkg/types"
"github.com/containous/traefik/pkg/version"
assetfs "github.com/elazarl/go-bindata-assetfs"
thoasstats "github.com/thoas/stats"
"github.com/unrolled/render"
)
// ResourceIdentifier a resource identifier
type ResourceIdentifier struct {
ID string `json:"id"`
Path string `json:"path"`
}
// ProviderRepresentation a provider with resource identifiers
type ProviderRepresentation struct {
Routers []ResourceIdentifier `json:"routers,omitempty"`
Middlewares []ResourceIdentifier `json:"middlewares,omitempty"`
Services []ResourceIdentifier `json:"services,omitempty"`
}
// RouterRepresentation extended version of a router configuration with an ID
type RouterRepresentation struct {
*config.Router
ID string `json:"id"`
}
// MiddlewareRepresentation extended version of a middleware configuration with an ID
type MiddlewareRepresentation struct {
*config.Middleware
ID string `json:"id"`
}
// ServiceRepresentation extended version of a service configuration with an ID
type ServiceRepresentation struct {
*config.Service
ID string `json:"id"`
}
// Handler expose api routes
type Handler struct {
EntryPoint string
Dashboard bool
Debug bool
CurrentConfigurations *safe.Safe
Statistics *types.Statistics
Stats *thoasstats.Stats
// StatsRecorder *middlewares.StatsRecorder // FIXME stats
DashboardAssets *assetfs.AssetFS
}
var templateRenderer jsonRenderer = render.New(render.Options{Directory: "nowhere"})
type jsonRenderer interface {
JSON(w io.Writer, status int, v interface{}) error
}
type serviceInfoRepresentation struct {
*config.ServiceInfo
ServerStatus map[string]string `json:"serverStatus,omitempty"`
}
// RunTimeRepresentation is the configuration information exposed by the API handler.
type RunTimeRepresentation struct {
Routers map[string]*config.RouterInfo `json:"routers,omitempty"`
Middlewares map[string]*config.MiddlewareInfo `json:"middlewares,omitempty"`
Services map[string]*serviceInfoRepresentation `json:"services,omitempty"`
TCPRouters map[string]*config.TCPRouterInfo `json:"tcpRouters,omitempty"`
TCPServices map[string]*config.TCPServiceInfo `json:"tcpServices,omitempty"`
}
// Handler serves the configuration and status of Traefik on API endpoints.
type Handler struct {
dashboard bool
debug bool
// runtimeConfiguration is the data set used to create all the data representations exposed by the API.
runtimeConfiguration *config.RuntimeConfiguration
statistics *types.Statistics
// stats *thoasstats.Stats // FIXME stats
// StatsRecorder *middlewares.StatsRecorder // FIXME stats
dashboardAssets *assetfs.AssetFS
}
// New returns a Handler defined by staticConfig, and if provided, by runtimeConfig.
// It finishes populating the information provided in the runtimeConfig.
func New(staticConfig static.Configuration, runtimeConfig *config.RuntimeConfiguration) *Handler {
rConfig := runtimeConfig
if rConfig == nil {
rConfig = &config.RuntimeConfiguration{}
}
return &Handler{
dashboard: staticConfig.API.Dashboard,
statistics: staticConfig.API.Statistics,
dashboardAssets: staticConfig.API.DashboardAssets,
runtimeConfiguration: rConfig,
debug: staticConfig.Global.Debug,
}
}
// Append add api routes on a router
func (h Handler) Append(router *mux.Router) {
if h.Debug {
if h.debug {
DebugHandler{}.Append(router)
}
router.Methods(http.MethodGet).Path("/api/rawdata").HandlerFunc(h.getRawData)
router.Methods(http.MethodGet).Path("/api/providers").HandlerFunc(h.getProvidersHandler)
router.Methods(http.MethodGet).Path("/api/providers/{provider}").HandlerFunc(h.getProviderHandler)
router.Methods(http.MethodGet).Path("/api/providers/{provider}/routers").HandlerFunc(h.getRoutersHandler)
router.Methods(http.MethodGet).Path("/api/providers/{provider}/routers/{router}").HandlerFunc(h.getRouterHandler)
router.Methods(http.MethodGet).Path("/api/providers/{provider}/middlewares").HandlerFunc(h.getMiddlewaresHandler)
router.Methods(http.MethodGet).Path("/api/providers/{provider}/middlewares/{middleware}").HandlerFunc(h.getMiddlewareHandler)
router.Methods(http.MethodGet).Path("/api/providers/{provider}/services").HandlerFunc(h.getServicesHandler)
router.Methods(http.MethodGet).Path("/api/providers/{provider}/services/{service}").HandlerFunc(h.getServiceHandler)
router.Methods(http.MethodGet).Path("/api/rawdata").HandlerFunc(h.getRuntimeConfiguration)
// FIXME stats
// health route
@ -86,268 +77,29 @@ func (h Handler) Append(router *mux.Router) {
version.Handler{}.Append(router)
if h.Dashboard {
DashboardHandler{Assets: h.DashboardAssets}.Append(router)
if h.dashboard {
DashboardHandler{Assets: h.dashboardAssets}.Append(router)
}
}
func (h Handler) getRawData(rw http.ResponseWriter, request *http.Request) {
if h.CurrentConfigurations != nil {
currentConfigurations, ok := h.CurrentConfigurations.Get().(config.Configurations)
if !ok {
rw.WriteHeader(http.StatusOK)
return
}
err := templateRenderer.JSON(rw, http.StatusOK, currentConfigurations)
if err != nil {
log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
func (h Handler) getRuntimeConfiguration(rw http.ResponseWriter, request *http.Request) {
siRepr := make(map[string]*serviceInfoRepresentation, len(h.runtimeConfiguration.Services))
for k, v := range h.runtimeConfiguration.Services {
siRepr[k] = &serviceInfoRepresentation{
ServiceInfo: v,
ServerStatus: v.GetAllStatus(),
}
}
}
func (h Handler) getProvidersHandler(rw http.ResponseWriter, request *http.Request) {
// FIXME handle currentConfiguration
if h.CurrentConfigurations != nil {
currentConfigurations, ok := h.CurrentConfigurations.Get().(config.Configurations)
if !ok {
rw.WriteHeader(http.StatusOK)
return
}
var providers []ResourceIdentifier
for name := range currentConfigurations {
providers = append(providers, ResourceIdentifier{
ID: name,
Path: "/api/providers/" + name,
})
}
err := templateRenderer.JSON(rw, http.StatusOK, providers)
if err != nil {
log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
}
func (h Handler) getProviderHandler(rw http.ResponseWriter, request *http.Request) {
providerID := mux.Vars(request)["provider"]
currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations)
provider, ok := currentConfigurations[providerID]
if !ok {
http.NotFound(rw, request)
return
rtRepr := RunTimeRepresentation{
Routers: h.runtimeConfiguration.Routers,
Middlewares: h.runtimeConfiguration.Middlewares,
Services: siRepr,
TCPRouters: h.runtimeConfiguration.TCPRouters,
TCPServices: h.runtimeConfiguration.TCPServices,
}
if provider.HTTP == nil {
http.NotFound(rw, request)
return
}
var routers []ResourceIdentifier
for name := range provider.HTTP.Routers {
routers = append(routers, ResourceIdentifier{
ID: name,
Path: "/api/providers/" + providerID + "/routers",
})
}
var services []ResourceIdentifier
for name := range provider.HTTP.Services {
services = append(services, ResourceIdentifier{
ID: name,
Path: "/api/providers/" + providerID + "/services",
})
}
var middlewares []ResourceIdentifier
for name := range provider.HTTP.Middlewares {
middlewares = append(middlewares, ResourceIdentifier{
ID: name,
Path: "/api/providers/" + providerID + "/middlewares",
})
}
providers := ProviderRepresentation{Routers: routers, Middlewares: middlewares, Services: services}
err := templateRenderer.JSON(rw, http.StatusOK, providers)
if err != nil {
log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
func (h Handler) getRoutersHandler(rw http.ResponseWriter, request *http.Request) {
providerID := mux.Vars(request)["provider"]
currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations)
provider, ok := currentConfigurations[providerID]
if !ok {
http.NotFound(rw, request)
return
}
if provider.HTTP == nil {
http.NotFound(rw, request)
return
}
var routers []RouterRepresentation
for name, router := range provider.HTTP.Routers {
routers = append(routers, RouterRepresentation{Router: router, ID: name})
}
err := templateRenderer.JSON(rw, http.StatusOK, routers)
if err != nil {
log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
func (h Handler) getRouterHandler(rw http.ResponseWriter, request *http.Request) {
providerID := mux.Vars(request)["provider"]
routerID := mux.Vars(request)["router"]
currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations)
provider, ok := currentConfigurations[providerID]
if !ok {
http.NotFound(rw, request)
return
}
if provider.HTTP == nil {
http.NotFound(rw, request)
return
}
router, ok := provider.HTTP.Routers[routerID]
if !ok {
http.NotFound(rw, request)
return
}
err := templateRenderer.JSON(rw, http.StatusOK, router)
if err != nil {
log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
func (h Handler) getMiddlewaresHandler(rw http.ResponseWriter, request *http.Request) {
providerID := mux.Vars(request)["provider"]
currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations)
provider, ok := currentConfigurations[providerID]
if !ok {
http.NotFound(rw, request)
return
}
if provider.HTTP == nil {
http.NotFound(rw, request)
return
}
var middlewares []MiddlewareRepresentation
for name, middleware := range provider.HTTP.Middlewares {
middlewares = append(middlewares, MiddlewareRepresentation{Middleware: middleware, ID: name})
}
err := templateRenderer.JSON(rw, http.StatusOK, middlewares)
if err != nil {
log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
func (h Handler) getMiddlewareHandler(rw http.ResponseWriter, request *http.Request) {
providerID := mux.Vars(request)["provider"]
middlewareID := mux.Vars(request)["middleware"]
currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations)
provider, ok := currentConfigurations[providerID]
if !ok {
http.NotFound(rw, request)
return
}
if provider.HTTP == nil {
http.NotFound(rw, request)
return
}
middleware, ok := provider.HTTP.Middlewares[middlewareID]
if !ok {
http.NotFound(rw, request)
return
}
err := templateRenderer.JSON(rw, http.StatusOK, middleware)
if err != nil {
log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
func (h Handler) getServicesHandler(rw http.ResponseWriter, request *http.Request) {
providerID := mux.Vars(request)["provider"]
currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations)
provider, ok := currentConfigurations[providerID]
if !ok {
http.NotFound(rw, request)
return
}
if provider.HTTP == nil {
http.NotFound(rw, request)
return
}
var services []ServiceRepresentation
for name, service := range provider.HTTP.Services {
services = append(services, ServiceRepresentation{Service: service, ID: name})
}
err := templateRenderer.JSON(rw, http.StatusOK, services)
if err != nil {
log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
func (h Handler) getServiceHandler(rw http.ResponseWriter, request *http.Request) {
providerID := mux.Vars(request)["provider"]
serviceID := mux.Vars(request)["service"]
currentConfigurations := h.CurrentConfigurations.Get().(config.Configurations)
provider, ok := currentConfigurations[providerID]
if !ok {
http.NotFound(rw, request)
return
}
if provider.HTTP == nil {
http.NotFound(rw, request)
return
}
service, ok := provider.HTTP.Services[serviceID]
if !ok {
http.NotFound(rw, request)
return
}
err := templateRenderer.JSON(rw, http.StatusOK, service)
err := templateRenderer.JSON(rw, http.StatusOK, rtRepr)
if err != nil {
log.FromContext(request.Context()).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)

View file

@ -1,6 +1,8 @@
package api
import (
"encoding/json"
"flag"
"io/ioutil"
"net/http"
"net/http/httptest"
@ -8,188 +10,122 @@ import (
"github.com/containous/mux"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/safe"
"github.com/containous/traefik/pkg/config/static"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var updateExpected = flag.Bool("update_expected", false, "Update expected files in testdata")
func TestHandler_Configuration(t *testing.T) {
type expected struct {
statusCode int
body string
json string
}
testCases := []struct {
desc string
path string
configuration config.Configurations
expected expected
desc string
path string
conf config.RuntimeConfiguration
expected expected
}{
{
desc: "Get all the providers",
path: "/api/providers",
configuration: config.Configurations{
"foo": {
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"bar": {EntryPoints: []string{"foo", "bar"}},
},
},
},
},
expected: expected{statusCode: http.StatusOK, body: `[{"id":"foo","path":"/api/providers/foo"}]`},
},
{
desc: "Get a provider",
path: "/api/providers/foo",
configuration: config.Configurations{
"foo": {
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"bar": {EntryPoints: []string{"foo", "bar"}},
},
Middlewares: map[string]*config.Middleware{
"bar": {
AddPrefix: &config.AddPrefix{Prefix: "bar"},
},
},
Services: map[string]*config.Service{
"foo": {
LoadBalancer: &config.LoadBalancerService{
Method: "wrr",
desc: "Get rawdata",
path: "/api/rawdata",
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",
},
},
},
},
},
expected: expected{statusCode: http.StatusOK, body: `{"routers":[{"id":"bar","path":"/api/providers/foo/routers"}],"middlewares":[{"id":"bar","path":"/api/providers/foo/middlewares"}],"services":[{"id":"foo","path":"/api/providers/foo/services"}]}`},
},
{
desc: "Provider not found",
path: "/api/providers/foo",
configuration: config.Configurations{},
expected: expected{statusCode: http.StatusNotFound, body: "404 page not found\n"},
},
{
desc: "Get all routers",
path: "/api/providers/foo/routers",
configuration: config.Configurations{
"foo": {
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"bar": {EntryPoints: []string{"foo", "bar"}},
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",
},
},
},
},
},
expected: expected{statusCode: http.StatusOK, body: `[{"entryPoints":["foo","bar"],"id":"bar"}]`},
},
{
desc: "Get a router",
path: "/api/providers/foo/routers/bar",
configuration: config.Configurations{
"foo": {
HTTP: &config.HTTPConfiguration{
Routers: map[string]*config.Router{
"bar": {EntryPoints: []string{"foo", "bar"}},
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: expected{statusCode: http.StatusOK, body: `{"entryPoints":["foo","bar"]}`},
},
{
desc: "Router not found",
path: "/api/providers/foo/routers/bar",
configuration: config.Configurations{
"foo": {},
},
expected: expected{statusCode: http.StatusNotFound, body: "404 page not found\n"},
},
{
desc: "Get all services",
path: "/api/providers/foo/services",
configuration: config.Configurations{
"foo": {
HTTP: &config.HTTPConfiguration{
Services: map[string]*config.Service{
"foo": {
LoadBalancer: &config.LoadBalancerService{
Method: "wrr",
TCPServices: map[string]*config.TCPServiceInfo{
"myprovider.tcpfoo-service": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
},
expected: expected{statusCode: http.StatusOK, body: `[{"loadbalancer":{"method":"wrr","passHostHeader":false},"id":"foo"}]`},
},
{
desc: "Get a service",
path: "/api/providers/foo/services/foo",
configuration: config.Configurations{
"foo": {
HTTP: &config.HTTPConfiguration{
Services: map[string]*config.Service{
"foo": {
LoadBalancer: &config.LoadBalancerService{
Method: "wrr",
},
},
TCPRouters: map[string]*config.TCPRouterInfo{
"myprovider.tcpbar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.tcpfoo-service",
Rule: "HostSNI(`foo.bar`)",
},
},
"myprovider.tcptest": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "myprovider.tcpfoo-service",
Rule: "HostSNI(`foo.bar.other`)",
},
},
},
},
expected: expected{statusCode: http.StatusOK, body: `{"loadbalancer":{"method":"wrr","passHostHeader":false}}`},
},
{
desc: "Service not found",
path: "/api/providers/foo/services/bar",
configuration: config.Configurations{
"foo": {},
expected: expected{
statusCode: http.StatusOK,
json: "testdata/getrawdata.json",
},
expected: expected{statusCode: http.StatusNotFound, body: "404 page not found\n"},
},
{
desc: "Get all middlewares",
path: "/api/providers/foo/middlewares",
configuration: config.Configurations{
"foo": {
HTTP: &config.HTTPConfiguration{
Middlewares: map[string]*config.Middleware{
"bar": {
AddPrefix: &config.AddPrefix{Prefix: "bar"},
},
},
},
},
},
expected: expected{statusCode: http.StatusOK, body: `[{"addPrefix":{"prefix":"bar"},"id":"bar"}]`},
},
{
desc: "Get a middleware",
path: "/api/providers/foo/middlewares/bar",
configuration: config.Configurations{
"foo": {
HTTP: &config.HTTPConfiguration{
Middlewares: map[string]*config.Middleware{
"bar": {
AddPrefix: &config.AddPrefix{Prefix: "bar"},
},
},
},
},
},
expected: expected{statusCode: http.StatusOK, body: `{"addPrefix":{"prefix":"bar"}}`},
},
{
desc: "Middleware not found",
path: "/api/providers/foo/middlewares/bar",
configuration: config.Configurations{
"foo": {},
},
expected: expected{statusCode: http.StatusNotFound, body: "404 page not found\n"},
},
}
@ -198,15 +134,11 @@ func TestHandler_Configuration(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
currentConfiguration := &safe.Safe{}
currentConfiguration.Set(test.configuration)
handler := Handler{
CurrentConfigurations: currentConfiguration,
}
rtConf := &test.conf
handler := New(static.Configuration{API: &static.API{}, Global: &static.Global{}}, rtConf)
router := mux.NewRouter()
handler.Append(router)
rtConf.PopulateUsedBy()
server := httptest.NewServer(router)
@ -215,12 +147,30 @@ func TestHandler_Configuration(t *testing.T) {
assert.Equal(t, test.expected.statusCode, resp.StatusCode)
content, err := ioutil.ReadAll(resp.Body)
contents, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
assert.Equal(t, test.expected.body, string(content))
if test.expected.json == "" {
return
}
if *updateExpected {
var rtRepr RunTimeRepresentation
err := json.Unmarshal(contents, &rtRepr)
require.NoError(t, err)
newJSON, err := json.MarshalIndent(rtRepr, "", "\t")
require.NoError(t, err)
err = ioutil.WriteFile(test.expected.json, newJSON, 0644)
require.NoError(t, err)
}
data, err := ioutil.ReadFile(test.expected.json)
require.NoError(t, err)
assert.JSONEq(t, string(data), string(contents))
})
}
}

106
pkg/api/testdata/getrawdata.json vendored Normal file
View file

@ -0,0 +1,106 @@
{
"routers": {
"myprovider.bar": {
"entryPoints": [
"web"
],
"middlewares": [
"auth",
"anotherprovider.addPrefixTest"
],
"service": "myprovider.foo-service",
"rule": "Host(`foo.bar`)"
},
"myprovider.test": {
"entryPoints": [
"web"
],
"middlewares": [
"addPrefixTest",
"auth"
],
"service": "myprovider.foo-service",
"rule": "Host(`foo.bar.other`)"
}
},
"middlewares": {
"anotherprovider.addPrefixTest": {
"addPrefix": {
"prefix": "/toto"
},
"usedBy": [
"myprovider.bar"
]
},
"myprovider.addPrefixTest": {
"addPrefix": {
"prefix": "/titi"
},
"usedBy": [
"myprovider.test"
]
},
"myprovider.auth": {
"basicAuth": {
"users": [
"admin:admin"
]
},
"usedBy": [
"myprovider.bar",
"myprovider.test"
]
}
},
"services": {
"myprovider.foo-service": {
"loadbalancer": {
"servers": [
{
"url": "http://127.0.0.1",
"weight": 1
}
],
"method": "wrr",
"passHostHeader": false
},
"usedBy": [
"myprovider.bar",
"myprovider.test"
]
}
},
"tcpRouters": {
"myprovider.tcpbar": {
"entryPoints": [
"web"
],
"service": "myprovider.tcpfoo-service",
"rule": "HostSNI(`foo.bar`)"
},
"myprovider.tcptest": {
"entryPoints": [
"web"
],
"service": "myprovider.tcpfoo-service",
"rule": "HostSNI(`foo.bar.other`)"
}
},
"tcpServices": {
"myprovider.tcpfoo-service": {
"loadbalancer": {
"servers": [
{
"address": "127.0.0.1",
"weight": 1
}
],
"method": "wrr"
},
"usedBy": [
"myprovider.tcpbar",
"myprovider.tcptest"
]
}
}
}

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)
}
})
}
}

View file

@ -10,12 +10,18 @@ import (
"sync"
"time"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/log"
"github.com/containous/traefik/pkg/safe"
"github.com/go-kit/kit/metrics"
"github.com/vulcand/oxy/roundrobin"
)
const (
serverUp = "UP"
serverDown = "DOWN"
)
var singleton *HealthCheck
var once sync.Once
@ -221,3 +227,38 @@ func checkHealth(serverURL *url.URL, backend *BackendConfig) error {
return nil
}
// NewLBStatusUpdater returns a new LbStatusUpdater
func NewLBStatusUpdater(bh BalancerHandler, svinfo *config.ServiceInfo) *LbStatusUpdater {
return &LbStatusUpdater{
BalancerHandler: bh,
serviceInfo: svinfo,
}
}
// LbStatusUpdater wraps a BalancerHandler and a ServiceInfo,
// so it can keep track of the status of a server in the ServiceInfo.
type LbStatusUpdater struct {
BalancerHandler
serviceInfo *config.ServiceInfo // can be nil
}
// RemoveServer removes the given server from the BalancerHandler,
// and updates the status of the server to "DOWN".
func (lb *LbStatusUpdater) RemoveServer(u *url.URL) error {
err := lb.BalancerHandler.RemoveServer(u)
if err == nil && lb.serviceInfo != nil {
lb.serviceInfo.UpdateStatus(u.String(), serverDown)
}
return err
}
// UpsertServer adds the given server to the BalancerHandler,
// and updates the status of the server to "UP".
func (lb *LbStatusUpdater) UpsertServer(u *url.URL, options ...roundrobin.ServerOption) error {
err := lb.BalancerHandler.UpsertServer(u, options...)
if err == nil && lb.serviceInfo != nil {
lb.serviceInfo.UpdateStatus(u.String(), serverUp)
}
return err
}

View file

@ -9,6 +9,7 @@ import (
"testing"
"time"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/testhelpers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -367,6 +368,8 @@ type testLoadBalancer struct {
numRemovedServers int
numUpsertedServers int
servers []*url.URL
// options is just to make sure that LBStatusUpdater forwards options on Upsert to its BalancerHandler
options []roundrobin.ServerOption
}
func (lb *testLoadBalancer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
@ -386,6 +389,7 @@ func (lb *testLoadBalancer) UpsertServer(u *url.URL, options ...roundrobin.Serve
defer lb.Unlock()
lb.numUpsertedServers++
lb.servers = append(lb.servers, u)
lb.options = append(lb.options, options...)
return nil
}
@ -393,14 +397,23 @@ func (lb *testLoadBalancer) Servers() []*url.URL {
return lb.servers
}
func (lb *testLoadBalancer) Options() []roundrobin.ServerOption {
return lb.options
}
func (lb *testLoadBalancer) removeServer(u *url.URL) {
var i int
var serverURL *url.URL
found := false
for i, serverURL = range lb.servers {
if *serverURL == *u {
found = true
break
}
}
if !found {
return
}
lb.servers = append(lb.servers[:i], lb.servers[i+1:]...)
}
@ -427,3 +440,32 @@ func (th *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
th.done()
}
}
func TestLBStatusUpdater(t *testing.T) {
lb := &testLoadBalancer{RWMutex: &sync.RWMutex{}}
svInfo := &config.ServiceInfo{}
lbsu := NewLBStatusUpdater(lb, svInfo)
newServer, err := url.Parse("http://foo.com")
assert.Nil(t, err)
err = lbsu.UpsertServer(newServer, roundrobin.Weight(1))
assert.Nil(t, err)
assert.Equal(t, len(lbsu.Servers()), 1)
assert.Equal(t, len(lbsu.BalancerHandler.(*testLoadBalancer).Options()), 1)
statuses := svInfo.GetAllStatus()
assert.Equal(t, len(statuses), 1)
for k, v := range statuses {
assert.Equal(t, k, newServer.String())
assert.Equal(t, v, serverUp)
break
}
err = lbsu.RemoveServer(newServer)
assert.Nil(t, err)
assert.Equal(t, len(lbsu.Servers()), 0)
statuses = svInfo.GetAllStatus()
assert.Equal(t, len(statuses), 1)
for k, v := range statuses {
assert.Equal(t, k, newServer.String())
assert.Equal(t, v, serverDown)
break
}
}

View file

@ -8,13 +8,13 @@ import (
)
// NewBuilder creates a builder.
func NewBuilder(configs map[string]*config.Middleware) *Builder {
func NewBuilder(configs map[string]*config.MiddlewareInfo) *Builder {
return &Builder{configs: configs}
}
// Builder holds builder configuration.
type Builder struct {
configs map[string]*config.Middleware
configs map[string]*config.MiddlewareInfo
}
// Build Builds the response modifier.

View file

@ -166,7 +166,12 @@ func TestBuilderBuild(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
builder := NewBuilder(test.conf)
rtConf := config.NewRuntimeConfig(config.Configuration{
HTTP: &config.HTTPConfiguration{
Middlewares: test.conf,
},
})
builder := NewBuilder(rtConf.Middlewares)
rm := builder.Build(context.Background(), test.middlewares)

View file

@ -39,7 +39,7 @@ const (
// Builder the middleware builder
type Builder struct {
configs map[string]*config.Middleware
configs map[string]*config.MiddlewareInfo
serviceBuilder serviceBuilder
}
@ -48,7 +48,7 @@ type serviceBuilder interface {
}
// NewBuilder creates a new Builder
func NewBuilder(configs map[string]*config.Middleware, serviceBuilder serviceBuilder) *Builder {
func NewBuilder(configs map[string]*config.MiddlewareInfo, serviceBuilder serviceBuilder) *Builder {
return &Builder{configs: configs, serviceBuilder: serviceBuilder}
}
@ -60,20 +60,29 @@ func (b *Builder) BuildChain(ctx context.Context, middlewares []string) *alice.C
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
constructorContext := internal.AddProviderInContext(ctx, middlewareName)
if _, ok := b.configs[middlewareName]; !ok {
if midInf, ok := b.configs[middlewareName]; !ok || midInf.Middleware == nil {
return nil, fmt.Errorf("middleware %q does not exist", middlewareName)
}
var err error
if constructorContext, err = checkRecursion(constructorContext, middlewareName); err != nil {
b.configs[middlewareName].Err = err
return nil, err
}
constructor, err := b.buildConstructor(constructorContext, middlewareName, *b.configs[middlewareName])
constructor, err := b.buildConstructor(constructorContext, middlewareName)
if err != nil {
return nil, fmt.Errorf("error during instanciation of %s: %v", middlewareName, err)
b.configs[middlewareName].Err = err
return nil, err
}
return constructor(next)
handler, err := constructor(next)
if err != nil {
b.configs[middlewareName].Err = err
return nil, err
}
return handler, nil
})
}
return &chain
@ -90,7 +99,9 @@ func checkRecursion(ctx context.Context, middlewareName string) (context.Context
return context.WithValue(ctx, middlewareStackKey, append(currentStack, middlewareName)), nil
}
func (b *Builder) buildConstructor(ctx context.Context, middlewareName string, config config.Middleware) (alice.Constructor, error) {
// it is the responsibility of the caller to make sure that b.configs[middlewareName].Middleware exists
func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (alice.Constructor, error) {
config := b.configs[middlewareName]
var middleware alice.Constructor
badConf := errors.New("cannot create middleware: multi-types middleware not supported, consider declaring two different pieces of middleware instead")

View file

@ -14,7 +14,7 @@ import (
)
func TestBuilder_BuildChainNilConfig(t *testing.T) {
testConfig := map[string]*config.Middleware{
testConfig := map[string]*config.MiddlewareInfo{
"empty": {},
}
middlewaresBuilder := NewBuilder(testConfig, nil)
@ -25,7 +25,7 @@ func TestBuilder_BuildChainNilConfig(t *testing.T) {
}
func TestBuilder_BuildChainNonExistentChain(t *testing.T) {
testConfig := map[string]*config.Middleware{
testConfig := map[string]*config.MiddlewareInfo{
"foobar": {},
}
middlewaresBuilder := NewBuilder(testConfig, nil)
@ -264,7 +264,12 @@ func TestBuilder_BuildChainWithContext(t *testing.T) {
ctx = internal.AddProviderInContext(ctx, test.contextProvider+".foobar")
}
builder := NewBuilder(test.configuration, nil)
rtConf := config.NewRuntimeConfig(config.Configuration{
HTTP: &config.HTTPConfiguration{
Middlewares: test.configuration,
},
})
builder := NewBuilder(rtConf.Middlewares, nil)
result := builder.BuildChain(ctx, test.buildChain)
@ -310,7 +315,12 @@ func TestBuilder_buildConstructor(t *testing.T) {
},
}
middlewaresBuilder := NewBuilder(testConfig, nil)
rtConf := config.NewRuntimeConfig(config.Configuration{
HTTP: &config.HTTPConfiguration{
Middlewares: testConfig,
},
})
middlewaresBuilder := NewBuilder(rtConf.Middlewares, nil)
testCases := []struct {
desc string
@ -344,7 +354,8 @@ func TestBuilder_buildConstructor(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
constructor, err := middlewaresBuilder.buildConstructor(context.Background(), test.middlewareID, *testConfig[test.middlewareID])
constructor, err := middlewaresBuilder.buildConstructor(context.Background(), test.middlewareID)
require.NoError(t, err)
middleware, err2 := constructor(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))

View file

@ -6,10 +6,10 @@ import (
"github.com/containous/alice"
"github.com/containous/mux"
"github.com/containous/traefik/pkg/api"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/config/static"
"github.com/containous/traefik/pkg/log"
"github.com/containous/traefik/pkg/metrics"
"github.com/containous/traefik/pkg/safe"
"github.com/containous/traefik/pkg/types"
)
@ -19,7 +19,8 @@ type chainBuilder interface {
}
// NewRouteAppenderAggregator Creates a new RouteAppenderAggregator
func NewRouteAppenderAggregator(ctx context.Context, chainBuilder chainBuilder, conf static.Configuration, entryPointName string, currentConfiguration *safe.Safe) *RouteAppenderAggregator {
func NewRouteAppenderAggregator(ctx context.Context, chainBuilder chainBuilder, conf static.Configuration,
entryPointName string, runtimeConfiguration *config.RuntimeConfiguration) *RouteAppenderAggregator {
aggregator := &RouteAppenderAggregator{}
if conf.Providers != nil && conf.Providers.Rest != nil {
@ -29,17 +30,9 @@ func NewRouteAppenderAggregator(ctx context.Context, chainBuilder chainBuilder,
if conf.API != nil && conf.API.EntryPoint == entryPointName {
chain := chainBuilder.BuildChain(ctx, conf.API.Middlewares)
aggregator.AddAppender(&WithMiddleware{
appender: api.Handler{
EntryPoint: conf.API.EntryPoint,
Dashboard: conf.API.Dashboard,
Statistics: conf.API.Statistics,
DashboardAssets: conf.API.DashboardAssets,
CurrentConfigurations: currentConfiguration,
Debug: conf.Global.Debug,
},
appender: api.New(conf, runtimeConfiguration),
routerMiddlewares: chain,
})
}
if conf.Ping != nil && conf.Ping.EntryPoint == entryPointName {

View file

@ -62,7 +62,7 @@ func TestNewRouteAppenderAggregator(t *testing.T) {
"/wrong": http.StatusBadGateway,
"/ping": http.StatusOK,
// "/.well-known/acme-challenge/token": http.StatusNotFound, // FIXME
"/api/providers": http.StatusUnauthorized,
"/api/rawdata": http.StatusUnauthorized,
},
},
{

View file

@ -3,9 +3,9 @@ package router
import (
"context"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/config/static"
"github.com/containous/traefik/pkg/provider/acme"
"github.com/containous/traefik/pkg/safe"
"github.com/containous/traefik/pkg/server/middleware"
"github.com/containous/traefik/pkg/types"
)
@ -27,8 +27,8 @@ type RouteAppenderFactory struct {
}
// NewAppender Creates a new RouteAppender
func (r *RouteAppenderFactory) NewAppender(ctx context.Context, middlewaresBuilder *middleware.Builder, currentConfiguration *safe.Safe) types.RouteAppender {
aggregator := NewRouteAppenderAggregator(ctx, middlewaresBuilder, r.staticConfiguration, r.entryPointName, currentConfiguration)
func (r *RouteAppenderFactory) NewAppender(ctx context.Context, middlewaresBuilder *middleware.Builder, runtimeConfiguration *config.RuntimeConfiguration) types.RouteAppender {
aggregator := NewRouteAppenderAggregator(ctx, middlewaresBuilder, r.staticConfiguration, r.entryPointName, runtimeConfiguration)
if r.acmeProvider != nil && r.acmeProvider.HTTPChallenge != nil && r.acmeProvider.HTTPChallenge.EntryPoint == r.entryPointName {
aggregator.AddAppender(r.acmeProvider)

View file

@ -23,7 +23,7 @@ const (
)
// NewManager Creates a new Manager
func NewManager(routers map[string]*config.Router,
func NewManager(routers map[string]*config.RouterInfo,
serviceManager *service.Manager, middlewaresBuilder *middleware.Builder, modifierBuilder *responsemodifiers.Builder,
) *Manager {
return &Manager{
@ -38,7 +38,7 @@ func NewManager(routers map[string]*config.Router,
// Manager A route/router manager
type Manager struct {
routerHandlers map[string]http.Handler
configs map[string]*config.Router
configs map[string]*config.RouterInfo
serviceManager *service.Manager
middlewaresBuilder *middleware.Builder
modifierBuilder *responsemodifiers.Builder
@ -75,8 +75,17 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t
return entryPointHandlers
}
func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*config.Router {
entryPointsRouters := make(map[string]map[string]*config.Router)
func contains(entryPoints []string, entryPointName string) bool {
for _, name := range entryPoints {
if name == entryPointName {
return true
}
}
return false
}
func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*config.RouterInfo {
entryPointsRouters := make(map[string]map[string]*config.RouterInfo)
for rtName, rt := range m.configs {
if (tls && rt.TLS == nil) || (!tls && rt.TLS != nil) {
@ -95,7 +104,7 @@ func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string, tls
}
if _, ok := entryPointsRouters[entryPointName]; !ok {
entryPointsRouters[entryPointName] = make(map[string]*config.Router)
entryPointsRouters[entryPointName] = make(map[string]*config.RouterInfo)
}
entryPointsRouters[entryPointName][rtName] = rt
@ -105,7 +114,7 @@ func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string, tls
return entryPointsRouters
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.Router) (http.Handler, error) {
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.RouterInfo) (http.Handler, error) {
router, err := rules.NewRouter()
if err != nil {
return nil, err
@ -117,12 +126,14 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
handler, err := m.buildRouterHandler(ctxRouter, routerName)
if err != nil {
routerConfig.Err = err.Error()
logger.Error(err)
continue
}
err = router.AddRoute(routerConfig.Rule, routerConfig.Priority, handler)
if err != nil {
routerConfig.Err = err.Error()
logger.Error(err)
continue
}
@ -166,7 +177,7 @@ func (m *Manager) buildRouterHandler(ctx context.Context, routerName string) (ht
return m.routerHandlers[routerName], nil
}
func (m *Manager) buildHTTPHandler(ctx context.Context, router *config.Router, routerName string) (http.Handler, error) {
func (m *Manager) buildHTTPHandler(ctx context.Context, router *config.RouterInfo, routerName string) (http.Handler, error) {
qualifiedNames := make([]string, len(router.Middlewares))
for i, name := range router.Middlewares {
qualifiedNames[i] = internal.GetQualifiedName(ctx, name)
@ -186,12 +197,3 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *config.Router, r
return alice.New().Extend(*mHandler).Append(tHandler).Then(sHandler)
}
func contains(entryPoints []string, entryPointName string) bool {
for _, name := range entryPoints {
if name == entryPointName {
return true
}
}
return false
}

View file

@ -314,11 +314,17 @@ func TestRouterManager_Get(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
serviceManager := service.NewManager(test.serviceConfig, http.DefaultTransport)
middlewaresBuilder := middleware.NewBuilder(test.middlewaresConfig, serviceManager)
responseModifierFactory := responsemodifiers.NewBuilder(test.middlewaresConfig)
routerManager := NewManager(test.routersConfig, serviceManager, middlewaresBuilder, responseModifierFactory)
rtConf := config.NewRuntimeConfig(config.Configuration{
HTTP: &config.HTTPConfiguration{
Services: test.serviceConfig,
Routers: test.routersConfig,
Middlewares: test.middlewaresConfig,
},
})
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport)
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
routerManager := NewManager(rtConf.Routers, serviceManager, middlewaresBuilder, responseModifierFactory)
handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false)
@ -413,11 +419,17 @@ func TestAccessLog(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
serviceManager := service.NewManager(test.serviceConfig, http.DefaultTransport)
middlewaresBuilder := middleware.NewBuilder(test.middlewaresConfig, serviceManager)
responseModifierFactory := responsemodifiers.NewBuilder(test.middlewaresConfig)
routerManager := NewManager(test.routersConfig, serviceManager, middlewaresBuilder, responseModifierFactory)
rtConf := config.NewRuntimeConfig(config.Configuration{
HTTP: &config.HTTPConfiguration{
Services: test.serviceConfig,
Routers: test.routersConfig,
Middlewares: test.middlewaresConfig,
},
})
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport)
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
routerManager := NewManager(rtConf.Routers, serviceManager, middlewaresBuilder, responseModifierFactory)
handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false)
@ -443,6 +455,310 @@ func TestAccessLog(t *testing.T) {
}
}
func TestRuntimeConfiguration(t *testing.T) {
testCases := []struct {
desc string
serviceConfig map[string]*config.Service
routerConfig map[string]*config.Router
middlewareConfig map[string]*config.Middleware
expectedError int
}{
{
desc: "No error",
serviceConfig: map[string]*config.Service{
"foo-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",
},
},
},
},
routerConfig: map[string]*config.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`bar.foo`)",
},
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
expectedError: 0,
},
{
desc: "One router with wrong rule",
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
routerConfig: map[string]*config.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "WrongRule(`bar.foo`)",
},
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
expectedError: 1,
},
{
desc: "All router with wrong rule",
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
routerConfig: map[string]*config.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "WrongRule(`bar.foo`)",
},
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "WrongRule(`foo.bar`)",
},
},
expectedError: 2,
},
{
desc: "Router with unknown service",
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
routerConfig: map[string]*config.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "wrong-service",
Rule: "Host(`bar.foo`)",
},
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
expectedError: 1,
},
{
desc: "Router with broken service",
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: nil,
},
},
routerConfig: map[string]*config.Router{
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
},
},
expectedError: 2,
},
{
desc: "Router with middleware",
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
middlewareConfig: map[string]*config.Middleware{
"auth": {
BasicAuth: &config.BasicAuth{
Users: []string{"admin:admin"},
},
},
"addPrefixTest": {
AddPrefix: &config.AddPrefix{
Prefix: "/toto",
},
},
},
routerConfig: map[string]*config.Router{
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"auth", "addPrefixTest"},
},
"test": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar.other`)",
Middlewares: []string{"addPrefixTest", "auth"},
},
},
},
{
desc: "Router with unknown middleware",
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
middlewareConfig: map[string]*config.Middleware{
"auth": {
BasicAuth: &config.BasicAuth{
Users: []string{"admin:admin"},
},
},
},
routerConfig: map[string]*config.Router{
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"unknown"},
},
},
expectedError: 1,
},
{
desc: "Router with broken middleware",
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: "http://127.0.0.1",
Weight: 1,
},
},
Method: "wrr",
},
},
},
middlewareConfig: map[string]*config.Middleware{
"auth": {
BasicAuth: &config.BasicAuth{
Users: []string{"foo"},
},
},
},
routerConfig: map[string]*config.Router{
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host(`foo.bar`)",
Middlewares: []string{"auth"},
},
},
expectedError: 2,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
entryPoints := []string{"web"}
rtConf := config.NewRuntimeConfig(config.Configuration{
HTTP: &config.HTTPConfiguration{
Services: test.serviceConfig,
Routers: test.routerConfig,
Middlewares: test.middlewareConfig,
},
})
serviceManager := service.NewManager(rtConf.Services, http.DefaultTransport)
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
responseModifierFactory := responsemodifiers.NewBuilder(map[string]*config.MiddlewareInfo{})
routerManager := NewManager(rtConf.Routers, serviceManager, middlewaresBuilder, responseModifierFactory)
_ = routerManager.BuildHandlers(context.Background(), entryPoints, false)
// even though rtConf was passed by argument to the manager builders above,
// it's ok to use it as the result we check, because everything worth checking
// can be accessed by pointers in it.
var allErrors int
for _, v := range rtConf.Services {
if v.Err != nil {
allErrors++
}
}
for _, v := range rtConf.Routers {
if v.Err != "" {
allErrors++
}
}
for _, v := range rtConf.Middlewares {
if v.Err != nil {
allErrors++
}
}
assert.Equal(t, test.expectedError, allErrors)
})
}
}
type staticTransport struct {
res *http.Response
}
@ -480,11 +796,17 @@ func BenchmarkRouterServe(b *testing.B) {
}
entryPoints := []string{"web"}
serviceManager := service.NewManager(serviceConfig, &staticTransport{res})
middlewaresBuilder := middleware.NewBuilder(map[string]*config.Middleware{}, serviceManager)
responseModifierFactory := responsemodifiers.NewBuilder(map[string]*config.Middleware{})
routerManager := NewManager(routersConfig, serviceManager, middlewaresBuilder, responseModifierFactory)
rtConf := config.NewRuntimeConfig(config.Configuration{
HTTP: &config.HTTPConfiguration{
Services: serviceConfig,
Routers: routersConfig,
Middlewares: map[string]*config.Middleware{},
},
})
serviceManager := service.NewManager(rtConf.Services, &staticTransport{res})
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager)
responseModifierFactory := responsemodifiers.NewBuilder(rtConf.Middlewares)
routerManager := NewManager(rtConf.Routers, serviceManager, middlewaresBuilder, responseModifierFactory)
handlers := routerManager.BuildHandlers(context.Background(), entryPoints, false)
@ -498,6 +820,7 @@ func BenchmarkRouterServe(b *testing.B) {
}
}
func BenchmarkService(b *testing.B) {
res := &http.Response{
StatusCode: 200,
@ -518,7 +841,12 @@ func BenchmarkService(b *testing.B) {
},
}
serviceManager := service.NewManager(serviceConfig, &staticTransport{res})
rtConf := config.NewRuntimeConfig(config.Configuration{
HTTP: &config.HTTPConfiguration{
Services: serviceConfig,
},
})
serviceManager := service.NewManager(rtConf.Services, &staticTransport{res})
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)

View file

@ -3,6 +3,7 @@ package tcp
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"github.com/containous/traefik/pkg/config"
@ -14,14 +15,14 @@ import (
)
// NewManager Creates a new Manager
func NewManager(routers map[string]*config.TCPRouter,
func NewManager(conf *config.RuntimeConfiguration,
serviceManager *tcpservice.Manager,
httpHandlers map[string]http.Handler,
httpsHandlers map[string]http.Handler,
tlsConfig *tls.Config,
) *Manager {
return &Manager{
configs: routers,
configs: conf.TCPRouters,
serviceManager: serviceManager,
httpHandlers: httpHandlers,
httpsHandlers: httpsHandlers,
@ -31,7 +32,7 @@ func NewManager(routers map[string]*config.TCPRouter,
// Manager is a route/router manager
type Manager struct {
configs map[string]*config.TCPRouter
configs map[string]*config.TCPRouterInfo
serviceManager *tcpservice.Manager
httpHandlers map[string]http.Handler
httpsHandlers map[string]http.Handler
@ -60,7 +61,7 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) m
return entryPointHandlers
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.TCPRouter, handlerHTTP http.Handler, handlerHTTPS http.Handler) (*tcp.Router, error) {
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.TCPRouterInfo, handlerHTTP http.Handler, handlerHTTPS http.Handler) (*tcp.Router, error) {
router := &tcp.Router{}
router.HTTPHandler(handlerHTTP)
router.HTTPSHandler(handlerHTTPS, m.tlsConfig)
@ -71,18 +72,21 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
handler, err := m.serviceManager.BuildTCP(ctxRouter, routerConfig.Service)
if err != nil {
routerConfig.Err = err.Error()
logger.Error(err)
continue
}
domains, err := rules.ParseHostSNI(routerConfig.Rule)
if err != nil {
logger.Debugf("Unknown rule %s", routerConfig.Rule)
routerErr := fmt.Errorf("unknown rule %s", routerConfig.Rule)
routerConfig.Err = routerErr.Error()
logger.Debug(routerErr)
continue
}
for _, domain := range domains {
logger.Debugf("Add route %s on TCP", domain)
logger.Debugf("Adding route %s on TCP", domain)
switch {
case routerConfig.TLS != nil:
if routerConfig.TLS.Passthrough {
@ -101,8 +105,17 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
return router, nil
}
func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string) map[string]map[string]*config.TCPRouter {
entryPointsRouters := make(map[string]map[string]*config.TCPRouter)
func contains(entryPoints []string, entryPointName string) bool {
for _, name := range entryPoints {
if name == entryPointName {
return true
}
}
return false
}
func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string) map[string]map[string]*config.TCPRouterInfo {
entryPointsRouters := make(map[string]map[string]*config.TCPRouterInfo)
for rtName, rt := range m.configs {
eps := rt.EntryPoints
@ -118,7 +131,7 @@ func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string) map
}
if _, ok := entryPointsRouters[entryPointName]; !ok {
entryPointsRouters[entryPointName] = make(map[string]*config.TCPRouter)
entryPointsRouters[entryPointName] = make(map[string]*config.TCPRouterInfo)
}
entryPointsRouters[entryPointName][rtName] = rt
@ -127,12 +140,3 @@ func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string) map
return entryPointsRouters
}
func contains(entryPoints []string, entryPointName string) bool {
for _, name := range entryPoints {
if name == entryPointName {
return true
}
}
return false
}

View file

@ -0,0 +1,223 @@
package tcp
import (
"context"
"testing"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/server/service/tcp"
"github.com/stretchr/testify/assert"
)
func TestRuntimeConfiguration(t *testing.T) {
testCases := []struct {
desc string
serviceConfig map[string]*config.TCPServiceInfo
routerConfig map[string]*config.TCPRouterInfo
expectedError int
}{
{
desc: "No error",
serviceConfig: map[string]*config.TCPServiceInfo{
"foo-service": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Port: "8085",
Address: "127.0.0.1:8085",
},
{
Address: "127.0.0.1:8086",
Port: "8086",
},
},
Method: "wrr",
},
},
},
},
routerConfig: map[string]*config.TCPRouterInfo{
"foo": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "HostSNI(`bar.foo`)",
},
},
"bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "HostSNI(`foo.bar`)",
},
},
},
expectedError: 0,
},
{
desc: "One router with wrong rule",
serviceConfig: map[string]*config.TCPServiceInfo{
"foo-service": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1:80",
},
},
Method: "wrr",
},
},
},
},
routerConfig: map[string]*config.TCPRouterInfo{
"foo": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "WrongRule(`bar.foo`)",
},
},
"bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "HostSNI(`foo.bar`)",
},
},
},
expectedError: 1,
},
{
desc: "All router with wrong rule",
serviceConfig: map[string]*config.TCPServiceInfo{
"foo-service": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1:80",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
routerConfig: map[string]*config.TCPRouterInfo{
"foo": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "WrongRule(`bar.foo`)",
},
},
"bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "WrongRule(`foo.bar`)",
},
},
},
expectedError: 2,
},
{
desc: "Router with unknown service",
serviceConfig: map[string]*config.TCPServiceInfo{
"foo-service": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "127.0.0.1:80",
Weight: 1,
},
},
Method: "wrr",
},
},
},
},
routerConfig: map[string]*config.TCPRouterInfo{
"foo": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "wrong-service",
Rule: "HostSNI(`bar.foo`)",
},
},
"bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "HostSNI(`foo.bar`)",
},
},
},
expectedError: 1,
},
{
desc: "Router with broken service",
serviceConfig: map[string]*config.TCPServiceInfo{
"foo-service": {
TCPService: &config.TCPService{
LoadBalancer: nil,
},
},
},
routerConfig: map[string]*config.TCPRouterInfo{
"bar": {
TCPRouter: &config.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "HostSNI(`foo.bar`)",
},
},
},
expectedError: 2,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
entryPoints := []string{"web"}
conf := &config.RuntimeConfiguration{
TCPServices: test.serviceConfig,
TCPRouters: test.routerConfig,
}
serviceManager := tcp.NewManager(conf)
routerManager := NewManager(conf, serviceManager,
nil, nil, nil)
_ = routerManager.BuildHandlers(context.Background(), entryPoints)
// even though conf was passed by argument to the manager builders above,
// it's ok to use it as the result we check, because everything worth checking
// can be accessed by pointers in it.
var allErrors int
for _, v := range conf.TCPServices {
if v.Err != nil {
allErrors++
}
}
for _, v := range conf.TCPRouters {
if v.Err != "" {
allErrors++
}
}
assert.Equal(t, test.expectedError, allErrors)
})
}
}

View file

@ -50,7 +50,7 @@ type Server struct {
// RouteAppenderFactory the route appender factory interface
type RouteAppenderFactory interface {
NewAppender(ctx context.Context, middlewaresBuilder *middleware.Builder, currentConfigurations *safe.Safe) types.RouteAppender
NewAppender(ctx context.Context, middlewaresBuilder *middleware.Builder, runtimeConfiguration *config.RuntimeConfiguration) types.RouteAppender
}
func setupTracing(conf *static.Tracing) tracing.TrackingBackend {

View file

@ -69,47 +69,46 @@ func (s *Server) loadConfigurationTCP(configurations config.Configurations) map[
s.tlsManager.UpdateConfigs(conf.TLSStores, conf.TLSOptions, conf.TLS)
handlersNonTLS, handlersTLS := s.createHTTPHandlers(ctx, *conf.HTTP, entryPoints)
routersTCP := s.createTCPRouters(ctx, conf.TCP, entryPoints, handlersNonTLS, handlersTLS, s.tlsManager.Get("default", "default"))
rtConf := config.NewRuntimeConfig(conf)
handlersNonTLS, handlersTLS := s.createHTTPHandlers(ctx, rtConf, entryPoints)
routersTCP := s.createTCPRouters(ctx, rtConf, entryPoints, handlersNonTLS, handlersTLS, s.tlsManager.Get("default", "default"))
rtConf.PopulateUsedBy()
return routersTCP
}
func (s *Server) createTCPRouters(ctx context.Context, configuration *config.TCPConfiguration, entryPoints []string, handlers map[string]http.Handler, handlersTLS map[string]http.Handler, tlsConfig *tls.Config) map[string]*tcpCore.Router {
// the given configuration must not be nil. its fields will get mutated.
func (s *Server) createTCPRouters(ctx context.Context, configuration *config.RuntimeConfiguration, entryPoints []string, handlers map[string]http.Handler, handlersTLS map[string]http.Handler, tlsConfig *tls.Config) map[string]*tcpCore.Router {
if configuration == nil {
return make(map[string]*tcpCore.Router)
}
serviceManager := tcp.NewManager(configuration.Services)
routerManager := routertcp.NewManager(configuration.Routers, serviceManager, handlers, handlersTLS, tlsConfig)
serviceManager := tcp.NewManager(configuration)
routerManager := routertcp.NewManager(configuration, serviceManager, handlers, handlersTLS, tlsConfig)
return routerManager.BuildHandlers(ctx, entryPoints)
}
func (s *Server) createHTTPHandlers(ctx context.Context, configuration config.HTTPConfiguration, entryPoints []string) (map[string]http.Handler, map[string]http.Handler) {
// createHTTPHandlers returns, for the given configuration and entryPoints, the HTTP handlers for non-TLS connections, and for the TLS ones. the given configuration must not be nil. its fields will get mutated.
func (s *Server) createHTTPHandlers(ctx context.Context, configuration *config.RuntimeConfiguration, entryPoints []string) (map[string]http.Handler, map[string]http.Handler) {
serviceManager := service.NewManager(configuration.Services, s.defaultRoundTripper)
middlewaresBuilder := middleware.NewBuilder(configuration.Middlewares, serviceManager)
responseModifierFactory := responsemodifiers.NewBuilder(configuration.Middlewares)
routerManager := router.NewManager(configuration.Routers, serviceManager, middlewaresBuilder, responseModifierFactory)
handlersNonTLS := routerManager.BuildHandlers(ctx, entryPoints, false)
handlersTLS := routerManager.BuildHandlers(ctx, entryPoints, true)
routerHandlers := make(map[string]http.Handler)
for _, entryPointName := range entryPoints {
internalMuxRouter := mux.NewRouter().
SkipClean(true)
internalMuxRouter := mux.NewRouter().SkipClean(true)
ctx = log.With(ctx, log.Str(log.EntryPointName, entryPointName))
factory := s.entryPointsTCP[entryPointName].RouteAppenderFactory
if factory != nil {
// FIXME remove currentConfigurations
appender := factory.NewAppender(ctx, middlewaresBuilder, &s.currentConfigurations)
appender := factory.NewAppender(ctx, middlewaresBuilder, configuration)
appender.Append(internalMuxRouter)
}

View file

@ -47,7 +47,8 @@ func TestReuseService(t *testing.T) {
srv := NewServer(staticConfig, nil, entryPoints, nil)
entrypointsHandlers, _ := srv.createHTTPHandlers(context.Background(), *dynamicConfigs, []string{"http"})
rtConf := config.NewRuntimeConfig(config.Configuration{HTTP: dynamicConfigs})
entrypointsHandlers, _ := srv.createHTTPHandlers(context.Background(), rtConf, []string{"http"})
// Test that the /ok path returns a status 200.
responseRecorderOk := &httptest.ResponseRecorder{}

View file

@ -258,7 +258,8 @@ func TestServerResponseEmptyBackend(t *testing.T) {
}
srv := NewServer(globalConfig, nil, entryPointsConfig, nil)
entryPoints, _ := srv.createHTTPHandlers(context.Background(), *test.config(testServer.URL), []string{"http"})
rtConf := config.NewRuntimeConfig(config.Configuration{HTTP: test.config(testServer.URL)})
entryPoints, _ := srv.createHTTPHandlers(context.Background(), rtConf, []string{"http"})
responseRecorder := &httptest.ResponseRecorder{}
request := httptest.NewRequest(http.MethodGet, testServer.URL+requestPath, nil)

View file

@ -26,7 +26,7 @@ const (
)
// NewManager creates a new Manager
func NewManager(configs map[string]*config.Service, defaultRoundTripper http.RoundTripper) *Manager {
func NewManager(configs map[string]*config.ServiceInfo, defaultRoundTripper http.RoundTripper) *Manager {
return &Manager{
bufferPool: newBufferPool(),
defaultRoundTripper: defaultRoundTripper,
@ -40,7 +40,7 @@ type Manager struct {
bufferPool httputil.BufferPool
defaultRoundTripper http.RoundTripper
balancers map[string][]healthcheck.BalancerHandler
configs map[string]*config.Service
configs map[string]*config.ServiceInfo
}
// BuildHTTP Creates a http.Handler for a service configuration.
@ -50,15 +50,25 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string, respons
serviceName = internal.GetQualifiedName(ctx, serviceName)
ctx = internal.AddProviderInContext(ctx, serviceName)
if conf, ok := m.configs[serviceName]; ok {
// TODO Should handle multiple service types
// FIXME Check if the service is declared multiple times with different types
if conf.LoadBalancer != nil {
return m.getLoadBalancerServiceHandler(ctx, serviceName, conf.LoadBalancer, responseModifier)
}
return nil, fmt.Errorf("the service %q doesn't have any load balancer", serviceName)
conf, ok := m.configs[serviceName]
if !ok {
return nil, fmt.Errorf("the service %q does not exist", serviceName)
}
return nil, fmt.Errorf("the service %q does not exits", serviceName)
// TODO Should handle multiple service types
// FIXME Check if the service is declared multiple times with different types
if conf.LoadBalancer == nil {
conf.Err = fmt.Errorf("the service %q doesn't have any load balancer", serviceName)
return nil, conf.Err
}
lb, err := m.getLoadBalancerServiceHandler(ctx, serviceName, conf.LoadBalancer, responseModifier)
if err != nil {
conf.Err = err
return nil, err
}
return lb, nil
}
func (m *Manager) getLoadBalancerServiceHandler(
@ -158,7 +168,7 @@ func buildHealthCheckOptions(ctx context.Context, lb healthcheck.BalancerHandler
}
if timeout >= interval {
logger.Warnf("Health check timeout for backend '%s' should be lower than the health check interval. Interval set to timeout + 1 second (%s).", backend)
logger.Warnf("Health check timeout for backend '%s' should be lower than the health check interval. Interval set to timeout + 1 second (%s).", backend, interval)
}
return &healthcheck.Options{
@ -229,7 +239,8 @@ func (m *Manager) getLoadBalancer(ctx context.Context, serviceName string, servi
}
}
if err := m.upsertServers(ctx, lb, service.Servers); err != nil {
lbsu := healthcheck.NewLBStatusUpdater(lb, m.configs[serviceName])
if err := m.upsertServers(ctx, lbsu, service.Servers); err != nil {
return nil, fmt.Errorf("error configuring load balancer for service %s: %v", serviceName, err)
}

View file

@ -275,33 +275,39 @@ func TestManager_Build(t *testing.T) {
testCases := []struct {
desc string
serviceName string
configs map[string]*config.Service
configs map[string]*config.ServiceInfo
providerName string
}{
{
desc: "Simple service name",
serviceName: "serviceName",
configs: map[string]*config.Service{
configs: map[string]*config.ServiceInfo{
"serviceName": {
LoadBalancer: &config.LoadBalancerService{Method: "wrr"},
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{Method: "wrr"},
},
},
},
},
{
desc: "Service name with provider",
serviceName: "provider-1.serviceName",
configs: map[string]*config.Service{
configs: map[string]*config.ServiceInfo{
"provider-1.serviceName": {
LoadBalancer: &config.LoadBalancerService{Method: "wrr"},
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{Method: "wrr"},
},
},
},
},
{
desc: "Service name with provider in context",
serviceName: "serviceName",
configs: map[string]*config.Service{
configs: map[string]*config.ServiceInfo{
"provider-1.serviceName": {
LoadBalancer: &config.LoadBalancerService{Method: "wrr"},
Service: &config.Service{
LoadBalancer: &config.LoadBalancerService{Method: "wrr"},
},
},
},
providerName: "provider-1",

View file

@ -13,13 +13,13 @@ import (
// Manager is the TCPHandlers factory
type Manager struct {
configs map[string]*config.TCPService
configs map[string]*config.TCPServiceInfo
}
// NewManager creates a new manager
func NewManager(configs map[string]*config.TCPService) *Manager {
func NewManager(conf *config.RuntimeConfiguration) *Manager {
return &Manager{
configs: configs,
configs: conf.TCPServices,
}
}
@ -29,23 +29,23 @@ func (m *Manager) BuildTCP(rootCtx context.Context, serviceName string) (tcp.Han
ctx := internal.AddProviderInContext(rootCtx, serviceQualifiedName)
ctx = log.With(ctx, log.Str(log.ServiceName, serviceName))
// FIXME Check if the service is declared multiple times with different types
conf, ok := m.configs[serviceQualifiedName]
if !ok {
return nil, fmt.Errorf("the service %q does not exits", serviceQualifiedName)
return nil, fmt.Errorf("the service %q does not exist", serviceQualifiedName)
}
if conf.LoadBalancer == nil {
return nil, fmt.Errorf("the service %q doesn't have any TCP load balancer", serviceQualifiedName)
conf.Err = fmt.Errorf("the service %q doesn't have any TCP load balancer", serviceQualifiedName)
return nil, conf.Err
}
logger := log.FromContext(ctx)
// FIXME Check if the service is declared multiple times with different types
loadBalancer := tcp.NewRRLoadBalancer()
for _, server := range conf.LoadBalancer.Servers {
if _, err := parseIP(server.Address); err != nil {
logger.Errorf("Invalid IP address for a %q server %q: %v", serviceQualifiedName, server.Address, err)
if _, _, err := net.SplitHostPort(server.Address); err != nil {
logger.Errorf("In service %q: %v", serviceQualifiedName, err)
continue
}
@ -57,20 +57,5 @@ func (m *Manager) BuildTCP(rootCtx context.Context, serviceName string) (tcp.Han
loadBalancer.AddServer(handler)
}
return loadBalancer, nil
}
func parseIP(s string) (string, error) {
ip, _, err := net.SplitHostPort(s)
if err == nil {
return ip, nil
}
ipNoPort := net.ParseIP(s)
if ipNoPort == nil {
return "", fmt.Errorf("invalid IP Address %s", ipNoPort)
}
return ipNoPort.String(), nil
}

View file

@ -5,6 +5,8 @@ import (
"testing"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/server/internal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -12,49 +14,166 @@ func TestManager_BuildTCP(t *testing.T) {
testCases := []struct {
desc string
serviceName string
configs map[string]*config.TCPService
configs map[string]*config.TCPServiceInfo
providerName string
expectedError string
}{
{
desc: "without configuration",
serviceName: "test",
configs: nil,
expectedError: `the service "test" does not exits`,
expectedError: `the service "test" does not exist`,
},
{
desc: "missing lb configuration",
serviceName: "test",
configs: map[string]*config.TCPService{
"test": {},
configs: map[string]*config.TCPServiceInfo{
"test": {
TCPService: &config.TCPService{},
},
},
expectedError: `the service "test" doesn't have any TCP load balancer`,
},
{
desc: "no such host",
desc: "no such host, server is skipped, error is logged",
serviceName: "test",
configs: map[string]*config.TCPService{
configs: map[string]*config.TCPServiceInfo{
"test": {
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{Address: "test:31"},
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{Address: "test:31"},
},
},
},
},
},
},
{
desc: "invalid IP address",
desc: "invalid IP address, server is skipped, error is logged",
serviceName: "test",
configs: map[string]*config.TCPService{
configs: map[string]*config.TCPServiceInfo{
"test": {
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{Address: "foobar"},
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{Address: "foobar"},
},
},
},
},
},
},
{
desc: "Simple service name",
serviceName: "serviceName",
configs: map[string]*config.TCPServiceInfo{
"serviceName": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{Method: "wrr"},
},
},
},
},
{
desc: "Service name with provider",
serviceName: "provider-1.serviceName",
configs: map[string]*config.TCPServiceInfo{
"provider-1.serviceName": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{Method: "wrr"},
},
},
},
},
{
desc: "Service name with provider in context",
serviceName: "serviceName",
configs: map[string]*config.TCPServiceInfo{
"provider-1.serviceName": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{Method: "wrr"},
},
},
},
providerName: "provider-1",
},
{
desc: "Server with correct host:port as address",
serviceName: "serviceName",
configs: map[string]*config.TCPServiceInfo{
"provider-1.serviceName": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "foobar.com:80",
},
},
Method: "wrr",
},
},
},
},
providerName: "provider-1",
},
{
desc: "Server with correct ip:port as address",
serviceName: "serviceName",
configs: map[string]*config.TCPServiceInfo{
"provider-1.serviceName": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "192.168.0.12:80",
},
},
Method: "wrr",
},
},
},
},
providerName: "provider-1",
},
{
desc: "missing port in address with hostname, server is skipped, error is logged",
serviceName: "serviceName",
configs: map[string]*config.TCPServiceInfo{
"provider-1.serviceName": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "foobar.com",
},
},
Method: "wrr",
},
},
},
},
providerName: "provider-1",
},
{
desc: "missing port in address with ip, server is skipped, error is logged",
serviceName: "serviceName",
configs: map[string]*config.TCPServiceInfo{
"provider-1.serviceName": {
TCPService: &config.TCPService{
LoadBalancer: &config.TCPLoadBalancerService{
Servers: []config.TCPServer{
{
Address: "192.168.0.12",
},
},
Method: "wrr",
},
},
},
},
providerName: "provider-1",
},
}
for _, test := range testCases {
@ -62,19 +181,22 @@ func TestManager_BuildTCP(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
manager := NewManager(test.configs)
manager := NewManager(&config.RuntimeConfiguration{
TCPServices: test.configs,
})
handler, err := manager.BuildTCP(context.Background(), test.serviceName)
ctx := context.Background()
if len(test.providerName) > 0 {
ctx = internal.AddProviderInContext(ctx, test.providerName+".foobar")
}
handler, err := manager.BuildTCP(ctx, test.serviceName)
if test.expectedError != "" {
if err == nil {
require.Error(t, err)
} else {
require.EqualError(t, err, test.expectedError)
require.Nil(t, handler)
}
assert.EqualError(t, err, test.expectedError)
require.Nil(t, handler)
} else {
require.NoError(t, err)
assert.Nil(t, err)
require.NotNil(t, handler)
}
})