diff --git a/provider/docker/config.go b/provider/docker/config.go index 51f7dfc1b..944d937af 100644 --- a/provider/docker/config.go +++ b/provider/docker/config.go @@ -37,20 +37,23 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C "getStickinessCookieName": getFuncStringLabel(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), // Frontend functions - "getBackend": getBackend, - "getPriority": getFuncStringLabel(label.TraefikFrontendPriority, label.DefaultFrontendPriority), - "getPassHostHeader": getFuncStringLabel(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), - "getPassTLSCert": getFuncBoolLabel(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), - "getEntryPoints": getFuncSliceStringLabel(label.TraefikFrontendEntryPoints), - "getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic), - "getWhitelistSourceRange": getFuncSliceStringLabel(label.TraefikFrontendWhitelistSourceRange), - "getFrontendRule": p.getFrontendRule, - "hasRedirect": hasRedirect, - "getRedirectEntryPoint": getFuncStringLabel(label.TraefikFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint), - "getRedirectRegex": getFuncStringLabel(label.TraefikFrontendRedirectRegex, ""), - "getRedirectReplacement": getFuncStringLabel(label.TraefikFrontendRedirectReplacement, ""), - "hasErrorPages": hasErrorPages, - "getErrorPages": getErrorPages, + "getBackend": getBackend, + "getPriority": getFuncStringLabel(label.TraefikFrontendPriority, label.DefaultFrontendPriority), + "getPassHostHeader": getFuncStringLabel(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), + "getPassTLSCert": getFuncBoolLabel(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getEntryPoints": getFuncSliceStringLabel(label.TraefikFrontendEntryPoints), + "getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic), + "getWhitelistSourceRange": getFuncSliceStringLabel(label.TraefikFrontendWhitelistSourceRange), + "getFrontendRule": p.getFrontendRule, + "hasRedirect": hasRedirect, + "getRedirectEntryPoint": getFuncStringLabel(label.TraefikFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint), + "getRedirectRegex": getFuncStringLabel(label.TraefikFrontendRedirectRegex, ""), + "getRedirectReplacement": getFuncStringLabel(label.TraefikFrontendRedirectReplacement, ""), + "hasErrorPages": hasErrorPages, + "getErrorPages": getErrorPages, + "hasRateLimits": hasFunc(label.TraefikFrontendRateLimitExtractorFunc), + "getRateLimitsExtractorFunc": getFuncStringLabel(label.TraefikFrontendRateLimitExtractorFunc, ""), + "getRateLimits": getRateLimits, // Headers "hasRequestHeaders": hasFunc(label.TraefikFrontendRequestHeaders), "getRequestHeaders": getFuncMapLabel(label.TraefikFrontendRequestHeaders), @@ -118,6 +121,8 @@ func (p *Provider) buildConfiguration(containersInspected []dockerData) *types.C "getServiceResponseHeaders": getFuncServiceMapLabel(label.SuffixFrontendResponseHeaders), "hasServiceErrorPages": hasServiceErrorPages, "getServiceErrorPages": getServiceErrorPages, + "hasServiceRateLimits": hasFuncServiceLabel(label.SuffixFrontendRateLimitExtractorFunc), + "getServiceRateLimits": getServiceRateLimits, } // filter containers filteredContainers := fun.Filter(func(container dockerData) bool { diff --git a/provider/docker/config_container.go b/provider/docker/config_container.go index 8123ddba5..f39bb7cc3 100644 --- a/provider/docker/config_container.go +++ b/provider/docker/config_container.go @@ -182,6 +182,11 @@ func getErrorPages(container dockerData) map[string]*types.ErrorPage { return label.ParseErrorPages(container.Labels, prefix, label.RegexpFrontendErrorPage) } +func getRateLimits(container dockerData) map[string]*types.Rate { + prefix := label.Prefix + label.BaseFrontendRateLimit + return label.ParseRateSets(container.Labels, prefix, label.RegexpFrontendRateLimit) +} + // Label functions func getFuncInt64Label(labelName string, defaultValue int64) func(container dockerData) int64 { diff --git a/provider/docker/config_container_docker_test.go b/provider/docker/config_container_docker_test.go index 9bf0d2c22..3a666deb2 100644 --- a/provider/docker/config_container_docker_test.go +++ b/provider/docker/config_container_docker_test.go @@ -4,7 +4,9 @@ import ( "reflect" "strconv" "testing" + "time" + "github.com/containous/flaeg" "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" docker "github.com/docker/docker/api/types" @@ -236,6 +238,65 @@ func TestDockerBuildConfiguration(t *testing.T) { }, }, }, + { + containers: []docker.ContainerJSON{ + containerJSON( + name("test1"), + labels(map[string]string{ + label.TraefikBackend: "foobar", + label.TraefikFrontendRateLimitExtractorFunc: "client.ip", + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", + }), + ports(nat.PortMap{ + "80/tcp": {}, + }), + withNetwork("bridge", ipv4("127.0.0.1")), + ), + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-test1-docker-localhost-0": { + EntryPoints: []string{}, + BasicAuth: []string{}, + PassHostHeader: true, + Backend: "backend-foobar", + Routes: map[string]types.Route{ + "route-frontend-Host-test1-docker-localhost-0": { + Rule: "Host:test1.docker.localhost", + }, + }, + RateLimit: &types.RateLimit{ + ExtractorFunc: "client.ip", + RateSet: map[string]*types.Rate{ + "foo": { + Period: flaeg.Duration(6 * time.Second), + Average: 12, + Burst: 18, + }, + "bar": { + Period: flaeg.Duration(3 * time.Second), + Average: 6, + Burst: 9, + }, + }, + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foobar": { + Servers: map[string]types.Server{ + "server-test1": { + URL: "http://127.0.0.1:80", + Weight: 0, + }, + }, + }, + }, + }, } for caseID, test := range testCases { diff --git a/provider/docker/config_service.go b/provider/docker/config_service.go index 0de24f173..65015ee4b 100644 --- a/provider/docker/config_service.go +++ b/provider/docker/config_service.go @@ -108,6 +108,11 @@ func getServiceErrorPages(container dockerData, serviceName string) map[string]* return label.ParseErrorPages(serviceLabels, label.BaseFrontendErrorPage, label.RegexpBaseFrontendErrorPage) } +func getServiceRateLimits(container dockerData, serviceName string) map[string]*types.Rate { + serviceLabels := getServiceLabels(container, serviceName) + return label.ParseRateSets(serviceLabels, label.BaseFrontendRateLimit, label.RegexpBaseFrontendRateLimit) +} + // Service label functions func getFuncServiceMapLabel(labelSuffix string) func(container dockerData, serviceName string) map[string]string { diff --git a/provider/label/label.go b/provider/label/label.go index 6eece3fa0..c33228ffd 100644 --- a/provider/label/label.go +++ b/provider/label/label.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/containous/flaeg" "github.com/containous/traefik/log" "github.com/containous/traefik/types" ) @@ -44,6 +45,12 @@ var ( // RegexpFrontendErrorPage used to extract error pages from label RegexpFrontendErrorPage = regexp.MustCompile(`^traefik\.frontend\.errors\.(?P[^ .]+)\.(?P[^ .]+)$`) + + // RegexpBaseFrontendRateLimit used to extract rate limits from service's label + RegexpBaseFrontendRateLimit = regexp.MustCompile(`^frontend\.rateLimit\.rateSet\.(?P[^ .]+)\.(?P[^ .]+)$`) + + // RegexpFrontendRateLimit used to extract rate limits from label + RegexpFrontendRateLimit = regexp.MustCompile(`^traefik\.frontend\.rateLimit\.rateSet\.(?P[^ .]+)\.(?P[^ .]+)$`) ) // ServicePropertyValues is a map of services properties @@ -295,6 +302,58 @@ func ParseErrorPages(labels map[string]string, labelPrefix string, labelRegex *r return errorPages } +// ParseRateSets parse rate limits to create Rate struct +func ParseRateSets(labels map[string]string, labelPrefix string, labelRegex *regexp.Regexp) map[string]*types.Rate { + rateSets := make(map[string]*types.Rate) + + for lblName, rawValue := range labels { + if strings.HasPrefix(lblName, labelPrefix) && len(rawValue) > 0 { + submatch := labelRegex.FindStringSubmatch(lblName) + if len(submatch) != 3 { + log.Errorf("Invalid rate limit label: %s, sub-match: %v", lblName, submatch) + continue + } + + limitName := submatch[1] + + ep, ok := rateSets[limitName] + if !ok { + ep = &types.Rate{} + rateSets[limitName] = ep + } + + switch submatch[2] { + case "period": + var d flaeg.Duration + err := d.Set(rawValue) + if err != nil { + log.Errorf("Unable to parse %q: %q. %v", lblName, rawValue, err) + continue + } + ep.Period = d + case "average": + value, err := strconv.ParseInt(rawValue, 10, 64) + if err != nil { + log.Errorf("Unable to parse %q: %q. %v", lblName, rawValue, err) + continue + } + ep.Average = value + case "burst": + value, err := strconv.ParseInt(rawValue, 10, 64) + if err != nil { + log.Errorf("Unable to parse %q: %q. %v", lblName, rawValue, err) + continue + } + ep.Burst = value + default: + log.Errorf("Invalid rate limit label: %s", lblName) + continue + } + } + } + return rateSets +} + // IsEnabled Check if a container is enabled in Træfik func IsEnabled(labels map[string]string, exposedByDefault bool) bool { return GetBoolValue(labels, TraefikEnable, exposedByDefault) diff --git a/provider/label/label_test.go b/provider/label/label_test.go index 53e002e76..83a8be3e3 100644 --- a/provider/label/label_test.go +++ b/provider/label/label_test.go @@ -2,7 +2,9 @@ package label import ( "testing" + "time" + "github.com/containous/flaeg" "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" ) @@ -1081,3 +1083,46 @@ func TestParseErrorPages(t *testing.T) { }) } } + +func TestParseRateSets(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected map[string]*types.Rate + }{ + { + desc: "2 rate limits", + labels: map[string]string{ + Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitPeriod: "6", + Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitAverage: "12", + Prefix + BaseFrontendRateLimit + "foo." + SuffixRateLimitBurst: "18", + Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitPeriod: "3", + Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitAverage: "6", + Prefix + BaseFrontendRateLimit + "bar." + SuffixRateLimitBurst: "9", + }, + expected: map[string]*types.Rate{ + "foo": { + Period: flaeg.Duration(6 * time.Second), + Average: 12, + Burst: 18, + }, + "bar": { + Period: flaeg.Duration(3 * time.Second), + Average: 6, + Burst: 9, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + rateSets := ParseRateSets(test.labels, Prefix+BaseFrontendRateLimit, RegexpFrontendRateLimit) + + assert.EqualValues(t, test.expected, rateSets) + }) + } +} diff --git a/provider/label/names.go b/provider/label/names.go index 44aaf20f1..20e3b1cf6 100644 --- a/provider/label/names.go +++ b/provider/label/names.go @@ -50,6 +50,7 @@ const ( SuffixFrontendPassHostHeader = "frontend.passHostHeader" SuffixFrontendPassTLSCert = "frontend.passTLSCert" SuffixFrontendPriority = "frontend.priority" + SuffixFrontendRateLimitExtractorFunc = "frontend.rateLimit.extractorFunc" SuffixFrontendRedirectEntryPoint = "frontend.redirect.entryPoint" SuffixFrontendRedirectRegex = "frontend.redirect.regex" SuffixFrontendRedirectReplacement = "frontend.redirect.replacement" @@ -83,11 +84,12 @@ const ( TraefikFrontendPassHostHeader = Prefix + SuffixFrontendPassHostHeader TraefikFrontendPassTLSCert = Prefix + SuffixFrontendPassTLSCert TraefikFrontendPriority = Prefix + SuffixFrontendPriority - TraefikFrontendRule = Prefix + SuffixFrontendRule - TraefikFrontendRuleType = Prefix + SuffixFrontendRuleType + TraefikFrontendRateLimitExtractorFunc = Prefix + SuffixFrontendRateLimitExtractorFunc TraefikFrontendRedirectEntryPoint = Prefix + SuffixFrontendRedirectEntryPoint TraefikFrontendRedirectRegex = Prefix + SuffixFrontendRedirectRegex TraefikFrontendRedirectReplacement = Prefix + SuffixFrontendRedirectReplacement + TraefikFrontendRule = Prefix + SuffixFrontendRule + TraefikFrontendRuleType = Prefix + SuffixFrontendRuleType TraefikFrontendValue = Prefix + SuffixFrontendValue TraefikFrontendWhitelistSourceRange = Prefix + SuffixFrontendWhitelistSourceRange TraefikFrontendRequestHeaders = Prefix + SuffixFrontendRequestHeaders @@ -114,4 +116,8 @@ const ( SuffixErrorPageBackend = "backend" SuffixErrorPageQuery = "query" SuffixErrorPageStatus = "status" + BaseFrontendRateLimit = "frontend.rateLimit.rateSet." + SuffixRateLimitPeriod = "period" + SuffixRateLimitAverage = "average" + SuffixRateLimitBurst = "burst" ) diff --git a/templates/docker.tmpl b/templates/docker.tmpl index 57f866163..5a20607e0 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -95,6 +95,18 @@ {{end}} {{end}} + {{ if hasServiceRateLimits $container $serviceName }} + [frontends."frontend-{{getServiceBackend $container $serviceName}}".rateLimit] + extractorFunc = "{{ getRateLimitsExtractorFunc $container $serviceName }}" + [frontends."frontend-{{getServiceBackend $container $serviceName}}".rateLimit.rateSet] + {{ range $limitName, $rateLimit := getServiceRateLimits $container $serviceName }} + [frontends."frontend-{{getServiceBackend $container $serviceName}}".rateLimit.rateSet.{{ $limitName }}] + period = "{{ $rateLimit.Period }}" + average = {{ $rateLimit.Average }} + burst = {{ $rateLimit.Burst }} + {{end}} + {{end}} + [frontends."frontend-{{getServiceBackend $container $serviceName}}".routes."service-{{$serviceName | replace "/" "" | replace "." "-"}}"] rule = "{{getServiceFrontendRule $container $serviceName}}" @@ -155,6 +167,18 @@ {{end}} {{end}} + {{ if hasRateLimits $container }} + [frontends."frontend-{{$frontend}}".rateLimit] + extractorFunc = "{{ getRateLimitsExtractorFunc $container }}" + [frontends."frontend-{{$frontend}}".rateLimit.rateSet] + {{ range $limitName, $rateLimit := getRateLimits $container }} + [frontends."frontend-{{$frontend}}".rateLimit.rateSet.{{ $limitName }}] + period = "{{ $rateLimit.Period }}" + average = {{ $rateLimit.Average }} + burst = {{ $rateLimit.Burst }} + {{end}} + {{end}} + [frontends."frontend-{{$frontend}}".headers] {{if hasSSLRedirectHeaders $container}} SSLRedirect = {{getSSLRedirectHeaders $container}}