From 54ca1abd2b575d29035b87320a7678f9532cd834 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Wed, 15 Jul 2020 21:58:16 +0200 Subject: [PATCH 01/21] fix: goreleaser. --- .goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index a2fbee302..a2fd7a3f2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -7,7 +7,7 @@ before: builds: - binary: traefik - main: ./cmd/traefik/traefik.go + main: ./cmd/traefik/ env: - CGO_ENABLED=0 ldflags: From 4957e498af84b9d3ac0ac0944a03bcdf897737a1 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Wed, 15 Jul 2020 22:00:19 +0200 Subject: [PATCH 02/21] Prepare release v2.3.0-rc2 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17cf18976..64b7382de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [v2.3.0-rc2](https://github.com/containous/traefik/tree/v2.3.0-rc2) (2020-07-15) +[All Commits](https://github.com/containous/traefik/compare/v2.3.0-rc1...v2.3.0-rc2) + +**Misc:** +- fix: goreleaser build commands. + ## [v2.3.0-rc1](https://github.com/containous/traefik/tree/v2.3.0-rc1) (2020-07-15) [All Commits](https://github.com/containous/traefik/compare/v2.2.0-rc1...v2.3.0-rc1) From 25b74ce1f31764ae6aebce79589b8953f3ece72d Mon Sep 17 00:00:00 2001 From: Simon Heimberg Date: Thu, 16 Jul 2020 12:38:03 +0200 Subject: [PATCH 03/21] Add example for entrypoint on one ip address --- docs/content/routing/entrypoints.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 52290000e..dc6d97d55 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -168,7 +168,7 @@ The format is: If both TCP and UDP are wanted for the same port, two entryPoints definitions are needed, such as in the example below. -??? example "Both TCP and UDP on port 3179" +??? example "Both TCP and UDP on Port 3179" ```toml tab="File (TOML)" ## Static configuration @@ -194,6 +194,30 @@ If both TCP and UDP are wanted for the same port, two entryPoints definitions ar --entryPoints.udpep.address=:3179/udp ``` +??? example "Listen on Specific IP Addresses Only" + + ```toml tab="File (TOML)" + [entryPoints.specificIPv4] + address = "192.168.2.7:8888" + [entryPoints.specificIPv6] + address = "[2001:db8::1]:8888" + ``` + + ```yaml tab="File (yaml)" + entryPoints: + specificIPv4: + address: "192.168.2.7:8888" + specificIPv6: + address: "[2001:db8::1]:8888" + ``` + + ```bash tab="CLI" + entrypoints.specificIPv4.address=192.168.2.7:8888 + entrypoints.specificIPv6.address=[2001:db8::1]:8888 + ``` + + Full details for how to specify `address` can be found in [net.Listen](https://golang.org/pkg/net/#Listen) (and [net.Dial](https://golang.org/pkg/net/#Dial)) of the doc for go. + ### Forwarded Headers You can configure Traefik to trust the forwarded headers information (`X-Forwarded-*`). From fae2d93525a381b7b494baf10d6deb4afd9a9966 Mon Sep 17 00:00:00 2001 From: Manuel Zapf Date: Thu, 16 Jul 2020 17:18:03 +0200 Subject: [PATCH 04/21] Get Entrypoints Port Address without protocol for redirect --- .../fixtures/redirection_with_protocol.json | 30 +++++++++++++++++++ pkg/provider/traefik/internal.go | 2 +- pkg/provider/traefik/internal_test.go | 22 ++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 pkg/provider/traefik/fixtures/redirection_with_protocol.json diff --git a/pkg/provider/traefik/fixtures/redirection_with_protocol.json b/pkg/provider/traefik/fixtures/redirection_with_protocol.json new file mode 100644 index 000000000..4ffbb756c --- /dev/null +++ b/pkg/provider/traefik/fixtures/redirection_with_protocol.json @@ -0,0 +1,30 @@ +{ + "http": { + "routers": { + "web-to-websecure": { + "entryPoints": [ + "web" + ], + "middlewares": [ + "redirect-web-to-websecure" + ], + "service": "noop@internal", + "rule": "HostRegexp(`{host:.+}`)" + } + }, + "middlewares": { + "redirect-web-to-websecure": { + "redirectScheme": { + "scheme": "https", + "port": "443", + "permanent": true + } + } + }, + "services": { + "noop": {} + } + }, + "tcp": {}, + "tls": {} +} \ No newline at end of file diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index a63a2f49b..4c341f56b 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -142,7 +142,7 @@ func (i *Provider) getEntryPointPort(name string, def *static.Redirections) (str return "", fmt.Errorf("'to' entry point field references a non-existing entry point: %s", def.EntryPoint.To) } - _, port, err := net.SplitHostPort(dst.Address) + _, port, err := net.SplitHostPort(dst.GetAddress()) if err != nil { return "", fmt.Errorf("invalid entry point %q address %q: %w", name, i.staticCfg.EntryPoints[def.EntryPoint.To].Address, err) diff --git a/pkg/provider/traefik/internal_test.go b/pkg/provider/traefik/internal_test.go index dd6ef8c6a..b9e0af9c4 100644 --- a/pkg/provider/traefik/internal_test.go +++ b/pkg/provider/traefik/internal_test.go @@ -232,6 +232,28 @@ func Test_createConfiguration(t *testing.T) { }, }, }, + { + desc: "redirection_with_protocol.json", + staticCfg: static.Configuration{ + EntryPoints: map[string]*static.EntryPoint{ + "web": { + Address: ":80", + HTTP: static.HTTPConfig{ + Redirections: &static.Redirections{ + EntryPoint: &static.RedirectEntryPoint{ + To: "websecure", + Scheme: "https", + Permanent: true, + }, + }, + }, + }, + "websecure": { + Address: ":443/tcp", + }, + }, + }, + }, } for _, test := range testCases { From 45f52ca29cc178dda500f7136c359ea3707ce068 Mon Sep 17 00:00:00 2001 From: Mickael Jeanroy <1067361+mjeanroy@users.noreply.github.com> Date: Thu, 16 Jul 2020 17:36:04 +0200 Subject: [PATCH 05/21] fix: access logs header names filtering is case insensitive --- integration/helloworld/helloworld.pb.go | 3 +- pkg/middlewares/accesslog/logger.go | 12 +++ pkg/middlewares/accesslog/logger_test.go | 109 +++++++++++++++++++++-- 3 files changed, 115 insertions(+), 9 deletions(-) diff --git a/integration/helloworld/helloworld.pb.go b/integration/helloworld/helloworld.pb.go index 7b3335444..96320f02a 100644 --- a/integration/helloworld/helloworld.pb.go +++ b/integration/helloworld/helloworld.pb.go @@ -16,9 +16,10 @@ It has these top-level messages: package helloworld import ( - proto "github.com/golang/protobuf/proto" fmt "fmt" math "math" + + proto "github.com/golang/protobuf/proto" ) import ( diff --git a/pkg/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go index c876efcb3..13af24c9b 100644 --- a/pkg/middlewares/accesslog/logger.go +++ b/pkg/middlewares/accesslog/logger.go @@ -6,6 +6,7 @@ import ( "io" "net" "net/http" + "net/textproto" "net/url" "os" "path/filepath" @@ -100,6 +101,17 @@ func NewHandler(config *types.AccessLog) (*Handler, error) { Level: logrus.InfoLevel, } + // Transform headers names in config to a canonical form, to be used as is without further transformations. + if config.Fields != nil && config.Fields.Headers != nil && len(config.Fields.Headers.Names) > 0 { + fields := map[string]string{} + + for h, v := range config.Fields.Headers.Names { + fields[textproto.CanonicalMIMEHeaderKey(h)] = v + } + + config.Fields.Headers.Names = fields + } + logHandler := &Handler{ config: config, logger: logger, diff --git a/pkg/middlewares/accesslog/logger_test.go b/pkg/middlewares/accesslog/logger_test.go index 6d1567db0..eeeceba7b 100644 --- a/pkg/middlewares/accesslog/logger_test.go +++ b/pkg/middlewares/accesslog/logger_test.go @@ -41,11 +41,7 @@ var ( ) func TestLogRotation(t *testing.T) { - tempDir, err := ioutil.TempDir("", "traefik_") - if err != nil { - t.Fatalf("Error setting up temporary directory: %s", err) - } - defer os.RemoveAll(tempDir) + tempDir := createTempDir(t, "traefik_") fileName := filepath.Join(tempDir, "traefik.log") rotatedFileName := fileName + ".rotated" @@ -119,9 +115,106 @@ func lineCount(t *testing.T, fileName string) int { return count } +func TestLoggerHeaderFields(t *testing.T) { + tmpDir := createTempDir(t, CommonFormat) + + expectedValue := "expectedValue" + + testCases := []struct { + desc string + accessLogFields types.AccessLogFields + header string + expected string + }{ + { + desc: "with default mode", + header: "User-Agent", + expected: types.AccessLogDrop, + accessLogFields: types.AccessLogFields{ + DefaultMode: types.AccessLogDrop, + Headers: &types.FieldHeaders{ + DefaultMode: types.AccessLogDrop, + Names: map[string]string{}, + }, + }, + }, + { + desc: "with exact header name", + header: "User-Agent", + expected: types.AccessLogKeep, + accessLogFields: types.AccessLogFields{ + DefaultMode: types.AccessLogDrop, + Headers: &types.FieldHeaders{ + DefaultMode: types.AccessLogDrop, + Names: map[string]string{ + "User-Agent": types.AccessLogKeep, + }, + }, + }, + }, + { + desc: "with case insensitive match on header name", + header: "User-Agent", + expected: types.AccessLogKeep, + accessLogFields: types.AccessLogFields{ + DefaultMode: types.AccessLogDrop, + Headers: &types.FieldHeaders{ + DefaultMode: types.AccessLogDrop, + Names: map[string]string{ + "user-agent": types.AccessLogKeep, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + logFile, err := ioutil.TempFile(tmpDir, "*.log") + require.NoError(t, err) + + config := &types.AccessLog{ + FilePath: logFile.Name(), + Format: CommonFormat, + Fields: &test.accessLogFields, + } + + logger, err := NewHandler(config) + require.NoError(t, err) + defer logger.Close() + + if config.FilePath != "" { + _, err = os.Stat(config.FilePath) + require.NoError(t, err, fmt.Sprintf("logger should create %s", config.FilePath)) + } + + req := &http.Request{ + Header: map[string][]string{}, + URL: &url.URL{ + Path: testPath, + }, + } + req.Header.Set(test.header, expectedValue) + + logger.ServeHTTP(httptest.NewRecorder(), req, http.HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { + writer.WriteHeader(http.StatusOK) + })) + + logData, err := ioutil.ReadFile(logFile.Name()) + require.NoError(t, err) + + if test.expected == types.AccessLogDrop { + assert.NotContains(t, string(logData), expectedValue) + } else { + assert.Contains(t, string(logData), expectedValue) + } + }) + } +} + func TestLoggerCLF(t *testing.T) { tmpDir := createTempDir(t, CommonFormat) - defer os.RemoveAll(tmpDir) logFilePath := filepath.Join(tmpDir, logFileNameSuffix) config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat} @@ -136,7 +229,6 @@ func TestLoggerCLF(t *testing.T) { func TestAsyncLoggerCLF(t *testing.T) { tmpDir := createTempDir(t, CommonFormat) - defer os.RemoveAll(tmpDir) logFilePath := filepath.Join(tmpDir, logFileNameSuffix) config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat, BufferingSize: 1024} @@ -358,7 +450,6 @@ func TestLoggerJSON(t *testing.T) { t.Parallel() tmpDir := createTempDir(t, JSONFormat) - defer os.RemoveAll(tmpDir) logFilePath := filepath.Join(tmpDir, logFileNameSuffix) @@ -642,6 +733,8 @@ func createTempDir(t *testing.T, prefix string) string { tmpDir, err := ioutil.TempDir("", prefix) require.NoError(t, err, "failed to create temp dir") + t.Cleanup(func() { _ = os.RemoveAll(tmpDir) }) + return tmpDir } From 1dc6f39b559c79df2e5a56003b635dcabdd716ed Mon Sep 17 00:00:00 2001 From: Neil McAllister Date: Fri, 17 Jul 2020 01:08:03 -0700 Subject: [PATCH 06/21] Update availability info --- docs/content/plugins/overview.md | 3 +++ docs/content/plugins/using-plugins.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/content/plugins/overview.md b/docs/content/plugins/overview.md index 902b61a75..afa5c2716 100644 --- a/docs/content/plugins/overview.md +++ b/docs/content/plugins/overview.md @@ -10,6 +10,9 @@ For example, Traefik plugins can add features to modify requests or headers, iss Traefik Pilot can also monitor connected Traefik instances and issue alerts when one is not responding, or when it is subject to security vulnerabilities. +!!! note "Availability" + Plugins are available for Traefik v2.3.0-rc1 and later. + !!! danger "Experimental Features" Plugins can potentially modify the behavior of Traefik in unforeseen ways. Exercise caution when adding new plugins to production Traefik instances. diff --git a/docs/content/plugins/using-plugins.md b/docs/content/plugins/using-plugins.md index d80e92b08..65810a9b6 100644 --- a/docs/content/plugins/using-plugins.md +++ b/docs/content/plugins/using-plugins.md @@ -1,6 +1,6 @@ # Using Plugins -Since v2.3, plugins are available to any Traefik instance that is [registered](overview.md#connecting-to-traefik-pilot) with Traefik Pilot. +Plugins are available to any instance of Traefik v2.3 or later that is [registered](overview.md#connecting-to-traefik-pilot) with Traefik Pilot. Plugins are hosted on GitHub, but you can browse plugins to add to your registered Traefik instances from the Traefik Pilot UI. !!! danger "Experimental Features" From 44a244b1cb7380dc58a1cf913720b58ea2347be2 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 17 Jul 2020 11:04:04 +0200 Subject: [PATCH 07/21] file parser: skip nil value. --- pkg/config/file/raw_node.go | 4 ++++ pkg/config/file/raw_node_test.go | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/pkg/config/file/raw_node.go b/pkg/config/file/raw_node.go index 8b5509ad5..9f9b3857b 100644 --- a/pkg/config/file/raw_node.go +++ b/pkg/config/file/raw_node.go @@ -28,6 +28,10 @@ func decodeRaw(node *parser.Node, vData reflect.Value, filters ...string) error sortedKeys := sortKeys(vData, filters) for _, key := range sortedKeys { + if vData.MapIndex(key).IsNil() { + continue + } + value := reflect.ValueOf(vData.MapIndex(key).Interface()) child := &parser.Node{Name: key.String()} diff --git a/pkg/config/file/raw_node_test.go b/pkg/config/file/raw_node_test.go index 5b4ae64ae..6811543cf 100644 --- a/pkg/config/file/raw_node_test.go +++ b/pkg/config/file/raw_node_test.go @@ -524,6 +524,20 @@ func Test_decodeRawToNode(t *testing.T) { }, }, }, + { + desc: "nil value", + data: map[string]interface{}{ + "fii": map[interface{}]interface{}{ + "fuu": nil, + }, + }, + expected: &parser.Node{ + Name: "traefik", + Children: []*parser.Node{ + {Name: "fii"}, + }, + }, + }, } for _, test := range testCases { From 0b7aaa3643cafa105c5a63ce7b41ffe91347d2c9 Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Fri, 17 Jul 2020 15:38:04 +0200 Subject: [PATCH 08/21] Fix domain fronting Co-authored-by: Ludovic Fernandez --- docs/content/routing/routers/index.md | 5 + integration/acme_test.go | 2 +- .../fixtures/https/https_domain_fronting.toml | 53 +++++++++ integration/headers_test.go | 4 +- integration/https_test.go | 109 ++++++++++++++++-- pkg/server/router/tcp/router.go | 41 ++++++- 6 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 integration/fixtures/https/https_domain_fronting.toml diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 81d5a4083..d97e079a7 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -472,6 +472,11 @@ It refers to a [TLS Options](../../https/tls.md#tls-options) and will be applied the TLS option is picked from the mapping mentioned above and based on the server name provided during the TLS handshake, and it all happens before routing actually occurs. +!!! info "Domain Fronting" + + In the case of domain fronting, + if the TLS options associated with the Host Header and the SNI are different then Traefik will respond with a status code `421`. + ??? example "Configuring the TLS options" ```toml tab="File (TOML)" diff --git a/integration/acme_test.go b/integration/acme_test.go index a5e34e695..9c757b61d 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -444,7 +444,7 @@ func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase acmeTestCase) { // A real file is needed to have the right mode on acme.json file defer os.Remove("/tmp/acme.json") - backend := startTestServer("9010", http.StatusOK) + backend := startTestServer("9010", http.StatusOK, "") defer backend.Close() for _, sub := range testCase.subCases { diff --git a/integration/fixtures/https/https_domain_fronting.toml b/integration/fixtures/https/https_domain_fronting.toml new file mode 100644 index 000000000..80011ee8e --- /dev/null +++ b/integration/fixtures/https/https_domain_fronting.toml @@ -0,0 +1,53 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints.websecure] + address = ":4443" + +[api] + insecure = true + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers.router1] + rule = "Host(`site1.www.snitest.com`)" + service = "service1" + [http.routers.router1.tls] + +[http.routers.router2] + rule = "Host(`site2.www.snitest.com`)" + service = "service2" + [http.routers.router2.tls] + +[http.routers.router3] + rule = "Host(`site3.www.snitest.com`)" + service = "service3" + [http.routers.router3.tls] + options = "mytls" + +[http.services.service1] + [[http.services.service1.loadBalancer.servers]] + url = "http://127.0.0.1:9010" + +[http.services.service2] + [[http.services.service2.loadBalancer.servers]] + url = "http://127.0.0.1:9020" + +[http.services.service3] + [[http.services.service3.loadBalancer.servers]] + url = "http://127.0.0.1:9030" + +[[tls.certificates]] + certFile = "fixtures/https/wildcard.www.snitest.com.cert" + keyFile = "fixtures/https/wildcard.www.snitest.com.key" + +[tls.options] + [tls.options.mytls] + maxVersion = "VersionTLS12" diff --git a/integration/headers_test.go b/integration/headers_test.go index d598873ea..a5cc4259a 100644 --- a/integration/headers_test.go +++ b/integration/headers_test.go @@ -35,7 +35,7 @@ func (s *HeadersSuite) TestCorsResponses(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - backend := startTestServer("9000", http.StatusOK) + backend := startTestServer("9000", http.StatusOK, "") defer backend.Close() err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) @@ -124,7 +124,7 @@ func (s *HeadersSuite) TestSecureHeadersResponses(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - backend := startTestServer("9000", http.StatusOK) + backend := startTestServer("9000", http.StatusOK, "") defer backend.Close() err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) diff --git a/integration/https_test.go b/integration/https_test.go index 9ac854eba..6b39e343f 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -73,8 +73,8 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -129,8 +129,8 @@ func (s *HTTPSSuite) TestWithTLSOptions(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -215,8 +215,8 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.net`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -733,9 +733,12 @@ func (s *HTTPSSuite) TestWithRootCAsFileForHTTPSOnBackend(c *check.C) { c.Assert(err, checker.IsNil) } -func startTestServer(port string, statusCode int) (ts *httptest.Server) { +func startTestServer(port string, statusCode int, textContent string) (ts *httptest.Server) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(statusCode) + if textContent != "" { + _, _ = w.Write([]byte(textContent)) + } }) listener, err := net.Listen("tcp", "127.0.0.1:"+port) if err != nil { @@ -787,8 +790,8 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithNoChange(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr1.TLSClientConfig.ServerName+"`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -856,8 +859,8 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)")) c.Assert(err, checker.IsNil) - backend1 := startTestServer("9010", http.StatusNoContent) - backend2 := startTestServer("9020", http.StatusResetContent) + backend1 := startTestServer("9010", http.StatusNoContent, "") + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend1.Close() defer backend2.Close() @@ -919,7 +922,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion(c err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)")) c.Assert(err, checker.IsNil) - backend2 := startTestServer("9020", http.StatusResetContent) + backend2 := startTestServer("9020", http.StatusResetContent, "") defer backend2.Close() @@ -1111,3 +1114,85 @@ func (s *HTTPSSuite) TestWithSNIDynamicCaseInsensitive(c *check.C) { proto := conn.ConnectionState().NegotiatedProtocol c.Assert(proto, checker.Equals, "h2") } + +// TestWithDomainFronting verify the domain fronting behavior +func (s *HTTPSSuite) TestWithDomainFronting(c *check.C) { + backend := startTestServer("9010", http.StatusOK, "server1") + defer backend.Close() + backend2 := startTestServer("9020", http.StatusOK, "server2") + defer backend2.Close() + backend3 := startTestServer("9030", http.StatusOK, "server3") + defer backend3.Close() + + file := s.adaptFile(c, "fixtures/https/https_domain_fronting.toml", struct{}{}) + defer os.Remove(file) + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`site1.www.snitest.com`)")) + c.Assert(err, checker.IsNil) + + testCases := []struct { + desc string + hostHeader string + serverName string + expectedContent string + expectedStatusCode int + }{ + { + desc: "SimpleCase", + hostHeader: "site1.www.snitest.com", + serverName: "site1.www.snitest.com", + expectedContent: "server1", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Domain Fronting with same tlsOptions should follow header", + hostHeader: "site1.www.snitest.com", + serverName: "site2.www.snitest.com", + expectedContent: "server1", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Domain Fronting with same tlsOptions should follow header (2)", + hostHeader: "site2.www.snitest.com", + serverName: "site1.www.snitest.com", + expectedContent: "server2", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Domain Fronting with different tlsOptions should produce a 421", + hostHeader: "site2.www.snitest.com", + serverName: "site3.www.snitest.com", + expectedContent: "", + expectedStatusCode: http.StatusMisdirectedRequest, + }, + { + desc: "Domain Fronting with different tlsOptions should produce a 421 (2)", + hostHeader: "site3.www.snitest.com", + serverName: "site1.www.snitest.com", + expectedContent: "", + expectedStatusCode: http.StatusMisdirectedRequest, + }, + { + desc: "Case insensitive", + hostHeader: "sIte1.www.snitest.com", + serverName: "sitE1.www.snitest.com", + expectedContent: "server1", + expectedStatusCode: http.StatusOK, + }, + } + + for _, test := range testCases { + test := test + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443", nil) + c.Assert(err, checker.IsNil) + req.Host = test.hostHeader + err = try.RequestWithTransport(req, 500*time.Millisecond, &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, ServerName: test.serverName}}, try.StatusCodeIs(test.expectedStatusCode), try.BodyContains(test.expectedContent)) + c.Assert(err, checker.IsNil) + } +} diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index e5cee5065..8d8dbc7b8 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "strings" "github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/log" @@ -99,14 +100,13 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err) } - router.HTTPSHandler(handlerHTTPS, defaultTLSConf) - if len(configsHTTP) > 0 { router.AddRouteHTTPTLS("*", defaultTLSConf) } // Keyed by domain, then by options reference. tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{} + tlsOptionsForHost := map[string]string{} for routerHTTPName, routerHTTPConfig := range configsHTTP { if len(routerHTTPConfig.TLS.Options) == 0 || routerHTTPConfig.TLS.Options == defaultTLSConfigName { continue @@ -148,10 +148,33 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string routerName: routerHTTPName, TLSConfig: tlsConf, } + + lowerDomain := strings.ToLower(domain) + if _, ok := tlsOptionsForHost[lowerDomain]; ok { + // Multiple tlsOptions fallback to default + tlsOptionsForHost[lowerDomain] = "default" + } else { + tlsOptionsForHost[lowerDomain] = routerHTTPConfig.TLS.Options + } } } } + sniCheck := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.TLS != nil && !strings.EqualFold(req.Host, req.TLS.ServerName) { + tlsOptionSNI := findTLSOptionName(tlsOptionsForHost, req.TLS.ServerName) + tlsOptionHeader := findTLSOptionName(tlsOptionsForHost, req.Host) + + if tlsOptionHeader != tlsOptionSNI { + http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest) + return + } + } + handlerHTTPS.ServeHTTP(rw, req) + }) + + router.HTTPSHandler(sniCheck, defaultTLSConf) + logger := log.FromContext(ctx) for hostSNI, tlsConfigs := range tlsOptionsForHostSNI { if len(tlsConfigs) == 1 { @@ -248,3 +271,17 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string return router, nil } + +func findTLSOptionName(tlsOptionsForHost map[string]string, host string) string { + tlsOptions, ok := tlsOptionsForHost[host] + if ok { + return tlsOptions + } + + tlsOptions, ok = tlsOptionsForHost[strings.ToLower(host)] + if ok { + return tlsOptions + } + + return "default" +} From ff16925f63cdaf2837392399b8a8b3dce7cc7b0d Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 17 Jul 2020 17:54:04 +0200 Subject: [PATCH 09/21] Prepare release v2.2.6 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ee548bd..129ddfa3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [v2.2.6](https://github.com/containous/traefik/tree/v2.2.6) (2020-07-17) +[All Commits](https://github.com/containous/traefik/compare/v2.2.5...v2.2.6) + +**Bug fixes:** +- **[logs]** fix: access logs header names filtering is case insensitive ([#6900](https://github.com/containous/traefik/pull/6900) by [mjeanroy](https://github.com/mjeanroy)) +- **[provider]** Get Entrypoints Port Address without protocol for redirect ([#7047](https://github.com/containous/traefik/pull/7047) by [SantoDE](https://github.com/SantoDE)) +- **[tls]** Fix domain fronting ([#7064](https://github.com/containous/traefik/pull/7064) by [juliens](https://github.com/juliens)) + +**Documentation:** +- fix: documentation references. ([#7049](https://github.com/containous/traefik/pull/7049) by [ldez](https://github.com/ldez)) +- Add example for entrypoint on one ip address ([#6483](https://github.com/containous/traefik/pull/6483) by [SimonHeimberg](https://github.com/SimonHeimberg)) + ## [v2.2.5](https://github.com/containous/traefik/tree/v2.2.5) (2020-07-13) [All Commits](https://github.com/containous/traefik/compare/v2.2.4...v2.2.5) From dcd0cda0c62126ee8a9dd9574ed096ca80d34b24 Mon Sep 17 00:00:00 2001 From: Stephen Solka Date: Sun, 19 Jul 2020 07:10:03 -0400 Subject: [PATCH 10/21] prefer NoError/Error over Nil/NotNil --- pkg/healthcheck/healthcheck_test.go | 6 +++--- pkg/provider/kubernetes/ingress/kubernetes_test.go | 2 +- pkg/server/middleware/middlewares_test.go | 2 +- pkg/server/service/tcp/service_test.go | 2 +- pkg/server/service/udp/service_test.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go index a1fe3f265..00b79be2f 100644 --- a/pkg/healthcheck/healthcheck_test.go +++ b/pkg/healthcheck/healthcheck_test.go @@ -448,9 +448,9 @@ func TestLBStatusUpdater(t *testing.T) { svInfo := &runtime.ServiceInfo{} lbsu := NewLBStatusUpdater(lb, svInfo) newServer, err := url.Parse("http://foo.com") - assert.Nil(t, err) + assert.NoError(t, err) err = lbsu.UpsertServer(newServer, roundrobin.Weight(1)) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, len(lbsu.Servers()), 1) assert.Equal(t, len(lbsu.BalancerHandler.(*testLoadBalancer).Options()), 1) statuses := svInfo.GetAllStatus() @@ -461,7 +461,7 @@ func TestLBStatusUpdater(t *testing.T) { break } err = lbsu.RemoveServer(newServer) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, len(lbsu.Servers()), 0) statuses = svInfo.GetAllStatus() assert.Equal(t, len(statuses), 1) diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index ddd11ff7e..57e6d4794 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -1179,7 +1179,7 @@ func TestGetCertificates(t *testing.T) { if test.errResult != "" { assert.EqualError(t, err, test.errResult) } else { - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, test.result, tlsConfigs) } }) diff --git a/pkg/server/middleware/middlewares_test.go b/pkg/server/middleware/middlewares_test.go index 5acee8396..68008b644 100644 --- a/pkg/server/middleware/middlewares_test.go +++ b/pkg/server/middleware/middlewares_test.go @@ -276,7 +276,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { handlers, err := result.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) if test.expectedError != nil { - require.NotNil(t, err) + require.Error(t, err) require.Equal(t, test.expectedError.Error(), err.Error()) } else { require.NoError(t, err) diff --git a/pkg/server/service/tcp/service_test.go b/pkg/server/service/tcp/service_test.go index be5008d3d..b1943463d 100644 --- a/pkg/server/service/tcp/service_test.go +++ b/pkg/server/service/tcp/service_test.go @@ -193,7 +193,7 @@ func TestManager_BuildTCP(t *testing.T) { assert.EqualError(t, err, test.expectedError) require.Nil(t, handler) } else { - assert.Nil(t, err) + assert.NoError(t, err) require.NotNil(t, handler) } }) diff --git a/pkg/server/service/udp/service_test.go b/pkg/server/service/udp/service_test.go index 5308ad114..a17828d47 100644 --- a/pkg/server/service/udp/service_test.go +++ b/pkg/server/service/udp/service_test.go @@ -193,7 +193,7 @@ func TestManager_BuildUDP(t *testing.T) { assert.EqualError(t, err, test.expectedError) require.Nil(t, handler) } else { - assert.Nil(t, err) + assert.NoError(t, err) require.NotNil(t, handler) } }) From 2c7f6e4def0436a42a06118a455fcdf7b2e2fcb3 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 20 Jul 2020 18:32:03 +0200 Subject: [PATCH 11/21] fix: drop host port to compare with SNI. --- integration/https_test.go | 30 ++++++++++++++++++++++++++++ pkg/server/router/tcp/router.go | 35 ++++++++++++++++++++++++++------- pkg/tcp/router.go | 3 ++- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/integration/https_test.go b/integration/https_test.go index 6b39e343f..e6c6c0553 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -1150,6 +1150,34 @@ func (s *HTTPSSuite) TestWithDomainFronting(c *check.C) { expectedContent: "server1", expectedStatusCode: http.StatusOK, }, + { + desc: "Simple case with port in the Host Header", + hostHeader: "site3.www.snitest.com:4443", + serverName: "site3.www.snitest.com", + expectedContent: "server3", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Spaces after the host header", + hostHeader: "site3.www.snitest.com ", + serverName: "site3.www.snitest.com", + expectedContent: "server3", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Spaces after the servername", + hostHeader: "site3.www.snitest.com", + serverName: "site3.www.snitest.com ", + expectedContent: "server3", + expectedStatusCode: http.StatusOK, + }, + { + desc: "Spaces after the servername and host header", + hostHeader: "site3.www.snitest.com ", + serverName: "site3.www.snitest.com ", + expectedContent: "server3", + expectedStatusCode: http.StatusOK, + }, { desc: "Domain Fronting with same tlsOptions should follow header", hostHeader: "site1.www.snitest.com", @@ -1189,9 +1217,11 @@ func (s *HTTPSSuite) TestWithDomainFronting(c *check.C) { for _, test := range testCases { test := test + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443", nil) c.Assert(err, checker.IsNil) req.Host = test.hostHeader + err = try.RequestWithTransport(req, 500*time.Millisecond, &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, ServerName: test.serverName}}, try.StatusCodeIs(test.expectedStatusCode), try.BodyContains(test.expectedContent)) c.Assert(err, checker.IsNil) } diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 8d8dbc7b8..f164039c7 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "errors" "fmt" + "net" "net/http" "strings" @@ -141,6 +142,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string continue } + // domain is already in lower case thanks to the domain parsing if tlsOptionsForHostSNI[domain] == nil { tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig) } @@ -149,27 +151,46 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string TLSConfig: tlsConf, } - lowerDomain := strings.ToLower(domain) - if _, ok := tlsOptionsForHost[lowerDomain]; ok { + if _, ok := tlsOptionsForHost[domain]; ok { // Multiple tlsOptions fallback to default - tlsOptionsForHost[lowerDomain] = "default" + tlsOptionsForHost[domain] = "default" } else { - tlsOptionsForHost[lowerDomain] = routerHTTPConfig.TLS.Options + tlsOptionsForHost[domain] = routerHTTPConfig.TLS.Options } } } } sniCheck := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - if req.TLS != nil && !strings.EqualFold(req.Host, req.TLS.ServerName) { - tlsOptionSNI := findTLSOptionName(tlsOptionsForHost, req.TLS.ServerName) - tlsOptionHeader := findTLSOptionName(tlsOptionsForHost, req.Host) + if req.TLS == nil { + handlerHTTPS.ServeHTTP(rw, req) + return + } + + host, _, err := net.SplitHostPort(req.Host) + if err != nil { + host = req.Host + } + + host = strings.TrimSpace(host) + serverName := strings.TrimSpace(req.TLS.ServerName) + + // Domain Fronting + if !strings.EqualFold(host, serverName) { + tlsOptionSNI := findTLSOptionName(tlsOptionsForHost, serverName) + tlsOptionHeader := findTLSOptionName(tlsOptionsForHost, host) if tlsOptionHeader != tlsOptionSNI { + log.WithoutContext(). + WithField("host", host). + WithField("req.Host", req.Host). + WithField("req.TLS.ServerName", req.TLS.ServerName). + Debugf("TLS options difference: SNI=%s, Header:%s", tlsOptionSNI, tlsOptionHeader) http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest) return } } + handlerHTTPS.ServeHTTP(rw, req) }) diff --git a/pkg/tcp/router.go b/pkg/tcp/router.go index c66e987ce..601f18190 100644 --- a/pkg/tcp/router.go +++ b/pkg/tcp/router.go @@ -11,6 +11,7 @@ import ( "time" "github.com/containous/traefik/v2/pkg/log" + "github.com/containous/traefik/v2/pkg/types" ) // Router is a TCP router. @@ -65,7 +66,7 @@ func (r *Router) ServeTCP(conn WriteCloser) { } // FIXME Optimize and test the routing table before helloServerName - serverName = strings.ToLower(serverName) + serverName = types.CanonicalDomain(serverName) if r.routingTable != nil && serverName != "" { if target, ok := r.routingTable[serverName]; ok { target.ServeTCP(r.GetConn(conn, peeked)) From bbbc18fd843ee3774569a29a2d0d293ccc3f14f4 Mon Sep 17 00:00:00 2001 From: Romain Date: Mon, 20 Jul 2020 18:48:04 +0200 Subject: [PATCH 12/21] Prepare release 2.2.7 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 129ddfa3b..585647ac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [v2.2.7](https://github.com/containous/traefik/tree/v2.2.7) (2020-07-20) +[All Commits](https://github.com/containous/traefik/compare/v2.2.6...v2.2.7) + +**Bug fixes:** +- **[server,tls]** fix: drop host port to compare with SNI. ([#7071](https://github.com/containous/traefik/pull/7071) by [ldez](https://github.com/ldez)) + ## [v2.2.6](https://github.com/containous/traefik/tree/v2.2.6) (2020-07-17) [All Commits](https://github.com/containous/traefik/compare/v2.2.5...v2.2.6) From a136c46148f0edcda609300a437e3a96aa487433 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Tue, 21 Jul 2020 15:32:04 +0200 Subject: [PATCH 13/21] Use semantic versioning to enable ingress class support --- pkg/provider/kubernetes/ingress/client.go | 38 +++++++++---------- .../kubernetes/ingress/client_mock_test.go | 17 ++++----- pkg/provider/kubernetes/ingress/kubernetes.go | 4 +- .../kubernetes/ingress/kubernetes_test.go | 24 ++++++------ 4 files changed, 40 insertions(+), 43 deletions(-) diff --git a/pkg/provider/kubernetes/ingress/client.go b/pkg/provider/kubernetes/ingress/client.go index f4af6c377..84d6bb835 100644 --- a/pkg/provider/kubernetes/ingress/client.go +++ b/pkg/provider/kubernetes/ingress/client.go @@ -5,11 +5,11 @@ import ( "errors" "fmt" "io/ioutil" - "strconv" "time" "github.com/containous/traefik/v2/pkg/log" "github.com/golang/protobuf/proto" + "github.com/hashicorp/go-version" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" networkingv1beta1 "k8s.io/api/networking/v1beta1" @@ -55,7 +55,7 @@ type Client interface { GetSecret(namespace, name string) (*corev1.Secret, bool, error) GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error) UpdateIngressStatus(ing *networkingv1beta1.Ingress, ip, hostname string) error - GetServerVersion() (major, minor int, err error) + GetServerVersion() (*version.Version, error) } type clientWrapper struct { @@ -163,13 +163,13 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (< } } - // If the kubernetes cluster is v1.18+, we can use the new IngressClass objects - major, minor, err := c.GetServerVersion() + serverVersion, err := c.GetServerVersion() if err != nil { - return nil, err + log.WithoutContext().Errorf("Failed to get server version: %v", err) + return eventCh, nil } - if major >= 1 && minor >= 18 { + if supportsIngressClass(serverVersion) { c.clusterFactory = informers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod) c.clusterFactory.Networking().V1beta1().IngressClasses().Informer().AddEventHandler(eventHandler) c.clusterFactory.Start(stopCh) @@ -381,23 +381,13 @@ func (c *clientWrapper) newResourceEventHandler(events chan<- interface{}) cache } // GetServerVersion returns the cluster server version, or an error. -func (c *clientWrapper) GetServerVersion() (major, minor int, err error) { - version, err := c.clientset.Discovery().ServerVersion() +func (c *clientWrapper) GetServerVersion() (*version.Version, error) { + serverVersion, err := c.clientset.Discovery().ServerVersion() if err != nil { - return 0, 0, fmt.Errorf("could not determine cluster API version: %w", err) + return nil, fmt.Errorf("could not retrieve server version: %w", err) } - major, err = strconv.Atoi(version.Major) - if err != nil { - return 0, 0, fmt.Errorf("could not determine cluster major API version: %w", err) - } - - minor, err = strconv.Atoi(version.Minor) - if err != nil { - return 0, 0, fmt.Errorf("could not determine cluster minor API version: %w", err) - } - - return major, minor, nil + return version.NewVersion(serverVersion.GitVersion) } // eventHandlerFunc will pass the obj on to the events channel or drop it. @@ -432,3 +422,11 @@ func (c *clientWrapper) isWatchedNamespace(ns string) bool { } return false } + +// IngressClass objects are supported since Kubernetes v1.18. +// See https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class +func supportsIngressClass(serverVersion *version.Version) bool { + ingressClassVersion := version.Must(version.NewVersion("1.18")) + + return ingressClassVersion.LessThanOrEqual(serverVersion) +} diff --git a/pkg/provider/kubernetes/ingress/client_mock_test.go b/pkg/provider/kubernetes/ingress/client_mock_test.go index 6bf5716c1..410c96d52 100644 --- a/pkg/provider/kubernetes/ingress/client_mock_test.go +++ b/pkg/provider/kubernetes/ingress/client_mock_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "github.com/containous/traefik/v2/pkg/provider/kubernetes/k8s" + "github.com/hashicorp/go-version" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" "k8s.io/api/networking/v1beta1" @@ -20,8 +21,7 @@ type clientMock struct { endpoints []*corev1.Endpoints ingressClass *networkingv1beta1.IngressClass - serverMajor int - serverMinor int + serverVersion *version.Version apiServiceError error apiSecretError error @@ -31,11 +31,10 @@ type clientMock struct { watchChan chan interface{} } -func newClientMock(major, minor int, paths ...string) clientMock { - c := clientMock{ - serverMajor: major, - serverMinor: minor, - } +func newClientMock(serverVersion string, paths ...string) clientMock { + c := clientMock{} + + c.serverVersion = version.Must(version.NewVersion(serverVersion)) for _, path := range paths { yamlContent, err := ioutil.ReadFile(path) @@ -75,8 +74,8 @@ func (c clientMock) GetIngresses() []*v1beta1.Ingress { return c.ingresses } -func (c clientMock) GetServerVersion() (major, minor int, err error) { - return c.serverMajor, c.serverMinor, nil +func (c clientMock) GetServerVersion() (*version.Version, error) { + return c.serverVersion, nil } func (c clientMock) GetService(namespace, name string) (*corev1.Service, bool, error) { diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index c6401e44e..b67251548 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -183,7 +183,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl TCP: &dynamic.TCPConfiguration{}, } - major, minor, err := client.GetServerVersion() + serverVersion, err := client.GetServerVersion() if err != nil { log.FromContext(ctx).Errorf("Failed to get server version: %v", err) return conf @@ -191,7 +191,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl var ingressClass *networkingv1beta1.IngressClass - if major >= 1 && minor >= 18 { + if supportsIngressClass(serverVersion) { ic, err := client.GetIngressClass() if err != nil { log.FromContext(ctx).Errorf("Failed to find an ingress class: %v", err) diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 57e6d4794..a819ce16e 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -24,10 +24,10 @@ func Bool(v bool) *bool { return &v } func TestLoadConfigurationFromIngresses(t *testing.T) { testCases := []struct { - desc string - ingressClass string - serverMinor int - expected *dynamic.Configuration + desc string + ingressClass string + serverVersion string + expected *dynamic.Configuration }{ { desc: "Empty ingresses", @@ -925,8 +925,8 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, { - desc: "v18 Ingress with ingressClass", - serverMinor: 18, + desc: "v18 Ingress with ingressClass", + serverVersion: "v1.18", expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{}, HTTP: &dynamic.HTTPConfiguration{ @@ -953,8 +953,8 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, { - desc: "v18 Ingress with missing ingressClass", - serverMinor: 18, + desc: "v18 Ingress with missing ingressClass", + serverVersion: "v1.18", expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{}, HTTP: &dynamic.HTTPConfiguration{ @@ -993,12 +993,12 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { paths = append(paths, generateTestFilename("_ingressclass", test.desc)) } - serverMinor := 17 - if test.serverMinor != 0 { - serverMinor = test.serverMinor + serverVersion := test.serverVersion + if serverVersion == "" { + serverVersion = "v1.17" } - clientMock := newClientMock(1, serverMinor, paths...) + clientMock := newClientMock(serverVersion, paths...) p := Provider{IngressClass: test.ingressClass} conf := p.loadConfigurationFromIngresses(context.Background(), clientMock) From 1443c8d4c6b028a2d3cc6aa406d50a64dda21984 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Tue, 21 Jul 2020 18:06:04 +0200 Subject: [PATCH 14/21] Add migration documentation for IngressClass --- docs/content/migration/v2.md | 5 +++++ .../reference/dynamic-configuration/kubernetes-crd-rbac.yml | 1 + pkg/tls/certificate_store.go | 6 +++--- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index c6fd2d983..f8ced1a5e 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -320,3 +320,8 @@ Since `v2.2.5` this rule has been removed, and you should not use it anymore. ### File Provider The file parser has been changed, since v2.3 the unknown options/fields in a dynamic configuration file are treated as errors. + +### IngressClass + +In `v2.3`, the support of `IngressClass`, which is available since Kubernetes version `1.18`, has been introduced. +In order to be able to use this new resource the [Kubernetes RBAC](../reference/dynamic-configuration/kubernetes-crd.md#rbac) must be updated. diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml index 3e7337031..88b788089 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml @@ -18,6 +18,7 @@ rules: - extensions resources: - ingresses + - ingressclasses verbs: - get - list diff --git a/pkg/tls/certificate_store.go b/pkg/tls/certificate_store.go index 9958e8641..524a5cdba 100644 --- a/pkg/tls/certificate_store.go +++ b/pkg/tls/certificate_store.go @@ -13,14 +13,14 @@ import ( "github.com/patrickmn/go-cache" ) -// CertificateStore store for dynamic and static certificates. +// CertificateStore store for dynamic certificates. type CertificateStore struct { DynamicCerts *safe.Safe DefaultCertificate *tls.Certificate CertCache *cache.Cache } -// NewCertificateStore create a store for dynamic and static certificates. +// NewCertificateStore create a store for dynamic certificates. func NewCertificateStore() *CertificateStore { return &CertificateStore{ DynamicCerts: &safe.Safe{}, @@ -37,7 +37,7 @@ func (c CertificateStore) getDefaultCertificateDomains() []string { x509Cert, err := x509.ParseCertificate(c.DefaultCertificate.Certificate[0]) if err != nil { - log.WithoutContext().Errorf("Could not parse default certicate: %v", err) + log.WithoutContext().Errorf("Could not parse default certificate: %v", err) return allCerts } From a6c6127e3372d9b47d84a4cd574d52d2971c7bb8 Mon Sep 17 00:00:00 2001 From: Filip Kszczot Date: Tue, 28 Jul 2020 01:02:03 +0200 Subject: [PATCH 15/21] spelling(docs/content/routing/providers/docker.md) --- docs/content/https/acme.md | 2 +- docs/content/routing/providers/docker.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index f07cf410d..4cca2ec62 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -362,7 +362,7 @@ For complete details, refer to your provider's _Additional configuration_ link. | [Zonomi](https://zonomi.com) | `zonomi` | `ZONOMI_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/zonomi) | [^1]: more information about the HTTP message format can be found [here](https://go-acme.github.io/lego/dns/httpreq/) -[^2]: [providing_credentials_to_your_application](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application) +[^2]: [providing_credentials_to_your_application](https://cloud.google.com/docs/authentication/production) [^3]: [google/default.go](https://github.com/golang/oauth2/blob/36a7019397c4c86cf59eeab3bc0d188bac444277/google/default.go#L61-L76) [^4]: `docker stack` remark: there is no way to support terminal attached to container when deploying with `docker stack`, so you might need to run container with `docker run -it` to generate certificates using `manual` provider. [^5]: The `Global API Key` needs to be used, not the `Origin CA Key`. diff --git a/docs/content/routing/providers/docker.md b/docs/content/routing/providers/docker.md index e19aafaa5..99128c188 100644 --- a/docs/content/routing/providers/docker.md +++ b/docs/content/routing/providers/docker.md @@ -535,7 +535,7 @@ You can declare UDP Routers and/or Services using labels. my-container: # ... labels: - - "traefik.udp.routers.my-router.entrypoint=udp" + - "traefik.udp.routers.my-router.entrypoints=udp" - "traefik.udp.services.my-service.loadbalancer.server.port=4123" ``` From e63db782c11c7b8bfce30be4c902e7ef8f9f33d2 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 28 Jul 2020 10:08:03 +0200 Subject: [PATCH 16/21] fix: clean X-Forwarded-Prefix header for the dashboard. --- pkg/api/dashboard.go | 23 +++++++++++++++-- pkg/api/dashboard_test.go | 54 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 pkg/api/dashboard_test.go diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index e71252e48..b3168de8e 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -2,6 +2,7 @@ package api import ( "net/http" + "net/url" "github.com/containous/traefik/v2/pkg/log" assetfs "github.com/elazarl/go-bindata-assetfs" @@ -23,11 +24,29 @@ func (g DashboardHandler) Append(router *mux.Router) { // Expose dashboard router.Methods(http.MethodGet). Path("/"). - HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - http.Redirect(response, request, request.Header.Get("X-Forwarded-Prefix")+"/dashboard/", http.StatusFound) + HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + http.Redirect(resp, req, safePrefix(req)+"/dashboard/", http.StatusFound) }) router.Methods(http.MethodGet). PathPrefix("/dashboard/"). Handler(http.StripPrefix("/dashboard/", http.FileServer(g.Assets))) } + +func safePrefix(req *http.Request) string { + prefix := req.Header.Get("X-Forwarded-Prefix") + if prefix == "" { + return "" + } + + parse, err := url.Parse(prefix) + if err != nil { + return "" + } + + if parse.Host != "" { + return "" + } + + return parse.Path +} diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go new file mode 100644 index 000000000..c945a7000 --- /dev/null +++ b/pkg/api/dashboard_test.go @@ -0,0 +1,54 @@ +package api + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_safePrefix(t *testing.T) { + testCases := []struct { + desc string + value string + expected string + }{ + { + desc: "host", + value: "https://example.com", + expected: "", + }, + { + desc: "host with path", + value: "https://example.com/foo/bar?test", + expected: "", + }, + { + desc: "path", + value: "/foo/bar", + expected: "/foo/bar", + }, + { + desc: "path without leading slash", + value: "foo/bar", + expected: "foo/bar", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + req, err := http.NewRequest(http.MethodGet, "http://localhost", nil) + require.NoError(t, err) + + req.Header.Set("X-Forwarded-Prefix", test.value) + + prefix := safePrefix(req) + + assert.Equal(t, test.expected, prefix) + }) + } +} From 3908ef611a6b9f12230f8755f7fa4bf8c2aa74c2 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 28 Jul 2020 10:44:05 +0200 Subject: [PATCH 17/21] Fix documenation for ECS --- docs/content/providers/ecs.md | 32 ++++++++++++++++++++++--- integration/helloworld/helloworld.pb.go | 2 -- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/content/providers/ecs.md b/docs/content/providers/ecs.md index f4720640c..22e4fd57a 100644 --- a/docs/content/providers/ecs.md +++ b/docs/content/providers/ecs.md @@ -79,9 +79,35 @@ providers: # ... ``` -Search for services in all clusters. -If set to true the configured clusters will be ignored and the clusters will be discovered. -If set to false the services will be discovered only in configured clusters. +Search for services in clusters list. + +- If set to `true` the configured clusters will be ignored and the clusters will be discovered. +- If set to `false` the services will be discovered only in configured clusters. + +### `clusters` + +_Optional, Default=["default"]_ + +```toml tab="File (TOML)" +[providers.ecs] + cluster = ["default"] + # ... +``` + +```yaml tab="File (YAML)" +providers: + ecs: + clusters: + - default + # ... +``` + +```bash tab="CLI" +--providers.ecs.clusters=default +# ... +``` + +Search for services in clusters list. ### `exposedByDefault` diff --git a/integration/helloworld/helloworld.pb.go b/integration/helloworld/helloworld.pb.go index 96320f02a..61b302678 100644 --- a/integration/helloworld/helloworld.pb.go +++ b/integration/helloworld/helloworld.pb.go @@ -20,9 +20,7 @@ import ( math "math" proto "github.com/golang/protobuf/proto" -) -import ( context "context" grpc "google.golang.org/grpc" From fdf2a68a111396d360da1cd420efd32825343125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20M=C3=BCller?= Date: Tue, 28 Jul 2020 17:18:03 +0200 Subject: [PATCH 18/21] doc: add name of used key for kubernetes client auth --- docs/content/https/tls.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/content/https/tls.md b/docs/content/https/tls.md index 40d0585b4..9b1023386 100644 --- a/docs/content/https/tls.md +++ b/docs/content/https/tls.md @@ -428,6 +428,7 @@ metadata: spec: clientAuth: + # the CA certificate is extracted from key `tls.ca` of the given secrets. secretNames: - secretCA clientAuthType: RequireAndVerifyClientCert From fc52d1cfba85cd48fe3055c5d82a30b6694a1e52 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 28 Jul 2020 17:34:03 +0200 Subject: [PATCH 19/21] Prepare release v2.2.8 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 585647ac7..89af253d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [v2.2.8](https://github.com/containous/traefik/tree/v2.2.8) (2020-07-28) +[All Commits](https://github.com/containous/traefik/compare/v2.2.7...v2.2.8) + +**Bug fixes:** +- **[webui]** fix: clean X-Forwarded-Prefix header for the dashboard. ([#7109](https://github.com/containous/traefik/pull/7109) by [ldez](https://github.com/ldez)) + +**Documentation:** +- **[docker]** spelling(docs/content/routing/providers/docker.md) ([#7101](https://github.com/containous/traefik/pull/7101) by [szczot3k](https://github.com/szczot3k)) +- **[k8s]** doc: add name of used key for kubernetes client auth ([#7068](https://github.com/containous/traefik/pull/7068) by [smueller18](https://github.com/smueller18)) + ## [v2.2.7](https://github.com/containous/traefik/tree/v2.2.7) (2020-07-20) [All Commits](https://github.com/containous/traefik/compare/v2.2.6...v2.2.7) From dafb14ff378e45681af6daea670232c625b0dbc6 Mon Sep 17 00:00:00 2001 From: Romain Date: Tue, 28 Jul 2020 17:50:04 +0200 Subject: [PATCH 20/21] Support Kubernetes Ingress pathType Co-authored-by: jbdoumenjou Co-authored-by: kevinpollet --- docs/content/providers/kubernetes-ingress.md | 15 +- .../routing/providers/kubernetes-ingress.md | 20 ++- pkg/provider/kubernetes/ingress/client.go | 2 +- ...8-Ingress-with-empty-pathType_endpoint.yml | 11 ++ ...18-Ingress-with-empty-pathType_ingress.yml | 16 ++ ...18-Ingress-with-empty-pathType_service.yml | 10 ++ ...8-Ingress-with-exact-pathType_endpoint.yml | 11 ++ ...18-Ingress-with-exact-pathType_ingress.yml | 14 ++ ...18-Ingress-with-exact-pathType_service.yml | 10 ++ ...plementationSpecific-pathType_endpoint.yml | 11 ++ ...mplementationSpecific-pathType_ingress.yml | 16 ++ ...mplementationSpecific-pathType_service.yml | 10 ++ ...gress-with-ingress-annotation_endpoint.yml | 11 ++ ...ngress-with-ingress-annotation_ingress.yml | 15 ++ ...ngress-with-ingress-annotation_service.yml | 10 ++ .../v18-Ingress-with-no-pathType_endpoint.yml | 11 ++ .../v18-Ingress-with-no-pathType_ingress.yml | 15 ++ .../v18-Ingress-with-no-pathType_service.yml | 10 ++ ...-Ingress-with-prefix-pathType_endpoint.yml | 11 ++ ...8-Ingress-with-prefix-pathType_ingress.yml | 14 ++ ...8-Ingress-with-prefix-pathType_service.yml | 10 ++ pkg/provider/kubernetes/ingress/kubernetes.go | 25 +-- .../kubernetes/ingress/kubernetes_test.go | 169 ++++++++++++++++++ 23 files changed, 423 insertions(+), 24 deletions(-) create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_service.yml diff --git a/docs/content/providers/kubernetes-ingress.md b/docs/content/providers/kubernetes-ingress.md index b0dea1fdc..c5a855147 100644 --- a/docs/content/providers/kubernetes-ingress.md +++ b/docs/content/providers/kubernetes-ingress.md @@ -258,16 +258,13 @@ Value of `kubernetes.io/ingress.class` annotation that identifies Ingress object If the parameter is non-empty, only Ingresses containing an annotation with the same value are processed. Otherwise, Ingresses missing the annotation, having an empty value, or with the value `traefik` are processed. -#### ingressClass on Kubernetes 1.18+ +!!! info "Kubernetes 1.18+" -If you cluster is running kubernetes 1.18+, -you can also leverage the newly Introduced `IngressClass` resource to define which Ingress Objects to handle. -In that case, Traefik will look for an `IngressClass` in your cluster with the controller of *traefik.io/ingress-controller* inside the spec. - -!!! note "" - Please note, the ingressClass configuration on the provider is not used then anymore. - -Please see [this article](https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/) for more information. + If the Kubernetes cluster version is 1.18+, + the new `IngressClass` resource can be leveraged to identify Ingress objects that should be processed. + In that case, Traefik will look for an `IngressClass` in the cluster with the controller value equal to *traefik.io/ingress-controller*. + + Please see [this article](https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/) for more information. ### `ingressEndpoint` diff --git a/docs/content/routing/providers/kubernetes-ingress.md b/docs/content/routing/providers/kubernetes-ingress.md index 7b7ac27d0..83ef155b1 100644 --- a/docs/content/routing/providers/kubernetes-ingress.md +++ b/docs/content/routing/providers/kubernetes-ingress.md @@ -322,9 +322,23 @@ which in turn will create the resulting routers, services, handlers, etc. traefik.ingress.kubernetes.io/service.sticky.cookie.httponly: "true" ``` -### TLS +## Path Types on Kubernetes 1.18+ + +If the Kubernetes cluster version is 1.18+, +the new `pathType` property can be leveraged to define the rules matchers: -#### Communication Between Traefik and Pods +- `Exact`: This path type forces the rule matcher to `Path` +- `Prefix`: This path type forces the rule matcher to `PathPrefix` + +Please see [this documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types) for more information. + +!!! warning "Multiple Matches" + In the case of multiple matches, Traefik will not ensure the priority of a Path matcher over a PathPrefix matcher, + as stated in [this documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#multiple-matches). + +## TLS + +### Communication Between Traefik and Pods Traefik automatically requests endpoint information based on the service provided in the ingress spec. Although Traefik will connect directly to the endpoints (pods), @@ -346,7 +360,7 @@ and will connect via TLS automatically. If this is not an option, you may need to skip TLS certificate verification. See the [insecureSkipVerify](../../routing/overview.md#insecureskipverify) setting for more details. -#### Certificates Management +### Certificates Management ??? example "Using a secret" diff --git a/pkg/provider/kubernetes/ingress/client.go b/pkg/provider/kubernetes/ingress/client.go index 84d6bb835..fc54c48d5 100644 --- a/pkg/provider/kubernetes/ingress/client.go +++ b/pkg/provider/kubernetes/ingress/client.go @@ -341,7 +341,7 @@ func (c *clientWrapper) GetIngressClass() (*networkingv1beta1.IngressClass, erro for _, ic := range ingressClasses { if ic.Spec.Controller == traefikDefaultIngressClassController { - return ic, err + return ic, nil } } diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_ingress.yml new file mode 100644 index 000000000..b01a04d51 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_ingress.yml @@ -0,0 +1,16 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing + annotations: + traefik.ingress.kubernetes.io/router.pathmatcher: Path +spec: + rules: + - http: + paths: + - path: /bar + pathType: "" + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_service.yml new file mode 100644 index 000000000..7c58aeed5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-empty-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_ingress.yml new file mode 100644 index 000000000..d3f9848f6 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_ingress.yml @@ -0,0 +1,14 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing +spec: + rules: + - http: + paths: + - path: /bar + pathType: Exact + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_service.yml new file mode 100644 index 000000000..7c58aeed5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-exact-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_ingress.yml new file mode 100644 index 000000000..8b1cc9674 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_ingress.yml @@ -0,0 +1,16 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing + annotations: + traefik.ingress.kubernetes.io/router.pathmatcher: Path +spec: + rules: + - http: + paths: + - path: /bar + pathType: ImplementationSpecific + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_service.yml new file mode 100644 index 000000000..7c58aeed5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-implementationSpecific-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_ingress.yml new file mode 100644 index 000000000..e223946d0 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_ingress.yml @@ -0,0 +1,15 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing + annotations: + kubernetes.io/ingress.class: traefik +spec: + rules: + - http: + paths: + - path: /bar + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_service.yml new file mode 100644 index 000000000..7c58aeed5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-ingress-annotation_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_ingress.yml new file mode 100644 index 000000000..740d39d1c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_ingress.yml @@ -0,0 +1,15 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing + annotations: + traefik.ingress.kubernetes.io/router.pathmatcher: Path +spec: + rules: + - http: + paths: + - path: /bar + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_service.yml new file mode 100644 index 000000000..7c58aeed5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-no-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_ingress.yml new file mode 100644 index 000000000..2ae20e2ef --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_ingress.yml @@ -0,0 +1,14 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing +spec: + rules: + - http: + paths: + - path: /bar + pathType: Prefix + backend: + serviceName: service1 + servicePort: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_service.yml new file mode 100644 index 000000000..7c58aeed5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v18-Ingress-with-prefix-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIp: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index b67251548..119c0115e 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -194,13 +194,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl if supportsIngressClass(serverVersion) { ic, err := client.GetIngressClass() if err != nil { - log.FromContext(ctx).Errorf("Failed to find an ingress class: %v", err) - return conf - } - - if ic == nil { - log.FromContext(ctx).Errorf("No ingress class for the traefik-controller in the cluster") - return conf + log.FromContext(ctx).Warnf("Failed to find an ingress class: %v", err) } ingressClass = ic @@ -337,8 +331,12 @@ func (p *Provider) updateIngressStatus(ing *v1beta1.Ingress, k8sClient Client) e } func (p *Provider) shouldProcessIngress(providerIngressClass string, ingress *networkingv1beta1.Ingress, ingressClass *networkingv1beta1.IngressClass) bool { - return ingressClass != nil && ingress.Spec.IngressClassName != nil && ingressClass.ObjectMeta.Name == *ingress.Spec.IngressClassName || - providerIngressClass == ingress.Annotations[annotationKubernetesIngressClass] || + // configuration through the new kubernetes ingressClass + if ingress.Spec.IngressClassName != nil { + return ingressClass != nil && ingressClass.ObjectMeta.Name == *ingress.Spec.IngressClassName + } + + return providerIngressClass == ingress.Annotations[annotationKubernetesIngressClass] || len(providerIngressClass) == 0 && ingress.Annotations[annotationKubernetesIngressClass] == traefikDefaultIngressClass } @@ -549,8 +547,13 @@ func loadRouter(rule v1beta1.IngressRule, pa v1beta1.HTTPIngressPath, rtConfig * if len(pa.Path) > 0 { matcher := defaultPathMatcher - if rtConfig != nil && rtConfig.Router != nil && rtConfig.Router.PathMatcher != "" { - matcher = rtConfig.Router.PathMatcher + + if pa.PathType == nil || *pa.PathType == "" || *pa.PathType == v1beta1.PathTypeImplementationSpecific { + if rtConfig != nil && rtConfig.Router != nil && rtConfig.Router.PathMatcher != "" { + matcher = rtConfig.Router.PathMatcher + } + } else if *pa.PathType == v1beta1.PathTypeExact { + matcher = "Path" } rules = append(rules, fmt.Sprintf("%s(`%s`)", matcher, pa.Path)) diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index a819ce16e..f1daee211 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -952,6 +952,146 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, }, + { + desc: "v18 Ingress with no pathType", + serverVersion: "v1.18", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "Path(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v18 Ingress with empty pathType", + serverVersion: "v1.18", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "Path(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v18 Ingress with implementationSpecific pathType", + serverVersion: "v1.18", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "Path(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v18 Ingress with prefix pathType", + serverVersion: "v1.18", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "PathPrefix(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v18 Ingress with exact pathType", + serverVersion: "v1.18", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "Path(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, { desc: "v18 Ingress with missing ingressClass", serverVersion: "v1.18", @@ -964,10 +1104,39 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, }, + { + desc: "v18 Ingress with ingress annotation", + serverVersion: "v1.18", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "PathPrefix(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, } for _, test := range testCases { test := test + t.Run(test.desc, func(t *testing.T) { t.Parallel() From 3942962ef534b2fa5a8b3dbe9692cb5c6db6fcb0 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 28 Jul 2020 19:16:04 +0200 Subject: [PATCH 21/21] Prepare release v2.3.0-rc3 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c5b490d1..d193e1a66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## [v2.3.0-rc3](https://github.com/containous/traefik/tree/v2.3.0-rc3) (2020-07-28) +[All Commits](https://github.com/containous/traefik/compare/v2.3.0-rc2...v2.3.0-rc3) + +**Bug fixes:** +- **[k8s,k8s/ingress]** Support Kubernetes Ingress pathType ([#7087](https://github.com/containous/traefik/pull/7087) by [rtribotte](https://github.com/rtribotte)) +- **[k8s,k8s/ingress]** Use semantic versioning to enable ingress class support ([#7065](https://github.com/containous/traefik/pull/7065) by [kevinpollet](https://github.com/kevinpollet)) +- **[provider]** file parser: skip nil value. ([#7058](https://github.com/containous/traefik/pull/7058) by [ldez](https://github.com/ldez)) + +**Documentation:** +- **[ecs]** Fix documentation for ECS ([#7107](https://github.com/containous/traefik/pull/7107) by [mmatur](https://github.com/mmatur)) +- **[k8s]** Add migration documentation for IngressClass ([#7083](https://github.com/containous/traefik/pull/7083) by [kevinpollet](https://github.com/kevinpollet)) +- **[plugins]** Update availability info ([#7060](https://github.com/containous/traefik/pull/7060) by [PCM2](https://github.com/PCM2)) + +**Misc:** +- Merge current v2.2 branch into v2.3 ([#7116](https://github.com/containous/traefik/pull/7116) by [ldez](https://github.com/ldez)) +- Merge current v2.2 branch into v2.3 ([#7086](https://github.com/containous/traefik/pull/7086) by [jbdoumenjou](https://github.com/jbdoumenjou)) + ## [v2.2.8](https://github.com/containous/traefik/tree/v2.2.8) (2020-07-28) [All Commits](https://github.com/containous/traefik/compare/v2.2.7...v2.2.8)