From a6d462f6e83915b6c6816c3c483d217157690728 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 19 Jan 2024 15:12:05 +0100 Subject: [PATCH 01/36] feat: upgrade gh-action os --- .github/workflows/build.yaml | 4 ++-- .github/workflows/check_doc.yml | 2 +- .github/workflows/documentation.yml | 2 +- .github/workflows/experimental.yaml | 2 +- .github/workflows/test-integration.yaml | 4 ++-- .github/workflows/test-unit.yaml | 2 +- .github/workflows/validate.yaml | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d7cf99bcb..b0c36ebe8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,7 +12,7 @@ env: jobs: build-webui: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Check out code @@ -35,7 +35,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-20.04, macos-latest, windows-latest ] + os: [ ubuntu-22.04, macos-latest, windows-latest ] needs: - build-webui diff --git a/.github/workflows/check_doc.yml b/.github/workflows/check_doc.yml index 9f08efbb1..9851e1a54 100644 --- a/.github/workflows/check_doc.yml +++ b/.github/workflows/check_doc.yml @@ -9,7 +9,7 @@ jobs: docs: name: Check, verify and build documentation - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Check out code diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 5a1f215d3..fd08fe4de 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -14,7 +14,7 @@ jobs: docs: name: Doc Process - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 if: github.repository == 'traefik/traefik' steps: diff --git a/.github/workflows/experimental.yaml b/.github/workflows/experimental.yaml index a26178315..489401f50 100644 --- a/.github/workflows/experimental.yaml +++ b/.github/workflows/experimental.yaml @@ -15,7 +15,7 @@ jobs: experimental: if: github.repository == 'traefik/traefik' name: Build experimental image on branch - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index d5c4e5ca1..5155fa78f 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -15,7 +15,7 @@ env: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Check out code @@ -35,7 +35,7 @@ jobs: run: make binary test-integration: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: - build strategy: diff --git a/.github/workflows/test-unit.yaml b/.github/workflows/test-unit.yaml index 7719186f7..0ab4ab0a5 100644 --- a/.github/workflows/test-unit.yaml +++ b/.github/workflows/test-unit.yaml @@ -11,7 +11,7 @@ env: jobs: test-unit: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Check out code diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index f836e94ee..a05fed052 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -13,7 +13,7 @@ env: jobs: validate: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Check out code @@ -39,7 +39,7 @@ jobs: run: make validate validate-generate: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Check out code From 8da38ec0a56f164e37bb87daa8b2a68c1e02f64d Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 19 Jan 2024 15:44:05 +0100 Subject: [PATCH 02/36] fix: tailscale is required for Docker Desktop users --- integration/integration_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/integration/integration_test.go b/integration/integration_test.go index 0ded401de..4ce934535 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -37,6 +37,8 @@ import ( var showLog = flag.Bool("tlog", false, "always show Traefik logs") +const tailscaleSecretFilePath = "tailscale.secret" + type composeConfig struct { Services map[string]composeService `yaml:"services"` } @@ -99,6 +101,11 @@ func (s *BaseSuite) displayTraefikLogFile(path string) { } func (s *BaseSuite) SetupSuite() { + if isDockerDesktop(context.Background(), s.T()) { + _, err := os.Stat(tailscaleSecretFilePath) + require.NoError(s.T(), err, "Tailscale need to be configured when running integration tests with Docker Desktop: (https://doc.traefik.io/traefik/v2.11/contributing/building-testing/#testing)") + } + // configure default standard log. stdlog.SetFlags(stdlog.Lshortfile | stdlog.LstdFlags) // TODO @@ -124,7 +131,7 @@ func (s *BaseSuite) SetupSuite() { s.hostIP = "172.31.42.1" if isDockerDesktop(ctx, s.T()) { s.hostIP = getDockerDesktopHostIP(ctx, s.T()) - s.setupVPN("tailscale.secret") + s.setupVPN(tailscaleSecretFilePath) } } From 5e0855ecc7c5e9422cc7d6df565ca12523346b94 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 22 Jan 2024 15:30:05 +0100 Subject: [PATCH 03/36] feat: adds conformance test gateway api --- .github/workflows/test-conformance.yaml | 37 +++ .github/workflows/test-integration.yaml | 3 - .gitignore | 1 + Makefile | 5 + go.mod | 10 +- go.sum | 24 +- .../fixtures/k8s_gateway_conformance.toml | 21 ++ integration/integration_test.go | 6 +- integration/k8s_conformance_test.go | 229 ++++++++++++++++++ pkg/provider/kubernetes/gateway/kubernetes.go | 12 +- 10 files changed, 333 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/test-conformance.yaml create mode 100644 integration/fixtures/k8s_gateway_conformance.toml create mode 100644 integration/k8s_conformance_test.go diff --git a/.github/workflows/test-conformance.yaml b/.github/workflows/test-conformance.yaml new file mode 100644 index 000000000..70e2cbe7c --- /dev/null +++ b/.github/workflows/test-conformance.yaml @@ -0,0 +1,37 @@ +name: Test K8s Gateway API conformance + +on: + pull_request: + branches: + - '*' + paths: + - 'pkg/provider/kubernetes/gateway' + +env: + GO_VERSION: '1.21' + CGO_ENABLED: 0 + +jobs: + + test-conformance: + runs-on: ubuntu-20.04 + + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Avoid generating webui + run: touch webui/static/index.html + + - name: Build binary + run: make binary + + - name: K8s Gateway API conformance test + run: sudo make test-gateway-api-conformance diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 5155fa78f..0625da388 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -4,9 +4,6 @@ on: pull_request: branches: - '*' - push: - branches: - - 'gh-actions' env: GO_VERSION: '1.21' diff --git a/.gitignore b/.gitignore index 3fd473f24..03a5e9369 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ plugins-storage/ plugins-local/ traefik_changelog.md integration/tailscale.secret +integration/conformance-reports/ diff --git a/Makefile b/Makefile index 15565a3ce..93498e5c9 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,11 @@ test-unit: test-integration: binary GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -test.timeout=20m -failfast -v $(TESTFLAGS) +## Run the conformance tests +.PHONY: test-gateway-api-conformance +test-gateway-api-conformance: binary + GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -test.run K8sConformanceSuite -k8sConformance=true $(TESTFLAGS) + ## Pull all Docker images to avoid timeout during integration tests .PHONY: pull-images pull-images: diff --git a/go.mod b/go.mod index 1e3228e15..17fee2a7f 100644 --- a/go.mod +++ b/go.mod @@ -85,12 +85,13 @@ require ( golang.org/x/tools v0.14.0 google.golang.org/grpc v1.59.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.28.3 + k8s.io/api v0.28.4 k8s.io/apiextensions-apiserver v0.28.3 - k8s.io/apimachinery v0.28.3 - k8s.io/client-go v0.28.3 + k8s.io/apimachinery v0.28.4 + k8s.io/client-go v0.28.4 k8s.io/utils v0.0.0-20230726121419-3b25d923346b mvdan.cc/xurls/v2 v2.5.0 + sigs.k8s.io/controller-runtime v0.16.3 sigs.k8s.io/gateway-api v1.0.0 ) @@ -162,6 +163,7 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.7.0+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.7.0 // indirect github.com/exoscale/egoscale v0.100.1 // indirect github.com/fatih/color v1.15.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect @@ -236,6 +238,7 @@ require ( github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/spdystream v0.2.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -254,7 +257,6 @@ require ( github.com/nzdjb/go-metaname v1.0.0 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/ginkgo/v2 v2.11.0 // indirect - github.com/onsi/gomega v1.27.10 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc5 // indirect github.com/opencontainers/runc v1.1.5 // indirect diff --git a/go.sum b/go.sum index 9c0c8a0eb..28bc21641 100644 --- a/go.sum +++ b/go.sum @@ -114,6 +114,8 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= @@ -302,6 +304,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go. github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= +github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/exoscale/egoscale v0.100.1 h1:iXsV1Ei7daqe/6FYSCSDyrFs1iUG1l1X9qNh2uMw6z0= github.com/exoscale/egoscale v0.100.1/go.mod h1:BAb9p4rmyU+Wl400CJZO5270H2sXtdsZjLcm5xMKkz4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -358,6 +362,8 @@ github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= +github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -796,6 +802,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= @@ -1485,6 +1493,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= @@ -1619,14 +1629,14 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= -k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= +k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= +k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= -k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= -k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= -k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= -k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= +k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= +k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= +k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= +k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= @@ -1640,6 +1650,8 @@ nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0 nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= +sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= sigs.k8s.io/gateway-api v1.0.0 h1:iPTStSv41+d9p0xFydll6d7f7MOBGuqXM6p2/zVYMAs= sigs.k8s.io/gateway-api v1.0.0/go.mod h1:4cUgr0Lnp5FZ0Cdq8FdRwCvpiWws7LVhLHGIudLlf4c= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/integration/fixtures/k8s_gateway_conformance.toml b/integration/fixtures/k8s_gateway_conformance.toml new file mode 100644 index 000000000..5114f2dfb --- /dev/null +++ b/integration/fixtures/k8s_gateway_conformance.toml @@ -0,0 +1,21 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + noColor = true + +[api] + insecure = true + +[experimental] + kubernetesGateway = true + +[entryPoints] + [entryPoints.web] + address = ":80" + [entryPoints.websecure] + address = ":443" + +[providers.kubernetesGateway] diff --git a/integration/integration_test.go b/integration/integration_test.go index 0e9d22d78..51df6870c 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -35,7 +35,11 @@ import ( "gopkg.in/yaml.v3" ) -var showLog = flag.Bool("tlog", false, "always show Traefik logs") +var ( + showLog = flag.Bool("tlog", false, "always show Traefik logs") + k8sConformance = flag.Bool("k8sConformance", false, "run K8s Gateway API conformance test") + k8sConformanceRunTest = flag.String("k8sConformanceRunTest", "", "run a specific K8s Gateway API conformance test") +) const tailscaleSecretFilePath = "tailscale.secret" diff --git a/integration/k8s_conformance_test.go b/integration/k8s_conformance_test.go new file mode 100644 index 000000000..f5c869f6c --- /dev/null +++ b/integration/k8s_conformance_test.go @@ -0,0 +1,229 @@ +package integration + +import ( + "context" + "fmt" + "net" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "github.com/traefik/traefik/v3/integration/try" + "github.com/traefik/traefik/v3/pkg/version" + "gopkg.in/yaml.v3" + ktypes "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + kclientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + gatev1 "sigs.k8s.io/gateway-api/apis/v1" + gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatev1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + conformanceV1alpha1 "sigs.k8s.io/gateway-api/conformance/apis/v1alpha1" + "sigs.k8s.io/gateway-api/conformance/tests" + "sigs.k8s.io/gateway-api/conformance/utils/config" + ksuite "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +// K8sConformanceSuite tests suite. +type K8sConformanceSuite struct{ BaseSuite } + +func TestK8sConformanceSuite(t *testing.T) { + suite.Run(t, new(K8sConformanceSuite)) +} + +func (s *K8sConformanceSuite) SetupSuite() { + s.BaseSuite.SetupSuite() + + s.createComposeProject("k8s") + s.composeUp() + + abs, err := filepath.Abs("./fixtures/k8s/config.skip/kubeconfig.yaml") + require.NoError(s.T(), err) + + err = try.Do(60*time.Second, func() error { + _, err := os.Stat(abs) + return err + }) + require.NoError(s.T(), err) + + data, err := os.ReadFile(abs) + require.NoError(s.T(), err) + + content := strings.ReplaceAll(string(data), "https://server:6443", fmt.Sprintf("https://%s", net.JoinHostPort(s.getComposeServiceIP("server"), "6443"))) + + err = os.WriteFile(abs, []byte(content), 0o644) + require.NoError(s.T(), err) + + err = os.Setenv("KUBECONFIG", abs) + require.NoError(s.T(), err) +} + +func (s *K8sConformanceSuite) TearDownSuite() { + s.BaseSuite.TearDownSuite() + + generatedFiles := []string{ + "./fixtures/k8s/config.skip/kubeconfig.yaml", + "./fixtures/k8s/config.skip/k3s.log", + "./fixtures/k8s/rolebindings.yaml", + "./fixtures/k8s/ccm.yaml", + } + + for _, filename := range generatedFiles { + if err := os.Remove(filename); err != nil { + log.Warn().Err(err).Send() + } + } +} + +func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() { + if !*k8sConformance { + s.T().Skip("Skip because it can take a long time to execute. To enable pass the `k8sConformance` flag.") + } + + configFromFlags, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG")) + if err != nil { + s.T().Fatal(err) + } + + kClient, err := client.New(configFromFlags, client.Options{}) + if err != nil { + s.T().Fatalf("Error initializing Kubernetes client: %v", err) + } + + kClientSet, err := kclientset.NewForConfig(configFromFlags) + if err != nil { + s.T().Fatal(err) + } + + err = gatev1alpha2.AddToScheme(kClient.Scheme()) + require.NoError(s.T(), err) + err = gatev1beta1.AddToScheme(kClient.Scheme()) + require.NoError(s.T(), err) + err = gatev1.AddToScheme(kClient.Scheme()) + require.NoError(s.T(), err) + + s.traefikCmd(withConfigFile("fixtures/k8s_gateway_conformance.toml")) + + // Wait for traefik to start + err = try.GetRequest("http://127.0.0.1:8080/api/entrypoints", 10*time.Second, try.BodyContains(`"name":"web"`)) + require.NoError(s.T(), err) + + err = try.Do(10*time.Second, func() error { + gwc := &gatev1.GatewayClass{} + err := kClient.Get(context.Background(), ktypes.NamespacedName{Name: "my-gateway-class"}, gwc) + if err != nil { + return fmt.Errorf("error fetching GatewayClass: %w", err) + } + + return nil + }) + require.NoError(s.T(), err) + + opts := ksuite.Options{ + Client: kClient, + RestConfig: configFromFlags, + Clientset: kClientSet, + GatewayClassName: "my-gateway-class", + Debug: true, + CleanupBaseResources: true, + TimeoutConfig: config.TimeoutConfig{ + CreateTimeout: 5 * time.Second, + DeleteTimeout: 5 * time.Second, + GetTimeout: 5 * time.Second, + GatewayMustHaveAddress: 5 * time.Second, + GatewayMustHaveCondition: 5 * time.Second, + GatewayStatusMustHaveListeners: 10 * time.Second, + GatewayListenersMustHaveCondition: 5 * time.Second, + GWCMustBeAccepted: 60 * time.Second, // Pod creation in k3s cluster can be long. + HTTPRouteMustNotHaveParents: 5 * time.Second, + HTTPRouteMustHaveCondition: 5 * time.Second, + TLSRouteMustHaveCondition: 5 * time.Second, + RouteMustHaveParents: 5 * time.Second, + ManifestFetchTimeout: 5 * time.Second, + MaxTimeToConsistency: 5 * time.Second, + NamespacesMustBeReady: 60 * time.Second, // Pod creation in k3s cluster can be long. + RequestTimeout: 5 * time.Second, + LatestObservedGenerationSet: 5 * time.Second, + RequiredConsecutiveSuccesses: 0, + }, + SupportedFeatures: sets.New[ksuite.SupportedFeature](). + Insert(ksuite.GatewayCoreFeatures.UnsortedList()...), + EnableAllSupportedFeatures: false, + RunTest: *k8sConformanceRunTest, + // Until the feature are all supported, following tests are skipped. + SkipTests: []string{ + "HTTPExactPathMatching", + "HTTPRouteHostnameIntersection", + "GatewaySecretReferenceGrantAllInNamespace", + "HTTPRouteListenerHostnameMatching", + "HTTPRouteRequestHeaderModifier", + "GatewaySecretInvalidReferenceGrant", + "GatewayClassObservedGenerationBump", + "HTTPRouteInvalidNonExistentBackendRef", + "GatewayWithAttachedRoutes", + "HTTPRouteCrossNamespace", + "HTTPRouteDisallowedKind", + "HTTPRouteInvalidReferenceGrant", + "HTTPRouteObservedGenerationBump", + "GatewayInvalidRouteKind", + "TLSRouteSimpleSameNamespace", + "TLSRouteInvalidReferenceGrant", + "HTTPRouteInvalidCrossNamespaceParentRef", + "HTTPRouteInvalidParentRefNotMatchingSectionName", + "GatewaySecretReferenceGrantSpecific", + "GatewayModifyListeners", + "GatewaySecretMissingReferenceGrant", + "GatewayInvalidTLSConfiguration", + "HTTPRouteInvalidCrossNamespaceBackendRef", + "HTTPRouteMatchingAcrossRoutes", + "HTTPRoutePartiallyInvalidViaInvalidReferenceGrant", + "HTTPRouteRedirectHostAndStatus", + "HTTPRouteInvalidBackendRefUnknownKind", + "HTTPRoutePathMatchOrder", + "HTTPRouteSimpleSameNamespace", + "HTTPRouteMatching", + "HTTPRouteHeaderMatching", + "HTTPRouteReferenceGrant", + }, + } + + cSuite, err := ksuite.NewExperimentalConformanceTestSuite(ksuite.ExperimentalConformanceOptions{ + Options: opts, + Implementation: conformanceV1alpha1.Implementation{ + Organization: "traefik", + Project: "traefik", + URL: "https://traefik.io/", + Version: version.Version, + Contact: []string{"@traefik/maintainers"}, + }, + ConformanceProfiles: sets.New[ksuite.ConformanceProfileName]( + ksuite.HTTPConformanceProfileName, + ksuite.TLSConformanceProfileName, + ), + }) + require.NoError(s.T(), err) + + cSuite.Setup(s.T()) + err = cSuite.Run(s.T(), tests.ConformanceTests) + require.NoError(s.T(), err) + + report, err := cSuite.Report() + require.NoError(s.T(), err, "failed generating conformance report") + + report.GatewayAPIVersion = "1.0.0" + + rawReport, err := yaml.Marshal(report) + require.NoError(s.T(), err) + s.T().Logf("Conformance report:\n%s", string(rawReport)) + + require.NoError(s.T(), os.MkdirAll("./conformance-reports", 0o755)) + outFile := filepath.Join("conformance-reports", fmt.Sprintf("traefik-traefik-%d.yaml", time.Now().UnixNano())) + require.NoError(s.T(), os.WriteFile(outFile, rawReport, 0o600)) + s.T().Logf("Report written to: %s", outFile) +} diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index c86467b0f..e9ed7e6a7 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -230,6 +230,7 @@ func (p *Provider) loadConfigurationFromGateway(ctx context.Context, client Clie err := client.UpdateGatewayClassStatus(gatewayClass, metav1.Condition{ Type: string(gatev1.GatewayClassConditionStatusAccepted), Status: metav1.ConditionTrue, + ObservedGeneration: gatewayClass.Generation, Reason: "Handled", Message: "Handled by Traefik controller", LastTransitionTime: metav1.Now(), @@ -587,7 +588,16 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses [ Type: string(gatev1.GatewayConditionAccepted), Status: metav1.ConditionTrue, ObservedGeneration: gateway.Generation, - Reason: string(gatev1.GatewayConditionAccepted), + Reason: string(gatev1.GatewayReasonAccepted), + Message: "Gateway successfully scheduled", + LastTransitionTime: metav1.Now(), + }, + // update "Programmed" status with "Programmed" reason + metav1.Condition{ + Type: string(gatev1.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + ObservedGeneration: gateway.Generation, + Reason: string(gatev1.GatewayReasonProgrammed), Message: "Gateway successfully scheduled", LastTransitionTime: metav1.Now(), }, From 6cb2ff2af909d956cf19522143d221b843db9b40 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 22 Jan 2024 16:04:05 +0100 Subject: [PATCH 04/36] fix: gateway api conformance tests --- .github/workflows/test-conformance.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-conformance.yaml b/.github/workflows/test-conformance.yaml index 70e2cbe7c..488bf452f 100644 --- a/.github/workflows/test-conformance.yaml +++ b/.github/workflows/test-conformance.yaml @@ -5,7 +5,8 @@ on: branches: - '*' paths: - - 'pkg/provider/kubernetes/gateway' + - 'pkg/provider/kubernetes/gateway/**' + - 'integration/k8s_conformance_test.go' env: GO_VERSION: '1.21' From bab48bed223e851414e6a6c52d6ed677e782cf70 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 22 Jan 2024 16:38:06 +0100 Subject: [PATCH 05/36] fix: OpenTelemetry metrics flaky test --- pkg/metrics/opentelemetry_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/metrics/opentelemetry_test.go b/pkg/metrics/opentelemetry_test.go index c6bfe99eb..93fc31c99 100644 --- a/pkg/metrics/opentelemetry_test.go +++ b/pkg/metrics/opentelemetry_test.go @@ -287,8 +287,6 @@ func TestOpenTelemetry_GaugeCollectorSet(t *testing.T) { } func TestOpenTelemetry(t *testing.T) { - t.Parallel() - c := make(chan *string) defer close(c) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { From 177c4b0ed1cf04ee884fb8437d042874d6989c2a Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 22 Jan 2024 16:52:05 +0100 Subject: [PATCH 06/36] fix: flakiness test on configuration watcher --- pkg/server/configurationwatcher_test.go | 69 +++++++++++++------------ 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go index c65e6acf6..1f571effb 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -23,7 +23,7 @@ type mockProvider struct { throttleDuration time.Duration } -func (p *mockProvider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { +func (p *mockProvider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error { wait := p.wait if wait == 0 { wait = 20 * time.Millisecond @@ -48,7 +48,7 @@ func (p *mockProvider) Provide(configurationChan chan<- dynamic.Message, pool *s } // ThrottleDuration returns the throttle duration. -func (p mockProvider) ThrottleDuration() time.Duration { +func (p *mockProvider) ThrottleDuration() time.Duration { return p.throttleDuration } @@ -122,7 +122,7 @@ func TestWaitForRequiredProvider(t *testing.T) { config := &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo")), + th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar")), ), } @@ -166,19 +166,19 @@ func TestIgnoreTransientConfiguration(t *testing.T) { config := &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo")), + th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar")), ), } config2 := &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("baz")), + th.WithRouters(th.WithRouter("baz", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("toto")), ), } - watcher := NewConfigurationWatcher(routinesPool, &mockProvider{}, []string{"defaultEP"}, "") + watcher := NewConfigurationWatcher(routinesPool, &mockProvider{}, []string{}, "") publishedConfigCount := 0 var lastConfig dynamic.Configuration @@ -209,17 +209,20 @@ func TestIgnoreTransientConfiguration(t *testing.T) { Configuration: config, } - close(blockConfConsumer) - - // give some time so that the configuration can be processed + // give some time before closing the channel. time.Sleep(20 * time.Millisecond) - // after 20 milliseconds we should have 1 configs published + close(blockConfConsumer) + + // give some time so that the configuration can be processed. + time.Sleep(20 * time.Millisecond) + + // after 20 milliseconds we should have 1 config published. assert.Equal(t, 1, publishedConfigCount, "times configs were published") expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("defaultEP"))), + th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar@mock")), th.WithMiddlewares(), ), @@ -256,7 +259,7 @@ func TestListenProvidersThrottleProviderConfigReload(t *testing.T) { ProviderName: "mock", Configuration: &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i))), + th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i), th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar")), ), }, @@ -316,7 +319,7 @@ func TestListenProvidersSkipsSameConfigurationForProvider(t *testing.T) { ProviderName: "mock", Configuration: &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo")), + th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar")), ), }, @@ -348,14 +351,14 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) { configuration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo")), + th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar")), ), } transientConfiguration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("bad")), + th.WithRouters(th.WithRouter("bad", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bad")), ), } @@ -370,7 +373,7 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) { }, } - watcher := NewConfigurationWatcher(routinesPool, pvd, []string{"defaultEP"}, "") + watcher := NewConfigurationWatcher(routinesPool, pvd, []string{}, "") var lastConfig dynamic.Configuration watcher.AddListener(func(conf dynamic.Configuration) { @@ -387,7 +390,7 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) { expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("defaultEP"))), + th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar@mock")), th.WithMiddlewares(), ), @@ -416,14 +419,14 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) { configuration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo")), + th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar")), ), } transientConfiguration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("bad")), + th.WithRouters(th.WithRouter("bad", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bad")), ), } @@ -449,7 +452,7 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) { err := providerAggregator.AddProvider(pvd) assert.NoError(t, err) - watcher := NewConfigurationWatcher(routinesPool, providerAggregator, []string{"defaultEP"}, "") + watcher := NewConfigurationWatcher(routinesPool, providerAggregator, []string{}, "") var configurationReloads int var lastConfig dynamic.Configuration @@ -476,7 +479,7 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) { expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("defaultEP"))), + th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar@mock")), th.WithMiddlewares(), ), @@ -505,7 +508,7 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) { func TestApplyConfigUnderStress(t *testing.T) { routinesPool := safe.NewPool(context.Background()) - watcher := NewConfigurationWatcher(routinesPool, &mockProvider{}, []string{"defaultEP"}, "") + watcher := NewConfigurationWatcher(routinesPool, &mockProvider{}, []string{}, "") routinesPool.GoCtx(func(ctx context.Context) { i := 0 @@ -515,7 +518,7 @@ func TestApplyConfigUnderStress(t *testing.T) { return case watcher.allProvidersConfigs <- dynamic.Message{ProviderName: "mock", Configuration: &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i))), + th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i), th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar")), ), }}: @@ -550,28 +553,28 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) { configuration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo")), + th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar")), ), } transientConfiguration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("bad")), + th.WithRouters(th.WithRouter("bad", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bad")), ), } transientConfiguration2 := &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("bad2")), + th.WithRouters(th.WithRouter("bad2", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bad2")), ), } finalConfiguration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("final")), + th.WithRouters(th.WithRouter("final", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("final")), ), } @@ -591,7 +594,7 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) { err := providerAggregator.AddProvider(pvd) assert.NoError(t, err) - watcher := NewConfigurationWatcher(routinesPool, providerAggregator, []string{"defaultEP"}, "") + watcher := NewConfigurationWatcher(routinesPool, providerAggregator, []string{}, "") var configurationReloads int var lastConfig dynamic.Configuration @@ -610,7 +613,7 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) { expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("final@mock", th.WithEntryPoints("defaultEP"))), + th.WithRouters(th.WithRouter("final@mock", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("final@mock")), th.WithMiddlewares(), ), @@ -641,7 +644,7 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { configuration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo")), + th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar")), ), } @@ -653,7 +656,7 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { }, } - watcher := NewConfigurationWatcher(routinesPool, pvd, []string{"defaultEP"}, "") + watcher := NewConfigurationWatcher(routinesPool, pvd, []string{}, "") var publishedProviderConfig dynamic.Configuration @@ -672,8 +675,8 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters( - th.WithRouter("foo@mock", th.WithEntryPoints("defaultEP")), - th.WithRouter("foo@mock2", th.WithEntryPoints("defaultEP")), + th.WithRouter("foo@mock", th.WithEntryPoints("ep")), + th.WithRouter("foo@mock2", th.WithEntryPoints("ep")), ), th.WithLoadBalancerServices( th.WithService("bar@mock"), From f9831f5b1b8d4630801c5820c88d7d8a743bf4be Mon Sep 17 00:00:00 2001 From: Romain Date: Tue, 23 Jan 2024 09:22:05 +0100 Subject: [PATCH 07/36] Introduce static config hints Co-authored-by: Baptiste Mayelle --- .golangci.yml | 6 + cmd/traefik/traefik.go | 2 +- docs/content/migration/v2-to-v3.md | 214 ++++++- pkg/cli/deprecation.go | 541 ++++++++++++++++++ pkg/cli/deprecation_test.go | 404 +++++++++++++ pkg/cli/fixtures/traefik_deprecated.toml | 5 + .../fixtures/traefik_multiple_deprecated.toml | 8 + pkg/cli/fixtures/traefik_no_deprecated.toml | 3 + 8 files changed, 1176 insertions(+), 7 deletions(-) create mode 100644 pkg/cli/deprecation.go create mode 100644 pkg/cli/deprecation_test.go create mode 100644 pkg/cli/fixtures/traefik_deprecated.toml create mode 100644 pkg/cli/fixtures/traefik_multiple_deprecated.toml create mode 100644 pkg/cli/fixtures/traefik_no_deprecated.toml diff --git a/.golangci.yml b/.golangci.yml index 7128a4177..08685510f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -280,3 +280,9 @@ issues: text: 'unusedwrite: unused write to field' linters: - govet + - path: pkg/cli/deprecation.go + linters: + - goconst + - path: pkg/cli/loader_file.go + linters: + - goconst diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 0ac89fb46..1274e5104 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -53,7 +53,7 @@ func main() { // traefik config inits tConfig := cmd.NewTraefikConfiguration() - loaders := []cli.ResourceLoader{&tcli.FileLoader{}, &tcli.FlagLoader{}, &tcli.EnvLoader{}} + loaders := []cli.ResourceLoader{&tcli.DeprecationLoader{}, &tcli.FileLoader{}, &tcli.FlagLoader{}, &tcli.EnvLoader{}} cmdTraefik := &cli.Command{ Name: "traefik", diff --git a/docs/content/migration/v2-to-v3.md b/docs/content/migration/v2-to-v3.md index 70b27f820..4396e0887 100644 --- a/docs/content/migration/v2-to-v3.md +++ b/docs/content/migration/v2-to-v3.md @@ -19,6 +19,8 @@ and how it now looks like in v3. ### Docker & Docker Swarm +#### SwarmMode + In v3, the provider Docker has been split into 2 providers: - Docker provider (without Swarm support) @@ -43,7 +45,7 @@ In v3, the provider Docker has been split into 2 providers: This configuration is now unsupported and would prevent Traefik to start. -#### Remediation +##### Remediation In v3, the `swarmMode` should not be used with the Docker provider, and, to use Swarm, the Swarm provider should be used instead. @@ -64,7 +66,35 @@ In v3, the `swarmMode` should not be used with the Docker provider, and, to use --providers.swarm.endpoint=tcp://127.0.0.1:2377 ``` -### HTTP3 Experimental Configuration +#### TLS.CAOptional + +Docker provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + docker: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.docker.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.docker.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the Docker provider static configuration. + +### Experimental Configuration + +#### HTTP3 In v3, HTTP/3 is no longer an experimental feature. It can be enabled on entry points without the associated `experimental.http3` option, which is now removed. @@ -86,12 +116,14 @@ It is now unsupported and would prevent Traefik to start. --experimental.http3=true ``` -#### Remediation +##### Remediation The `http3` option should be removed from the static configuration experimental section. ### Consul provider +#### namespace + The Consul provider `namespace` option was deprecated in v2 and is now removed in v3. It is now unsupported and would prevent Traefik to start. @@ -111,7 +143,7 @@ It is now unsupported and would prevent Traefik to start. --consul.namespace=foobar ``` -#### Remediation +##### Remediation In v3, the `namespaces` option should be used instead of the `namespace` option. @@ -132,8 +164,36 @@ In v3, the `namespaces` option should be used instead of the `namespace` option. --consul.namespaces=foobar ``` +#### TLS.CAOptional + +Consul provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + consul: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.consul.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.consul.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the Consul provider static configuration. + ### ConsulCatalog provider +#### namespace + The ConsulCatalog provider `namespace` option was deprecated in v2 and is now removed in v3. It is now unsupported and would prevent Traefik to start. @@ -153,7 +213,7 @@ It is now unsupported and would prevent Traefik to start. --consulCatalog.namespace=foobar ``` -#### Remediation +##### Remediation In v3, the `namespaces` option should be used instead of the `namespace` option. @@ -174,8 +234,37 @@ In v3, the `namespaces` option should be used instead of the `namespace` option. --consulCatalog.namespaces=foobar ``` +#### Endpoint.TLS.CAOptional + +ConsulCatalog provider `endpoint.tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the Endpoint.TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + consulCatalog: + endpoint: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.consulCatalog.endpoint.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.consulCatalog.endpoint.tls.caOptional=true + ``` + +##### Remediation + +The `endpoint.tls.caOptional` option should be removed from the ConsulCatalog provider static configuration. + ### Nomad provider +#### namespace + The Nomad provider `namespace` option was deprecated in v2 and is now removed in v3. It is now unsupported and would prevent Traefik to start. @@ -195,7 +284,7 @@ It is now unsupported and would prevent Traefik to start. --nomad.namespace=foobar ``` -#### Remediation +##### Remediation In v3, the `namespaces` option should be used instead of the `namespace` option. @@ -216,6 +305,33 @@ In v3, the `namespaces` option should be used instead of the `namespace` option. --nomad.namespaces=foobar ``` +#### Endpoint.TLS.CAOptional + +Nomad provider `endpoint.tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the Endpoint.TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + nomad: + endpoint: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.nomad.endpoint.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.nomad.endpoint.tls.caOptional=true + ``` + +##### Remediation + +The `endpoint.tls.caOptional` option should be removed from the Nomad provider static configuration. + ### Rancher v1 Provider In v3, the Rancher v1 provider has been removed because Rancher v1 is [no longer actively maintaned](https://rancher.com/docs/os/v1.x/en/support/), @@ -271,6 +387,90 @@ This configuration is now unsupported and would prevent Traefik to start. All Marathon provider related configuration should be removed from the static configuration. +### HTTP Provider + +#### TLS.CAOptional + +HTTP provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + http: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.http.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.http.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the HTTP provider static configuration. + +### ETCD Provider + +#### TLS.CAOptional + +ETCD provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + etcd: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.etcd.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.etcd.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the ETCD provider static configuration. + +### Redis Provider + +#### TLS.CAOptional + +Redis provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + redis: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.redis.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.redis.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the Redis provider static configuration. + ### InfluxDB v1 InfluxDB v1.x maintenance [ended in 2021](https://www.influxdata.com/blog/influxdb-oss-and-enterprise-roadmap-update-from-influxdays-emea/). @@ -415,3 +615,5 @@ Here are two possible transition strategies: For legacy stacks that cannot immediately upgrade to the latest vendor agents supporting OTLP ingestion, using OpenTelemetry (OTel) collectors with appropriate exporters configuration is a viable solution. This allows continued compatibility with the existing infrastructure. + +Please check the [OpenTelemetry Tracing provider documention](../observability/tracing/opentelemetry.md) for more information. diff --git a/pkg/cli/deprecation.go b/pkg/cli/deprecation.go new file mode 100644 index 000000000..379d986a7 --- /dev/null +++ b/pkg/cli/deprecation.go @@ -0,0 +1,541 @@ +package cli + +import ( + "errors" + "os" + "reflect" + "strings" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/traefik/paerser/cli" + "github.com/traefik/paerser/flag" + "github.com/traefik/paerser/parser" +) + +type DeprecationLoader struct{} + +func (d DeprecationLoader) Load(args []string, cmd *cli.Command) (bool, error) { + if logDeprecation(cmd.Configuration, args) { + return true, errors.New("incompatible deprecated static option found") + } + + return false, nil +} + +// logDeprecation prints deprecation hints and returns whether incompatible deprecated options need to be removed. +func logDeprecation(traefikConfiguration interface{}, args []string) bool { + // This part doesn't handle properly a flag defined like this: + // --accesslog true + // where `true` could be considered as a new argument. + // This is not really an issue with the deprecation loader since it will filter the unknown nodes later in this + // function. + for i, arg := range args { + if !strings.Contains(arg, "=") { + args[i] = arg + "=true" + } + } + + labels, err := flag.Parse(args, nil) + if err != nil { + log.Error().Err(err).Msg("deprecated static options analysis failed") + return false + } + + node, err := parser.DecodeToNode(labels, "traefik") + if err != nil { + log.Error().Err(err).Msg("deprecated static options analysis failed") + return false + } + + if node != nil && len(node.Children) > 0 { + config := &configuration{} + filterUnknownNodes(reflect.TypeOf(config), node) + + if len(node.Children) > 0 { + // Telling parser to look for the label struct tag to allow empty values. + err = parser.AddMetadata(config, node, parser.MetadataOpts{TagName: "label"}) + if err != nil { + log.Error().Err(err).Msg("deprecated static options analysis failed") + return false + } + + err = parser.Fill(config, node, parser.FillerOpts{}) + if err != nil { + log.Error().Err(err).Msg("deprecated static options analysis failed") + return false + } + + if config.deprecationNotice(log.With().Str("loader", "FLAG").Logger()) { + return true + } + + // No further deprecation parsing and logging, + // as args configuration contains at least one deprecated option. + return false + } + } + + // FILE + ref, err := flag.Parse(args, traefikConfiguration) + if err != nil { + log.Error().Err(err).Msg("deprecated static options analysis failed") + return false + } + + configFileFlag := "traefik.configfile" + if _, ok := ref["traefik.configFile"]; ok { + configFileFlag = "traefik.configFile" + } + + config := &configuration{} + _, err = loadConfigFiles(ref[configFileFlag], config) + + if err == nil { + if config.deprecationNotice(log.With().Str("loader", "FILE").Logger()) { + return true + } + } + + config = &configuration{} + l := EnvLoader{} + _, err = l.Load(os.Args, &cli.Command{ + Configuration: config, + }) + + if err == nil { + if config.deprecationNotice(log.With().Str("loader", "ENV").Logger()) { + return true + } + } + + return false +} + +func filterUnknownNodes(fType reflect.Type, node *parser.Node) bool { + var children []*parser.Node + for _, child := range node.Children { + if hasKnownNodes(fType, child) { + children = append(children, child) + } + } + + node.Children = children + return len(node.Children) > 0 +} + +func hasKnownNodes(rootType reflect.Type, node *parser.Node) bool { + rType := rootType + if rootType.Kind() == reflect.Pointer { + rType = rootType.Elem() + } + + // unstructured type fitting anything, considering the current node as known. + if rType.Kind() == reflect.Map && rType.Elem().Kind() == reflect.Interface { + return true + } + + // unstructured type fitting anything, considering the current node as known. + if rType.Kind() == reflect.Interface { + return true + } + + // find matching field in struct type. + field, b := findTypedField(rType, node) + if !b { + return b + } + + if len(node.Children) > 0 { + return filterUnknownNodes(field.Type, node) + } + + return true +} + +func findTypedField(rType reflect.Type, node *parser.Node) (reflect.StructField, bool) { + // avoid panicking. + if rType.Kind() != reflect.Struct { + return reflect.StructField{}, false + } + + for i := 0; i < rType.NumField(); i++ { + cField := rType.Field(i) + + // ignore unexported fields. + if cField.PkgPath == "" { + if strings.EqualFold(cField.Name, node.Name) { + node.FieldName = cField.Name + return cField, true + } + } + } + + return reflect.StructField{}, false +} + +// configuration holds the static configuration removed/deprecated options. +type configuration struct { + Experimental *experimental `json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty" label:"allowEmpty" file:"allowEmpty"` + Pilot map[string]any `json:"pilot,omitempty" toml:"pilot,omitempty" yaml:"pilot,omitempty" label:"allowEmpty" file:"allowEmpty"` + Providers *providers `json:"providers,omitempty" toml:"providers,omitempty" yaml:"providers,omitempty" label:"allowEmpty" file:"allowEmpty"` + Tracing *tracing `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (c *configuration) deprecationNotice(logger zerolog.Logger) bool { + if c == nil { + return false + } + + var incompatible bool + if c.Pilot != nil { + incompatible = true + logger.Error().Msg("Pilot configuration has been removed in v3, please remove all Pilot-related static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#pilot") + } + + incompatibleExperimental := c.Experimental.deprecationNotice(logger) + incompatibleProviders := c.Providers.deprecationNotice(logger) + incompatibleTracing := c.Tracing.deprecationNotice(logger) + return incompatible || incompatibleExperimental || incompatibleProviders || incompatibleTracing +} + +type providers struct { + Docker *docker `json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" label:"allowEmpty" file:"allowEmpty"` + Swarm *swarm `json:"swarm,omitempty" toml:"swarm,omitempty" yaml:"swarm,omitempty" label:"allowEmpty" file:"allowEmpty"` + Consul *consul `json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" label:"allowEmpty" file:"allowEmpty"` + ConsulCatalog *consulCatalog `json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty" label:"allowEmpty" file:"allowEmpty"` + Nomad *nomad `json:"nomad,omitempty" toml:"nomad,omitempty" yaml:"nomad,omitempty" label:"allowEmpty" file:"allowEmpty"` + Marathon map[string]any `json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" label:"allowEmpty" file:"allowEmpty"` + Rancher map[string]any `json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" label:"allowEmpty" file:"allowEmpty"` + ETCD *etcd `json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" label:"allowEmpty" file:"allowEmpty"` + Redis *redis `json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" label:"allowEmpty" file:"allowEmpty"` + HTTP *http `json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (p *providers) deprecationNotice(logger zerolog.Logger) bool { + if p == nil { + return false + } + + var incompatible bool + + if p.Marathon != nil { + incompatible = true + logger.Error().Msg("Marathon provider has been removed in v3, please remove all Marathon-related static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#marathon-provider") + } + + if p.Rancher != nil { + incompatible = true + logger.Error().Msg("Rancher provider has been removed in v3, please remove all Rancher-related static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#rancher-v1-provider") + } + + dockerIncompatible := p.Docker.deprecationNotice(logger) + consulIncompatible := p.Consul.deprecationNotice(logger) + consulCatalogIncompatible := p.ConsulCatalog.deprecationNotice(logger) + nomadIncompatible := p.Nomad.deprecationNotice(logger) + swarmIncompatible := p.Swarm.deprecationNotice(logger) + etcdIncompatible := p.ETCD.deprecationNotice(logger) + redisIncompatible := p.Redis.deprecationNotice(logger) + httpIncompatible := p.HTTP.deprecationNotice(logger) + return incompatible || + dockerIncompatible || + consulIncompatible || + consulCatalogIncompatible || + nomadIncompatible || + swarmIncompatible || + etcdIncompatible || + redisIncompatible || + httpIncompatible +} + +type tls struct { + CAOptional *bool `json:"caOptional,omitempty" toml:"caOptional,omitempty" yaml:"caOptional,omitempty"` +} + +type docker struct { + SwarmMode *bool `json:"swarmMode,omitempty" toml:"swarmMode,omitempty" yaml:"swarmMode,omitempty"` + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (d *docker) deprecationNotice(logger zerolog.Logger) bool { + if d == nil { + return false + } + + var incompatible bool + + if d.SwarmMode != nil { + incompatible = true + logger.Error().Msg("Docker provider `swarmMode` option has been removed in v3, please use the Swarm Provider instead." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#docker-docker-swarm") + } + + if d.TLS != nil && d.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("Docker provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional") + } + + return incompatible +} + +type swarm struct { + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (s *swarm) deprecationNotice(logger zerolog.Logger) bool { + if s == nil { + return false + } + + var incompatible bool + + if s.TLS != nil && s.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("Swarm provider `tls.CAOptional` option does not exist, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start.") + } + + return incompatible +} + +type etcd struct { + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (e *etcd) deprecationNotice(logger zerolog.Logger) bool { + if e == nil { + return false + } + + var incompatible bool + + if e.TLS != nil && e.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("ETCD provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional_3") + } + + return incompatible +} + +type redis struct { + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (r *redis) deprecationNotice(logger zerolog.Logger) bool { + if r == nil { + return false + } + + var incompatible bool + + if r.TLS != nil && r.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("Redis provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional_4") + } + + return incompatible +} + +type consul struct { + Namespace *string `json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"` + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (c *consul) deprecationNotice(logger zerolog.Logger) bool { + if c == nil { + return false + } + + var incompatible bool + + if c.Namespace != nil { + incompatible = true + logger.Error().Msg("Consul provider `namespace` option has been removed, please use the `namespaces` option instead." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#consul-provider") + } + + if c.TLS != nil && c.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("Consul provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional_1") + } + + return incompatible +} + +type consulCatalog struct { + Namespace *string `json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"` + Endpoint *endpointConfig `json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +type endpointConfig struct { + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty"` +} + +func (c *consulCatalog) deprecationNotice(logger zerolog.Logger) bool { + if c == nil { + return false + } + + var incompatible bool + + if c.Namespace != nil { + incompatible = true + logger.Error().Msg("ConsulCatalog provider `namespace` option has been removed, please use the `namespaces` option instead." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#consulcatalog-provider") + } + + if c.Endpoint != nil && c.Endpoint.TLS != nil && c.Endpoint.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("ConsulCatalog provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#endpointtlscaoptional") + } + + return incompatible +} + +type nomad struct { + Namespace *string `json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"` + Endpoint *endpointConfig `json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (n *nomad) deprecationNotice(logger zerolog.Logger) bool { + if n == nil { + return false + } + + var incompatible bool + + if n.Namespace != nil { + incompatible = true + logger.Error().Msg("Nomad provider `namespace` option has been removed, please use the `namespaces` option instead." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#nomad-provider") + } + + if n.Endpoint != nil && n.Endpoint.TLS != nil && n.Endpoint.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("Nomad provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#endpointtlscaoptional_1") + } + + return incompatible +} + +type http struct { + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (h *http) deprecationNotice(logger zerolog.Logger) bool { + if h == nil { + return false + } + + var incompatible bool + + if h.TLS != nil && h.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("HTTP provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional_2") + } + + return incompatible +} + +type experimental struct { + HTTP3 *bool `json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty"` +} + +func (e *experimental) deprecationNotice(logger zerolog.Logger) bool { + if e == nil { + return false + } + + if e.HTTP3 != nil { + logger.Error().Msg("HTTP3 is not an experimental feature in v3 and the associated enablement has been removed." + + "Please remove its usage from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#http3-experimental-configuration") + + return true + } + + return false +} + +type tracing struct { + SpanNameLimit *int `json:"spanNameLimit,omitempty" toml:"spanNameLimit,omitempty" yaml:"spanNameLimit,omitempty"` + Jaeger map[string]any `json:"jaeger,omitempty" toml:"jaeger,omitempty" yaml:"jaeger,omitempty" label:"allowEmpty" file:"allowEmpty"` + Zipkin map[string]any `json:"zipkin,omitempty" toml:"zipkin,omitempty" yaml:"zipkin,omitempty" label:"allowEmpty" file:"allowEmpty"` + Datadog map[string]any `json:"datadog,omitempty" toml:"datadog,omitempty" yaml:"datadog,omitempty" label:"allowEmpty" file:"allowEmpty"` + Instana map[string]any `json:"instana,omitempty" toml:"instana,omitempty" yaml:"instana,omitempty" label:"allowEmpty" file:"allowEmpty"` + Haystack map[string]any `json:"haystack,omitempty" toml:"haystack,omitempty" yaml:"haystack,omitempty" label:"allowEmpty" file:"allowEmpty"` + Elastic map[string]any `json:"elastic,omitempty" toml:"elastic,omitempty" yaml:"elastic,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (t *tracing) deprecationNotice(logger zerolog.Logger) bool { + if t == nil { + return false + } + var incompatible bool + if t.SpanNameLimit != nil { + incompatible = true + logger.Error().Msg("SpanNameLimit option for Tracing has been removed in v3, as Span names are now of a fixed length." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + if t.Jaeger != nil { + incompatible = true + logger.Error().Msg("Jaeger Tracing backend has been removed in v3, please remove all Jaeger-related Tracing static configuration for Traefik to start." + + "In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + if t.Zipkin != nil { + incompatible = true + logger.Error().Msg("Zipkin Tracing backend has been removed in v3, please remove all Zipkin-related Tracing static configuration for Traefik to start." + + "In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + if t.Datadog != nil { + incompatible = true + logger.Error().Msg("Datadog Tracing backend has been removed in v3, please remove all Datadog-related Tracing static configuration for Traefik to start." + + "In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + if t.Instana != nil { + incompatible = true + logger.Error().Msg("Instana Tracing backend has been removed in v3, please remove all Instana-related Tracing static configuration for Traefik to start." + + "In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + if t.Haystack != nil { + incompatible = true + logger.Error().Msg("Haystack Tracing backend has been removed in v3, please remove all Haystack-related Tracing static configuration for Traefik to start." + + "In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + if t.Elastic != nil { + incompatible = true + logger.Error().Msg("Elastic Tracing backend has been removed in v3, please remove all Elastic-related Tracing static configuration for Traefik to start." + + "In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + return incompatible +} diff --git a/pkg/cli/deprecation_test.go b/pkg/cli/deprecation_test.go new file mode 100644 index 000000000..c225c0e7b --- /dev/null +++ b/pkg/cli/deprecation_test.go @@ -0,0 +1,404 @@ +package cli + +import ( + "testing" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traefik/paerser/cli" + "github.com/traefik/traefik/v3/cmd" +) + +func ptr[T any](t T) *T { + return &t +} + +func TestDeprecationNotice(t *testing.T) { + tests := []struct { + desc string + config configuration + }{ + { + desc: "Docker provider swarmMode option is incompatible", + config: configuration{ + Providers: &providers{ + Docker: &docker{ + SwarmMode: ptr(true), + }, + }, + }, + }, + { + desc: "Docker provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + Docker: &docker{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + { + desc: "Swarm provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + Swarm: &swarm{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + { + desc: "Consul provider namespace option is incompatible", + config: configuration{ + Providers: &providers{ + Consul: &consul{ + Namespace: ptr("foobar"), + }, + }, + }, + }, + { + desc: "Consul provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + Consul: &consul{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + { + desc: "ConsulCatalog provider namespace option is incompatible", + config: configuration{ + Providers: &providers{ + ConsulCatalog: &consulCatalog{ + Namespace: ptr("foobar"), + }, + }, + }, + }, + { + desc: "ConsulCatalog provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + ConsulCatalog: &consulCatalog{ + Endpoint: &endpointConfig{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + }, + { + desc: "Nomad provider namespace option is incompatible", + config: configuration{ + Providers: &providers{ + Nomad: &nomad{ + Namespace: ptr("foobar"), + }, + }, + }, + }, + { + desc: "Nomad provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + Nomad: &nomad{ + Endpoint: &endpointConfig{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + }, + { + desc: "Marathon configuration is incompatible", + config: configuration{ + Providers: &providers{ + Marathon: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "Rancher configuration is incompatible", + config: configuration{ + Providers: &providers{ + Rancher: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "ETCD provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + ETCD: &etcd{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + { + desc: "Redis provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + Redis: &redis{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + { + desc: "HTTP provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + HTTP: &http{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + { + desc: "Pilot configuration is incompatible", + config: configuration{ + Pilot: map[string]any{ + "foo": "bar", + }, + }, + }, + { + desc: "Experimental HTTP3 enablement configuration is incompatible", + config: configuration{ + Experimental: &experimental{ + HTTP3: ptr(true), + }, + }, + }, + { + desc: "Tracing SpanNameLimit option is incompatible", + config: configuration{ + Tracing: &tracing{ + SpanNameLimit: ptr(42), + }, + }, + }, + { + desc: "Tracing Jaeger configuration is incompatible", + config: configuration{ + Tracing: &tracing{ + Jaeger: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "Tracing Zipkin configuration is incompatible", + config: configuration{ + Tracing: &tracing{ + Zipkin: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "Tracing Datadog configuration is incompatible", + config: configuration{ + Tracing: &tracing{ + Datadog: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "Tracing Instana configuration is incompatible", + config: configuration{ + Tracing: &tracing{ + Instana: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "Tracing Haystack configuration is incompatible", + config: configuration{ + Tracing: &tracing{ + Haystack: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "Tracing Elastic configuration is incompatible", + config: configuration{ + Tracing: &tracing{ + Elastic: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var gotLog bool + var gotLevel zerolog.Level + testHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + gotLog = true + gotLevel = level + }) + + logger := log.With().Logger().Hook(testHook) + assert.True(t, test.config.deprecationNotice(logger)) + assert.True(t, gotLog) + assert.Equal(t, zerolog.ErrorLevel, gotLevel) + }) + } +} + +func TestLoad(t *testing.T) { + testCases := []struct { + desc string + args []string + env map[string]string + wantDeprecated bool + }{ + { + desc: "Empty", + args: []string{}, + wantDeprecated: false, + }, + { + desc: "[FLAG] providers.marathon is deprecated", + args: []string{ + "--access-log", + "--log.level=DEBUG", + "--entrypoints.test.http.tls", + "--providers.nomad.endpoint.tls.insecureskipverify=true", + "--providers.marathon", + }, + wantDeprecated: true, + }, + { + desc: "[FLAG] multiple deprecated", + args: []string{ + "--access-log", + "--log.level=DEBUG", + "--entrypoints.test.http.tls", + "--providers.marathon", + "--pilot.token=XXX", + }, + wantDeprecated: true, + }, + { + desc: "[FLAG] no deprecated", + args: []string{ + "--access-log", + "--log.level=DEBUG", + "--entrypoints.test.http.tls", + "--providers.docker", + }, + wantDeprecated: false, + }, + { + desc: "[ENV] providers.marathon is deprecated", + env: map[string]string{ + "TRAEFIK_ACCESS_LOG": "", + "TRAEFIK_LOG_LEVEL": "DEBUG", + "TRAEFIK_ENTRYPOINT_TEST_HTTP_TLS": "true", + "TRAEFIK_PROVIDERS_MARATHON": "true", + }, + wantDeprecated: true, + }, + { + desc: "[ENV] multiple deprecated", + env: map[string]string{ + "TRAEFIK_ACCESS_LOG": "true", + "TRAEFIK_LOG_LEVEL": "DEBUG", + "TRAEFIK_ENTRYPOINT_TEST_HTTP_TLS": "true", + "TRAEFIK_PROVIDERS_MARATHON": "true", + "TRAEFIK_PILOT_TOKEN": "xxx", + }, + wantDeprecated: true, + }, + { + desc: "[ENV] no deprecated", + env: map[string]string{ + "TRAEFIK_ACCESS_LOG": "true", + "TRAEFIK_LOG_LEVEL": "DEBUG", + "TRAEFIK_ENTRYPOINT_TEST_HTTP_TLS": "true", + }, + + wantDeprecated: false, + }, + { + desc: "[FILE] providers.marathon is deprecated", + args: []string{ + "--configfile=./fixtures/traefik_deprecated.toml", + }, + wantDeprecated: true, + }, + { + desc: "[FILE] multiple deprecated", + args: []string{ + "--configfile=./fixtures/traefik_multiple_deprecated.toml", + }, + wantDeprecated: true, + }, + { + desc: "[FILE] no deprecated", + args: []string{ + "--configfile=./fixtures/traefik_no_deprecated.toml", + }, + wantDeprecated: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + tconfig := cmd.NewTraefikConfiguration() + c := &cli.Command{Configuration: tconfig} + l := DeprecationLoader{} + + for name, val := range test.env { + t.Setenv(name, val) + } + deprecated, err := l.Load(test.args, c) + assert.Equal(t, test.wantDeprecated, deprecated) + if !test.wantDeprecated { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/cli/fixtures/traefik_deprecated.toml b/pkg/cli/fixtures/traefik_deprecated.toml new file mode 100644 index 000000000..21fa1d1c1 --- /dev/null +++ b/pkg/cli/fixtures/traefik_deprecated.toml @@ -0,0 +1,5 @@ +[accesslog] + +[entrypoints.test.http.tls] + +[providers.marathon] diff --git a/pkg/cli/fixtures/traefik_multiple_deprecated.toml b/pkg/cli/fixtures/traefik_multiple_deprecated.toml new file mode 100644 index 000000000..0847e9da3 --- /dev/null +++ b/pkg/cli/fixtures/traefik_multiple_deprecated.toml @@ -0,0 +1,8 @@ +[accesslog] + +[entrypoints.test.http.tls] + +[providers.marathon] + +[pilot] + token="xxx" diff --git a/pkg/cli/fixtures/traefik_no_deprecated.toml b/pkg/cli/fixtures/traefik_no_deprecated.toml new file mode 100644 index 000000000..282d2f873 --- /dev/null +++ b/pkg/cli/fixtures/traefik_no_deprecated.toml @@ -0,0 +1,3 @@ +[accesslog] + +[entrypoints.test.http.tls] From a3ac456199ecc8287938cc3a4ec7be8308477f21 Mon Sep 17 00:00:00 2001 From: mmatur Date: Tue, 23 Jan 2024 09:48:46 +0100 Subject: [PATCH 08/36] fix: OpenTelemetry unit tests --- pkg/metrics/opentelemetry_test.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pkg/metrics/opentelemetry_test.go b/pkg/metrics/opentelemetry_test.go index 93fc31c99..7e426cc11 100644 --- a/pkg/metrics/opentelemetry_test.go +++ b/pkg/metrics/opentelemetry_test.go @@ -287,8 +287,8 @@ func TestOpenTelemetry_GaugeCollectorSet(t *testing.T) { } func TestOpenTelemetry(t *testing.T) { - c := make(chan *string) - defer close(c) + c := make(chan *string, 5) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gzr, err := gzip.NewReader(r.Body) require.NoError(t, err) @@ -308,7 +308,11 @@ func TestOpenTelemetry(t *testing.T) { w.WriteHeader(http.StatusOK) })) - defer ts.Close() + + t.Cleanup(func() { + close(c) + ts.Close() + }) sURL, err := url.Parse(ts.URL) require.NoError(t, err) @@ -439,10 +443,11 @@ func TestOpenTelemetry(t *testing.T) { assertMessage(t, *msgEntryPointReqDurationHistogram, expectedEntryPointReqDuration) - // We need to unlock the HTTP Server for the last export call when stopping - // OpenTelemetry. + // Stopping OpenTelemetry. go func() { - <-c + StopOpenTelemetry() }() - StopOpenTelemetry() + + // We need to unlock the HTTP Server for the last export call. + <-c } From 21da705ec9dc312c4aca7676d321d8c5d46ac330 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 23 Jan 2024 11:04:05 +0100 Subject: [PATCH 09/36] fix: gateway api conformance tests --- .github/workflows/test-conformance.yaml | 6 +++++- Makefile | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-conformance.yaml b/.github/workflows/test-conformance.yaml index 488bf452f..707d9d5c0 100644 --- a/.github/workflows/test-conformance.yaml +++ b/.github/workflows/test-conformance.yaml @@ -34,5 +34,9 @@ jobs: - name: Build binary run: make binary + - name: Setcap + run: | + sudo setcap 'cap_net_bind_service=+ep' dist/linux/amd64/traefik + - name: K8s Gateway API conformance test - run: sudo make test-gateway-api-conformance + run: make test-gateway-api-conformance-ci diff --git a/Makefile b/Makefile index 93498e5c9..02a0a0b6d 100644 --- a/Makefile +++ b/Makefile @@ -104,6 +104,12 @@ test-integration: binary test-gateway-api-conformance: binary GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -test.run K8sConformanceSuite -k8sConformance=true $(TESTFLAGS) +## TODO: Need to be fixed to work in all situations. +## Run the conformance tests +.PHONY: test-gateway-api-conformance-ci +test-gateway-api-conformance-ci: + GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -test.run K8sConformanceSuite -k8sConformance=true $(TESTFLAGS) + ## Pull all Docker images to avoid timeout during integration tests .PHONY: pull-images pull-images: From 683e2ee5c6b120aabfe315fd15b560086f99d368 Mon Sep 17 00:00:00 2001 From: Romain Date: Tue, 23 Jan 2024 11:34:05 +0100 Subject: [PATCH 10/36] Bring back v2 rule matchers --- docs/content/migration/v2-to-v3.md | 100 +- .../dynamic-configuration/docker-labels.yml | 4 + .../reference/dynamic-configuration/file.toml | 4 + .../reference/dynamic-configuration/file.yaml | 4 + .../kubernetes-crd-definition-v1.yml | 8 + .../reference/dynamic-configuration/kv-ref.md | 4 + .../traefik.io_ingressroutes.yaml | 4 + .../traefik.io_ingressroutetcps.yaml | 4 + .../reference/static-configuration/cli-ref.md | 3 + .../reference/static-configuration/env-ref.md | 3 + .../reference/static-configuration/file.toml | 3 + .../reference/static-configuration/file.yaml | 2 + docs/content/routing/routers/index.md | 108 ++ go.mod | 2 +- go.sum | 4 +- integration/fixtures/k8s/01-traefik-crd.yml | 8 + .../fixtures/with_default_rule_syntax.toml | 41 + .../fixtures/without_default_rule_syntax.toml | 38 + integration/simple_test.go | 60 + integration/testdata/rawdata-gateway.json | 5 + pkg/config/dynamic/http_config.go | 6 +- pkg/config/dynamic/tcp_config.go | 9 + pkg/config/dynamic/zz_generated.deepcopy.go | 31 + pkg/config/static/static_config.go | 25 +- pkg/muxer/http/matcher_test.go | 22 +- pkg/muxer/http/matcher_v2.go | 226 +++ pkg/muxer/http/matcher_v2_test.go | 1535 +++++++++++++++++ pkg/muxer/http/mux.go | 57 +- pkg/muxer/http/mux_test.go | 6 +- pkg/muxer/tcp/matcher_test.go | 10 +- pkg/muxer/tcp/matcher_v2.go | 240 +++ pkg/muxer/tcp/matcher_v2_test.go | 1008 +++++++++++ pkg/muxer/tcp/mux.go | 59 +- pkg/muxer/tcp/mux_test.go | 4 +- .../kubernetes/crd/kubernetes_http.go | 1 + pkg/provider/kubernetes/crd/kubernetes_tcp.go | 1 + .../crd/traefikio/v1alpha1/ingressroute.go | 3 + .../crd/traefikio/v1alpha1/ingressroutetcp.go | 3 + pkg/provider/kubernetes/gateway/kubernetes.go | 5 +- .../kubernetes/gateway/kubernetes_test.go | 99 +- .../fixtures/api_insecure_with_dashboard.json | 10 +- .../traefik/fixtures/full_configuration.json | 16 +- .../traefik/fixtures/redirection.json | 8 +- .../traefik/fixtures/redirection_port.json | 8 +- .../fixtures/redirection_with_protocol.json | 8 +- pkg/provider/traefik/internal.go | 20 +- pkg/server/aggregator.go | 27 + pkg/server/aggregator_test.go | 1 + pkg/server/configurationwatcher_test.go | 6 + pkg/server/router/router.go | 2 +- pkg/server/router/tcp/manager.go | 8 +- pkg/server/router/tcp/router.go | 8 +- pkg/server/router/tcp/router_test.go | 4 +- pkg/server/server_entrypoint_tcp_test.go | 2 +- 54 files changed, 3773 insertions(+), 114 deletions(-) create mode 100644 integration/fixtures/with_default_rule_syntax.toml create mode 100644 integration/fixtures/without_default_rule_syntax.toml create mode 100644 pkg/muxer/http/matcher_v2.go create mode 100644 pkg/muxer/http/matcher_v2_test.go create mode 100644 pkg/muxer/tcp/matcher_v2.go create mode 100644 pkg/muxer/tcp/matcher_v2_test.go diff --git a/docs/content/migration/v2-to-v3.md b/docs/content/migration/v2-to-v3.md index 4396e0887..b9f7c5102 100644 --- a/docs/content/migration/v2-to-v3.md +++ b/docs/content/migration/v2-to-v3.md @@ -526,21 +526,16 @@ All Pilot related configuration should be removed from the static configuration. ## Dynamic configuration -### IPWhiteList +### Router Rule Matchers -In v3, we renamed the `IPWhiteList` middleware to `IPAllowList` without changing anything to the configuration. +In v3, a new rule matchers syntax has been introduced for HTTP and TCP routers. +The default rule matchers syntax is now the v3 one, but for backward compatibility this can be configured. +The v2 rule matchers syntax is deprecated and its support will be removed in the next major version. +For this reason, we encourage migrating to the new syntax. -### Deprecated Options Removal +#### New V3 Syntax Notable Changes -- The `tracing.datadog.globaltag` option has been removed. -- The `tls.caOptional` option has been removed from the ForwardAuth middleware, as well as from the HTTP, Consul, Etcd, Redis, ZooKeeper, Consul Catalog, and Docker providers. -- `sslRedirect`, `sslTemporaryRedirect`, `sslHost`, `sslForceHost` and `featurePolicy` options of the Headers middleware have been removed. -- The `forceSlash` option of the StripPrefix middleware has been removed. -- The `preferServerCipherSuites` option has been removed. - -### Matchers - -In v3, the `Headers` and `HeadersRegexp` matchers have been renamed to `Header` and `HeaderRegexp` respectively. +The `Headers` and `HeadersRegexp` matchers have been renamed to `Header` and `HeaderRegexp` respectively. `PathPrefix` no longer uses regular expressions to match path prefixes. @@ -555,6 +550,87 @@ and should be explicitly combined using logical operators to mimic previous beha `HostHeader` has been removed, use `Host` instead. +#### Remediation + +##### Configure the Default Syntax In Static Configuration + +The default rule matchers syntax is the expected syntax for any router that is not self opt-out from this default value. +It can be configured in the static configuration. + +??? example "An example configuration for the default rule matchers syntax" + + ```yaml tab="File (YAML)" + # static configuration + core: + defaultRuleSyntax: v2 + ``` + + ```toml tab="File (TOML)" + # static configuration + [core] + defaultRuleSyntax="v2" + ``` + + ```bash tab="CLI" + # static configuration + --core.defaultRuleSyntax=v2 + ``` + +##### Configure the Syntax Per Router + +The rule syntax can also be configured on a per-router basis. +This allows to have heterogeneous router configurations and ease migration. + +??? example "An example router with syntax configuration" + +```yaml tab="Docker & Swarm" +labels: + - "traefik.http.routers.test.ruleSyntax=v2" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + routes: + - match: PathPrefix(`/foo`, `/bar`) + syntax: v2 + kind: Rule +``` + +```yaml tab="Consul Catalog" +- "traefik.http.routers.test.ruleSyntax=v2" +``` + +```yaml tab="File (YAML)" +http: + routers: + test: + ruleSyntax: v2 +``` + +```toml tab="File (TOML)" +[http.routers] + [http.routers.test] + ruleSyntax = "v2" +``` + +### IPWhiteList + +In v3, we renamed the `IPWhiteList` middleware to `IPAllowList` without changing anything to the configuration. + +### Deprecated Options Removal + +- The `tracing.datadog.globaltag` option has been removed. +- The `tls.caOptional` option has been removed from the ForwardAuth middleware, as well as from the HTTP, Consul, Etcd, Redis, ZooKeeper, Consul Catalog, and Docker providers. +- `sslRedirect`, `sslTemporaryRedirect`, `sslHost`, `sslForceHost` and `featurePolicy` options of the Headers middleware have been removed. +- The `forceSlash` option of the StripPrefix middleware has been removed. +- The `preferServerCipherSuites` option has been removed. + ### TCP LoadBalancer `terminationDelay` option The TCP LoadBalancer `terminationDelay` option has been removed. diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 93a785ff4..df6db2ff0 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -132,6 +132,7 @@ - "traefik.http.routers.router0.middlewares=foobar, foobar" - "traefik.http.routers.router0.priority=42" - "traefik.http.routers.router0.rule=foobar" +- "traefik.http.routers.router0.rulesyntax=foobar" - "traefik.http.routers.router0.service=foobar" - "traefik.http.routers.router0.tls=true" - "traefik.http.routers.router0.tls.certresolver=foobar" @@ -144,6 +145,7 @@ - "traefik.http.routers.router1.middlewares=foobar, foobar" - "traefik.http.routers.router1.priority=42" - "traefik.http.routers.router1.rule=foobar" +- "traefik.http.routers.router1.rulesyntax=foobar" - "traefik.http.routers.router1.service=foobar" - "traefik.http.routers.router1.tls=true" - "traefik.http.routers.router1.tls.certresolver=foobar" @@ -183,6 +185,7 @@ - "traefik.tcp.routers.tcprouter0.middlewares=foobar, foobar" - "traefik.tcp.routers.tcprouter0.priority=42" - "traefik.tcp.routers.tcprouter0.rule=foobar" +- "traefik.tcp.routers.tcprouter0.rulesyntax=foobar" - "traefik.tcp.routers.tcprouter0.service=foobar" - "traefik.tcp.routers.tcprouter0.tls=true" - "traefik.tcp.routers.tcprouter0.tls.certresolver=foobar" @@ -196,6 +199,7 @@ - "traefik.tcp.routers.tcprouter1.middlewares=foobar, foobar" - "traefik.tcp.routers.tcprouter1.priority=42" - "traefik.tcp.routers.tcprouter1.rule=foobar" +- "traefik.tcp.routers.tcprouter1.rulesyntax=foobar" - "traefik.tcp.routers.tcprouter1.service=foobar" - "traefik.tcp.routers.tcprouter1.tls=true" - "traefik.tcp.routers.tcprouter1.tls.certresolver=foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index a8264d2ed..541affb9c 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -7,6 +7,7 @@ middlewares = ["foobar", "foobar"] service = "foobar" rule = "foobar" + ruleSyntax = "foobar" priority = 42 [http.routers.Router0.tls] options = "foobar" @@ -24,6 +25,7 @@ middlewares = ["foobar", "foobar"] service = "foobar" rule = "foobar" + ruleSyntax = "foobar" priority = 42 [http.routers.Router1.tls] options = "foobar" @@ -353,6 +355,7 @@ middlewares = ["foobar", "foobar"] service = "foobar" rule = "foobar" + ruleSyntax = "foobar" priority = 42 [tcp.routers.TCPRouter0.tls] passthrough = true @@ -371,6 +374,7 @@ middlewares = ["foobar", "foobar"] service = "foobar" rule = "foobar" + ruleSyntax = "foobar" priority = 42 [tcp.routers.TCPRouter1.tls] passthrough = true diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 7ae72ad7d..4e0019e42 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -11,6 +11,7 @@ http: - foobar service: foobar rule: foobar + ruleSyntax: foobar priority: 42 tls: options: foobar @@ -33,6 +34,7 @@ http: - foobar service: foobar rule: foobar + ruleSyntax: foobar priority: 42 tls: options: foobar @@ -409,6 +411,7 @@ tcp: - foobar service: foobar rule: foobar + ruleSyntax: foobar priority: 42 tls: passthrough: true @@ -432,6 +435,7 @@ tcp: - foobar service: foobar rule: foobar + ruleSyntax: foobar priority: 42 tls: passthrough: true diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index 5a4c63c3c..966a45177 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -195,6 +195,10 @@ spec: - name type: object type: array + syntax: + description: 'Syntax defines the router''s rule syntax. More + info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax' + type: string required: - kind - match @@ -402,6 +406,10 @@ spec: - port type: object type: array + syntax: + description: 'Syntax defines the router''s rule syntax. More + info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax_1' + type: string required: - match type: object diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index f078b166a..6d80b13b8 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -158,6 +158,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/routers/Router0/middlewares/1` | `foobar` | | `traefik/http/routers/Router0/priority` | `42` | | `traefik/http/routers/Router0/rule` | `foobar` | +| `traefik/http/routers/Router0/ruleSyntax` | `foobar` | | `traefik/http/routers/Router0/service` | `foobar` | | `traefik/http/routers/Router0/tls/certResolver` | `foobar` | | `traefik/http/routers/Router0/tls/domains/0/main` | `foobar` | @@ -173,6 +174,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/routers/Router1/middlewares/1` | `foobar` | | `traefik/http/routers/Router1/priority` | `42` | | `traefik/http/routers/Router1/rule` | `foobar` | +| `traefik/http/routers/Router1/ruleSyntax` | `foobar` | | `traefik/http/routers/Router1/service` | `foobar` | | `traefik/http/routers/Router1/tls/certResolver` | `foobar` | | `traefik/http/routers/Router1/tls/domains/0/main` | `foobar` | @@ -273,6 +275,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/tcp/routers/TCPRouter0/middlewares/1` | `foobar` | | `traefik/tcp/routers/TCPRouter0/priority` | `42` | | `traefik/tcp/routers/TCPRouter0/rule` | `foobar` | +| `traefik/tcp/routers/TCPRouter0/ruleSyntax` | `foobar` | | `traefik/tcp/routers/TCPRouter0/service` | `foobar` | | `traefik/tcp/routers/TCPRouter0/tls/certResolver` | `foobar` | | `traefik/tcp/routers/TCPRouter0/tls/domains/0/main` | `foobar` | @@ -289,6 +292,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/tcp/routers/TCPRouter1/middlewares/1` | `foobar` | | `traefik/tcp/routers/TCPRouter1/priority` | `42` | | `traefik/tcp/routers/TCPRouter1/rule` | `foobar` | +| `traefik/tcp/routers/TCPRouter1/ruleSyntax` | `foobar` | | `traefik/tcp/routers/TCPRouter1/service` | `foobar` | | `traefik/tcp/routers/TCPRouter1/tls/certResolver` | `foobar` | | `traefik/tcp/routers/TCPRouter1/tls/domains/0/main` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml index 41628b58a..8399f56fb 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml @@ -195,6 +195,10 @@ spec: - name type: object type: array + syntax: + description: 'Syntax defines the router''s rule syntax. More + info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax' + type: string required: - kind - match diff --git a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutetcps.yaml b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutetcps.yaml index 94226e14c..0f95de8d2 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutetcps.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutetcps.yaml @@ -129,6 +129,10 @@ spec: - port type: object type: array + syntax: + description: 'Syntax defines the router''s rule syntax. More + info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax_1' + type: string required: - match type: object diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index aabed9139..2f655ccb1 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -105,6 +105,9 @@ Activate TLS-ALPN-01 Challenge. (Default: ```true```) `--certificatesresolvers..tailscale`: Enables Tailscale certificate resolution. (Default: ```true```) +`--core.defaultrulesyntax`: +Defines the rule parser default syntax (v2 or v3) (Default: ```v3```) + `--entrypoints.`: Entry points definition. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 892fb2369..fdf733783 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -105,6 +105,9 @@ Activate TLS-ALPN-01 Challenge. (Default: ```true```) `TRAEFIK_CERTIFICATESRESOLVERS__TAILSCALE`: Enables Tailscale certificate resolution. (Default: ```true```) +`TRAEFIK_CORE_DEFAULTRULESYNTAX`: +Defines the rule parser default syntax (v2 or v3) (Default: ```v3```) + `TRAEFIK_ENTRYPOINTS_`: Entry points definition. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 3edf7ebc9..8f9326416 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -453,5 +453,8 @@ [experimental.localPlugins.LocalDescriptor1] moduleName = "foobar" +[core] + defaultRuleSyntax = "foobar" + [spiffe] workloadAPIAddr = "foobar" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index a9ccffe9a..1739759cb 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -486,5 +486,7 @@ experimental: LocalDescriptor1: moduleName: foobar kubernetesGateway: true +core: + defaultRuleSyntax: foobar spiffe: workloadAPIAddr: foobar diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 55b46dc9d..9a4b8c7d0 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -515,6 +515,60 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul In this configuration, the priority is configured to allow `Router-2` to handle requests with the `foobar.traefik.com` host. +### RuleSyntax + +In Traefik v3 a new rule syntax has been introduced ([migration guide](../../migration/v2-to-v3.md#router-rule-matchers)). +`ruleSyntax` option allows to configure the rule syntax to be used for parsing the rule on a per-router basis. +This allows to have heterogeneous router configurations and ease migration. + +??? example "Set rule syntax -- using the [File Provider](../../providers/file.md)" + + ```yaml tab="File (YAML)" + ## Dynamic configuration + http: + routers: + Router-v3: + rule: HostRegexp(`[a-z]+\\.traefik\\.com`) + ruleSyntax: v3 + Router-v2: + rule: HostRegexp(`{subdomain:[a-z]+}.traefik.com`) + ruleSyntax: v2 + ``` + + ```toml tab="File (TOML)" + ## Dynamic configuration + [http.routers] + [http.routers.Router-v3] + rule = "HostRegexp(`[a-z]+\\.traefik\\.com`)" + ruleSyntax = v3 + [http.routers.Router-v2] + rule = "HostRegexp(`{subdomain:[a-z]+}.traefik.com`)" + ruleSyntax = v2 + ``` + + ```yaml tab="Kubernetes traefik.io/v1alpha1" + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: default + + spec: + routes: + # route v3 + - match: HostRegexp(`[a-z]+\\.traefik\\.com`) + syntax: v3 + kind: Rule + + # route v2 + - match: HostRegexp(`{subdomain:[a-z]+}.traefik.com`) + syntax: v2 + kind: Rule + ``` + + In this configuration, the ruleSyntax is configured to allow `Router-v2` to use v2 syntax, + while for `Router-v3` it is configured to use v3 syntax. + ### Middlewares You can attach a list of [middlewares](../../middlewares/overview.md) to each HTTP router. @@ -1161,6 +1215,60 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul In this configuration, the priority is configured so that `Router-1` will handle requests from `192.168.0.12`. +### RuleSyntax + +In Traefik v3 a new rule syntax has been introduced ([migration guide](../../migration/v2-to-v3.md#router-rule-matchers)). +`ruleSyntax` option allows to configure the rule syntax to be used for parsing the rule on a per-router basis. +This allows to have heterogeneous router configurations and ease migration. + +??? example "Set rule syntax -- using the [File Provider](../../providers/file.md)" + + ```yaml tab="File (YAML)" + ## Dynamic configuration + tcp: + routers: + Router-v3: + rule: ClientIP(`192.168.0.11`) || ClientIP(`192.168.0.12`) + ruleSyntax: v3 + Router-v2: + rule: ClientIP(`192.168.0.11`, `192.168.0.12`) + ruleSyntax: v2 + ``` + + ```toml tab="File (TOML)" + ## Dynamic configuration + [tcp.routers] + [tcp.routers.Router-v3] + rule = "ClientIP(`192.168.0.11`) || ClientIP(`192.168.0.12`)" + ruleSyntax = v3 + [tcp.routers.Router-v2] + rule = "ClientIP(`192.168.0.11`, `192.168.0.12`)" + ruleSyntax = v2 + ``` + + ```yaml tab="Kubernetes traefik.io/v1alpha1" + apiVersion: traefik.io/v1alpha1 + kind: IngressRouteTCP + metadata: + name: test.route + namespace: default + + spec: + routes: + # route v3 + - match: ClientIP(`192.168.0.11`) || ClientIP(`192.168.0.12`) + syntax: v3 + kind: Rule + + # route v2 + - match: ClientIP(`192.168.0.11`, `192.168.0.12`) + syntax: v2 + kind: Rule + ``` + + In this configuration, the ruleSyntax is configured to allow `Router-v2` to use v2 syntax, + while for `Router-v3` it is configured to use v3 syntax. + ### Middlewares You can attach a list of [middlewares](../../middlewares/overview.md) to each TCP router. diff --git a/go.mod b/go.mod index 17fee2a7f..3685c0c5a 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-retryablehttp v0.7.4 github.com/hashicorp/go-version v1.6.0 - github.com/hashicorp/nomad/api v0.0.0-20231213195942-64e3dca9274b + github.com/hashicorp/nomad/api v0.0.0-20240122103822-8a4bd61caf74 github.com/http-wasm/http-wasm-host-go v0.5.2 github.com/influxdata/influxdb-client-go/v2 v2.7.0 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d diff --git a/go.sum b/go.sum index 28bc21641..c651525ee 100644 --- a/go.sum +++ b/go.sum @@ -598,8 +598,8 @@ github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= -github.com/hashicorp/nomad/api v0.0.0-20231213195942-64e3dca9274b h1:R1UDhkwGltpSPY9bCBBxIMQd+NY9BkN0vFHnJo/8o8w= -github.com/hashicorp/nomad/api v0.0.0-20231213195942-64e3dca9274b/go.mod h1:ijDwa6o1uG1jFSq6kERiX2PamKGpZzTmo0XOFNeFZgw= +github.com/hashicorp/nomad/api v0.0.0-20240122103822-8a4bd61caf74 h1:Q+WuGTnZkL2cJ7yNsg4Go4GNnRkcahGLiQP/WD41TTA= +github.com/hashicorp/nomad/api v0.0.0-20240122103822-8a4bd61caf74/go.mod h1:ijDwa6o1uG1jFSq6kERiX2PamKGpZzTmo0XOFNeFZgw= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 5a4c63c3c..966a45177 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -195,6 +195,10 @@ spec: - name type: object type: array + syntax: + description: 'Syntax defines the router''s rule syntax. More + info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax' + type: string required: - kind - match @@ -402,6 +406,10 @@ spec: - port type: object type: array + syntax: + description: 'Syntax defines the router''s rule syntax. More + info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax_1' + type: string required: - match type: object diff --git a/integration/fixtures/with_default_rule_syntax.toml b/integration/fixtures/with_default_rule_syntax.toml new file mode 100644 index 000000000..76001eac9 --- /dev/null +++ b/integration/fixtures/with_default_rule_syntax.toml @@ -0,0 +1,41 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[core] + defaultRuleSyntax = "v2" + +[log] + level = "DEBUG" + noColor = true + +[entryPoints] + [entryPoints.web] + address = ":8000" + +[api] + insecure = true + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.router1] + service = "service1" + rule = "PathPrefix(`/foo`, `/bar`)" + + [http.routers.router2] + service = "service1" + rule = "QueryRegexp(`foo`, `bar`)" + + [http.routers.router3] + service = "service1" + rule = "PathPrefix(`/foo`, `/bar`)" + ruleSyntax = "v3" + +[http.services] + [http.services.service1] + [http.services.service1.loadBalancer] + [http.services.service1.loadBalancer.servers] diff --git a/integration/fixtures/without_default_rule_syntax.toml b/integration/fixtures/without_default_rule_syntax.toml new file mode 100644 index 000000000..772ea7a85 --- /dev/null +++ b/integration/fixtures/without_default_rule_syntax.toml @@ -0,0 +1,38 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + noColor = true + +[entryPoints] + [entryPoints.web] + address = ":8000" + +[api] + insecure = true + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.router1] + service = "service1" + rule = "PathPrefix(`/foo`) || PathPrefix(`/bar`)" + + [http.routers.router2] + service = "service1" + rule = "PathPrefix(`/foo`, `/bar`)" + + [http.routers.router3] + service = "service1" + rule = "QueryRegexp(`foo`, `bar`)" + ruleSyntax = "v2" + +[http.services] + [http.services.service1] + [http.services.service1.loadBalancer] + [http.services.service1.loadBalancer.servers] diff --git a/integration/simple_test.go b/integration/simple_test.go index d8f0f45b7..94b4ec45e 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -659,6 +659,66 @@ func (s *SimpleSuite) TestSimpleConfigurationHostRequestTrailingPeriod() { } } +func (s *SimpleSuite) TestWithDefaultRuleSyntax() { + file := s.adaptFile("fixtures/with_default_rule_syntax.toml", struct{}{}) + + s.traefikCmd(withConfigFile(file)) + + err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix")) + require.NoError(s.T(), err) + + // router1 has no error + err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router1@file", 1*time.Second, try.BodyContains(`"status":"enabled"`)) + require.NoError(s.T(), err) + + err = try.GetRequest("http://127.0.0.1:8000/notfound", 1*time.Second, try.StatusCodeIs(http.StatusNotFound)) + require.NoError(s.T(), err) + + err = try.GetRequest("http://127.0.0.1:8000/foo", 1*time.Second, try.StatusCodeIs(http.StatusServiceUnavailable)) + require.NoError(s.T(), err) + + err = try.GetRequest("http://127.0.0.1:8000/bar", 1*time.Second, try.StatusCodeIs(http.StatusServiceUnavailable)) + require.NoError(s.T(), err) + + // router2 has an error because it uses the wrong rule syntax (v3 instead of v2) + err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router2@file", 1*time.Second, try.BodyContains("error while parsing rule QueryRegexp(`foo`, `bar`): unsupported function: QueryRegexp")) + require.NoError(s.T(), err) + + // router3 has an error because it uses the wrong rule syntax (v2 instead of v3) + err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router3@file", 1*time.Second, try.BodyContains("error while adding rule PathPrefix: unexpected number of parameters; got 2, expected one of [1]")) + require.NoError(s.T(), err) +} + +func (s *SimpleSuite) TestWithoutDefaultRuleSyntax() { + file := s.adaptFile("fixtures/without_default_rule_syntax.toml", struct{}{}) + + s.traefikCmd(withConfigFile(file)) + + err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix")) + require.NoError(s.T(), err) + + // router1 has no error + err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router1@file", 1*time.Second, try.BodyContains(`"status":"enabled"`)) + require.NoError(s.T(), err) + + err = try.GetRequest("http://127.0.0.1:8000/notfound", 1*time.Second, try.StatusCodeIs(http.StatusNotFound)) + require.NoError(s.T(), err) + + err = try.GetRequest("http://127.0.0.1:8000/foo", 1*time.Second, try.StatusCodeIs(http.StatusServiceUnavailable)) + require.NoError(s.T(), err) + + err = try.GetRequest("http://127.0.0.1:8000/bar", 1*time.Second, try.StatusCodeIs(http.StatusServiceUnavailable)) + require.NoError(s.T(), err) + + // router2 has an error because it uses the wrong rule syntax (v3 instead of v2) + err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router2@file", 1*time.Second, try.BodyContains("error while adding rule PathPrefix: unexpected number of parameters; got 2, expected one of [1]")) + require.NoError(s.T(), err) + + // router2 has an error because it uses the wrong rule syntax (v2 instead of v3) + err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router3@file", 1*time.Second, try.BodyContains("error while parsing rule QueryRegexp(`foo`, `bar`): unsupported function: QueryRegexp")) + require.NoError(s.T(), err) +} + func (s *SimpleSuite) TestRouterConfigErrors() { file := s.adaptFile("fixtures/router_errors.toml", struct{}{}) diff --git a/integration/testdata/rawdata-gateway.json b/integration/testdata/rawdata-gateway.json index f8e364e66..909ce1326 100644 --- a/integration/testdata/rawdata-gateway.json +++ b/integration/testdata/rawdata-gateway.json @@ -34,6 +34,7 @@ ], "service": "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", "rule": "Host(`foo.com`) \u0026\u0026 Path(`/bar`)", + "ruleSyntax": "v3", "priority": 31, "status": "enabled", "using": [ @@ -46,6 +47,7 @@ ], "service": "default-http-app-1-my-https-gateway-websecure-1c0cf64bde37d9d0df06-wrr", "rule": "Host(`foo.com`) \u0026\u0026 Path(`/bar`)", + "ruleSyntax": "v3", "priority": 31, "tls": {}, "status": "enabled", @@ -152,6 +154,7 @@ ], "service": "default-tcp-app-1-my-tcp-gateway-footcp-e3b0c44298fc1c149afb-wrr-0", "rule": "HostSNI(`*`)", + "ruleSyntax": "v3", "priority": -1, "status": "enabled", "using": [ @@ -164,6 +167,7 @@ ], "service": "default-tcp-app-1-my-tls-gateway-footlsterminate-e3b0c44298fc1c149afb-wrr-0", "rule": "HostSNI(`*`)", + "ruleSyntax": "v3", "priority": -1, "tls": { "passthrough": false @@ -179,6 +183,7 @@ ], "service": "default-tls-app-1-my-tls-gateway-footlspassthrough-2279fe75c5156dc5eb26-wrr-0", "rule": "HostSNI(`foo.bar`)", + "ruleSyntax": "v3", "priority": 18, "tls": { "passthrough": true diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index a447e94eb..0783d5592 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -37,8 +37,9 @@ type HTTPConfiguration struct { // Model is a set of default router's values. type Model struct { - Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` - TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` + Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` + TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` + DefaultRuleSyntax string `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` } // +k8s:deepcopy-gen=true @@ -59,6 +60,7 @@ type Router struct { Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"` Rule string `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"` + RuleSyntax string `json:"ruleSyntax,omitempty" toml:"ruleSyntax,omitempty" yaml:"ruleSyntax,omitempty" export:"true"` Priority int `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty" export:"true"` TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` DefaultRule bool `json:"-" toml:"-" yaml:"-" label:"-" file:"-"` diff --git a/pkg/config/dynamic/tcp_config.go b/pkg/config/dynamic/tcp_config.go index adedd0882..4bf82364f 100644 --- a/pkg/config/dynamic/tcp_config.go +++ b/pkg/config/dynamic/tcp_config.go @@ -16,11 +16,19 @@ type TCPConfiguration struct { Routers map[string]*TCPRouter `json:"routers,omitempty" toml:"routers,omitempty" yaml:"routers,omitempty" export:"true"` Services map[string]*TCPService `json:"services,omitempty" toml:"services,omitempty" yaml:"services,omitempty" export:"true"` Middlewares map[string]*TCPMiddleware `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` + Models map[string]*TCPModel `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` ServersTransports map[string]*TCPServersTransport `json:"serversTransports,omitempty" toml:"serversTransports,omitempty" yaml:"serversTransports,omitempty" label:"-" export:"true"` } // +k8s:deepcopy-gen=true +// TCPModel is a set of default router's values. +type TCPModel struct { + DefaultRuleSyntax string `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` +} + +// +k8s:deepcopy-gen=true + // TCPService holds a tcp service configuration (can only be of one type at the same time). type TCPService struct { LoadBalancer *TCPServersLoadBalancer `json:"loadBalancer,omitempty" toml:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty" export:"true"` @@ -56,6 +64,7 @@ type TCPRouter struct { Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"` Rule string `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"` + RuleSyntax string `json:"ruleSyntax,omitempty" toml:"ruleSyntax,omitempty" yaml:"ruleSyntax,omitempty" export:"true"` Priority int `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty" export:"true"` TLS *RouterTCPTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` } diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 847e350ba..3969d144d 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -1435,6 +1435,21 @@ func (in *TCPConfiguration) DeepCopyInto(out *TCPConfiguration) { (*out)[key] = outVal } } + if in.Models != nil { + in, out := &in.Models, &out.Models + *out = make(map[string]*TCPModel, len(*in)) + for key, val := range *in { + var outVal *TCPModel + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = new(TCPModel) + **out = **in + } + (*out)[key] = outVal + } + } if in.ServersTransports != nil { in, out := &in.ServersTransports, &out.ServersTransports *out = make(map[string]*TCPServersTransport, len(*in)) @@ -1552,6 +1567,22 @@ func (in *TCPMiddleware) DeepCopy() *TCPMiddleware { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TCPModel) DeepCopyInto(out *TCPModel) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPModel. +func (in *TCPModel) DeepCopy() *TCPModel { + if in == nil { + return nil + } + out := new(TCPModel) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TCPRouter) DeepCopyInto(out *TCPRouter) { *out = *in diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 9f0ce294f..bb5778671 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -71,11 +71,23 @@ type Configuration struct { CertificatesResolvers map[string]CertificateResolver `description:"Certificates resolvers configuration." json:"certificatesResolvers,omitempty" toml:"certificatesResolvers,omitempty" yaml:"certificatesResolvers,omitempty" export:"true"` - Experimental *Experimental `description:"experimental features." json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty" export:"true"` + Experimental *Experimental `description:"Experimental features." json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty" export:"true"` + + Core *Core `description:"Core controls." json:"core,omitempty" toml:"core,omitempty" yaml:"core,omitempty" export:"true"` Spiffe *SpiffeClientConfig `description:"SPIFFE integration configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" export:"true"` } +// Core configures Traefik core behavior. +type Core struct { + DefaultRuleSyntax string `description:"Defines the rule parser default syntax (v2 or v3)" json:"defaultRuleSyntax,omitempty" toml:"defaultRuleSyntax,omitempty" yaml:"defaultRuleSyntax,omitempty"` +} + +// SetDefaults sets the default values. +func (c *Core) SetDefaults() { + c.DefaultRuleSyntax = "v3" +} + // SpiffeClientConfig defines the SPIFFE client configuration. type SpiffeClientConfig struct { WorkloadAPIAddr string `description:"Defines the workload API address." json:"workloadAPIAddr,omitempty" toml:"workloadAPIAddr,omitempty" yaml:"workloadAPIAddr,omitempty"` @@ -317,6 +329,17 @@ func (c *Configuration) ValidateConfiguration() error { acmeEmail = resolver.ACME.Email } + if c.Core != nil { + switch c.Core.DefaultRuleSyntax { + case "v3": // NOOP + case "v2": + // TODO: point to migration guide. + log.Warn().Msgf("v2 rules syntax is now deprecated, please use v3 instead...") + default: + return fmt.Errorf("unsupported default rule syntax configuration: %q", c.Core.DefaultRuleSyntax) + } + } + if c.Tracing != nil && c.Tracing.OTLP != nil { if c.Tracing.OTLP.HTTP == nil && c.Tracing.OTLP.GRPC == nil { return errors.New("tracing OTLP: at least one of HTTP and gRPC options should be defined") diff --git a/pkg/muxer/http/matcher_test.go b/pkg/muxer/http/matcher_test.go index 1b527a392..c4e601d01 100644 --- a/pkg/muxer/http/matcher_test.go +++ b/pkg/muxer/http/matcher_test.go @@ -73,7 +73,7 @@ func TestClientIPMatcher(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) if test.expectedError { require.Error(t, err) return @@ -147,7 +147,7 @@ func TestMethodMatcher(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) if test.expectedError { require.Error(t, err) return @@ -265,7 +265,7 @@ func TestHostMatcher(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) if test.expectedError { require.Error(t, err) return @@ -365,7 +365,7 @@ func TestHostRegexpMatcher(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) if test.expectedError { require.Error(t, err) return @@ -439,7 +439,7 @@ func TestPathMatcher(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) if test.expectedError { require.Error(t, err) return @@ -532,7 +532,7 @@ func TestPathRegexpMatcher(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) if test.expectedError { require.Error(t, err) return @@ -604,7 +604,7 @@ func TestPathPrefixMatcher(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) if test.expectedError { require.Error(t, err) return @@ -692,7 +692,7 @@ func TestHeaderMatcher(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) if test.expectedError { require.Error(t, err) return @@ -800,7 +800,7 @@ func TestHeaderRegexpMatcher(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) if test.expectedError { require.Error(t, err) return @@ -889,7 +889,7 @@ func TestQueryMatcher(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) if test.expectedError { require.Error(t, err) return @@ -1003,7 +1003,7 @@ func TestQueryRegexpMatcher(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) if test.expectedError { require.Error(t, err) return diff --git a/pkg/muxer/http/matcher_v2.go b/pkg/muxer/http/matcher_v2.go new file mode 100644 index 000000000..72d5d826a --- /dev/null +++ b/pkg/muxer/http/matcher_v2.go @@ -0,0 +1,226 @@ +package http + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" + "github.com/traefik/traefik/v3/pkg/ip" + "github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator" +) + +var httpFuncsV2 = map[string]func(*matchersTree, ...string) error{ + "Host": hostV2, + "HostHeader": hostV2, + "HostRegexp": hostRegexpV2, + "ClientIP": clientIPV2, + "Path": pathV2, + "PathPrefix": pathPrefixV2, + "Method": methodsV2, + "Headers": headersV2, + "HeadersRegexp": headersRegexpV2, + "Query": queryV2, +} + +func pathV2(tree *matchersTree, paths ...string) error { + for _, path := range paths { + if !strings.HasPrefix(path, "/") { + return fmt.Errorf("path %q does not start with a '/'", path) + } + } + + tree.matcher = func(req *http.Request) bool { + for _, path := range paths { + if req.URL.Path == path { + return true + } + } + + return false + } + + return nil +} + +func pathPrefixV2(tree *matchersTree, paths ...string) error { + for _, path := range paths { + if !strings.HasPrefix(path, "/") { + return fmt.Errorf("path %q does not start with a '/'", path) + } + } + + tree.matcher = func(req *http.Request) bool { + for _, path := range paths { + if strings.HasPrefix(req.URL.Path, path) { + return true + } + } + + return false + } + + return nil +} + +func hostV2(tree *matchersTree, hosts ...string) error { + for i, host := range hosts { + if !IsASCII(host) { + return fmt.Errorf("invalid value %q for \"Host\" matcher, non-ASCII characters are not allowed", host) + } + + hosts[i] = strings.ToLower(host) + } + + tree.matcher = func(req *http.Request) bool { + reqHost := requestdecorator.GetCanonizedHost(req.Context()) + if len(reqHost) == 0 { + // If the request is an HTTP/1.0 request, then a Host may not be defined. + if req.ProtoAtLeast(1, 1) { + log.Ctx(req.Context()).Warn().Msgf("Could not retrieve CanonizedHost, rejecting %s", req.Host) + } + + return false + } + + flatH := requestdecorator.GetCNAMEFlatten(req.Context()) + if len(flatH) > 0 { + for _, host := range hosts { + if strings.EqualFold(reqHost, host) || strings.EqualFold(flatH, host) { + return true + } + log.Ctx(req.Context()).Debug().Msgf("CNAMEFlattening: request %s which resolved to %s, is not matched to route %s", reqHost, flatH, host) + } + return false + } + + for _, host := range hosts { + if reqHost == host { + return true + } + + // Check for match on trailing period on host + if last := len(host) - 1; last >= 0 && host[last] == '.' { + h := host[:last] + if reqHost == h { + return true + } + } + + // Check for match on trailing period on request + if last := len(reqHost) - 1; last >= 0 && reqHost[last] == '.' { + h := reqHost[:last] + if h == host { + return true + } + } + } + return false + } + + return nil +} + +func clientIPV2(tree *matchersTree, clientIPs ...string) error { + checker, err := ip.NewChecker(clientIPs) + if err != nil { + return fmt.Errorf("could not initialize IP Checker for \"ClientIP\" matcher: %w", err) + } + + strategy := ip.RemoteAddrStrategy{} + + tree.matcher = func(req *http.Request) bool { + ok, err := checker.Contains(strategy.GetIP(req)) + if err != nil { + log.Ctx(req.Context()).Warn().Err(err).Msg("\"ClientIP\" matcher: could not match remote address") + return false + } + + return ok + } + + return nil +} + +func methodsV2(tree *matchersTree, methods ...string) error { + route := mux.NewRouter().NewRoute() + route.Methods(methods...) + if err := route.GetError(); err != nil { + return err + } + + tree.matcher = func(req *http.Request) bool { + return route.Match(req, &mux.RouteMatch{}) + } + + return nil +} + +func headersV2(tree *matchersTree, headers ...string) error { + route := mux.NewRouter().NewRoute() + route.Headers(headers...) + if err := route.GetError(); err != nil { + return err + } + + tree.matcher = func(req *http.Request) bool { + return route.Match(req, &mux.RouteMatch{}) + } + + return nil +} + +func queryV2(tree *matchersTree, query ...string) error { + var queries []string + for _, elem := range query { + queries = append(queries, strings.SplitN(elem, "=", 2)...) + } + + route := mux.NewRouter().NewRoute() + route.Queries(queries...) + if err := route.GetError(); err != nil { + return err + } + + tree.matcher = func(req *http.Request) bool { + return route.Match(req, &mux.RouteMatch{}) + } + + return nil +} + +func hostRegexpV2(tree *matchersTree, hosts ...string) error { + router := mux.NewRouter() + + for _, host := range hosts { + if !IsASCII(host) { + return fmt.Errorf("invalid value %q for HostRegexp matcher, non-ASCII characters are not allowed", host) + } + + tmpRt := router.Host(host) + if tmpRt.GetError() != nil { + return tmpRt.GetError() + } + } + + tree.matcher = func(req *http.Request) bool { + return router.Match(req, &mux.RouteMatch{}) + } + + return nil +} + +func headersRegexpV2(tree *matchersTree, headers ...string) error { + route := mux.NewRouter().NewRoute() + route.HeadersRegexp(headers...) + if err := route.GetError(); err != nil { + return err + } + + tree.matcher = func(req *http.Request) bool { + return route.Match(req, &mux.RouteMatch{}) + } + + return nil +} diff --git a/pkg/muxer/http/matcher_v2_test.go b/pkg/muxer/http/matcher_v2_test.go new file mode 100644 index 000000000..eac62a005 --- /dev/null +++ b/pkg/muxer/http/matcher_v2_test.go @@ -0,0 +1,1535 @@ +package http + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator" + "github.com/traefik/traefik/v3/pkg/testhelpers" +) + +func TestClientIPV2Matcher(t *testing.T) { + testCases := []struct { + desc string + rule string + expected map[string]int + expectedError bool + }{ + { + desc: "invalid ClientIP matcher", + rule: "ClientIP(`1`)", + expectedError: true, + }, + { + desc: "invalid ClientIP matcher (no parameter)", + rule: "ClientIP()", + expectedError: true, + }, + { + desc: "invalid ClientIP matcher (empty parameter)", + rule: "ClientIP(``)", + expectedError: true, + }, + { + desc: "valid ClientIP matcher (many parameters)", + rule: "ClientIP(`127.0.0.1`, `192.168.1.0/24`)", + expected: map[string]int{ + "127.0.0.1": http.StatusOK, + "192.168.1.1": http.StatusOK, + }, + }, + { + desc: "valid ClientIP matcher", + rule: "ClientIP(`127.0.0.1`)", + expected: map[string]int{ + "127.0.0.1": http.StatusOK, + "192.168.1.1": http.StatusNotFound, + }, + }, + { + desc: "valid ClientIP matcher but invalid remote address", + rule: "ClientIP(`127.0.0.1`)", + expected: map[string]int{ + "1": http.StatusNotFound, + }, + }, + { + desc: "valid ClientIP matcher using CIDR", + rule: "ClientIP(`192.168.1.0/24`)", + expected: map[string]int{ + "192.168.1.1": http.StatusOK, + "192.168.1.100": http.StatusOK, + "192.168.2.1": http.StatusNotFound, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.rule, "v2", 0, handler) + if test.expectedError { + require.Error(t, err) + return + } + + require.NoError(t, err) + + results := make(map[string]int) + for remoteAddr := range test.expected { + w := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "https://example.com", http.NoBody) + req.RemoteAddr = remoteAddr + + muxer.ServeHTTP(w, req) + results[remoteAddr] = w.Code + } + assert.Equal(t, test.expected, results) + }) + } +} + +func TestMethodV2Matcher(t *testing.T) { + testCases := []struct { + desc string + rule string + expected map[string]int + expectedError bool + }{ + { + desc: "invalid Method matcher (no parameter)", + rule: "Method()", + expectedError: true, + }, + { + desc: "invalid Method matcher (empty parameter)", + rule: "Method(``)", + expectedError: true, + }, + { + desc: "valid Method matcher (many parameters)", + rule: "Method(`GET`, `POST`)", + expected: map[string]int{ + http.MethodGet: http.StatusOK, + http.MethodPost: http.StatusOK, + }, + }, + { + desc: "valid Method matcher", + rule: "Method(`GET`)", + expected: map[string]int{ + http.MethodGet: http.StatusOK, + http.MethodPost: http.StatusNotFound, + strings.ToLower(http.MethodGet): http.StatusNotFound, + }, + }, + { + desc: "valid Method matcher (lower case)", + rule: "Method(`get`)", + expected: map[string]int{ + http.MethodGet: http.StatusOK, + http.MethodPost: http.StatusNotFound, + strings.ToLower(http.MethodGet): http.StatusNotFound, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.rule, "v2", 0, handler) + if test.expectedError { + require.Error(t, err) + return + } + + require.NoError(t, err) + + results := make(map[string]int) + for method := range test.expected { + w := httptest.NewRecorder() + + req := httptest.NewRequest(method, "https://example.com", http.NoBody) + + muxer.ServeHTTP(w, req) + results[method] = w.Code + } + assert.Equal(t, test.expected, results) + }) + } +} + +func TestHostV2Matcher(t *testing.T) { + testCases := []struct { + desc string + rule string + expected map[string]int + expectedError bool + }{ + { + desc: "invalid Host matcher (no parameter)", + rule: "Host()", + expectedError: true, + }, + { + desc: "invalid Host matcher (empty parameter)", + rule: "Host(``)", + expectedError: true, + }, + { + desc: "invalid Host matcher (non-ASCII)", + rule: "Host(`🦭.com`)", + expectedError: true, + }, + { + desc: "valid Host matcher (many parameters)", + rule: "Host(`example.com`, `example.org`)", + expected: map[string]int{ + "https://example.com": http.StatusOK, + "https://example.com:8080": http.StatusOK, + "https://example.com/path": http.StatusOK, + "https://EXAMPLE.COM/path": http.StatusOK, + "https://example.org": http.StatusOK, + "https://example.org/path": http.StatusOK, + }, + }, + { + desc: "valid Host matcher", + rule: "Host(`example.com`)", + expected: map[string]int{ + "https://example.com": http.StatusOK, + "https://example.com:8080": http.StatusOK, + "https://example.com/path": http.StatusOK, + "https://EXAMPLE.COM/path": http.StatusOK, + "https://example.org": http.StatusNotFound, + "https://example.org/path": http.StatusNotFound, + }, + }, + { + desc: "valid Host matcher - matcher ending with a dot", + rule: "Host(`example.com.`)", + expected: map[string]int{ + "https://example.com": http.StatusOK, + "https://example.com/path": http.StatusOK, + "https://example.org": http.StatusNotFound, + "https://example.org/path": http.StatusNotFound, + "https://example.com.": http.StatusOK, + "https://example.com./path": http.StatusOK, + "https://example.org.": http.StatusNotFound, + "https://example.org./path": http.StatusNotFound, + }, + }, + { + desc: "valid Host matcher - URL ending with a dot", + rule: "Host(`example.com`)", + expected: map[string]int{ + "https://example.com.": http.StatusOK, + "https://example.com./path": http.StatusOK, + "https://example.org.": http.StatusNotFound, + "https://example.org./path": http.StatusNotFound, + }, + }, + { + desc: "valid Host matcher - matcher with UPPER case", + rule: "Host(`EXAMPLE.COM`)", + expected: map[string]int{ + "https://example.com": http.StatusOK, + "https://example.com/path": http.StatusOK, + "https://example.org": http.StatusNotFound, + "https://example.org/path": http.StatusNotFound, + }, + }, + { + desc: "valid Host matcher - puny-coded emoji", + rule: "Host(`xn--9t9h.com`)", + expected: map[string]int{ + "https://xn--9t9h.com": http.StatusOK, + "https://xn--9t9h.com/path": http.StatusOK, + "https://example.com": http.StatusNotFound, + "https://example.com/path": http.StatusNotFound, + // The request's sender must use puny-code. + "https://🦭.com": http.StatusNotFound, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.rule, "v2", 0, handler) + if test.expectedError { + require.Error(t, err) + return + } + + require.NoError(t, err) + + // RequestDecorator is necessary for the Host matcher + reqHost := requestdecorator.New(nil) + + results := make(map[string]int) + for calledURL := range test.expected { + w := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody) + + reqHost.ServeHTTP(w, req, muxer.ServeHTTP) + results[calledURL] = w.Code + } + assert.Equal(t, test.expected, results) + }) + } +} + +func TestHostRegexpV2Matcher(t *testing.T) { + testCases := []struct { + desc string + rule string + expected map[string]int + expectedError bool + }{ + { + desc: "invalid HostRegexp matcher (no parameter)", + rule: "HostRegexp()", + expectedError: true, + }, + { + desc: "invalid HostRegexp matcher (empty parameter)", + rule: "HostRegexp(``)", + expectedError: true, + }, + { + desc: "invalid HostRegexp matcher (non-ASCII)", + rule: "HostRegexp(`🦭.com`)", + expectedError: true, + }, + { + desc: "valid HostRegexp matcher (invalid regexp)", + rule: "HostRegexp(`(example.com`)", + // This is weird. + expectedError: false, + expected: map[string]int{ + "https://example.com": http.StatusNotFound, + "https://example.com:8080": http.StatusNotFound, + "https://example.com/path": http.StatusNotFound, + "https://example.org": http.StatusNotFound, + "https://example.org/path": http.StatusNotFound, + }, + }, + { + desc: "valid HostRegexp matcher (many parameters)", + rule: "HostRegexp(`example.com`, `example.org`)", + expected: map[string]int{ + "https://example.com": http.StatusOK, + "https://example.com:8080": http.StatusOK, + "https://example.com/path": http.StatusOK, + "https://example.org": http.StatusOK, + "https://example.org/path": http.StatusOK, + }, + }, + { + desc: "valid HostRegexp matcher with case sensitive regexp", + rule: "HostRegexp(`^[A-Z]+\\.com$`)", + expected: map[string]int{ + "https://example.com": http.StatusNotFound, + "https://EXAMPLE.com": http.StatusNotFound, + "https://example.com/path": http.StatusNotFound, + "https://example.org": http.StatusNotFound, + "https://example.org/path": http.StatusNotFound, + }, + }, + { + desc: "valid HostRegexp matcher with Traefik v2 syntax", + rule: "HostRegexp(`{domain:[a-zA-Z-]+\\.com}`)", + expected: map[string]int{ + "https://example.com": http.StatusOK, + "https://example.com/path": http.StatusOK, + "https://example.org": http.StatusNotFound, + "https://example.org/path": http.StatusNotFound, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.rule, "v2", 0, handler) + if test.expectedError { + require.Error(t, err) + return + } + require.NoError(t, err) + + // RequestDecorator is necessary for the HostRegexp matcher + reqHost := requestdecorator.New(nil) + + results := make(map[string]int) + for calledURL := range test.expected { + w := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody) + + reqHost.ServeHTTP(w, req, muxer.ServeHTTP) + results[calledURL] = w.Code + } + assert.Equal(t, test.expected, results) + }) + } +} + +func TestPathV2Matcher(t *testing.T) { + testCases := []struct { + desc string + rule string + expected map[string]int + expectedError bool + }{ + { + desc: "invalid Path matcher (no parameter)", + rule: "Path()", + expectedError: true, + }, + { + desc: "invalid Path matcher (empty parameter)", + rule: "Path(``)", + expectedError: true, + }, + { + desc: "invalid Path matcher (no leading /)", + rule: "Path(`css`)", + expectedError: true, + }, + { + desc: "valid Path matcher (many parameters)", + rule: "Path(`/css`, `/js`)", + expected: map[string]int{ + "https://example.com": http.StatusNotFound, + "https://example.com/html": http.StatusNotFound, + "https://example.org/css": http.StatusOK, + "https://example.com/css": http.StatusOK, + "https://example.com/css/": http.StatusNotFound, + "https://example.com/css/main.css": http.StatusNotFound, + "https://example.com/js": http.StatusOK, + "https://example.com/js/main.js": http.StatusNotFound, + }, + }, + { + desc: "valid Path matcher", + rule: "Path(`/css`)", + expected: map[string]int{ + "https://example.com": http.StatusNotFound, + "https://example.com/html": http.StatusNotFound, + "https://example.org/css": http.StatusOK, + "https://example.com/css": http.StatusOK, + "https://example.com/css/": http.StatusNotFound, + "https://example.com/css/main.css": http.StatusNotFound, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.rule, "v2", 0, handler) + if test.expectedError { + require.Error(t, err) + return + } + + require.NoError(t, err) + + results := make(map[string]int) + for calledURL := range test.expected { + w := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody) + + muxer.ServeHTTP(w, req) + results[calledURL] = w.Code + } + assert.Equal(t, test.expected, results) + }) + } +} + +func TestPathPrefixV2Matcher(t *testing.T) { + testCases := []struct { + desc string + rule string + expected map[string]int + expectedError bool + }{ + { + desc: "invalid PathPrefix matcher (no parameter)", + rule: "PathPrefix()", + expectedError: true, + }, + { + desc: "invalid PathPrefix matcher (empty parameter)", + rule: "PathPrefix(``)", + expectedError: true, + }, + { + desc: "invalid PathPrefix matcher (no leading /)", + rule: "PathPrefix(`css`)", + expectedError: true, + }, + { + desc: "valid PathPrefix matcher (many parameters)", + rule: "PathPrefix(`/css`, `/js`)", + expected: map[string]int{ + "https://example.com": http.StatusNotFound, + "https://example.com/html": http.StatusNotFound, + "https://example.org/css": http.StatusOK, + "https://example.com/css": http.StatusOK, + "https://example.com/css/": http.StatusOK, + "https://example.com/css/main.css": http.StatusOK, + "https://example.com/js/": http.StatusOK, + "https://example.com/js/main.js": http.StatusOK, + }, + }, + { + desc: "valid PathPrefix matcher", + rule: `PathPrefix("/css")`, + expected: map[string]int{ + "https://example.com": http.StatusNotFound, + "https://example.com/html": http.StatusNotFound, + "https://example.org/css": http.StatusOK, + "https://example.com/css": http.StatusOK, + "https://example.com/css/": http.StatusOK, + "https://example.com/css/main.css": http.StatusOK, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.rule, "v2", 0, handler) + if test.expectedError { + require.Error(t, err) + return + } + + require.NoError(t, err) + + results := make(map[string]int) + for calledURL := range test.expected { + w := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody) + + muxer.ServeHTTP(w, req) + results[calledURL] = w.Code + } + assert.Equal(t, test.expected, results) + }) + } +} + +func TestHeadersMatcher(t *testing.T) { + testCases := []struct { + desc string + rule string + expected map[*http.Header]int + expectedError bool + }{ + { + desc: "invalid Header matcher (no parameter)", + rule: "Headers()", + expectedError: true, + }, + { + desc: "invalid Header matcher (missing value parameter)", + rule: "Headers(`X-Forwarded-Host`)", + expectedError: true, + }, + { + desc: "invalid Header matcher (missing value parameter)", + rule: "Headers(`X-Forwarded-Host`, ``)", + expectedError: true, + }, + { + desc: "invalid Header matcher (missing key parameter)", + rule: "Headers(``, `example.com`)", + expectedError: true, + }, + { + desc: "invalid Header matcher (too many parameters)", + rule: "Headers(`X-Forwarded-Host`, `example.com`, `example.org`)", + expectedError: true, + }, + { + desc: "valid Header matcher", + rule: "Headers(`X-Forwarded-Proto`, `https`)", + expected: map[*http.Header]int{ + {"X-Forwarded-Proto": []string{"https"}}: http.StatusOK, + {"x-forwarded-proto": []string{"https"}}: http.StatusNotFound, + {"X-Forwarded-Proto": []string{"http", "https"}}: http.StatusOK, + {"X-Forwarded-Proto": []string{"https", "http"}}: http.StatusOK, + {"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound, + }, + }, + { + desc: "valid Header matcher (non-canonical form)", + rule: "Headers(`x-forwarded-proto`, `https`)", + expected: map[*http.Header]int{ + {"X-Forwarded-Proto": []string{"https"}}: http.StatusOK, + {"x-forwarded-proto": []string{"https"}}: http.StatusNotFound, + {"X-Forwarded-Proto": []string{"http", "https"}}: http.StatusOK, + {"X-Forwarded-Proto": []string{"https", "http"}}: http.StatusOK, + {"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound, + }, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.rule, "v2", 0, handler) + if test.expectedError { + require.Error(t, err) + return + } + + require.NoError(t, err) + + for headers := range test.expected { + w := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "https://example.com", http.NoBody) + req.Header = *headers + + muxer.ServeHTTP(w, req) + assert.Equal(t, test.expected[headers], w.Code, headers) + } + }) + } +} + +func TestHeaderRegexpV2Matcher(t *testing.T) { + testCases := []struct { + desc string + rule string + expected map[*http.Header]int + expectedError bool + }{ + { + desc: "invalid HeaderRegexp matcher (no parameter)", + rule: "HeaderRegexp()", + expectedError: true, + }, + { + desc: "invalid HeaderRegexp matcher (missing value parameter)", + rule: "HeadersRegexp(`X-Forwarded-Host`)", + expectedError: true, + }, + { + desc: "invalid HeaderRegexp matcher (missing value parameter)", + rule: "HeadersRegexp(`X-Forwarded-Host`, ``)", + expectedError: true, + }, + { + desc: "invalid HeaderRegexp matcher (missing key parameter)", + rule: "HeadersRegexp(``, `example.com`)", + expectedError: true, + }, + { + desc: "invalid HeaderRegexp matcher (invalid regexp)", + rule: "HeadersRegexp(`X-Forwarded-Host`,`(example.com`)", + expectedError: true, + }, + { + desc: "invalid HeaderRegexp matcher (too many parameters)", + rule: "HeadersRegexp(`X-Forwarded-Host`, `example.com`, `example.org`)", + expectedError: true, + }, + { + desc: "valid HeaderRegexp matcher", + rule: "HeadersRegexp(`X-Forwarded-Proto`, `^https?$`)", + expected: map[*http.Header]int{ + {"X-Forwarded-Proto": []string{"http"}}: http.StatusOK, + {"x-forwarded-proto": []string{"http"}}: http.StatusNotFound, + {"X-Forwarded-Proto": []string{"https"}}: http.StatusOK, + {"X-Forwarded-Proto": []string{"HTTPS"}}: http.StatusNotFound, + {"X-Forwarded-Proto": []string{"ws", "https"}}: http.StatusOK, + {"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound, + }, + }, + { + desc: "valid HeaderRegexp matcher (non-canonical form)", + rule: "HeadersRegexp(`x-forwarded-proto`, `^https?$`)", + expected: map[*http.Header]int{ + {"X-Forwarded-Proto": []string{"http"}}: http.StatusOK, + {"x-forwarded-proto": []string{"http"}}: http.StatusNotFound, + {"X-Forwarded-Proto": []string{"https"}}: http.StatusOK, + {"X-Forwarded-Proto": []string{"HTTPS"}}: http.StatusNotFound, + {"X-Forwarded-Proto": []string{"ws", "https"}}: http.StatusOK, + {"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound, + }, + }, + { + desc: "valid HeaderRegexp matcher with Traefik v2 syntax", + rule: "HeadersRegexp(`X-Forwarded-Proto`, `http{secure:s?}`)", + expected: map[*http.Header]int{ + {"X-Forwarded-Proto": []string{"http"}}: http.StatusNotFound, + {"X-Forwarded-Proto": []string{"https"}}: http.StatusNotFound, + {"X-Forwarded-Proto": []string{"http{secure:}"}}: http.StatusOK, + {"X-Forwarded-Proto": []string{"HTTP{secure:}"}}: http.StatusNotFound, + {"X-Forwarded-Proto": []string{"http{secure:s}"}}: http.StatusOK, + {"X-Forwarded-Proto": []string{"http{secure:S}"}}: http.StatusNotFound, + {"X-Forwarded-Proto": []string{"HTTPS"}}: http.StatusNotFound, + {"X-Forwarded-Proto": []string{"ws", "http{secure:s}"}}: http.StatusOK, + {"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.rule, "v2", 0, handler) + if test.expectedError { + require.Error(t, err) + return + } + + require.NoError(t, err) + + for headers := range test.expected { + w := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "https://example.com", http.NoBody) + req.Header = *headers + + muxer.ServeHTTP(w, req) + assert.Equal(t, test.expected[headers], w.Code, *headers) + } + }) + } +} + +func TestHostRegexp(t *testing.T) { + testCases := []struct { + desc string + hostExp string + urls map[string]int + }{ + { + desc: "capturing group", + hostExp: "HostRegexp(`{subdomain:(foo\\.)?bar\\.com}`)", + urls: map[string]int{ + "http://foo.bar.com": http.StatusOK, + "http://bar.com": http.StatusOK, + "http://fooubar.com": http.StatusNotFound, + "http://barucom": http.StatusNotFound, + "http://barcom": http.StatusNotFound, + }, + }, + { + desc: "non capturing group", + hostExp: "HostRegexp(`{subdomain:(?:foo\\.)?bar\\.com}`)", + urls: map[string]int{ + "http://foo.bar.com": http.StatusOK, + "http://bar.com": http.StatusOK, + "http://fooubar.com": http.StatusNotFound, + "http://barucom": http.StatusNotFound, + "http://barcom": http.StatusNotFound, + }, + }, + { + desc: "regex insensitive", + hostExp: "HostRegexp(`{dummy:[A-Za-z-]+\\.bar\\.com}`)", + urls: map[string]int{ + "http://FOO.bar.com": http.StatusOK, + "http://foo.bar.com": http.StatusOK, + "http://fooubar.com": http.StatusNotFound, + "http://barucom": http.StatusNotFound, + "http://barcom": http.StatusNotFound, + }, + }, + { + desc: "insensitive host", + hostExp: "HostRegexp(`{dummy:[a-z-]+\\.bar\\.com}`)", + urls: map[string]int{ + "http://FOO.bar.com": http.StatusOK, + "http://foo.bar.com": http.StatusOK, + "http://fooubar.com": http.StatusNotFound, + "http://barucom": http.StatusNotFound, + "http://barcom": http.StatusNotFound, + }, + }, + { + desc: "insensitive host simple", + hostExp: "HostRegexp(`foo.bar.com`)", + urls: map[string]int{ + "http://FOO.bar.com": http.StatusOK, + "http://foo.bar.com": http.StatusOK, + "http://fooubar.com": http.StatusNotFound, + "http://barucom": http.StatusNotFound, + "http://barcom": http.StatusNotFound, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.hostExp, "v2", 0, handler) + require.NoError(t, err) + + results := make(map[string]int) + for calledURL := range test.urls { + w := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody) + + muxer.ServeHTTP(w, req) + results[calledURL] = w.Code + } + assert.Equal(t, test.urls, results) + }) + } +} + +// This test is a copy from the v2 branch mux_test.go file. +func Test_addRoute(t *testing.T) { + testCases := []struct { + desc string + rule string + headers map[string]string + remoteAddr string + expected map[string]int + expectedError bool + }{ + { + desc: "no tree", + expectedError: true, + }, + { + desc: "Rule with no matcher", + rule: "rulewithnotmatcher", + expectedError: true, + }, + { + desc: "Host empty", + rule: "Host(``)", + expectedError: true, + }, + { + desc: "PathPrefix empty", + rule: "PathPrefix(``)", + expectedError: true, + }, + { + desc: "PathPrefix", + rule: "PathPrefix(`/foo`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusOK, + }, + }, + { + desc: "wrong PathPrefix", + rule: "PathPrefix(`/bar`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusNotFound, + }, + }, + { + desc: "Host", + rule: "Host(`localhost`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusOK, + }, + }, + { + desc: "Host IPv4", + rule: "Host(`127.0.0.1`)", + expected: map[string]int{ + "http://127.0.0.1/foo": http.StatusOK, + }, + }, + { + desc: "Host IPv6", + rule: "Host(`10::10`)", + expected: map[string]int{ + "http://10::10/foo": http.StatusOK, + }, + }, + { + desc: "Non-ASCII Host", + rule: "Host(`locàlhost`)", + expectedError: true, + }, + { + desc: "Non-ASCII HostRegexp", + rule: "HostRegexp(`locàlhost`)", + expectedError: true, + }, + { + desc: "HostHeader equivalent to Host", + rule: "HostHeader(`localhost`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusOK, + "http://bar/foo": http.StatusNotFound, + }, + }, + { + desc: "Host with trailing period in rule", + rule: "Host(`localhost.`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusOK, + }, + }, + { + desc: "Host with trailing period in domain", + rule: "Host(`localhost`)", + expected: map[string]int{ + "http://localhost./foo": http.StatusOK, + }, + }, + { + desc: "Host with trailing period in domain and rule", + rule: "Host(`localhost.`)", + expected: map[string]int{ + "http://localhost./foo": http.StatusOK, + }, + }, + { + desc: "wrong Host", + rule: "Host(`nope`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusNotFound, + }, + }, + { + desc: "Host and PathPrefix", + rule: "Host(`localhost`) && PathPrefix(`/foo`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusOK, + }, + }, + { + desc: "Host and PathPrefix wrong PathPrefix", + rule: "Host(`localhost`) && PathPrefix(`/bar`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusNotFound, + }, + }, + { + desc: "Host and PathPrefix wrong Host", + rule: "Host(`nope`) && PathPrefix(`/foo`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusNotFound, + }, + }, + { + desc: "Host and PathPrefix Host OR, first host", + rule: "Host(`nope`,`localhost`) && PathPrefix(`/foo`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusOK, + }, + }, + { + desc: "Host and PathPrefix Host OR, second host", + rule: "Host(`nope`,`localhost`) && PathPrefix(`/foo`)", + expected: map[string]int{ + "http://nope/foo": http.StatusOK, + }, + }, + { + desc: "Host and PathPrefix Host OR, first host and wrong PathPrefix", + rule: "Host(`nope,localhost`) && PathPrefix(`/bar`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusNotFound, + }, + }, + { + desc: "HostRegexp with capturing group", + rule: "HostRegexp(`{subdomain:(foo\\.)?bar\\.com}`)", + expected: map[string]int{ + "http://foo.bar.com": http.StatusOK, + "http://bar.com": http.StatusOK, + "http://fooubar.com": http.StatusNotFound, + "http://barucom": http.StatusNotFound, + "http://barcom": http.StatusNotFound, + }, + }, + { + desc: "HostRegexp with non capturing group", + rule: "HostRegexp(`{subdomain:(?:foo\\.)?bar\\.com}`)", + expected: map[string]int{ + "http://foo.bar.com": http.StatusOK, + "http://bar.com": http.StatusOK, + "http://fooubar.com": http.StatusNotFound, + "http://barucom": http.StatusNotFound, + "http://barcom": http.StatusNotFound, + }, + }, + { + desc: "Methods with GET", + rule: "Method(`GET`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusOK, + }, + }, + { + desc: "Methods with GET and POST", + rule: "Method(`GET`,`POST`)", + expected: map[string]int{ + "http://localhost/foo": http.StatusOK, + }, + }, + { + desc: "Methods with POST", + rule: "Method(`POST`)", + expected: map[string]int{ + // On v2 this test expect a http.StatusMethodNotAllowed status code. + // This was due to a custom behavior of mux https://github.com/containous/mux/blob/b2dd784e613f218225150a5e8b5742c5733bc1b6/mux.go#L130-L132. + // Unfortunately, this behavior cannot be ported easily due to the matcher func signature. + "http://localhost/foo": http.StatusNotFound, + }, + }, + { + desc: "Header with matching header", + rule: "Headers(`Content-Type`,`application/json`)", + headers: map[string]string{ + "Content-Type": "application/json", + }, + expected: map[string]int{ + "http://localhost/foo": http.StatusOK, + }, + }, + { + desc: "Header without matching header", + rule: "Headers(`Content-Type`,`application/foo`)", + headers: map[string]string{ + "Content-Type": "application/json", + }, + expected: map[string]int{ + "http://localhost/foo": http.StatusNotFound, + }, + }, + { + desc: "HeaderRegExp with matching header", + rule: "HeadersRegexp(`Content-Type`, `application/(text|json)`)", + headers: map[string]string{ + "Content-Type": "application/json", + }, + expected: map[string]int{ + "http://localhost/foo": http.StatusOK, + }, + }, + { + desc: "HeaderRegExp without matching header", + rule: "HeadersRegexp(`Content-Type`, `application/(text|json)`)", + headers: map[string]string{ + "Content-Type": "application/foo", + }, + expected: map[string]int{ + "http://localhost/foo": http.StatusNotFound, + }, + }, + { + desc: "HeaderRegExp with matching second header", + rule: "HeadersRegexp(`Content-Type`, `application/(text|json)`)", + headers: map[string]string{ + "Content-Type": "application/text", + }, + expected: map[string]int{ + "http://localhost/foo": http.StatusOK, + }, + }, + { + desc: "Query with multiple params", + rule: "Query(`foo=bar`, `bar=baz`)", + expected: map[string]int{ + "http://localhost/foo?foo=bar&bar=baz": http.StatusOK, + "http://localhost/foo?bar=baz": http.StatusNotFound, + }, + }, + { + desc: "Query with multiple equals", + rule: "Query(`foo=b=ar`)", + expected: map[string]int{ + "http://localhost/foo?foo=b=ar": http.StatusOK, + "http://localhost/foo?foo=bar": http.StatusNotFound, + }, + }, + { + desc: "Rule with simple path", + rule: `Path("/a")`, + expected: map[string]int{ + "http://plop/a": http.StatusOK, + }, + }, + { + desc: `Rule with a simple host`, + rule: `Host("plop")`, + expected: map[string]int{ + "http://plop": http.StatusOK, + }, + }, + { + desc: "Rule with Path AND Host", + rule: `Path("/a") && Host("plop")`, + expected: map[string]int{ + "http://plop/a": http.StatusOK, + "http://plopi/a": http.StatusNotFound, + }, + }, + { + desc: "Rule with Host OR Host", + rule: `Host("tchouk") || Host("pouet")`, + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + "http://pouet/a": http.StatusOK, + "http://plopi/a": http.StatusNotFound, + }, + }, + { + desc: "Rule with host OR (host AND path)", + rule: `Host("tchouk") || (Host("pouet") && Path("/powpow"))`, + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + "http://tchouk/powpow": http.StatusOK, + "http://pouet/powpow": http.StatusOK, + "http://pouet/toto": http.StatusNotFound, + "http://plopi/a": http.StatusNotFound, + }, + }, + { + desc: "Rule with host OR host AND path", + rule: `Host("tchouk") || Host("pouet") && Path("/powpow")`, + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + "http://tchouk/powpow": http.StatusOK, + "http://pouet/powpow": http.StatusOK, + "http://pouet/toto": http.StatusNotFound, + "http://plopi/a": http.StatusNotFound, + }, + }, + { + desc: "Rule with (host OR host) AND path", + rule: `(Host("tchouk") || Host("pouet")) && Path("/powpow")`, + expected: map[string]int{ + "http://tchouk/toto": http.StatusNotFound, + "http://tchouk/powpow": http.StatusOK, + "http://pouet/powpow": http.StatusOK, + "http://pouet/toto": http.StatusNotFound, + "http://plopi/a": http.StatusNotFound, + }, + }, + { + desc: "Rule with multiple host AND path", + rule: `(Host("tchouk","pouet")) && Path("/powpow")`, + expected: map[string]int{ + "http://tchouk/toto": http.StatusNotFound, + "http://tchouk/powpow": http.StatusOK, + "http://pouet/powpow": http.StatusOK, + "http://pouet/toto": http.StatusNotFound, + "http://plopi/a": http.StatusNotFound, + }, + }, + { + desc: "Rule with multiple host AND multiple path", + rule: `Host("tchouk","pouet") && Path("/powpow", "/titi")`, + expected: map[string]int{ + "http://tchouk/toto": http.StatusNotFound, + "http://tchouk/powpow": http.StatusOK, + "http://pouet/powpow": http.StatusOK, + "http://tchouk/titi": http.StatusOK, + "http://pouet/titi": http.StatusOK, + "http://pouet/toto": http.StatusNotFound, + "http://plopi/a": http.StatusNotFound, + }, + }, + { + desc: "Rule with (host AND path) OR (host AND path)", + rule: `(Host("tchouk") && Path("/titi")) || ((Host("pouet")) && Path("/powpow"))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusOK, + "http://tchouk/powpow": http.StatusNotFound, + "http://pouet/powpow": http.StatusOK, + "http://pouet/toto": http.StatusNotFound, + "http://plopi/a": http.StatusNotFound, + }, + }, + { + desc: "Rule without quote", + rule: `Host(tchouk)`, + expectedError: true, + }, + { + desc: "Rule case UPPER", + rule: `(HOST("tchouk") && PATHPREFIX("/titi"))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusOK, + "http://tchouk/powpow": http.StatusNotFound, + }, + }, + { + desc: "Rule case lower", + rule: `(host("tchouk") && pathprefix("/titi"))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusOK, + "http://tchouk/powpow": http.StatusNotFound, + }, + }, + { + desc: "Rule case CamelCase", + rule: `(Host("tchouk") && PathPrefix("/titi"))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusOK, + "http://tchouk/powpow": http.StatusNotFound, + }, + }, + { + desc: "Rule case Title", + rule: `(Host("tchouk") && Pathprefix("/titi"))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusOK, + "http://tchouk/powpow": http.StatusNotFound, + }, + }, + { + desc: "Rule Path with error", + rule: `Path("titi")`, + expectedError: true, + }, + { + desc: "Rule PathPrefix with error", + rule: `PathPrefix("titi")`, + expectedError: true, + }, + { + desc: "Rule HostRegexp with error", + rule: `HostRegexp("{test")`, + expectedError: true, + }, + { + desc: "Rule Headers with error", + rule: `Headers("titi")`, + expectedError: true, + }, + { + desc: "Rule HeadersRegexp with error", + rule: `HeadersRegexp("titi")`, + expectedError: true, + }, + { + desc: "Rule Query", + rule: `Query("titi")`, + expectedError: true, + }, + { + desc: "Rule Query with bad syntax", + rule: `Query("titi={test")`, + expectedError: true, + }, + { + desc: "Rule with Path without args", + rule: `Host("tchouk") && Path()`, + expectedError: true, + }, + { + desc: "Rule with an empty path", + rule: `Host("tchouk") && Path("")`, + expectedError: true, + }, + { + desc: "Rule with an empty path", + rule: `Host("tchouk") && Path("", "/titi")`, + expectedError: true, + }, + { + desc: "Rule with not", + rule: `!Host("tchouk")`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusNotFound, + "http://test/powpow": http.StatusOK, + }, + }, + { + desc: "Rule with not on Path", + rule: `!Path("/titi")`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusNotFound, + "http://tchouk/powpow": http.StatusOK, + }, + }, + { + desc: "Rule with not on multiple route with or", + rule: `!(Host("tchouk") || Host("toto"))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusNotFound, + "http://toto/powpow": http.StatusNotFound, + "http://test/powpow": http.StatusOK, + }, + }, + { + desc: "Rule with not on multiple route with and", + rule: `!(Host("tchouk") && Path("/titi"))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusNotFound, + "http://tchouk/toto": http.StatusOK, + "http://test/titi": http.StatusOK, + }, + }, + { + desc: "Rule with not on multiple route with and another not", + rule: `!(Host("tchouk") && !Path("/titi"))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusOK, + "http://toto/titi": http.StatusOK, + "http://tchouk/toto": http.StatusNotFound, + }, + }, + { + desc: "Rule with not on two rule", + rule: `!Host("tchouk") || !Path("/titi")`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusNotFound, + "http://tchouk/toto": http.StatusOK, + "http://test/titi": http.StatusOK, + }, + }, + { + desc: "Rule case with double not", + rule: `!(!(Host("tchouk") && Pathprefix("/titi")))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusOK, + "http://tchouk/powpow": http.StatusNotFound, + "http://test/titi": http.StatusNotFound, + }, + }, + { + desc: "Rule case with not domain", + rule: `!Host("tchouk") && Pathprefix("/titi")`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusNotFound, + "http://tchouk/powpow": http.StatusNotFound, + "http://toto/powpow": http.StatusNotFound, + "http://toto/titi": http.StatusOK, + }, + }, + { + desc: "Rule with multiple host AND multiple path AND not", + rule: `!(Host("tchouk","pouet") && Path("/powpow", "/titi"))`, + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + "http://tchouk/powpow": http.StatusNotFound, + "http://pouet/powpow": http.StatusNotFound, + "http://tchouk/titi": http.StatusNotFound, + "http://pouet/titi": http.StatusNotFound, + "http://pouet/toto": http.StatusOK, + "http://plopi/a": http.StatusOK, + }, + }, + { + desc: "ClientIP empty", + rule: "ClientIP(``)", + expectedError: true, + }, + { + desc: "Invalid ClientIP", + rule: "ClientIP(`invalid`)", + expectedError: true, + }, + { + desc: "Non matching ClientIP", + rule: "ClientIP(`10.10.1.1`)", + remoteAddr: "10.0.0.0", + expected: map[string]int{ + "http://tchouk/toto": http.StatusNotFound, + }, + }, + { + desc: "Non matching IPv6", + rule: "ClientIP(`10::10`)", + remoteAddr: "::1", + expected: map[string]int{ + "http://tchouk/toto": http.StatusNotFound, + }, + }, + { + desc: "Matching IP", + rule: "ClientIP(`10.0.0.0`)", + remoteAddr: "10.0.0.0:8456", + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + }, + }, + { + desc: "Matching IPv6", + rule: "ClientIP(`10::10`)", + remoteAddr: "10::10", + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + }, + }, + { + desc: "Matching IP among several IP", + rule: "ClientIP(`10.0.0.1`, `10.0.0.0`)", + remoteAddr: "10.0.0.0", + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + }, + }, + { + desc: "Non Matching IP with CIDR", + rule: "ClientIP(`11.0.0.0/24`)", + remoteAddr: "10.0.0.0", + expected: map[string]int{ + "http://tchouk/toto": http.StatusNotFound, + }, + }, + { + desc: "Non Matching IPv6 with CIDR", + rule: "ClientIP(`11::/16`)", + remoteAddr: "10::", + expected: map[string]int{ + "http://tchouk/toto": http.StatusNotFound, + }, + }, + { + desc: "Matching IP with CIDR", + rule: "ClientIP(`10.0.0.0/16`)", + remoteAddr: "10.0.0.0", + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + }, + }, + { + desc: "Matching IPv6 with CIDR", + rule: "ClientIP(`10::/16`)", + remoteAddr: "10::10", + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + }, + }, + { + desc: "Matching IP among several CIDR", + rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0/16`)", + remoteAddr: "10.0.0.0", + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + }, + }, + { + desc: "Matching IP among non matching CIDR and matching IP", + rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0`)", + remoteAddr: "10.0.0.0", + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + }, + }, + { + desc: "Matching IP among matching CIDR and non matching IP", + rule: "ClientIP(`11.0.0.0`, `10.0.0.0/16`)", + remoteAddr: "10.0.0.0", + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + }, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.rule, "v2", 0, handler) + if test.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + + // RequestDecorator is necessary for the hostV2 rule + reqHost := requestdecorator.New(nil) + + results := make(map[string]int) + for calledURL := range test.expected { + w := httptest.NewRecorder() + + req := testhelpers.MustNewRequest(http.MethodGet, calledURL, nil) + + // Useful for the ClientIP matcher + req.RemoteAddr = test.remoteAddr + + for key, value := range test.headers { + req.Header.Set(key, value) + } + reqHost.ServeHTTP(w, req, muxer.ServeHTTP) + results[calledURL] = w.Code + } + assert.Equal(t, test.expected, results) + } + }) + } +} diff --git a/pkg/muxer/http/mux.go b/pkg/muxer/http/mux.go index 979634aa1..33e700697 100644 --- a/pkg/muxer/http/mux.go +++ b/pkg/muxer/http/mux.go @@ -12,8 +12,9 @@ import ( // Muxer handles routing with rules. type Muxer struct { - routes routes - parser predicate.Parser + routes routes + parser predicate.Parser + parserV2 predicate.Parser } // NewMuxer returns a new muxer instance. @@ -28,8 +29,19 @@ func NewMuxer() (*Muxer, error) { return nil, fmt.Errorf("error while creating parser: %w", err) } + var matchersV2 []string + for matcher := range httpFuncsV2 { + matchersV2 = append(matchersV2, matcher) + } + + parserV2, err := rules.NewParser(matchersV2) + if err != nil { + return nil, fmt.Errorf("error while creating v2 parser: %w", err) + } + return &Muxer{ - parser: parser, + parser: parser, + parserV2: parserV2, }, nil } @@ -53,10 +65,26 @@ func GetRulePriority(rule string) int { } // AddRoute add a new route to the router. -func (m *Muxer) AddRoute(rule string, priority int, handler http.Handler) error { - parse, err := m.parser.Parse(rule) - if err != nil { - return fmt.Errorf("error while parsing rule %s: %w", rule, err) +func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler http.Handler) error { + var parse interface{} + var err error + var matcherFuncs map[string]func(*matchersTree, ...string) error + + switch syntax { + case "v2": + parse, err = m.parserV2.Parse(rule) + if err != nil { + return fmt.Errorf("error while parsing rule %s: %w", rule, err) + } + + matcherFuncs = httpFuncsV2 + default: + parse, err = m.parser.Parse(rule) + if err != nil { + return fmt.Errorf("error while parsing rule %s: %w", rule, err) + } + + matcherFuncs = httpFuncs } buildTree, ok := parse.(rules.TreeBuilder) @@ -65,7 +93,7 @@ func (m *Muxer) AddRoute(rule string, priority int, handler http.Handler) error } var matchers matchersTree - err = matchers.addRule(buildTree()) + err = matchers.addRule(buildTree(), matcherFuncs) if err != nil { return fmt.Errorf("error while adding rule %s: %w", rule, err) } @@ -87,6 +115,9 @@ func ParseDomains(rule string) ([]string, error) { for matcher := range httpFuncs { matchers = append(matchers, matcher) } + for matcher := range httpFuncsV2 { + matchers = append(matchers, matcher) + } parser, err := rules.NewParser(matchers) if err != nil { @@ -166,25 +197,27 @@ func (m *matchersTree) match(req *http.Request) bool { } } -func (m *matchersTree) addRule(rule *rules.Tree) error { +type matcherFuncs map[string]func(*matchersTree, ...string) error + +func (m *matchersTree) addRule(rule *rules.Tree, funcs matcherFuncs) error { switch rule.Matcher { case "and", "or": m.operator = rule.Matcher m.left = &matchersTree{} - err := m.left.addRule(rule.RuleLeft) + err := m.left.addRule(rule.RuleLeft, funcs) if err != nil { return fmt.Errorf("error while adding rule %s: %w", rule.Matcher, err) } m.right = &matchersTree{} - return m.right.addRule(rule.RuleRight) + return m.right.addRule(rule.RuleRight, funcs) default: err := rules.CheckRule(rule) if err != nil { return fmt.Errorf("error while checking rule %s: %w", rule.Matcher, err) } - err = httpFuncs[rule.Matcher](m, rule.Value...) + err = funcs[rule.Matcher](m, rule.Value...) if err != nil { return fmt.Errorf("error while adding rule %s: %w", rule.Matcher, err) } diff --git a/pkg/muxer/http/mux_test.go b/pkg/muxer/http/mux_test.go index a31a37881..efa8486a3 100644 --- a/pkg/muxer/http/mux_test.go +++ b/pkg/muxer/http/mux_test.go @@ -231,7 +231,7 @@ func TestMuxer(t *testing.T) { require.NoError(t, err) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) if test.expectedError { require.Error(t, err) return @@ -394,7 +394,7 @@ func Test_addRoutePriority(t *testing.T) { route.priority = GetRulePriority(route.rule) } - err := muxer.AddRoute(route.rule, route.priority, handler) + err := muxer.AddRoute(route.rule, "", route.priority, handler) require.NoError(t, err, route.rule) } @@ -519,7 +519,7 @@ func TestEmptyHost(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, handler) + err = muxer.AddRoute(test.rule, "", 0, handler) require.NoError(t, err) // RequestDecorator is necessary for the host rule diff --git a/pkg/muxer/tcp/matcher_test.go b/pkg/muxer/tcp/matcher_test.go index 3ba833d49..531492e89 100644 --- a/pkg/muxer/tcp/matcher_test.go +++ b/pkg/muxer/tcp/matcher_test.go @@ -38,7 +38,7 @@ func Test_HostSNICatchAll(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {})) + err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {})) require.NoError(t, err) handler, catchAll := muxer.Match(ConnData{ @@ -144,7 +144,7 @@ func Test_HostSNI(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {})) + err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {})) if test.buildErr { require.Error(t, err) return @@ -227,7 +227,7 @@ func Test_HostSNIRegexp(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {})) + err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {})) if test.buildErr { require.Error(t, err) return @@ -299,7 +299,7 @@ func Test_ClientIP(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {})) + err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {})) if test.buildErr { require.Error(t, err) return @@ -363,7 +363,7 @@ func Test_ALPN(t *testing.T) { muxer, err := NewMuxer() require.NoError(t, err) - err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {})) + err = muxer.AddRoute(test.rule, "", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {})) if test.buildErr { require.Error(t, err) return diff --git a/pkg/muxer/tcp/matcher_v2.go b/pkg/muxer/tcp/matcher_v2.go new file mode 100644 index 000000000..3d918b02e --- /dev/null +++ b/pkg/muxer/tcp/matcher_v2.go @@ -0,0 +1,240 @@ +package tcp + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/go-acme/lego/v4/challenge/tlsalpn01" + "github.com/rs/zerolog/log" + "github.com/traefik/traefik/v3/pkg/ip" +) + +var tcpFuncsV2 = map[string]func(*matchersTree, ...string) error{ + "ALPN": alpnV2, + "ClientIP": clientIPV2, + "HostSNI": hostSNIV2, + "HostSNIRegexp": hostSNIRegexpV2, +} + +func clientIPV2(tree *matchersTree, clientIPs ...string) error { + checker, err := ip.NewChecker(clientIPs) + if err != nil { + return fmt.Errorf("could not initialize IP Checker for \"ClientIP\" matcher: %w", err) + } + + tree.matcher = func(meta ConnData) bool { + if meta.remoteIP == "" { + return false + } + + ok, err := checker.Contains(meta.remoteIP) + if err != nil { + log.Warn().Err(err).Msg("ClientIP matcher: could not match remote address") + return false + } + return ok + } + + return nil +} + +// alpnV2 checks if any of the connection ALPN protocols matches one of the matcher protocols. +func alpnV2(tree *matchersTree, protos ...string) error { + if len(protos) == 0 { + return errors.New("empty value for \"ALPN\" matcher is not allowed") + } + + for _, proto := range protos { + if proto == tlsalpn01.ACMETLS1Protocol { + return fmt.Errorf("invalid protocol value for \"ALPN\" matcher, %q is not allowed", proto) + } + } + + tree.matcher = func(meta ConnData) bool { + for _, proto := range meta.alpnProtos { + for _, filter := range protos { + if proto == filter { + return true + } + } + } + + return false + } + + return nil +} + +// hostSNIV2 checks if the SNI Host of the connection match the matcher host. +func hostSNIV2(tree *matchersTree, hosts ...string) error { + if len(hosts) == 0 { + return errors.New("empty value for \"HostSNI\" matcher is not allowed") + } + + for i, host := range hosts { + // Special case to allow global wildcard + if host == "*" { + continue + } + + if !hostOrIP.MatchString(host) { + return fmt.Errorf("invalid value for \"HostSNI\" matcher, %q is not a valid hostname or IP", host) + } + + hosts[i] = strings.ToLower(host) + } + + tree.matcher = func(meta ConnData) bool { + // Since a HostSNI(`*`) rule has been provided as catchAll for non-TLS TCP, + // it allows matching with an empty serverName. + // Which is why we make sure to take that case into account before + // checking meta.serverName. + if hosts[0] == "*" { + return true + } + + if meta.serverName == "" { + return false + } + + for _, host := range hosts { + if host == "*" { + return true + } + + if host == meta.serverName { + return true + } + + // trim trailing period in case of FQDN + host = strings.TrimSuffix(host, ".") + if host == meta.serverName { + return true + } + } + + return false + } + + return nil +} + +// hostSNIRegexpV2 checks if the SNI Host of the connection matches the matcher host regexp. +func hostSNIRegexpV2(tree *matchersTree, templates ...string) error { + if len(templates) == 0 { + return fmt.Errorf("empty value for \"HostSNIRegexp\" matcher is not allowed") + } + + var regexps []*regexp.Regexp + + for _, template := range templates { + preparedPattern, err := preparePattern(template) + if err != nil { + return fmt.Errorf("invalid pattern value for \"HostSNIRegexp\" matcher, %q is not a valid pattern: %w", template, err) + } + + regexp, err := regexp.Compile(preparedPattern) + if err != nil { + return err + } + + regexps = append(regexps, regexp) + } + + tree.matcher = func(meta ConnData) bool { + for _, regexp := range regexps { + if regexp.MatchString(meta.serverName) { + return true + } + } + + return false + } + + return nil +} + +// preparePattern builds a regexp pattern from the initial user defined expression. +// This function reuses the code dedicated to host matching of the newRouteRegexp func from the gorilla/mux library. +// https://github.com/containous/mux/tree/8ffa4f6d063c1e2b834a73be6a1515cca3992618. +func preparePattern(template string) (string, error) { + // Check if it is well-formed. + idxs, errBraces := braceIndices(template) + if errBraces != nil { + return "", errBraces + } + + defaultPattern := "[^.]+" + pattern := bytes.NewBufferString("") + + // Host SNI matching is case-insensitive + _, _ = fmt.Fprint(pattern, "(?i)") + + pattern.WriteByte('^') + var end int + for i := 0; i < len(idxs); i += 2 { + // Set all values we are interested in. + raw := template[end:idxs[i]] + end = idxs[i+1] + parts := strings.SplitN(template[idxs[i]+1:end-1], ":", 2) + name := parts[0] + + patt := defaultPattern + if len(parts) == 2 { + patt = parts[1] + } + + // Name or pattern can't be empty. + if name == "" || patt == "" { + return "", fmt.Errorf("mux: missing name or pattern in %q", + template[idxs[i]:end]) + } + + // Build the regexp pattern. + _, _ = fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt) + } + + // Add the remaining. + raw := template[end:] + pattern.WriteString(regexp.QuoteMeta(raw)) + pattern.WriteByte('$') + + return pattern.String(), nil +} + +// varGroupName builds a capturing group name for the indexed variable. +// This function is a copy of varGroupName func from the gorilla/mux library. +// https://github.com/containous/mux/tree/8ffa4f6d063c1e2b834a73be6a1515cca3992618. +func varGroupName(idx int) string { + return "v" + strconv.Itoa(idx) +} + +// braceIndices returns the first level curly brace indices from a string. +// This function is a copy of braceIndices func from the gorilla/mux library. +// https://github.com/containous/mux/tree/8ffa4f6d063c1e2b834a73be6a1515cca3992618. +func braceIndices(s string) ([]int, error) { + var level, idx int + var idxs []int + for i := 0; i < len(s); i++ { + switch s[i] { + case '{': + if level++; level == 1 { + idx = i + } + case '}': + if level--; level == 0 { + idxs = append(idxs, idx, i+1) + } else if level < 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + } + } + if level != 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + return idxs, nil +} diff --git a/pkg/muxer/tcp/matcher_v2_test.go b/pkg/muxer/tcp/matcher_v2_test.go new file mode 100644 index 000000000..74ffcff80 --- /dev/null +++ b/pkg/muxer/tcp/matcher_v2_test.go @@ -0,0 +1,1008 @@ +package tcp + +import ( + "fmt" + "testing" + + "github.com/go-acme/lego/v4/challenge/tlsalpn01" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traefik/traefik/v3/pkg/tcp" +) + +// All the tests in the suite are a copy of tcp muxer tests on branch v2. +// Only the test for route priority has not been copied here, +// because the priority computation is no longer done when calling the muxer AddRoute method. +func Test_addTCPRouteV2(t *testing.T) { + testCases := []struct { + desc string + rule string + serverName string + remoteAddr string + protos []string + routeErr bool + matchErr bool + }{ + { + desc: "no tree", + routeErr: true, + }, + { + desc: "Rule with no matcher", + rule: "rulewithnotmatcher", + routeErr: true, + }, + { + desc: "Empty HostSNI rule", + rule: "HostSNI()", + serverName: "foobar", + routeErr: true, + }, + { + desc: "Empty HostSNI rule", + rule: "HostSNI(``)", + serverName: "foobar", + routeErr: true, + }, + { + desc: "Valid HostSNI rule matching", + rule: "HostSNI(`foobar`)", + serverName: "foobar", + }, + { + desc: "Valid negative HostSNI rule matching", + rule: "!HostSNI(`bar`)", + serverName: "foobar", + }, + { + desc: "Valid HostSNI rule matching with alternative case", + rule: "hostsni(`foobar`)", + serverName: "foobar", + }, + { + desc: "Valid HostSNI rule matching with alternative case", + rule: "HOSTSNI(`foobar`)", + serverName: "foobar", + }, + { + desc: "Valid HostSNI rule not matching", + rule: "HostSNI(`foobar`)", + serverName: "bar", + matchErr: true, + }, + { + desc: "Empty HostSNIRegexp rule", + rule: "HostSNIRegexp()", + serverName: "foobar", + routeErr: true, + }, + { + desc: "Empty HostSNIRegexp rule", + rule: "HostSNIRegexp(``)", + serverName: "foobar", + routeErr: true, + }, + { + desc: "Valid HostSNIRegexp rule matching", + rule: "HostSNIRegexp(`{subdomain:[a-z]+}.foobar`)", + serverName: "sub.foobar", + }, + { + desc: "Valid negative HostSNIRegexp rule matching", + rule: "!HostSNIRegexp(`bar`)", + serverName: "foobar", + }, + { + desc: "Valid HostSNIRegexp rule matching with alternative case", + rule: "hostsniregexp(`foobar`)", + serverName: "foobar", + }, + { + desc: "Valid HostSNIRegexp rule matching with alternative case", + rule: "HOSTSNIREGEXP(`foobar`)", + serverName: "foobar", + }, + { + desc: "Valid HostSNIRegexp rule not matching", + rule: "HostSNIRegexp(`foobar`)", + serverName: "bar", + matchErr: true, + }, + { + desc: "Valid negative HostSNI rule not matching", + rule: "!HostSNI(`bar`)", + serverName: "bar", + matchErr: true, + }, + { + desc: "Valid HostSNIRegexp rule matching empty servername", + rule: "HostSNIRegexp(`{subdomain:[a-z]*}`)", + serverName: "", + }, + { + desc: "Valid HostSNIRegexp rule with one name", + rule: "HostSNIRegexp(`{dummy}`)", + serverName: "toto", + }, + { + desc: "Valid HostSNIRegexp rule with one name 2", + rule: "HostSNIRegexp(`{dummy}`)", + serverName: "toto.com", + matchErr: true, + }, + { + desc: "Empty ClientIP rule", + rule: "ClientIP()", + routeErr: true, + }, + { + desc: "Empty ClientIP rule", + rule: "ClientIP(``)", + routeErr: true, + }, + { + desc: "Invalid ClientIP", + rule: "ClientIP(`invalid`)", + routeErr: true, + }, + { + desc: "Invalid remoteAddr", + rule: "ClientIP(`10.0.0.1`)", + remoteAddr: "not.an.IP:80", + matchErr: true, + }, + { + desc: "Valid ClientIP rule matching", + rule: "ClientIP(`10.0.0.1`)", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid negative ClientIP rule matching", + rule: "!ClientIP(`20.0.0.1`)", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid ClientIP rule matching with alternative case", + rule: "clientip(`10.0.0.1`)", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid ClientIP rule matching with alternative case", + rule: "CLIENTIP(`10.0.0.1`)", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid ClientIP rule not matching", + rule: "ClientIP(`10.0.0.1`)", + remoteAddr: "10.0.0.2:80", + matchErr: true, + }, + { + desc: "Valid negative ClientIP rule not matching", + rule: "!ClientIP(`10.0.0.2`)", + remoteAddr: "10.0.0.2:80", + matchErr: true, + }, + { + desc: "Valid ClientIP rule matching IPv6", + rule: "ClientIP(`10::10`)", + remoteAddr: "[10::10]:80", + }, + { + desc: "Valid negative ClientIP rule matching IPv6", + rule: "!ClientIP(`10::10`)", + remoteAddr: "[::1]:80", + }, + { + desc: "Valid ClientIP rule not matching IPv6", + rule: "ClientIP(`10::10`)", + remoteAddr: "[::1]:80", + matchErr: true, + }, + { + desc: "Valid ClientIP rule matching multiple IPs", + rule: "ClientIP(`10.0.0.1`, `10.0.0.0`)", + remoteAddr: "10.0.0.0:80", + }, + { + desc: "Valid ClientIP rule matching CIDR", + rule: "ClientIP(`11.0.0.0/24`)", + remoteAddr: "11.0.0.0:80", + }, + { + desc: "Valid ClientIP rule not matching CIDR", + rule: "ClientIP(`11.0.0.0/24`)", + remoteAddr: "10.0.0.0:80", + matchErr: true, + }, + { + desc: "Valid ClientIP rule matching CIDR IPv6", + rule: "ClientIP(`11::/16`)", + remoteAddr: "[11::]:80", + }, + { + desc: "Valid ClientIP rule not matching CIDR IPv6", + rule: "ClientIP(`11::/16`)", + remoteAddr: "[10::]:80", + matchErr: true, + }, + { + desc: "Valid ClientIP rule matching multiple CIDR", + rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0/16`)", + remoteAddr: "10.0.0.0:80", + }, + { + desc: "Valid ClientIP rule not matching CIDR and matching IP", + rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0`)", + remoteAddr: "10.0.0.0:80", + }, + { + desc: "Valid ClientIP rule matching CIDR and not matching IP", + rule: "ClientIP(`11.0.0.0`, `10.0.0.0/16`)", + remoteAddr: "10.0.0.0:80", + }, + { + desc: "Valid HostSNI and ClientIP rule matching", + rule: "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)", + serverName: "foobar", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid negative HostSNI and ClientIP rule matching", + rule: "!HostSNI(`bar`) && ClientIP(`10.0.0.1`)", + serverName: "foobar", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid HostSNI and negative ClientIP rule matching", + rule: "HostSNI(`foobar`) && !ClientIP(`10.0.0.2`)", + serverName: "foobar", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid negative HostSNI and negative ClientIP rule matching", + rule: "!HostSNI(`bar`) && !ClientIP(`10.0.0.2`)", + serverName: "foobar", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid negative HostSNI or negative ClientIP rule matching", + rule: "!(HostSNI(`bar`) || ClientIP(`10.0.0.2`))", + serverName: "foobar", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid negative HostSNI and negative ClientIP rule matching", + rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))", + serverName: "foobar", + remoteAddr: "10.0.0.2:80", + }, + { + desc: "Valid negative HostSNI and negative ClientIP rule matching", + rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))", + serverName: "bar", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid negative HostSNI and negative ClientIP rule matching", + rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))", + serverName: "bar", + remoteAddr: "10.0.0.2:80", + matchErr: true, + }, + { + desc: "Valid negative HostSNI and negative ClientIP rule matching", + rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))", + serverName: "foobar", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid HostSNI and ClientIP rule not matching", + rule: "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)", + serverName: "bar", + remoteAddr: "10.0.0.1:80", + matchErr: true, + }, + { + desc: "Valid HostSNI and ClientIP rule not matching", + rule: "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)", + serverName: "foobar", + remoteAddr: "10.0.0.2:80", + matchErr: true, + }, + { + desc: "Valid HostSNI or ClientIP rule matching", + rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)", + serverName: "foobar", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid HostSNI or ClientIP rule matching", + rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)", + serverName: "bar", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid HostSNI or ClientIP rule matching", + rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)", + serverName: "foobar", + remoteAddr: "10.0.0.2:80", + }, + { + desc: "Valid HostSNI or ClientIP rule not matching", + rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)", + serverName: "bar", + remoteAddr: "10.0.0.2:80", + matchErr: true, + }, + { + desc: "Valid HostSNI x 3 OR rule matching", + rule: "HostSNI(`foobar`) || HostSNI(`foo`) || HostSNI(`bar`)", + serverName: "foobar", + }, + { + desc: "Valid HostSNI x 3 OR rule not matching", + rule: "HostSNI(`foobar`) || HostSNI(`foo`) || HostSNI(`bar`)", + serverName: "baz", + matchErr: true, + }, + { + desc: "Valid HostSNI and ClientIP Combined rule matching", + rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)", + serverName: "foobar", + remoteAddr: "10.0.0.2:80", + }, + { + desc: "Valid HostSNI and ClientIP Combined rule matching", + rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)", + serverName: "bar", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid HostSNI and ClientIP Combined rule not matching", + rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)", + serverName: "bar", + remoteAddr: "10.0.0.2:80", + matchErr: true, + }, + { + desc: "Valid HostSNI and ClientIP Combined rule not matching", + rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)", + serverName: "baz", + remoteAddr: "10.0.0.1:80", + matchErr: true, + }, + { + desc: "Valid HostSNI and ClientIP complex combined rule matching", + rule: "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))", + serverName: "bar", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Valid HostSNI and ClientIP complex combined rule not matching", + rule: "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))", + serverName: "baz", + remoteAddr: "10.0.0.1:80", + matchErr: true, + }, + { + desc: "Valid HostSNI and ClientIP complex combined rule not matching", + rule: "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))", + serverName: "bar", + remoteAddr: "10.0.0.3:80", + matchErr: true, + }, + { + desc: "Valid HostSNI and ClientIP more complex (but absurd) combined rule matching", + rule: "(HostSNI(`foobar`) || (HostSNI(`bar`) && !HostSNI(`foobar`))) && ((ClientIP(`10.0.0.1`) && !ClientIP(`10.0.0.2`)) || ClientIP(`10.0.0.2`)) ", + serverName: "bar", + remoteAddr: "10.0.0.1:80", + }, + { + desc: "Invalid ALPN rule matching ACME-TLS/1", + rule: fmt.Sprintf("ALPN(`%s`)", tlsalpn01.ACMETLS1Protocol), + protos: []string{"foo"}, + routeErr: true, + }, + { + desc: "Valid ALPN rule matching single protocol", + rule: "ALPN(`foo`)", + protos: []string{"foo"}, + }, + { + desc: "Valid ALPN rule matching ACME-TLS/1 protocol", + rule: "ALPN(`foo`)", + protos: []string{tlsalpn01.ACMETLS1Protocol}, + matchErr: true, + }, + { + desc: "Valid ALPN rule not matching single protocol", + rule: "ALPN(`foo`)", + protos: []string{"bar"}, + matchErr: true, + }, + { + desc: "Valid alternative case ALPN rule matching single protocol without another being supported", + rule: "ALPN(`foo`) && !alpn(`h2`)", + protos: []string{"foo", "bar"}, + }, + { + desc: "Valid alternative case ALPN rule not matching single protocol because of another being supported", + rule: "ALPN(`foo`) && !alpn(`h2`)", + protos: []string{"foo", "h2", "bar"}, + matchErr: true, + }, + { + desc: "Valid complex alternative case ALPN and HostSNI rule", + rule: "ALPN(`foo`) && (!alpn(`h2`) || hostsni(`foo`))", + protos: []string{"foo", "bar"}, + serverName: "foo", + }, + { + desc: "Valid complex alternative case ALPN and HostSNI rule not matching by SNI", + rule: "ALPN(`foo`) && (!alpn(`h2`) || hostsni(`foo`))", + protos: []string{"foo", "bar", "h2"}, + serverName: "bar", + matchErr: true, + }, + { + desc: "Valid complex alternative case ALPN and HostSNI rule matching by ALPN", + rule: "ALPN(`foo`) && (!alpn(`h2`) || hostsni(`foo`))", + protos: []string{"foo", "bar"}, + serverName: "bar", + }, + { + desc: "Valid complex alternative case ALPN and HostSNI rule not matching by protos", + rule: "ALPN(`foo`) && (!alpn(`h2`) || hostsni(`foo`))", + protos: []string{"h2", "bar"}, + serverName: "bar", + matchErr: true, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + msg := "BYTES" + handler := tcp.HandlerFunc(func(conn tcp.WriteCloser) { + _, err := conn.Write([]byte(msg)) + require.NoError(t, err) + }) + + router, err := NewMuxer() + require.NoError(t, err) + + err = router.AddRoute(test.rule, "v2", 0, handler) + if test.routeErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + + addr := "0.0.0.0:0" + if test.remoteAddr != "" { + addr = test.remoteAddr + } + + conn := &fakeConn{ + call: map[string]int{}, + remoteAddr: fakeAddr{addr: addr}, + } + + connData, err := NewConnData(test.serverName, conn, test.protos) + require.NoError(t, err) + + matchingHandler, _ := router.Match(connData) + if test.matchErr { + require.Nil(t, matchingHandler) + return + } + + require.NotNil(t, matchingHandler) + + matchingHandler.ServeTCP(conn) + + n, ok := conn.call[msg] + assert.Equal(t, 1, n) + assert.True(t, ok) + }) + } +} + +func TestParseHostSNIV2(t *testing.T) { + testCases := []struct { + description string + expression string + domain []string + errorExpected bool + }{ + { + description: "Unknown rule", + expression: "Foobar(`foo.bar`,`test.bar`)", + errorExpected: true, + }, + { + description: "Many hostSNI rules", + expression: "HostSNI(`foo.bar`,`test.bar`)", + domain: []string{"foo.bar", "test.bar"}, + }, + { + description: "Many hostSNI rules upper", + expression: "HOSTSNI(`foo.bar`,`test.bar`)", + domain: []string{"foo.bar", "test.bar"}, + }, + { + description: "Many hostSNI rules lower", + expression: "hostsni(`foo.bar`,`test.bar`)", + domain: []string{"foo.bar", "test.bar"}, + }, + { + description: "No hostSNI rule", + expression: "ClientIP(`10.1`)", + }, + { + description: "HostSNI rule and another rule", + expression: "HostSNI(`foo.bar`) && ClientIP(`10.1`)", + domain: []string{"foo.bar"}, + }, + { + description: "HostSNI rule to lower and another rule", + expression: "HostSNI(`Foo.Bar`) && ClientIP(`10.1`)", + domain: []string{"foo.bar"}, + }, + { + description: "HostSNI rule with no domain", + expression: "HostSNI() && ClientIP(`10.1`)", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.expression, func(t *testing.T) { + t.Parallel() + + domains, err := ParseHostSNI(test.expression) + + if test.errorExpected { + require.Errorf(t, err, "unable to parse correctly the domains in the HostSNI rule from %q", test.expression) + } else { + require.NoError(t, err, "%s: Error while parsing domain.", test.expression) + } + + assert.EqualValues(t, test.domain, domains, "%s: Error parsing domains from expression.", test.expression) + }) + } +} + +func Test_HostSNICatchAllV2(t *testing.T) { + testCases := []struct { + desc string + rule string + isCatchAll bool + }{ + { + desc: "HostSNI(`foobar`) is not catchAll", + rule: "HostSNI(`foobar`)", + }, + { + desc: "HostSNI(`*`) is catchAll", + rule: "HostSNI(`*`)", + isCatchAll: true, + }, + { + desc: "HOSTSNI(`*`) is catchAll", + rule: "HOSTSNI(`*`)", + isCatchAll: true, + }, + { + desc: `HostSNI("*") is catchAll`, + rule: `HostSNI("*")`, + isCatchAll: true, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + muxer, err := NewMuxer() + require.NoError(t, err) + + err = muxer.AddRoute(test.rule, "v2", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {})) + require.NoError(t, err) + + handler, catchAll := muxer.Match(ConnData{ + serverName: "foobar", + }) + require.NotNil(t, handler) + assert.Equal(t, test.isCatchAll, catchAll) + }) + } +} + +func Test_HostSNIV2(t *testing.T) { + testCases := []struct { + desc string + ruleHosts []string + serverName string + buildErr bool + matchErr bool + }{ + { + desc: "Empty", + buildErr: true, + }, + { + desc: "Non ASCII host", + ruleHosts: []string{"héhé"}, + buildErr: true, + }, + { + desc: "Not Matching hosts", + ruleHosts: []string{"foobar"}, + serverName: "bar", + matchErr: true, + }, + { + desc: "Matching globing host `*`", + ruleHosts: []string{"*"}, + serverName: "foobar", + }, + { + desc: "Matching globing host `*` and empty serverName", + ruleHosts: []string{"*"}, + serverName: "", + }, + { + desc: "Matching globing host `*` and another non matching host", + ruleHosts: []string{"foo", "*"}, + serverName: "bar", + }, + { + desc: "Matching globing host `*` and another non matching host, and empty servername", + ruleHosts: []string{"foo", "*"}, + serverName: "", + matchErr: true, + }, + { + desc: "Not Matching globing host with subdomain", + ruleHosts: []string{"*.bar"}, + buildErr: true, + }, + { + desc: "Not Matching host with trailing dot with ", + ruleHosts: []string{"foobar."}, + serverName: "foobar.", + }, + { + desc: "Matching host with trailing dot", + ruleHosts: []string{"foobar."}, + serverName: "foobar", + }, + { + desc: "Matching hosts", + ruleHosts: []string{"foobar", "foo-bar.baz"}, + serverName: "foobar", + }, + { + desc: "Matching hosts with subdomains", + ruleHosts: []string{"foo.bar"}, + serverName: "foo.bar", + }, + { + desc: "Matching IPv4", + ruleHosts: []string{"127.0.0.1"}, + serverName: "127.0.0.1", + }, + { + desc: "Matching IPv6", + ruleHosts: []string{"10::10"}, + serverName: "10::10", + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + matcherTree := &matchersTree{} + err := hostSNIV2(matcherTree, test.ruleHosts...) + if test.buildErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + meta := ConnData{ + serverName: test.serverName, + } + + assert.Equal(t, test.matchErr, !matcherTree.match(meta)) + }) + } +} + +func Test_HostSNIRegexpV2(t *testing.T) { + testCases := []struct { + desc string + pattern string + serverNames map[string]bool + buildErr bool + }{ + { + desc: "unbalanced braces", + pattern: "subdomain:(foo\\.)?bar\\.com}", + buildErr: true, + }, + { + desc: "empty group name", + pattern: "{:(foo\\.)?bar\\.com}", + buildErr: true, + }, + { + desc: "empty capturing group", + pattern: "{subdomain:}", + buildErr: true, + }, + { + desc: "malformed capturing group", + pattern: "{subdomain:(foo\\.?bar\\.com}", + buildErr: true, + }, + { + desc: "not interpreted as a regexp", + pattern: "bar.com", + serverNames: map[string]bool{ + "bar.com": true, + "barucom": false, + }, + }, + { + desc: "capturing group", + pattern: "{subdomain:(foo\\.)?bar\\.com}", + serverNames: map[string]bool{ + "foo.bar.com": true, + "bar.com": true, + "fooubar.com": false, + "barucom": false, + "barcom": false, + }, + }, + { + desc: "non capturing group", + pattern: "{subdomain:(?:foo\\.)?bar\\.com}", + serverNames: map[string]bool{ + "foo.bar.com": true, + "bar.com": true, + "fooubar.com": false, + "barucom": false, + "barcom": false, + }, + }, + { + desc: "regex insensitive", + pattern: "{dummy:[A-Za-z-]+\\.bar\\.com}", + serverNames: map[string]bool{ + "FOO.bar.com": true, + "foo.bar.com": true, + "fooubar.com": false, + "barucom": false, + "barcom": false, + }, + }, + { + desc: "insensitive host", + pattern: "{dummy:[a-z-]+\\.bar\\.com}", + serverNames: map[string]bool{ + "FOO.bar.com": true, + "foo.bar.com": true, + "fooubar.com": false, + "barucom": false, + "barcom": false, + }, + }, + { + desc: "insensitive host simple", + pattern: "foo.bar.com", + serverNames: map[string]bool{ + "FOO.bar.com": true, + "foo.bar.com": true, + "fooubar.com": false, + "barucom": false, + "barcom": false, + }, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + matchersTree := &matchersTree{} + err := hostSNIRegexpV2(matchersTree, test.pattern) + if test.buildErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + for serverName, match := range test.serverNames { + meta := ConnData{ + serverName: serverName, + } + + assert.Equal(t, match, matchersTree.match(meta)) + } + }) + } +} + +func Test_ClientIPV2(t *testing.T) { + testCases := []struct { + desc string + ruleCIDRs []string + remoteIP string + buildErr bool + matchErr bool + }{ + { + desc: "Empty", + buildErr: true, + }, + { + desc: "Malformed CIDR", + ruleCIDRs: []string{"héhé"}, + buildErr: true, + }, + { + desc: "Not matching empty remote IP", + ruleCIDRs: []string{"20.20.20.20"}, + matchErr: true, + }, + { + desc: "Not matching IP", + ruleCIDRs: []string{"20.20.20.20"}, + remoteIP: "10.10.10.10", + matchErr: true, + }, + { + desc: "Matching IP", + ruleCIDRs: []string{"10.10.10.10"}, + remoteIP: "10.10.10.10", + }, + { + desc: "Not matching multiple IPs", + ruleCIDRs: []string{"20.20.20.20", "30.30.30.30"}, + remoteIP: "10.10.10.10", + matchErr: true, + }, + { + desc: "Matching multiple IPs", + ruleCIDRs: []string{"10.10.10.10", "20.20.20.20", "30.30.30.30"}, + remoteIP: "20.20.20.20", + }, + { + desc: "Not matching CIDR", + ruleCIDRs: []string{"20.0.0.0/24"}, + remoteIP: "10.10.10.10", + matchErr: true, + }, + { + desc: "Matching CIDR", + ruleCIDRs: []string{"20.0.0.0/8"}, + remoteIP: "20.10.10.10", + }, + { + desc: "Not matching multiple CIDRs", + ruleCIDRs: []string{"10.0.0.0/24", "20.0.0.0/24"}, + remoteIP: "10.10.10.10", + matchErr: true, + }, + { + desc: "Matching multiple CIDRs", + ruleCIDRs: []string{"10.0.0.0/8", "20.0.0.0/8"}, + remoteIP: "20.10.10.10", + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + matchersTree := &matchersTree{} + err := clientIPV2(matchersTree, test.ruleCIDRs...) + if test.buildErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + meta := ConnData{ + remoteIP: test.remoteIP, + } + + assert.Equal(t, test.matchErr, !matchersTree.match(meta)) + }) + } +} + +func Test_ALPNV2(t *testing.T) { + testCases := []struct { + desc string + ruleALPNProtos []string + connProto string + buildErr bool + matchErr bool + }{ + { + desc: "Empty", + buildErr: true, + }, + { + desc: "ACME TLS proto", + ruleALPNProtos: []string{tlsalpn01.ACMETLS1Protocol}, + buildErr: true, + }, + { + desc: "Not matching empty proto", + ruleALPNProtos: []string{"h2"}, + matchErr: true, + }, + { + desc: "Not matching ALPN", + ruleALPNProtos: []string{"h2"}, + connProto: "mqtt", + matchErr: true, + }, + { + desc: "Matching ALPN", + ruleALPNProtos: []string{"h2"}, + connProto: "h2", + }, + { + desc: "Not matching multiple ALPNs", + ruleALPNProtos: []string{"h2", "mqtt"}, + connProto: "h2c", + matchErr: true, + }, + { + desc: "Matching multiple ALPNs", + ruleALPNProtos: []string{"h2", "h2c", "mqtt"}, + connProto: "h2c", + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + matchersTree := &matchersTree{} + err := alpnV2(matchersTree, test.ruleALPNProtos...) + if test.buildErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + meta := ConnData{ + alpnProtos: []string{test.connProto}, + } + + assert.Equal(t, test.matchErr, !matchersTree.match(meta)) + }) + } +} diff --git a/pkg/muxer/tcp/mux.go b/pkg/muxer/tcp/mux.go index f23ce629d..35c4be8a6 100644 --- a/pkg/muxer/tcp/mux.go +++ b/pkg/muxer/tcp/mux.go @@ -41,8 +41,9 @@ func NewConnData(serverName string, conn tcp.WriteCloser, alpnProtos []string) ( // Muxer defines a muxer that handles TCP routing with rules. type Muxer struct { - routes routes - parser predicate.Parser + routes routes + parser predicate.Parser + parserV2 predicate.Parser } // NewMuxer returns a TCP muxer. @@ -57,7 +58,20 @@ func NewMuxer() (*Muxer, error) { return nil, fmt.Errorf("error while creating rules parser: %w", err) } - return &Muxer{parser: parser}, nil + var matchersV2 []string + for matcher := range tcpFuncsV2 { + matchersV2 = append(matchersV2, matcher) + } + + parserV2, err := rules.NewParser(matchersV2) + if err != nil { + return nil, fmt.Errorf("error while creating v2 rules parser: %w", err) + } + + return &Muxer{ + parser: parser, + parserV2: parserV2, + }, nil } // Match returns the handler of the first route matching the connection metadata, @@ -106,10 +120,26 @@ func GetRulePriority(rule string) int { // AddRoute adds a new route, associated to the given handler, at the given // priority, to the muxer. -func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error { - parse, err := m.parser.Parse(rule) - if err != nil { - return fmt.Errorf("error while parsing rule %s: %w", rule, err) +func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler tcp.Handler) error { + var parse interface{} + var err error + var matcherFuncs map[string]func(*matchersTree, ...string) error + + switch syntax { + case "v2": + parse, err = m.parserV2.Parse(rule) + if err != nil { + return fmt.Errorf("error while parsing rule %s: %w", rule, err) + } + + matcherFuncs = tcpFuncsV2 + default: + parse, err = m.parser.Parse(rule) + if err != nil { + return fmt.Errorf("error while parsing rule %s: %w", rule, err) + } + + matcherFuncs = tcpFuncs } buildTree, ok := parse.(rules.TreeBuilder) @@ -120,7 +150,7 @@ func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error { ruleTree := buildTree() var matchers matchersTree - err = matchers.addRule(ruleTree) + err = matchers.addRule(ruleTree, matcherFuncs) if err != nil { return fmt.Errorf("error while adding rule %s: %w", rule, err) } @@ -155,6 +185,9 @@ func ParseHostSNI(rule string) ([]string, error) { for matcher := range tcpFuncs { matchers = append(matchers, matcher) } + for matcher := range tcpFuncsV2 { + matchers = append(matchers, matcher) + } parser, err := rules.NewParser(matchers) if err != nil { @@ -237,25 +270,27 @@ func (m *matchersTree) match(meta ConnData) bool { } } -func (m *matchersTree) addRule(rule *rules.Tree) error { +type matcherFuncs map[string]func(*matchersTree, ...string) error + +func (m *matchersTree) addRule(rule *rules.Tree, funcs matcherFuncs) error { switch rule.Matcher { case "and", "or": m.operator = rule.Matcher m.left = &matchersTree{} - err := m.left.addRule(rule.RuleLeft) + err := m.left.addRule(rule.RuleLeft, funcs) if err != nil { return err } m.right = &matchersTree{} - return m.right.addRule(rule.RuleRight) + return m.right.addRule(rule.RuleRight, funcs) default: err := rules.CheckRule(rule) if err != nil { return err } - err = tcpFuncs[rule.Matcher](m, rule.Value...) + err = funcs[rule.Matcher](m, rule.Value...) if err != nil { return err } diff --git a/pkg/muxer/tcp/mux_test.go b/pkg/muxer/tcp/mux_test.go index 5c52089b4..3b108b7ea 100644 --- a/pkg/muxer/tcp/mux_test.go +++ b/pkg/muxer/tcp/mux_test.go @@ -277,7 +277,7 @@ func Test_addTCPRoute(t *testing.T) { router, err := NewMuxer() require.NoError(t, err) - err = router.AddRoute(test.rule, 0, handler) + err = router.AddRoute(test.rule, "", 0, handler) if test.routeErr { require.Error(t, err) return @@ -447,7 +447,7 @@ func Test_Priority(t *testing.T) { matchedRule := "" for rule, priority := range test.rules { rule := rule - err := muxer.AddRoute(rule, priority, tcp.HandlerFunc(func(conn tcp.WriteCloser) { + err := muxer.AddRoute(rule, "", priority, tcp.HandlerFunc(func(conn tcp.WriteCloser) { matchedRule = rule })) require.NoError(t, err) diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index ca4f78219..6e3b1f2a5 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -112,6 +112,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli r := &dynamic.Router{ Middlewares: mds, Priority: route.Priority, + RuleSyntax: route.Syntax, EntryPoints: ingressRoute.Spec.EntryPoints, Rule: route.Match, Service: serviceName, diff --git a/pkg/provider/kubernetes/crd/kubernetes_tcp.go b/pkg/provider/kubernetes/crd/kubernetes_tcp.go index b0759786f..d5fb434a3 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_tcp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go @@ -102,6 +102,7 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client Middlewares: mds, Rule: route.Match, Priority: route.Priority, + RuleSyntax: route.Syntax, Service: serviceName, } diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go index 6471381ae..475d160d4 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go @@ -33,6 +33,9 @@ type Route struct { // Priority defines the router's priority. // More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#priority Priority int `json:"priority,omitempty"` + // Syntax defines the router's rule syntax. + // More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax + Syntax string `json:"syntax,omitempty"` // Services defines the list of Service. // It can contain any combination of TraefikService and/or reference to a Kubernetes Service. Services []Service `json:"services,omitempty"` diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go index 5669e8f4f..5cfd69d4a 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go @@ -29,6 +29,9 @@ type RouteTCP struct { // Priority defines the router's priority. // More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#priority_1 Priority int `json:"priority,omitempty"` + // Syntax defines the router's rule syntax. + // More info: https://doc.traefik.io/traefik/v3.0/routing/routers/#rulesyntax_1 + Syntax string `json:"syntax,omitempty"` // Services defines the list of TCP services. Services []ServiceTCP `json:"services,omitempty"` // Middlewares defines the list of references to MiddlewareTCP resources. diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index e9ed7e6a7..c632cde73 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -770,6 +770,7 @@ func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, li router := dynamic.Router{ Rule: rule, + RuleSyntax: "v3", EntryPoints: []string{ep}, } @@ -908,6 +909,7 @@ func gatewayTCPRouteToTCPConf(ctx context.Context, ep string, listener gatev1.Li router := dynamic.TCPRouter{ Rule: "HostSNI(`*`)", EntryPoints: []string{ep}, + RuleSyntax: "v3", } if listener.Protocol == gatev1.TLSProtocolType && listener.TLS != nil { @@ -1072,6 +1074,7 @@ func gatewayTLSRouteToTCPConf(ctx context.Context, ep string, listener gatev1.Li router := dynamic.TCPRouter{ Rule: rule, + RuleSyntax: "v3", EntryPoints: []string{ep}, TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: listener.TLS.Mode != nil && *listener.TLS.Mode == gatev1.TLSModePassthrough, @@ -1395,7 +1398,7 @@ func extractHeaderRules(headers []gatev1.HTTPHeaderMatch) ([]string, error) { switch *header.Type { case gatev1.HeaderMatchExact: - headerRules = append(headerRules, fmt.Sprintf("Headers(`%s`,`%s`)", header.Name, header.Value)) + headerRules = append(headerRules, fmt.Sprintf("Header(`%s`,`%s`)", header.Name, header.Value)) default: return nil, fmt.Errorf("unsupported header match type %s", *header.Type) } diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 7c747a270..df3b95a0b 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -550,6 +550,7 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, @@ -609,6 +610,7 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "api@internal", Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, @@ -641,6 +643,7 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, @@ -704,6 +707,7 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"websecure"}, Service: "default-http-app-1-my-gateway-websecure-1c0cf64bde37d9d0df06-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, @@ -773,6 +777,7 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-1-my-gateway-web-66e726cd8903b49727ae-wrr", Rule: "(Host(`foo.com`) || Host(`bar.com`)) && PathPrefix(`/`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, @@ -832,6 +837,7 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-1-my-gateway-web-3b78e2feb3295ddd87f0-wrr", Rule: "(Host(`foo.com`) || HostRegexp(`^[a-zA-Z0-9-]+\\.bar\\.com$`)) && PathPrefix(`/`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, @@ -891,6 +897,7 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-1-my-gateway-web-b0521a61fb43068694b4-wrr", Rule: "(Host(`foo.com`) || HostRegexp(`^[a-zA-Z0-9-]+\\.foo\\.com$`)) && PathPrefix(`/`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, @@ -949,11 +956,13 @@ func TestLoadHTTPRoutes(t *testing.T) { "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", }, "default-http-app-1-my-gateway-web-d737b4933fa88e68ab8a": { EntryPoints: []string{"web"}, Rule: "Host(`foo.com`) && Path(`/bir`)", + RuleSyntax: "v3", Service: "default-http-app-1-my-gateway-web-d737b4933fa88e68ab8a-wrr", }, }, @@ -1039,6 +1048,7 @@ func TestLoadHTTPRoutes(t *testing.T) { "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", }, }, @@ -1124,11 +1134,13 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-1-my-gateway-http-web-1c0cf64bde37d9d0df06-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", }, "default-http-app-1-my-gateway-https-websecure-1c0cf64bde37d9d0df06": { EntryPoints: []string{"websecure"}, Service: "default-http-app-1-my-gateway-https-websecure-1c0cf64bde37d9d0df06-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, @@ -1213,11 +1225,13 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", }, "default-http-app-1-my-gateway-websecure-1c0cf64bde37d9d0df06": { EntryPoints: []string{"websecure"}, Service: "default-http-app-1-my-gateway-websecure-1c0cf64bde37d9d0df06-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, @@ -1293,20 +1307,22 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-330d644a7f2079e8f454": { + "default-http-app-1-my-gateway-web-4a1b73e6f83804949a37": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-330d644a7f2079e8f454-wrr", - Rule: "Host(`foo.com`) && PathPrefix(`/bar`) && Headers(`my-header`,`foo`) && Headers(`my-header2`,`bar`)", + Service: "default-http-app-1-my-gateway-web-4a1b73e6f83804949a37-wrr", + Rule: "Host(`foo.com`) && PathPrefix(`/bar`) && Header(`my-header`,`foo`) && Header(`my-header2`,`bar`)", + RuleSyntax: "v3", }, - "default-http-app-1-my-gateway-web-fe80e69a38713941ea22": { + "default-http-app-1-my-gateway-web-aaba0f24fd26e1ca2276": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-fe80e69a38713941ea22-wrr", - Rule: "Host(`foo.com`) && Path(`/bar`) && Headers(`my-header`,`bar`)", + Service: "default-http-app-1-my-gateway-web-aaba0f24fd26e1ca2276-wrr", + Rule: "Host(`foo.com`) && Path(`/bar`) && Header(`my-header`,`bar`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-330d644a7f2079e8f454-wrr": { + "default-http-app-1-my-gateway-web-4a1b73e6f83804949a37-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1316,7 +1332,7 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, }, - "default-http-app-1-my-gateway-web-fe80e69a38713941ea22-wrr": { + "default-http-app-1-my-gateway-web-aaba0f24fd26e1ca2276-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1371,6 +1387,7 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-default-my-gateway-web-efde1997778109a1f6eb-wrr", Rule: "Host(`foo.com`) && Path(`/foo`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, @@ -1430,11 +1447,13 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-default-my-gateway-web-efde1997778109a1f6eb-wrr", Rule: "Host(`foo.com`) && Path(`/foo`)", + RuleSyntax: "v3", }, "bar-http-app-bar-my-gateway-web-66f5c78d03d948e36597": { EntryPoints: []string{"web"}, Service: "bar-http-app-bar-my-gateway-web-66f5c78d03d948e36597-wrr", Rule: "Host(`bar.com`) && Path(`/bar`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, @@ -1520,6 +1539,7 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "bar-http-app-bar-my-gateway-web-66f5c78d03d948e36597-wrr", Rule: "Host(`bar.com`) && Path(`/bar`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, @@ -1579,6 +1599,7 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr", Rule: "Host(`example.org`) && PathPrefix(`/`)", + RuleSyntax: "v3", Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0"}, }, }, @@ -1647,6 +1668,7 @@ func TestLoadHTTPRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr", Rule: "Host(`example.org`) && PathPrefix(`/`)", + RuleSyntax: "v3", Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0"}, }, }, @@ -1912,6 +1934,7 @@ func TestLoadTCPRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "default-tcp-app-1-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, @@ -1969,11 +1992,13 @@ func TestLoadTCPRoutes(t *testing.T) { EntryPoints: []string{"tcp-1"}, Service: "default-tcp-app-1-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, "default-tcp-app-2-my-tcp-gateway-tcp-2-e3b0c44298fc1c149afb": { EntryPoints: []string{"tcp-2"}, Service: "default-tcp-app-2-my-tcp-gateway-tcp-2-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, @@ -2053,6 +2078,7 @@ func TestLoadTCPRoutes(t *testing.T) { EntryPoints: []string{"tcp-1"}, Service: "default-tcp-app-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, @@ -2144,6 +2170,7 @@ func TestLoadTCPRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "default-tcp-app-1-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, @@ -2203,6 +2230,7 @@ func TestLoadTCPRoutes(t *testing.T) { EntryPoints: []string{"tls"}, Service: "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, }, }, @@ -2266,6 +2294,7 @@ func TestLoadTCPRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "default-tcp-app-default-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, @@ -2321,11 +2350,13 @@ func TestLoadTCPRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "default-tcp-app-default-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, "bar-tcp-app-bar-my-tcp-gateway-tcp-e3b0c44298fc1c149afb": { EntryPoints: []string{"tcp"}, Service: "bar-tcp-app-bar-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, @@ -2403,6 +2434,7 @@ func TestLoadTCPRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "bar-tcp-app-bar-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, @@ -2696,6 +2728,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "default-tcp-app-1-my-tls-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, }, }, @@ -2761,6 +2794,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "default-tcp-app-1-my-tls-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -2819,6 +2853,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "default-tls-app-1-my-tls-gateway-tcp-f0dd0dd89f82eae1c270-wrr-0", Rule: "HostSNI(`foo.example.com`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -2878,12 +2913,14 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tls"}, Service: "default-tcp-app-1-my-tls-gateway-tls-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, }, "default-tls-app-1-my-tls-gateway-tcp-673acf455cb2dab0b43a": { EntryPoints: []string{"tcp"}, Service: "default-tls-app-1-my-tls-gateway-tcp-673acf455cb2dab0b43a-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -2973,6 +3010,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tls"}, Service: "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, }, }, @@ -3042,6 +3080,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tls"}, Service: "default-tls-app-1-my-gateway-tls-f0dd0dd89f82eae1c270-wrr-0", Rule: "HostSNI(`foo.example.com`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -3100,6 +3139,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tls"}, Service: "default-tls-app-1-my-gateway-tls-f0dd0dd89f82eae1c270-wrr-0", Rule: "HostSNI(`foo.example.com`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -3158,6 +3198,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tls"}, Service: "default-tls-app-1-my-gateway-tls-f0dd0dd89f82eae1c270-wrr-0", Rule: "HostSNI(`foo.example.com`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -3216,6 +3257,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tls"}, Service: "default-tls-app-1-my-gateway-tls-d5342d75658583f03593-wrr-0", Rule: "HostSNI(`foo.example.com`) || HostSNI(`bar.example.com`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -3274,6 +3316,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tls"}, Service: "default-tls-app-default-my-gateway-tls-06ae57dcf13ab4c60ee5-wrr-0", Rule: "HostSNI(`foo.default`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -3332,6 +3375,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tls"}, Service: "default-tls-app-default-my-gateway-tls-06ae57dcf13ab4c60ee5-wrr-0", Rule: "HostSNI(`foo.default`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -3340,6 +3384,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tls"}, Service: "bar-tls-app-bar-my-gateway-tls-2279fe75c5156dc5eb26-wrr-0", Rule: "HostSNI(`foo.bar`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -3420,6 +3465,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tls"}, Service: "bar-tls-app-bar-my-gateway-tls-2279fe75c5156dc5eb26-wrr-0", Rule: "HostSNI(`foo.bar`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -3478,6 +3524,7 @@ func TestLoadTLSRoutes(t *testing.T) { EntryPoints: []string{"tcp-1"}, Service: "default-tls-app-my-gateway-tcp-1-673acf455cb2dab0b43a-wrr", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -3702,17 +3749,20 @@ func TestLoadMixedRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "default-tcp-app-1-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, "default-tcp-app-1-my-gateway-tls-1-e3b0c44298fc1c149afb": { EntryPoints: []string{"tls-1"}, Service: "default-tcp-app-1-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, }, "default-tls-app-1-my-gateway-tls-2-59130f7db6718b7700c1": { EntryPoints: []string{"tls-2"}, Service: "default-tls-app-1-my-gateway-tls-2-59130f7db6718b7700c1-wrr-0", Rule: "HostSNI(`pass.tls.foo.example.com`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -3771,11 +3821,13 @@ func TestLoadMixedRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-1-my-gateway-web-a431b128267aabc954fd-wrr", Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", }, "default-http-app-1-my-gateway-websecure-a431b128267aabc954fd": { EntryPoints: []string{"websecure"}, Service: "default-http-app-1-my-gateway-websecure-a431b128267aabc954fd-wrr", Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, @@ -3881,17 +3933,20 @@ func TestLoadMixedRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, "default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb": { EntryPoints: []string{"tls-1"}, Service: "default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, }, "default-tls-app-default-my-gateway-tls-2-59130f7db6718b7700c1": { EntryPoints: []string{"tls-2"}, Service: "default-tls-app-default-my-gateway-tls-2-59130f7db6718b7700c1-wrr-0", Rule: "HostSNI(`pass.tls.foo.example.com`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -3950,11 +4005,13 @@ func TestLoadMixedRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-default-my-gateway-web-a431b128267aabc954fd-wrr", Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", }, "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd": { EntryPoints: []string{"websecure"}, Service: "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd-wrr", Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, @@ -4032,17 +4089,20 @@ func TestLoadMixedRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, "default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb": { EntryPoints: []string{"tls-1"}, Service: "default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, }, "default-tls-app-default-my-gateway-tls-2-59130f7db6718b7700c1": { EntryPoints: []string{"tls-2"}, Service: "default-tls-app-default-my-gateway-tls-2-59130f7db6718b7700c1-wrr-0", Rule: "HostSNI(`pass.tls.foo.example.com`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -4051,11 +4111,13 @@ func TestLoadMixedRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "bar-tcp-app-bar-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, "bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb": { EntryPoints: []string{"tls-1"}, Service: "bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, }, }, @@ -4144,22 +4206,26 @@ func TestLoadMixedRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-default-my-gateway-web-a431b128267aabc954fd-wrr", Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", }, "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd": { EntryPoints: []string{"websecure"}, Service: "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd-wrr", Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd": { EntryPoints: []string{"web"}, Service: "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr", Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", }, "bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd": { EntryPoints: []string{"websecure"}, Service: "bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd-wrr", Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, @@ -4273,17 +4339,20 @@ func TestLoadMixedRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "bar-tcp-app-bar-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, "bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb": { EntryPoints: []string{"tls-1"}, Service: "bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, }, "bar-tls-app-bar-my-gateway-tls-2-59130f7db6718b7700c1": { EntryPoints: []string{"tls-2"}, Service: "bar-tls-app-bar-my-gateway-tls-2-59130f7db6718b7700c1-wrr-0", Rule: "HostSNI(`pass.tls.foo.example.com`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ Passthrough: true, }, @@ -4342,11 +4411,13 @@ func TestLoadMixedRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr", Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", }, "bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd": { EntryPoints: []string{"websecure"}, Service: "bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd-wrr", Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, @@ -4423,11 +4494,13 @@ func TestLoadMixedRoutes(t *testing.T) { EntryPoints: []string{"tcp"}, Service: "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", }, "default-tcp-app-default-my-gateway-tls-e3b0c44298fc1c149afb": { EntryPoints: []string{"tls"}, Service: "default-tcp-app-default-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0", Rule: "HostSNI(`*`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, }, }, @@ -4474,11 +4547,13 @@ func TestLoadMixedRoutes(t *testing.T) { EntryPoints: []string{"web"}, Service: "default-http-app-default-my-gateway-web-a431b128267aabc954fd-wrr", Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", }, "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd": { EntryPoints: []string{"websecure"}, Service: "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd-wrr", Rule: "PathPrefix(`/`)", + RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, @@ -4807,7 +4882,7 @@ func Test_extractRule(t *testing.T) { }, }, }, - expectedRule: "Path(`/foo/`) || Headers(`my-header`,`foo`)", + expectedRule: "Path(`/foo/`) || Header(`my-header`,`foo`)", }, { desc: "Path && Header rules", @@ -4828,7 +4903,7 @@ func Test_extractRule(t *testing.T) { }, }, }, - expectedRule: "Path(`/foo/`) && Headers(`my-header`,`foo`)", + expectedRule: "Path(`/foo/`) && Header(`my-header`,`foo`)", }, { desc: "Host && Path && Header rules", @@ -4850,7 +4925,7 @@ func Test_extractRule(t *testing.T) { }, }, }, - expectedRule: "Host(`foo.com`) && Path(`/foo/`) && Headers(`my-header`,`foo`)", + expectedRule: "Host(`foo.com`) && Path(`/foo/`) && Header(`my-header`,`foo`)", }, { desc: "Host && (Path || Header) rules", @@ -4874,7 +4949,7 @@ func Test_extractRule(t *testing.T) { }, }, }, - expectedRule: "Host(`foo.com`) && (Path(`/foo/`) || Headers(`my-header`,`foo`))", + expectedRule: "Host(`foo.com`) && (Path(`/foo/`) || Header(`my-header`,`foo`))", }, } diff --git a/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json b/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json index 992a447d1..1dec58871 100644 --- a/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json +++ b/pkg/provider/traefik/fixtures/api_insecure_with_dashboard.json @@ -22,6 +22,11 @@ "priority": 2147483645 } }, + "services": { + "api": {}, + "dashboard": {}, + "noop": {} + }, "middlewares": { "dashboard_redirect": { "redirectRegex": { @@ -38,11 +43,6 @@ ] } } - }, - "services": { - "api": {}, - "dashboard": {}, - "noop": {} } }, "tcp": {}, diff --git a/pkg/provider/traefik/fixtures/full_configuration.json b/pkg/provider/traefik/fixtures/full_configuration.json index f09614e2e..6e9f2d4b3 100644 --- a/pkg/provider/traefik/fixtures/full_configuration.json +++ b/pkg/provider/traefik/fixtures/full_configuration.json @@ -54,6 +54,14 @@ "priority": 2147483647 } }, + "services": { + "api": {}, + "dashboard": {}, + "noop": {}, + "ping": {}, + "prometheus": {}, + "rest": {} + }, "middlewares": { "dashboard_redirect": { "redirectRegex": { @@ -70,14 +78,6 @@ ] } } - }, - "services": { - "api": {}, - "dashboard": {}, - "noop": {}, - "ping": {}, - "prometheus": {}, - "rest": {} } }, "tcp": {}, diff --git a/pkg/provider/traefik/fixtures/redirection.json b/pkg/provider/traefik/fixtures/redirection.json index 2b3b271fa..73ae77db3 100644 --- a/pkg/provider/traefik/fixtures/redirection.json +++ b/pkg/provider/traefik/fixtures/redirection.json @@ -12,6 +12,9 @@ "rule": "HostRegexp(`^.+$`)" } }, + "services": { + "noop": {} + }, "middlewares": { "redirect-web-to-websecure": { "redirectScheme": { @@ -20,11 +23,8 @@ "permanent": true } } - }, - "services": { - "noop": {} } }, "tcp": {}, "tls": {} -} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/redirection_port.json b/pkg/provider/traefik/fixtures/redirection_port.json index ead9bc0b1..a9e75438a 100644 --- a/pkg/provider/traefik/fixtures/redirection_port.json +++ b/pkg/provider/traefik/fixtures/redirection_port.json @@ -12,6 +12,9 @@ "rule": "HostRegexp(`^.+$`)" } }, + "services": { + "noop": {} + }, "middlewares": { "redirect-web-to-443": { "redirectScheme": { @@ -20,11 +23,8 @@ "permanent": true } } - }, - "services": { - "noop": {} } }, "tcp": {}, "tls": {} -} +} \ No newline at end of file diff --git a/pkg/provider/traefik/fixtures/redirection_with_protocol.json b/pkg/provider/traefik/fixtures/redirection_with_protocol.json index 2b3b271fa..73ae77db3 100644 --- a/pkg/provider/traefik/fixtures/redirection_with_protocol.json +++ b/pkg/provider/traefik/fixtures/redirection_with_protocol.json @@ -12,6 +12,9 @@ "rule": "HostRegexp(`^.+$`)" } }, + "services": { + "noop": {} + }, "middlewares": { "redirect-web-to-websecure": { "redirectScheme": { @@ -20,11 +23,8 @@ "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 71a6c321a..9a4105623 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -65,6 +65,7 @@ func (i *Provider) createConfiguration(ctx context.Context) *dynamic.Configurati TCP: &dynamic.TCPConfiguration{ Routers: make(map[string]*dynamic.TCPRouter), Services: make(map[string]*dynamic.TCPService), + Models: make(map[string]*dynamic.TCPModel), ServersTransports: make(map[string]*dynamic.TCPServersTransport), }, TLS: &dynamic.TLSConfiguration{ @@ -191,8 +192,13 @@ func (i *Provider) getEntryPointPort(name string, def *static.Redirections) (str } func (i *Provider) entryPointModels(cfg *dynamic.Configuration) { + defaultRuleSyntax := "" + if i.staticCfg.Core != nil && i.staticCfg.Core.DefaultRuleSyntax != "" { + defaultRuleSyntax = i.staticCfg.Core.DefaultRuleSyntax + } + for name, ep := range i.staticCfg.EntryPoints { - if len(ep.HTTP.Middlewares) == 0 && ep.HTTP.TLS == nil { + if len(ep.HTTP.Middlewares) == 0 && ep.HTTP.TLS == nil && defaultRuleSyntax == "" { continue } @@ -208,7 +214,19 @@ func (i *Provider) entryPointModels(cfg *dynamic.Configuration) { } } + m.DefaultRuleSyntax = defaultRuleSyntax + cfg.HTTP.Models[name] = m + + if cfg.TCP == nil { + continue + } + + mTCP := &dynamic.TCPModel{ + DefaultRuleSyntax: defaultRuleSyntax, + } + + cfg.TCP.Models[name] = mTCP } } diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index bd4fda0f5..c6a88e590 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -24,6 +24,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint Routers: make(map[string]*dynamic.TCPRouter), Services: make(map[string]*dynamic.TCPService), Middlewares: make(map[string]*dynamic.TCPMiddleware), + Models: make(map[string]*dynamic.TCPModel), ServersTransports: make(map[string]*dynamic.TCPServersTransport), }, UDP: &dynamic.UDPConfiguration{ @@ -152,6 +153,13 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration { for name, rt := range cfg.HTTP.Routers { router := rt.DeepCopy() + if !router.DefaultRule && router.RuleSyntax == "" { + for _, model := range cfg.HTTP.Models { + router.RuleSyntax = model.DefaultRuleSyntax + break + } + } + eps := router.EntryPoints router.EntryPoints = nil @@ -183,6 +191,25 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration { cfg.HTTP.Routers = rts + if cfg.TCP == nil || len(cfg.TCP.Models) == 0 { + return cfg + } + + tcpRouters := make(map[string]*dynamic.TCPRouter) + + for _, rt := range cfg.TCP.Routers { + router := rt.DeepCopy() + + if router.RuleSyntax == "" { + for _, model := range cfg.TCP.Models { + router.RuleSyntax = model.DefaultRuleSyntax + break + } + } + } + + cfg.TCP.Routers = tcpRouters + return cfg } diff --git a/pkg/server/aggregator_test.go b/pkg/server/aggregator_test.go index ea1fb639f..1ef3bd8ac 100644 --- a/pkg/server/aggregator_test.go +++ b/pkg/server/aggregator_test.go @@ -473,6 +473,7 @@ func Test_mergeConfiguration_defaultTCPEntryPoint(t *testing.T) { Services: map[string]*dynamic.TCPService{ "service-1@provider-1": {}, }, + Models: map[string]*dynamic.TCPModel{}, ServersTransports: make(map[string]*dynamic.TCPServersTransport), } diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go index 36125db64..a0fed4cae 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -92,6 +92,7 @@ func TestNewConfigurationWatcher(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{}, + Models: map[string]*dynamic.TCPModel{}, ServersTransports: map[string]*dynamic.TCPServersTransport{}, }, TLS: &dynamic.TLSConfiguration{ @@ -231,6 +232,7 @@ func TestIgnoreTransientConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{}, + Models: map[string]*dynamic.TCPModel{}, ServersTransports: map[string]*dynamic.TCPServersTransport{}, }, UDP: &dynamic.UDPConfiguration{ @@ -400,6 +402,7 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{}, + Models: map[string]*dynamic.TCPModel{}, ServersTransports: map[string]*dynamic.TCPServersTransport{}, }, UDP: &dynamic.UDPConfiguration{ @@ -490,6 +493,7 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{}, + Models: map[string]*dynamic.TCPModel{}, ServersTransports: map[string]*dynamic.TCPServersTransport{}, }, UDP: &dynamic.UDPConfiguration{ @@ -625,6 +629,7 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{}, + Models: map[string]*dynamic.TCPModel{}, ServersTransports: map[string]*dynamic.TCPServersTransport{}, }, UDP: &dynamic.UDPConfiguration{ @@ -693,6 +698,7 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { Routers: map[string]*dynamic.TCPRouter{}, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{}, + Models: map[string]*dynamic.TCPModel{}, ServersTransports: map[string]*dynamic.TCPServersTransport{}, }, TLS: &dynamic.TLSConfiguration{ diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index 4b383e076..bfa99cfb0 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -131,7 +131,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string continue } - if err = muxer.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil { + if err = muxer.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil { routerConfig.AddError(err, true) logger.Error().Err(err).Send() continue diff --git a/pkg/server/router/tcp/manager.go b/pkg/server/router/tcp/manager.go index df24ed704..cd10d5ccf 100644 --- a/pkg/server/router/tcp/manager.go +++ b/pkg/server/router/tcp/manager.go @@ -311,7 +311,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim if routerConfig.TLS == nil { logger.Debug().Msgf("Adding route for %q", routerConfig.Rule) - if err := router.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil { + if err := router.muxerTCP.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil { routerConfig.AddError(err, true) logger.Error().Err(err).Send() } @@ -321,7 +321,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim if routerConfig.TLS.Passthrough { logger.Debug().Msgf("Adding Passthrough route for %q", routerConfig.Rule) - if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil { + if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil { routerConfig.AddError(err, true) logger.Error().Err(err).Send() } @@ -355,7 +355,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim logger.Debug().Msgf("Adding special TLS closing route for %q because broken TLS options %s", routerConfig.Rule, tlsOptionsName) - if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.Priority, &brokenTLSRouter{}); err != nil { + if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, &brokenTLSRouter{}); err != nil { routerConfig.AddError(err, true) logger.Error().Err(err).Send() } @@ -389,7 +389,7 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim logger.Debug().Msgf("Adding TLS route for %q", routerConfig.Rule) - if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil { + if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil { routerConfig.AddError(err, true) logger.Error().Err(err).Send() continue diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 711a60313..74f563486 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -201,9 +201,9 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) { conn.Close() } -// AddRoute defines a handler for the given rule. -func (r *Router) AddRoute(rule string, priority int, target tcp.Handler) error { - return r.muxerTCP.AddRoute(rule, priority, target) +// AddTCPRoute defines a handler for the given rule. +func (r *Router) AddTCPRoute(rule string, priority int, target tcp.Handler) error { + return r.muxerTCP.AddRoute(rule, "", priority, target) } // AddHTTPTLSConfig defines a handler for a given sniHost and sets the matching tlsConfig. @@ -267,7 +267,7 @@ func (r *Router) SetHTTPSForwarder(handler tcp.Handler) { } rule := "HostSNI(`" + sniHost + "`)" - if err := r.muxerHTTPS.AddRoute(rule, tcpmuxer.GetRulePriority(rule), tcpHandler); err != nil { + if err := r.muxerHTTPS.AddRoute(rule, "", tcpmuxer.GetRulePriority(rule), tcpHandler); err != nil { log.Error().Err(err).Msg("Error while adding route for host") } } diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index 29bb2c418..1bcd987b1 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -947,10 +947,10 @@ func TestPostgres(t *testing.T) { // This test requires to have a TLS route, but does not actually check the // content of the handler. It would require to code a TLS handshake to // check the SNI and content of the handlerFunc. - err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", 0, nil) + err = router.muxerTCPTLS.AddRoute("HostSNI(`test.localhost`)", "", 0, nil) require.NoError(t, err) - err = router.AddRoute("HostSNI(`*`)", 0, tcp2.HandlerFunc(func(conn tcp2.WriteCloser) { + err = router.muxerTCP.AddRoute("HostSNI(`*`)", "", 0, tcp2.HandlerFunc(func(conn tcp2.WriteCloser) { _, _ = conn.Write([]byte("OK")) _ = conn.Close() })) diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index d050803f3..d53f160f8 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -47,7 +47,7 @@ func TestShutdownTCP(t *testing.T) { router, err := tcprouter.NewRouter() require.NoError(t, err) - err = router.AddRoute("HostSNI(`*`)", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) { + err = router.AddTCPRoute("HostSNI(`*`)", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) { _, err := http.ReadRequest(bufio.NewReader(conn)) if err != nil { return From 9befe0dd51d35cd020d173ccd66e79a8b8707dbb Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Tue, 23 Jan 2024 16:46:05 +0100 Subject: [PATCH 11/36] Fix flaky test --- pkg/server/configurationwatcher_test.go | 153 ++++++++++++++++-------- 1 file changed, 102 insertions(+), 51 deletions(-) diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go index 1f571effb..70794f550 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -171,56 +171,7 @@ func TestIgnoreTransientConfiguration(t *testing.T) { ), } - config2 := &dynamic.Configuration{ - HTTP: th.BuildConfiguration( - th.WithRouters(th.WithRouter("baz", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("toto")), - ), - } - - watcher := NewConfigurationWatcher(routinesPool, &mockProvider{}, []string{}, "") - - publishedConfigCount := 0 - var lastConfig dynamic.Configuration - blockConfConsumer := make(chan struct{}) - watcher.AddListener(func(config dynamic.Configuration) { - publishedConfigCount++ - lastConfig = config - <-blockConfConsumer - }) - - watcher.Start() - - t.Cleanup(watcher.Stop) - t.Cleanup(routinesPool.Stop) - - watcher.allProvidersConfigs <- dynamic.Message{ - ProviderName: "mock", - Configuration: config, - } - - watcher.allProvidersConfigs <- dynamic.Message{ - ProviderName: "mock", - Configuration: config2, - } - - watcher.allProvidersConfigs <- dynamic.Message{ - ProviderName: "mock", - Configuration: config, - } - - // give some time before closing the channel. - time.Sleep(20 * time.Millisecond) - - close(blockConfConsumer) - - // give some time so that the configuration can be processed. - time.Sleep(20 * time.Millisecond) - - // after 20 milliseconds we should have 1 config published. - assert.Equal(t, 1, publishedConfigCount, "times configs were published") - - expected := dynamic.Configuration{ + expectedConfig := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))), th.WithLoadBalancerServices(th.WithService("bar@mock")), @@ -243,7 +194,107 @@ func TestIgnoreTransientConfiguration(t *testing.T) { }, } - assert.Equal(t, expected, lastConfig) + expectedConfig3 := dynamic.Configuration{ + HTTP: th.BuildConfiguration( + th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))), + th.WithLoadBalancerServices(th.WithService("bar-config3@mock")), + th.WithMiddlewares(), + ), + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{ + "default": tls.DefaultTLSOptions, + }, + Stores: map[string]tls.Store{}, + }, + } + + config2 := &dynamic.Configuration{ + HTTP: th.BuildConfiguration( + th.WithRouters(th.WithRouter("baz", th.WithEntryPoints("ep"))), + th.WithLoadBalancerServices(th.WithService("toto")), + ), + } + + config3 := &dynamic.Configuration{ + HTTP: th.BuildConfiguration( + th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), + th.WithLoadBalancerServices(th.WithService("bar-config3")), + ), + } + watcher := NewConfigurationWatcher(routinesPool, &mockProvider{}, []string{}, "") + + // To be able to "block" the writes, we change the chan to remove buffering. + watcher.allProvidersConfigs = make(chan dynamic.Message) + + publishedConfigCount := 0 + + firstConfigHandled := make(chan struct{}) + blockConfConsumer := make(chan struct{}) + blockConfConsumerAssert := make(chan struct{}) + watcher.AddListener(func(config dynamic.Configuration) { + publishedConfigCount++ + + if publishedConfigCount > 2 { + t.Fatal("More than 2 published configuration") + } + + if publishedConfigCount == 1 { + assert.Equal(t, expectedConfig, config) + close(firstConfigHandled) + + <-blockConfConsumer + time.Sleep(500 * time.Millisecond) + } + + if publishedConfigCount == 2 { + assert.Equal(t, expectedConfig3, config) + close(blockConfConsumerAssert) + } + }) + + watcher.Start() + + t.Cleanup(watcher.Stop) + t.Cleanup(routinesPool.Stop) + + watcher.allProvidersConfigs <- dynamic.Message{ + ProviderName: "mock", + Configuration: config, + } + + <-firstConfigHandled + + watcher.allProvidersConfigs <- dynamic.Message{ + ProviderName: "mock", + Configuration: config2, + } + + watcher.allProvidersConfigs <- dynamic.Message{ + ProviderName: "mock", + Configuration: config, + } + + close(blockConfConsumer) + + watcher.allProvidersConfigs <- dynamic.Message{ + ProviderName: "mock", + Configuration: config3, + } + + select { + case <-blockConfConsumerAssert: + case <-time.After(10 * time.Second): + t.Fatal("Timeout") + } } func TestListenProvidersThrottleProviderConfigReload(t *testing.T) { From b5251c6ac469a4ebc43a29712d9a0840de23284d Mon Sep 17 00:00:00 2001 From: Halimao <1065621723@qq.com> Date: Wed, 24 Jan 2024 18:58:05 +0800 Subject: [PATCH 12/36] misc(Makefile): add `help` target to display the help msg --- Makefile | 53 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index 1a8f40b37..94b8a831c 100644 --- a/Makefile +++ b/Makefile @@ -22,40 +22,41 @@ LINT_EXECUTABLES = misspell shellcheck DOCKER_BUILD_PLATFORMS ?= linux/amd64,linux/arm64 .PHONY: default +#? default: Run `make generate` and `make binary` default: generate binary -## Create the "dist" directory +#? dist: Create the "dist" directory dist: mkdir -p dist -## Build WebUI Docker image .PHONY: build-webui-image +#? build-webui-image: Build WebUI Docker image build-webui-image: docker build -t traefik-webui -f webui/Dockerfile webui -## Clean WebUI static generated assets .PHONY: clean-webui +#? clean-webui: Clean WebUI static generated assets clean-webui: rm -r webui/static mkdir -p webui/static printf 'For more information see `webui/readme.md`' > webui/static/DONT-EDIT-FILES-IN-THIS-DIRECTORY.md -## Generate WebUI webui/static/index.html: $(MAKE) build-webui-image docker run --rm -v "$(PWD)/webui/static":'/src/webui/static' traefik-webui npm run build:nc docker run --rm -v "$(PWD)/webui/static":'/src/webui/static' traefik-webui chown -R $(shell id -u):$(shell id -g) ./static .PHONY: generate-webui +#? generate-webui: Generate WebUI generate-webui: webui/static/index.html -## Generate code .PHONY: generate +#? generate: Generate code (Dynamic and Static configuration documentation reference files) generate: go generate -## Build the binary .PHONY: binary +#? binary: Build the binary binary: generate-webui dist @echo SHA: $(VERSION) $(CODENAME) $(DATE) CGO_ENABLED=0 GOGC=off GOOS=${GOOS} GOARCH=${GOARCH} go build ${FLAGS[*]} -ldflags "-s -w \ @@ -80,27 +81,27 @@ binary-windows-amd64: export BIN_NAME := traefik.exe binary-windows-amd64: @$(MAKE) binary -## Build the binary for the standard platforms (linux, darwin, windows) .PHONY: crossbinary-default +#? crossbinary-default: Build the binary for the standard platforms (linux, darwin, windows) crossbinary-default: generate generate-webui $(CURDIR)/script/crossbinary-default.sh -## Run the unit and integration tests .PHONY: test +#? test: Run the unit and integration tests test: test-unit test-integration -## Run the unit tests .PHONY: test-unit +#? test-unit: Run the unit tests test-unit: GOOS=$(GOOS) GOARCH=$(GOARCH) go test -cover "-coverprofile=cover.out" -v $(TESTFLAGS) ./pkg/... ./cmd/... -## Run the integration tests .PHONY: test-integration +#? test-integration: Run the integration tests test-integration: binary GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -test.timeout=20m -failfast -v $(TESTFLAGS) -## Pull all Docker images to avoid timeout during integration tests .PHONY: pull-images +#? pull-images: Pull all Docker images to avoid timeout during integration tests pull-images: grep --no-filename -E '^\s+image:' ./integration/resources/compose/*.yml \ | awk '{print $$2}' \ @@ -108,21 +109,21 @@ pull-images: | uniq \ | xargs -P 6 -n 1 docker pull -## Lint run golangci-lint .PHONY: lint +#? lint: Run golangci-lint lint: golangci-lint run -## Validate code and docs .PHONY: validate-files +#? validate-files: Validate code and docs validate-files: lint $(foreach exec,$(LINT_EXECUTABLES),\ $(if $(shell which $(exec)),,$(error "No $(exec) in PATH"))) $(CURDIR)/script/validate-misspell.sh $(CURDIR)/script/validate-shell-script.sh -## Validate code, docs, and vendor .PHONY: validate +#? validate: Validate code, docs, and vendor validate: lint $(foreach exec,$(EXECUTABLES),\ $(if $(shell which $(exec)),,$(error "No $(exec) in PATH"))) @@ -136,51 +137,57 @@ multi-arch-image-%: binary-linux-amd64 binary-linux-arm64 docker buildx build $(DOCKER_BUILDX_ARGS) -t traefik/traefik:$* --platform=$(DOCKER_BUILD_PLATFORMS) -f Dockerfile . -## Clean up static directory and build a Docker Traefik image .PHONY: build-image +#? build-image: Clean up static directory and build a Docker Traefik image build-image: export DOCKER_BUILDX_ARGS := --load build-image: export DOCKER_BUILD_PLATFORMS := linux/$(GOARCH) build-image: clean-webui @$(MAKE) multi-arch-image-latest -## Build a Docker Traefik image without re-building the webui when it's already built .PHONY: build-image-dirty +#? build-image-dirty: Build a Docker Traefik image without re-building the webui when it's already built build-image-dirty: export DOCKER_BUILDX_ARGS := --load build-image-dirty: export DOCKER_BUILD_PLATFORMS := linux/$(GOARCH) build-image-dirty: @$(MAKE) multi-arch-image-latest -## Build documentation site .PHONY: docs +#? docs: Build documentation site docs: make -C ./docs docs -## Serve the documentation site locally .PHONY: docs-serve +#? docs-serve: Serve the documentation site locally docs-serve: make -C ./docs docs-serve -## Pull image for doc building .PHONY: docs-pull-images +#? docs-pull-images: Pull image for doc building docs-pull-images: make -C ./docs docs-pull-images -## Generate CRD clientset and CRD manifests .PHONY: generate-crd +#? generate-crd: Generate CRD clientset and CRD manifests generate-crd: @$(CURDIR)/script/code-gen-docker.sh -## Generate code from dynamic configuration https://github.com/traefik/genconf .PHONY: generate-genconf +#? generate-genconf: Generate code from dynamic configuration github.com/traefik/genconf generate-genconf: go run ./cmd/internal/gen/ -## Create packages for the release .PHONY: release-packages +#? release-packages: Create packages for the release release-packages: generate-webui $(CURDIR)/script/release-packages.sh -## Format the Code .PHONY: fmt +#? fmt: Format the Code fmt: gofmt -s -l -w $(SRCS) + +.PHONY: help +#? help: Get more info on make commands +help: Makefile + @echo " Choose a command run in traefik:" + @sed -n 's/^#?//p' $< | column -t -s ':' | sort | sed -e 's/^/ /' From 547cd815997d6b29468dcfc6da7f2fba5655da9a Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 24 Jan 2024 15:20:09 +0100 Subject: [PATCH 13/36] Prepare release v2.11.0-rc2 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a38879596..6b48349f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [v2.11.0-rc2](https://github.com/traefik/traefik/tree/v2.11.0-rc2) (2024-01-24) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.0-rc1...v2.11.0-rc2) + +**Bug fixes:** +- **[middleware,tcp]** Add missing TCP IPAllowList middleware constructor ([#10331](https://github.com/traefik/traefik/pull/10331) by [youkoulayley](https://github.com/youkoulayley)) +- **[nomad]** Update the Nomad API dependency to v1.7.2 ([#10327](https://github.com/traefik/traefik/pull/10327) by [jrasell](https://github.com/jrasell)) + +**Documentation:** +- Improve Concepts documentation page ([#10315](https://github.com/traefik/traefik/pull/10315) by [oliver-dvorski](https://github.com/oliver-dvorski)) + ## [v2.11.0-rc1](https://github.com/traefik/traefik/tree/v2.11.0-rc1) (2024-01-02) [All Commits](https://github.com/traefik/traefik/compare/0a7964300166d167f68d5502bc245b3b9c8842b4...v2.11.0-rc1) From aece9a1051fbf5a5f44fb1a9b2d23bdb76b9eb4c Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 24 Jan 2024 16:58:05 +0100 Subject: [PATCH 14/36] fix: opentelemetry unit tests --- pkg/metrics/datadog_test.go | 7 +-- pkg/metrics/influxdb2_test.go | 19 ++---- pkg/metrics/opentelemetry_test.go | 100 +++++++++++++++++++----------- 3 files changed, 71 insertions(+), 55 deletions(-) diff --git a/pkg/metrics/datadog_test.go b/pkg/metrics/datadog_test.go index 68e79ba71..6a53bf065 100644 --- a/pkg/metrics/datadog_test.go +++ b/pkg/metrics/datadog_test.go @@ -13,12 +13,13 @@ import ( ) func TestDatadog(t *testing.T) { + t.Cleanup(StopDatadog) + udp.SetAddr(":18125") // This is needed to make sure that UDP Listener listens for data a bit longer, otherwise it will quit after a millisecond udp.Timeout = 5 * time.Second datadogRegistry := RegisterDatadog(context.Background(), &types.Datadog{Address: ":18125", PushInterval: ptypes.Duration(time.Second), AddEntryPointsLabels: true, AddRoutersLabels: true, AddServicesLabels: true}) - defer StopDatadog() if !datadogRegistry.IsEpEnabled() || !datadogRegistry.IsRouterEnabled() || !datadogRegistry.IsSvcEnabled() { t.Errorf("DatadogRegistry should return true for IsEnabled(), IsRouterEnabled() and IsSvcEnabled()") @@ -27,9 +28,7 @@ func TestDatadog(t *testing.T) { } func TestDatadogWithPrefix(t *testing.T) { - t.Cleanup(func() { - StopDatadog() - }) + t.Cleanup(StopDatadog) udp.SetAddr(":18125") // This is needed to make sure that UDP Listener listens for data a bit longer, otherwise it will quit after a millisecond diff --git a/pkg/metrics/influxdb2_test.go b/pkg/metrics/influxdb2_test.go index 89b558075..e75141ff4 100644 --- a/pkg/metrics/influxdb2_test.go +++ b/pkg/metrics/influxdb2_test.go @@ -6,7 +6,6 @@ import ( "io" "net/http" "net/http/httptest" - "regexp" "strconv" "testing" "time" @@ -26,7 +25,6 @@ func TestInfluxDB2(t *testing.T) { c <- &bodyStr _, _ = fmt.Fprintln(w, "ok") })) - defer ts.Close() influxDB2Registry := RegisterInfluxDB2(context.Background(), &types.InfluxDB2{ @@ -39,7 +37,11 @@ func TestInfluxDB2(t *testing.T) { AddRoutersLabels: true, AddServicesLabels: true, }) - defer StopInfluxDB2() + + t.Cleanup(func() { + StopInfluxDB2() + ts.Close() + }) if !influxDB2Registry.IsEpEnabled() || !influxDB2Registry.IsRouterEnabled() || !influxDB2Registry.IsSvcEnabled() { t.Fatalf("InfluxDB2Registry should return true for IsEnabled(), IsRouterEnabled() and IsSvcEnabled()") @@ -137,14 +139,3 @@ func TestInfluxDB2(t *testing.T) { assertMessage(t, *msgServiceRetries, expectedServiceRetries) } - -func assertMessage(t *testing.T, msg string, patterns []string) { - t.Helper() - for _, pattern := range patterns { - re := regexp.MustCompile(pattern) - match := re.FindStringSubmatch(msg) - if len(match) != 2 { - t.Errorf("Got %q %v, want %q", msg, match, pattern) - } - } -} diff --git a/pkg/metrics/opentelemetry_test.go b/pkg/metrics/opentelemetry_test.go index 7e426cc11..28c660357 100644 --- a/pkg/metrics/opentelemetry_test.go +++ b/pkg/metrics/opentelemetry_test.go @@ -4,10 +4,12 @@ import ( "compress/gzip" "context" "encoding/json" + "fmt" "io" "net/http" "net/http/httptest" "net/url" + "regexp" "strconv" "testing" "time" @@ -310,7 +312,7 @@ func TestOpenTelemetry(t *testing.T) { })) t.Cleanup(func() { - close(c) + StopOpenTelemetry() ts.Close() }) @@ -335,57 +337,53 @@ func TestOpenTelemetry(t *testing.T) { `({"key":"service.name","value":{"stringValue":"traefik"}})`, `({"key":"service.version","value":{"stringValue":"` + version.Version + `"}})`, } - msgMisc := <-c - assertMessage(t, *msgMisc, expected) + tryAssertMessage(t, c, expected) // TODO: the len of startUnixNano is no supposed to be 20, it should be 19 - expected = append(expected, + expectedConfig := []string{ `({"name":"traefik_config_reloads_total","description":"Config reloads","unit":"1","sum":{"dataPoints":\[{"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_config_last_reload_success","description":"Last config reload success","unit":"ms","gauge":{"dataPoints":\[{"timeUnixNano":"[\d]{19}","asDouble":1}\]}})`, `({"name":"traefik_open_connections","description":"How many open connections exist, by entryPoint and protocol","unit":"1","gauge":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test"}},{"key":"protocol","value":{"stringValue":"TCP"}}\],"timeUnixNano":"[\d]{19}","asDouble":1}\]}})`, - ) + } registry.ConfigReloadsCounter().Add(1) registry.LastConfigReloadSuccessGauge().Set(1) registry.OpenConnectionsGauge().With("entrypoint", "test", "protocol", "TCP").Set(1) - msgServer := <-c - assertMessage(t, *msgServer, expected) + tryAssertMessage(t, c, expectedConfig) - expected = append(expected, + expectedTLSCerts := []string{ `({"name":"traefik_tls_certs_not_after","description":"Certificate expiration timestamp","unit":"ms","gauge":{"dataPoints":\[{"attributes":\[{"key":"key","value":{"stringValue":"value"}}\],"timeUnixNano":"[\d]{19}","asDouble":1}\]}})`, - ) + } registry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1) - msgTLS := <-c - assertMessage(t, *msgTLS, expected) + tryAssertMessage(t, c, expectedTLSCerts) - expected = append(expected, + expectedEntryPoints := []string{ `({"name":"traefik_entrypoint_requests_total","description":"How many HTTP requests processed on an entrypoint, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"entrypoint","value":{"stringValue":"test1"}},{"key":"method","value":{"stringValue":"GET"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_entrypoint_requests_tls_total","description":"How many HTTP requests with TLS processed on an entrypoint, partitioned by TLS Version and TLS cipher Used.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test2"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_entrypoint_request_duration_seconds","description":"How long it took to process the request on an entrypoint, partitioned by status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test3"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`, `({"name":"traefik_entrypoint_requests_bytes_total","description":"The total size of requests in bytes handled by an entrypoint, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"entrypoint","value":{"stringValue":"test1"}},{"key":"method","value":{"stringValue":"GET"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_entrypoint_responses_bytes_total","description":"The total size of responses in bytes handled by an entrypoint, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"entrypoint","value":{"stringValue":"test1"}},{"key":"method","value":{"stringValue":"GET"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, - ) + } registry.EntryPointReqsCounter().With(nil, "entrypoint", "test1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1) registry.EntryPointReqsTLSCounter().With("entrypoint", "test2", "tls_version", "foo", "tls_cipher", "bar").Add(1) registry.EntryPointReqDurationHistogram().With("entrypoint", "test3").Observe(10000) registry.EntryPointReqsBytesCounter().With("entrypoint", "test1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1) registry.EntryPointRespsBytesCounter().With("entrypoint", "test1", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1) - msgEntrypoint := <-c - assertMessage(t, *msgEntrypoint, expected) + tryAssertMessage(t, c, expectedEntryPoints) - expected = append(expected, + expectedRouters := []string{ `({"name":"traefik_router_requests_total","description":"How many HTTP requests are processed on a router, partitioned by service, status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1},{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_router_requests_tls_total","description":"How many HTTP requests with TLS are processed on a router, partitioned by service, TLS Version, and TLS cipher Used.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"router","value":{"stringValue":"demo"}},{"key":"service","value":{"stringValue":"test"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_router_request_duration_seconds","description":"How long it took to process the request on a router, partitioned by service, status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"router","value":{"stringValue":"demo"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`, `({"name":"traefik_router_requests_bytes_total","description":"The total size of requests in bytes handled by a router, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_router_responses_bytes_total","description":"The total size of responses in bytes handled by a router, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"router","value":{"stringValue":"RouterReqsCounter"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, - ) + } registry.RouterReqsCounter().With(nil, "router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1) registry.RouterReqsCounter().With(nil, "router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1) @@ -393,18 +391,17 @@ func TestOpenTelemetry(t *testing.T) { registry.RouterReqDurationHistogram().With("router", "demo", "service", "test", "code", strconv.Itoa(http.StatusOK)).Observe(10000) registry.RouterReqsBytesCounter().With("router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1) registry.RouterRespsBytesCounter().With("router", "RouterReqsCounter", "service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1) - msgRouter := <-c - assertMessage(t, *msgRouter, expected) + tryAssertMessage(t, c, expectedRouters) - expected = append(expected, + expectedServices := []string{ `({"name":"traefik_service_requests_total","description":"How many HTTP requests processed on a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1},{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_service_requests_tls_total","description":"How many HTTP requests with TLS processed on a service, partitioned by TLS version and TLS cipher.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"service","value":{"stringValue":"test"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_service_request_duration_seconds","description":"How long it took to process the request on a service, partitioned by status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`, `({"name":"traefik_service_server_up","description":"service server is up, described by gauge value of 0 or 1.","unit":"1","gauge":{"dataPoints":\[{"attributes":\[{"key":"service","value":{"stringValue":"test"}},{"key":"url","value":{"stringValue":"http://127.0.0.1"}}\],"timeUnixNano":"[\d]{19}","asDouble":1}\]}})`, `({"name":"traefik_service_requests_bytes_total","description":"The total size of requests in bytes received by a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_service_responses_bytes_total","description":"The total size of responses in bytes returned by a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, - ) + } registry.ServiceReqsCounter().With(nil, "service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1) registry.ServiceReqsCounter().With(nil, "service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1) @@ -413,21 +410,19 @@ func TestOpenTelemetry(t *testing.T) { registry.ServiceServerUpGauge().With("service", "test", "url", "http://127.0.0.1").Set(1) registry.ServiceReqsBytesCounter().With("service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1) registry.ServiceRespsBytesCounter().With("service", "ServiceReqsCounter", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1) - msgService := <-c - assertMessage(t, *msgService, expected) + tryAssertMessage(t, c, expectedServices) - expected = append(expected, + expectedServicesRetries := []string{ `({"attributes":\[{"key":"service","value":{"stringValue":"foobar"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1})`, `({"attributes":\[{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":2})`, - ) + } registry.ServiceRetriesCounter().With("service", "test").Add(1) registry.ServiceRetriesCounter().With("service", "test").Add(1) registry.ServiceRetriesCounter().With("service", "foobar").Add(1) - msgServiceRetries := <-c - assertMessage(t, *msgServiceRetries, expected) + tryAssertMessage(t, c, expectedServicesRetries) // We cannot rely on the previous expected pattern, // because this pattern was for matching only one dataPoint in the histogram, @@ -439,15 +434,46 @@ func TestOpenTelemetry(t *testing.T) { registry.EntryPointReqDurationHistogram().With("entrypoint", "myEntrypoint").Observe(10000) registry.EntryPointReqDurationHistogram().With("entrypoint", "myEntrypoint").Observe(20000) - msgEntryPointReqDurationHistogram := <-c - assertMessage(t, *msgEntryPointReqDurationHistogram, expectedEntryPointReqDuration) - - // Stopping OpenTelemetry. - go func() { - StopOpenTelemetry() - }() - - // We need to unlock the HTTP Server for the last export call. - <-c + tryAssertMessage(t, c, expectedEntryPointReqDuration) +} + +func assertMessage(t *testing.T, msg string, expected []string) { + t.Helper() + errs := verifyMessage(msg, expected) + for _, err := range errs { + t.Error(err) + } +} + +func tryAssertMessage(t *testing.T, c chan *string, expected []string) { + t.Helper() + + var errs []error + timeout := time.After(1 * time.Second) + for { + select { + case <-timeout: + for _, err := range errs { + t.Error(err) + } + case msg := <-c: + errs = verifyMessage(*msg, expected) + if len(errs) == 0 { + return + } + } + } +} + +func verifyMessage(msg string, expected []string) []error { + var errs []error + for _, pattern := range expected { + re := regexp.MustCompile(pattern) + match := re.FindStringSubmatch(msg) + if len(match) != 2 { + errs = append(errs, fmt.Errorf("Got %q %v, want %q", msg, match, pattern)) + } + } + return errs } From 03d2e35488178ef40fa7df2bf5e8854497f6d796 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 24 Jan 2024 18:54:05 +0100 Subject: [PATCH 15/36] fix: remove snapshot from release target --- script/release-packages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/release-packages.sh b/script/release-packages.sh index f442e12dd..5f67c3c4c 100755 --- a/script/release-packages.sh +++ b/script/release-packages.sh @@ -11,7 +11,7 @@ fi rm -rf dist for os in linux darwin windows freebsd openbsd; do - goreleaser release --snapshot --skip=publish -p 2 --timeout="90m" --config "$(go run ./internal/release "$os")" + goreleaser release --skip=publish -p 2 --timeout="90m" --config "$(go run ./internal/release "$os")" go clean -cache done From 49f04f2772acc58dc583114da5335e980bc01811 Mon Sep 17 00:00:00 2001 From: Andi Sardina Ramos Date: Thu, 25 Jan 2024 10:56:05 +0200 Subject: [PATCH 16/36] fix: URL encode resource's id before calling API endpoints --- pkg/api/handler.go | 2 +- pkg/api/handler_entrypoint.go | 11 ++- pkg/api/handler_entrypoint_test.go | 16 +++++ pkg/api/handler_http.go | 31 ++++++-- pkg/api/handler_http_test.go | 71 +++++++++++++++++++ pkg/api/handler_tcp.go | 31 ++++++-- pkg/api/handler_tcp_test.go | 64 +++++++++++++++++ pkg/api/handler_udp.go | 21 ++++-- pkg/api/handler_udp_test.go | 43 +++++++++++ .../testdata/entrypoint-foo-slash-bar.json | 5 ++ .../testdata/middleware-foo-slash-bar.json | 12 ++++ pkg/api/testdata/router-foo-slash-bar.json | 17 +++++ pkg/api/testdata/service-foo-slash-bar.json | 21 ++++++ .../testdata/tcpmiddleware-foo-slash-bar.json | 13 ++++ pkg/api/testdata/tcprouter-foo-slash-bar.json | 13 ++++ .../testdata/tcpservice-foo-slash-bar.json | 17 +++++ pkg/api/testdata/udprouter-foo-slash-bar.json | 12 ++++ .../testdata/udpservice-foo-slash-bar.json | 17 +++++ webui/src/_mixins/GetTableProps.js | 2 +- webui/src/_services/HttpService.js | 6 +- webui/src/_services/TcpService.js | 6 +- webui/src/_services/UdpService.js | 4 +- webui/src/pages/_commons/RouterDetail.vue | 2 +- 23 files changed, 408 insertions(+), 29 deletions(-) create mode 100644 pkg/api/testdata/entrypoint-foo-slash-bar.json create mode 100644 pkg/api/testdata/middleware-foo-slash-bar.json create mode 100644 pkg/api/testdata/router-foo-slash-bar.json create mode 100644 pkg/api/testdata/service-foo-slash-bar.json create mode 100644 pkg/api/testdata/tcpmiddleware-foo-slash-bar.json create mode 100644 pkg/api/testdata/tcprouter-foo-slash-bar.json create mode 100644 pkg/api/testdata/tcpservice-foo-slash-bar.json create mode 100644 pkg/api/testdata/udprouter-foo-slash-bar.json create mode 100644 pkg/api/testdata/udpservice-foo-slash-bar.json diff --git a/pkg/api/handler.go b/pkg/api/handler.go index f0623d4cc..df4db9f98 100644 --- a/pkg/api/handler.go +++ b/pkg/api/handler.go @@ -76,7 +76,7 @@ func New(staticConfig static.Configuration, runtimeConfig *runtime.Configuration // createRouter creates API routes and router. func (h Handler) createRouter() *mux.Router { - router := mux.NewRouter() + router := mux.NewRouter().UseEncodedPath() if h.staticConfig.API.Debug { DebugHandler{}.Append(router) diff --git a/pkg/api/handler_entrypoint.go b/pkg/api/handler_entrypoint.go index 38801e8c7..2a7aee691 100644 --- a/pkg/api/handler_entrypoint.go +++ b/pkg/api/handler_entrypoint.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "sort" "strconv" @@ -49,7 +50,13 @@ func (h Handler) getEntryPoints(rw http.ResponseWriter, request *http.Request) { } func (h Handler) getEntryPoint(rw http.ResponseWriter, request *http.Request) { - entryPointID := mux.Vars(request)["entryPointID"] + scapedEntryPointID := mux.Vars(request)["entryPointID"] + + entryPointID, err := url.PathUnescape(scapedEntryPointID) + if err != nil { + writeError(rw, fmt.Sprintf("unable to decode entryPointID %q: %s", scapedEntryPointID, err), http.StatusBadRequest) + return + } rw.Header().Set("Content-Type", "application/json") @@ -64,7 +71,7 @@ func (h Handler) getEntryPoint(rw http.ResponseWriter, request *http.Request) { Name: entryPointID, } - err := json.NewEncoder(rw).Encode(result) + err = json.NewEncoder(rw).Encode(result) if err != nil { log.FromContext(request.Context()).Error(err) writeError(rw, err.Error(), http.StatusInternalServerError) diff --git a/pkg/api/handler_entrypoint_test.go b/pkg/api/handler_entrypoint_test.go index 114724b40..5172e4689 100644 --- a/pkg/api/handler_entrypoint_test.go +++ b/pkg/api/handler_entrypoint_test.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "os" "strconv" "testing" @@ -169,6 +170,21 @@ func TestHandler_EntryPoints(t *testing.T) { jsonFile: "testdata/entrypoint-bar.json", }, }, + { + desc: "one entry point by id containing slash", + path: "/api/entrypoints/" + url.PathEscape("foo / bar"), + conf: static.Configuration{ + Global: &static.Global{}, + API: &static.API{}, + EntryPoints: map[string]*static.EntryPoint{ + "foo / bar": {Address: ":81"}, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/entrypoint-foo-slash-bar.json", + }, + }, { desc: "one entry point by id, that does not exist", path: "/api/entrypoints/foo", diff --git a/pkg/api/handler_http.go b/pkg/api/handler_http.go index b2643cdfa..7afbc7f43 100644 --- a/pkg/api/handler_http.go +++ b/pkg/api/handler_http.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "sort" "strconv" "strings" @@ -99,7 +100,13 @@ func (h Handler) getRouters(rw http.ResponseWriter, request *http.Request) { } func (h Handler) getRouter(rw http.ResponseWriter, request *http.Request) { - routerID := mux.Vars(request)["routerID"] + scapedRouterID := mux.Vars(request)["routerID"] + + routerID, err := url.PathUnescape(scapedRouterID) + if err != nil { + writeError(rw, fmt.Sprintf("unable to decode routerID %q: %s", scapedRouterID, err), http.StatusBadRequest) + return + } rw.Header().Set("Content-Type", "application/json") @@ -111,7 +118,7 @@ func (h Handler) getRouter(rw http.ResponseWriter, request *http.Request) { result := newRouterRepresentation(routerID, router) - err := json.NewEncoder(rw).Encode(result) + err = json.NewEncoder(rw).Encode(result) if err != nil { log.FromContext(request.Context()).Error(err) writeError(rw, err.Error(), http.StatusInternalServerError) @@ -151,7 +158,13 @@ func (h Handler) getServices(rw http.ResponseWriter, request *http.Request) { } func (h Handler) getService(rw http.ResponseWriter, request *http.Request) { - serviceID := mux.Vars(request)["serviceID"] + scapedServiceID := mux.Vars(request)["serviceID"] + + serviceID, err := url.PathUnescape(scapedServiceID) + if err != nil { + writeError(rw, fmt.Sprintf("unable to decode serviceID %q: %s", scapedServiceID, err), http.StatusBadRequest) + return + } rw.Header().Add("Content-Type", "application/json") @@ -163,7 +176,7 @@ func (h Handler) getService(rw http.ResponseWriter, request *http.Request) { result := newServiceRepresentation(serviceID, service) - err := json.NewEncoder(rw).Encode(result) + err = json.NewEncoder(rw).Encode(result) if err != nil { log.FromContext(request.Context()).Error(err) writeError(rw, err.Error(), http.StatusInternalServerError) @@ -203,7 +216,13 @@ func (h Handler) getMiddlewares(rw http.ResponseWriter, request *http.Request) { } func (h Handler) getMiddleware(rw http.ResponseWriter, request *http.Request) { - middlewareID := mux.Vars(request)["middlewareID"] + scapedMiddlewareID := mux.Vars(request)["middlewareID"] + + middlewareID, err := url.PathUnescape(scapedMiddlewareID) + if err != nil { + writeError(rw, fmt.Sprintf("unable to decode middlewareID %q: %s", scapedMiddlewareID, err), http.StatusBadRequest) + return + } rw.Header().Set("Content-Type", "application/json") @@ -215,7 +234,7 @@ func (h Handler) getMiddleware(rw http.ResponseWriter, request *http.Request) { result := newMiddlewareRepresentation(middlewareID, middleware) - err := json.NewEncoder(rw).Encode(result) + err = json.NewEncoder(rw).Encode(result) if err != nil { log.FromContext(request.Context()).Error(err) writeError(rw, err.Error(), http.StatusInternalServerError) diff --git a/pkg/api/handler_http_test.go b/pkg/api/handler_http_test.go index b186a2d15..d7f905c5d 100644 --- a/pkg/api/handler_http_test.go +++ b/pkg/api/handler_http_test.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "os" "strconv" "testing" @@ -223,6 +224,27 @@ func TestHandler_HTTP(t *testing.T) { jsonFile: "testdata/router-bar.json", }, }, + { + desc: "one router by id containing slash", + path: "/api/http/routers/" + url.PathEscape("foo / bar@myprovider"), + conf: runtime.Configuration{ + Routers: map[string]*runtime.RouterInfo{ + "foo / bar@myprovider": { + Router: &dynamic.Router{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + Middlewares: []string{"auth", "addPrefixTest@anotherprovider"}, + }, + Status: "enabled", + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/router-foo-slash-bar.json", + }, + }, { desc: "one router by id, implicitly using default TLS options", path: "/api/http/routers/baz@myprovider", @@ -583,6 +605,35 @@ func TestHandler_HTTP(t *testing.T) { jsonFile: "testdata/service-bar.json", }, }, + { + desc: "one service by id containing slash", + path: "/api/http/services/" + url.PathEscape("foo / bar@myprovider"), + conf: runtime.Configuration{ + Services: map[string]*runtime.ServiceInfo{ + "foo / bar@myprovider": func() *runtime.ServiceInfo { + si := &runtime.ServiceInfo{ + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://127.0.0.1", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + } + si.UpdateServerStatus("http://127.0.0.1", "UP") + return si + }(), + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/service-foo-slash-bar.json", + }, + }, { desc: "one service by id, that does not exist", path: "/api/http/services/nono@myprovider", @@ -819,6 +870,26 @@ func TestHandler_HTTP(t *testing.T) { jsonFile: "testdata/middleware-auth.json", }, }, + { + desc: "one middleware by id containing slash", + path: "/api/http/middlewares/" + url.PathEscape("foo / bar@myprovider"), + conf: runtime.Configuration{ + Middlewares: map[string]*runtime.MiddlewareInfo{ + "foo / bar@myprovider": { + Middleware: &dynamic.Middleware{ + AddPrefix: &dynamic.AddPrefix{ + Prefix: "/titi", + }, + }, + UsedBy: []string{"test@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/middleware-foo-slash-bar.json", + }, + }, { desc: "one middleware by id, that does not exist", path: "/api/http/middlewares/foo@myprovider", diff --git a/pkg/api/handler_tcp.go b/pkg/api/handler_tcp.go index 390e1179c..b738bf953 100644 --- a/pkg/api/handler_tcp.go +++ b/pkg/api/handler_tcp.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "sort" "strconv" "strings" @@ -92,7 +93,13 @@ func (h Handler) getTCPRouters(rw http.ResponseWriter, request *http.Request) { } func (h Handler) getTCPRouter(rw http.ResponseWriter, request *http.Request) { - routerID := mux.Vars(request)["routerID"] + scapedRouterID := mux.Vars(request)["routerID"] + + routerID, err := url.PathUnescape(scapedRouterID) + if err != nil { + writeError(rw, fmt.Sprintf("unable to decode routerID %q: %s", scapedRouterID, err), http.StatusBadRequest) + return + } rw.Header().Set("Content-Type", "application/json") @@ -104,7 +111,7 @@ func (h Handler) getTCPRouter(rw http.ResponseWriter, request *http.Request) { result := newTCPRouterRepresentation(routerID, router) - err := json.NewEncoder(rw).Encode(result) + err = json.NewEncoder(rw).Encode(result) if err != nil { log.FromContext(request.Context()).Error(err) writeError(rw, err.Error(), http.StatusInternalServerError) @@ -144,7 +151,13 @@ func (h Handler) getTCPServices(rw http.ResponseWriter, request *http.Request) { } func (h Handler) getTCPService(rw http.ResponseWriter, request *http.Request) { - serviceID := mux.Vars(request)["serviceID"] + scapedServiceID := mux.Vars(request)["serviceID"] + + serviceID, err := url.PathUnescape(scapedServiceID) + if err != nil { + writeError(rw, fmt.Sprintf("unable to decode serviceID %q: %s", scapedServiceID, err), http.StatusBadRequest) + return + } rw.Header().Set("Content-Type", "application/json") @@ -156,7 +169,7 @@ func (h Handler) getTCPService(rw http.ResponseWriter, request *http.Request) { result := newTCPServiceRepresentation(serviceID, service) - err := json.NewEncoder(rw).Encode(result) + err = json.NewEncoder(rw).Encode(result) if err != nil { log.FromContext(request.Context()).Error(err) writeError(rw, err.Error(), http.StatusInternalServerError) @@ -196,7 +209,13 @@ func (h Handler) getTCPMiddlewares(rw http.ResponseWriter, request *http.Request } func (h Handler) getTCPMiddleware(rw http.ResponseWriter, request *http.Request) { - middlewareID := mux.Vars(request)["middlewareID"] + scapedMiddlewareID := mux.Vars(request)["middlewareID"] + + middlewareID, err := url.PathUnescape(scapedMiddlewareID) + if err != nil { + writeError(rw, fmt.Sprintf("unable to decode middlewareID %q: %s", scapedMiddlewareID, err), http.StatusBadRequest) + return + } rw.Header().Set("Content-Type", "application/json") @@ -208,7 +227,7 @@ func (h Handler) getTCPMiddleware(rw http.ResponseWriter, request *http.Request) result := newTCPMiddlewareRepresentation(middlewareID, middleware) - err := json.NewEncoder(rw).Encode(result) + err = json.NewEncoder(rw).Encode(result) if err != nil { log.FromContext(request.Context()).Error(err) writeError(rw, err.Error(), http.StatusInternalServerError) diff --git a/pkg/api/handler_tcp_test.go b/pkg/api/handler_tcp_test.go index d15ec1cdf..85e5fdc19 100644 --- a/pkg/api/handler_tcp_test.go +++ b/pkg/api/handler_tcp_test.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "os" "testing" @@ -212,6 +213,25 @@ func TestHandler_TCP(t *testing.T) { jsonFile: "testdata/tcprouter-bar.json", }, }, + { + desc: "one TCP router by id containing slash", + path: "/api/tcp/routers/" + url.PathEscape("foo / bar@myprovider"), + conf: runtime.Configuration{ + TCPRouters: map[string]*runtime.TCPRouterInfo{ + "foo / bar@myprovider": { + TCPRouter: &dynamic.TCPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + Rule: "Host(`foo.bar`)", + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/tcprouter-foo-slash-bar.json", + }, + }, { desc: "one TCP router by id, that does not exist", path: "/api/tcp/routers/foo@myprovider", @@ -476,6 +496,30 @@ func TestHandler_TCP(t *testing.T) { jsonFile: "testdata/tcpservice-bar.json", }, }, + { + desc: "one tcp service by id containing slash", + path: "/api/tcp/services/" + url.PathEscape("foo / bar@myprovider"), + conf: runtime.Configuration{ + TCPServices: map[string]*runtime.TCPServiceInfo{ + "foo / bar@myprovider": { + TCPService: &dynamic.TCPService{ + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "127.0.0.1:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/tcpservice-foo-slash-bar.json", + }, + }, { desc: "one tcp service by id, that does not exist", path: "/api/tcp/services/nono@myprovider", @@ -697,6 +741,26 @@ func TestHandler_TCP(t *testing.T) { jsonFile: "testdata/tcpmiddleware-ipwhitelist.json", }, }, + { + desc: "one middleware by id containing slash", + path: "/api/tcp/middlewares/" + url.PathEscape("foo / bar@myprovider"), + conf: runtime.Configuration{ + TCPMiddlewares: map[string]*runtime.TCPMiddlewareInfo{ + "foo / bar@myprovider": { + TCPMiddleware: &dynamic.TCPMiddleware{ + IPWhiteList: &dynamic.TCPIPWhiteList{ + SourceRange: []string{"127.0.0.1/32"}, + }, + }, + UsedBy: []string{"bar@myprovider", "test@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/tcpmiddleware-foo-slash-bar.json", + }, + }, { desc: "one middleware by id, that does not exist", path: "/api/tcp/middlewares/foo@myprovider", diff --git a/pkg/api/handler_udp.go b/pkg/api/handler_udp.go index 4cfe465d9..c3fe44a8a 100644 --- a/pkg/api/handler_udp.go +++ b/pkg/api/handler_udp.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "sort" "strconv" "strings" @@ -76,7 +77,13 @@ func (h Handler) getUDPRouters(rw http.ResponseWriter, request *http.Request) { } func (h Handler) getUDPRouter(rw http.ResponseWriter, request *http.Request) { - routerID := mux.Vars(request)["routerID"] + scapedRouterID := mux.Vars(request)["routerID"] + + routerID, err := url.PathUnescape(scapedRouterID) + if err != nil { + writeError(rw, fmt.Sprintf("unable to decode routerID %q: %s", scapedRouterID, err), http.StatusBadRequest) + return + } rw.Header().Set("Content-Type", "application/json") @@ -88,7 +95,7 @@ func (h Handler) getUDPRouter(rw http.ResponseWriter, request *http.Request) { result := newUDPRouterRepresentation(routerID, router) - err := json.NewEncoder(rw).Encode(result) + err = json.NewEncoder(rw).Encode(result) if err != nil { log.FromContext(request.Context()).Error(err) writeError(rw, err.Error(), http.StatusInternalServerError) @@ -128,7 +135,13 @@ func (h Handler) getUDPServices(rw http.ResponseWriter, request *http.Request) { } func (h Handler) getUDPService(rw http.ResponseWriter, request *http.Request) { - serviceID := mux.Vars(request)["serviceID"] + scapedServiceID := mux.Vars(request)["serviceID"] + + serviceID, err := url.PathUnescape(scapedServiceID) + if err != nil { + writeError(rw, fmt.Sprintf("unable to decode serviceID %q: %s", scapedServiceID, err), http.StatusBadRequest) + return + } rw.Header().Set("Content-Type", "application/json") @@ -140,7 +153,7 @@ func (h Handler) getUDPService(rw http.ResponseWriter, request *http.Request) { result := newUDPServiceRepresentation(serviceID, service) - err := json.NewEncoder(rw).Encode(result) + err = json.NewEncoder(rw).Encode(result) if err != nil { log.FromContext(request.Context()).Error(err) writeError(rw, err.Error(), http.StatusInternalServerError) diff --git a/pkg/api/handler_udp_test.go b/pkg/api/handler_udp_test.go index b091f8e1d..56c9b3a6f 100644 --- a/pkg/api/handler_udp_test.go +++ b/pkg/api/handler_udp_test.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "os" "testing" @@ -190,6 +191,24 @@ func TestHandler_UDP(t *testing.T) { jsonFile: "testdata/udprouter-bar.json", }, }, + { + desc: "one UDP router by id containing slash", + path: "/api/udp/routers/" + url.PathEscape("foo / bar@myprovider"), + conf: runtime.Configuration{ + UDPRouters: map[string]*runtime.UDPRouterInfo{ + "foo / bar@myprovider": { + UDPRouter: &dynamic.UDPRouter{ + EntryPoints: []string{"web"}, + Service: "foo-service@myprovider", + }, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/udprouter-foo-slash-bar.json", + }, + }, { desc: "one UDP router by id, that does not exist", path: "/api/udp/routers/foo@myprovider", @@ -453,6 +472,30 @@ func TestHandler_UDP(t *testing.T) { jsonFile: "testdata/udpservice-bar.json", }, }, + { + desc: "one udp service by id containing slash", + path: "/api/udp/services/" + url.PathEscape("foo / bar@myprovider"), + conf: runtime.Configuration{ + UDPServices: map[string]*runtime.UDPServiceInfo{ + "foo / bar@myprovider": { + UDPService: &dynamic.UDPService{ + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "127.0.0.1:2345", + }, + }, + }, + }, + UsedBy: []string{"foo@myprovider", "test@myprovider"}, + }, + }, + }, + expected: expected{ + statusCode: http.StatusOK, + jsonFile: "testdata/udpservice-foo-slash-bar.json", + }, + }, { desc: "one udp service by id, that does not exist", path: "/api/udp/services/nono@myprovider", diff --git a/pkg/api/testdata/entrypoint-foo-slash-bar.json b/pkg/api/testdata/entrypoint-foo-slash-bar.json new file mode 100644 index 000000000..5f0bcbafc --- /dev/null +++ b/pkg/api/testdata/entrypoint-foo-slash-bar.json @@ -0,0 +1,5 @@ +{ + "address": ":81", + "http": {}, + "name": "foo / bar" +} diff --git a/pkg/api/testdata/middleware-foo-slash-bar.json b/pkg/api/testdata/middleware-foo-slash-bar.json new file mode 100644 index 000000000..5e68c7e3d --- /dev/null +++ b/pkg/api/testdata/middleware-foo-slash-bar.json @@ -0,0 +1,12 @@ +{ + "addPrefix": { + "prefix": "/titi" + }, + "name": "foo / bar@myprovider", + "provider": "myprovider", + "status": "enabled", + "type": "addprefix", + "usedBy": [ + "test@myprovider" + ] +} diff --git a/pkg/api/testdata/router-foo-slash-bar.json b/pkg/api/testdata/router-foo-slash-bar.json new file mode 100644 index 000000000..f9e30c240 --- /dev/null +++ b/pkg/api/testdata/router-foo-slash-bar.json @@ -0,0 +1,17 @@ +{ + "entryPoints": [ + "web" + ], + "middlewares": [ + "auth", + "addPrefixTest@anotherprovider" + ], + "name": "foo / bar@myprovider", + "provider": "myprovider", + "rule": "Host(`foo.bar`)", + "service": "foo-service@myprovider", + "status": "enabled", + "using": [ + "web" + ] +} diff --git a/pkg/api/testdata/service-foo-slash-bar.json b/pkg/api/testdata/service-foo-slash-bar.json new file mode 100644 index 000000000..58cde8530 --- /dev/null +++ b/pkg/api/testdata/service-foo-slash-bar.json @@ -0,0 +1,21 @@ +{ + "loadBalancer": { + "passHostHeader": true, + "servers": [ + { + "url": "http://127.0.0.1" + } + ] + }, + "name": "foo / bar@myprovider", + "provider": "myprovider", + "serverStatus": { + "http://127.0.0.1": "UP" + }, + "status": "enabled", + "type": "loadbalancer", + "usedBy": [ + "foo@myprovider", + "test@myprovider" + ] +} diff --git a/pkg/api/testdata/tcpmiddleware-foo-slash-bar.json b/pkg/api/testdata/tcpmiddleware-foo-slash-bar.json new file mode 100644 index 000000000..985e0380a --- /dev/null +++ b/pkg/api/testdata/tcpmiddleware-foo-slash-bar.json @@ -0,0 +1,13 @@ +{ + "ipWhiteList": { + "sourceRange": ["127.0.0.1/32"] + }, + "name": "foo / bar@myprovider", + "provider": "myprovider", + "status": "enabled", + "type": "ipwhitelist", + "usedBy": [ + "bar@myprovider", + "test@myprovider" + ] +} diff --git a/pkg/api/testdata/tcprouter-foo-slash-bar.json b/pkg/api/testdata/tcprouter-foo-slash-bar.json new file mode 100644 index 000000000..4656ea9f9 --- /dev/null +++ b/pkg/api/testdata/tcprouter-foo-slash-bar.json @@ -0,0 +1,13 @@ +{ + "entryPoints": [ + "web" + ], + "name": "foo / bar@myprovider", + "provider": "myprovider", + "rule": "Host(`foo.bar`)", + "service": "foo-service@myprovider", + "status": "enabled", + "using": [ + "web" + ] +} diff --git a/pkg/api/testdata/tcpservice-foo-slash-bar.json b/pkg/api/testdata/tcpservice-foo-slash-bar.json new file mode 100644 index 000000000..b250966d5 --- /dev/null +++ b/pkg/api/testdata/tcpservice-foo-slash-bar.json @@ -0,0 +1,17 @@ +{ + "loadBalancer": { + "servers": [ + { + "address": "127.0.0.1:2345" + } + ] + }, + "name": "foo / bar@myprovider", + "provider": "myprovider", + "status": "enabled", + "type": "loadbalancer", + "usedBy": [ + "foo@myprovider", + "test@myprovider" + ] +} diff --git a/pkg/api/testdata/udprouter-foo-slash-bar.json b/pkg/api/testdata/udprouter-foo-slash-bar.json new file mode 100644 index 000000000..276c1a907 --- /dev/null +++ b/pkg/api/testdata/udprouter-foo-slash-bar.json @@ -0,0 +1,12 @@ +{ + "entryPoints": [ + "web" + ], + "name": "foo / bar@myprovider", + "provider": "myprovider", + "service": "foo-service@myprovider", + "status": "enabled", + "using": [ + "web" + ] +} diff --git a/pkg/api/testdata/udpservice-foo-slash-bar.json b/pkg/api/testdata/udpservice-foo-slash-bar.json new file mode 100644 index 000000000..b250966d5 --- /dev/null +++ b/pkg/api/testdata/udpservice-foo-slash-bar.json @@ -0,0 +1,17 @@ +{ + "loadBalancer": { + "servers": [ + { + "address": "127.0.0.1:2345" + } + ] + }, + "name": "foo / bar@myprovider", + "provider": "myprovider", + "status": "enabled", + "type": "loadbalancer", + "usedBy": [ + "foo@myprovider", + "test@myprovider" + ] +} diff --git a/webui/src/_mixins/GetTableProps.js b/webui/src/_mixins/GetTableProps.js index 014a397bb..020b54657 100644 --- a/webui/src/_mixins/GetTableProps.js +++ b/webui/src/_mixins/GetTableProps.js @@ -138,7 +138,7 @@ const GetTablePropsMixin = { return { onRowClick: row => this.$router.push({ - path: `/${type.replace('-', '/', 'gi')}/${row.name}` + path: `/${type.replace('-', '/', 'gi')}/${encodeURIComponent(row.name)}` }), columns: allColumns.filter(c => get(propsByType, `${type}.columns`, []).includes(c.name) diff --git a/webui/src/_services/HttpService.js b/webui/src/_services/HttpService.js index 0e206b24d..c239f20ed 100644 --- a/webui/src/_services/HttpService.js +++ b/webui/src/_services/HttpService.js @@ -14,7 +14,7 @@ function getAllRouters (params) { } function getRouterByName (name) { - return APP.api.get(`${apiBase}/routers/${name}`) + return APP.api.get(`${apiBase}/routers/${encodeURIComponent(name)}`) .then(body => { console.log('Success -> HttpService -> getRouterByName', body.data) return body.data @@ -32,7 +32,7 @@ function getAllServices (params) { } function getServiceByName (name) { - return APP.api.get(`${apiBase}/services/${name}`) + return APP.api.get(`${apiBase}/services/${encodeURIComponent(name)}`) .then(body => { console.log('Success -> HttpService -> getServiceByName', body.data) return body.data @@ -50,7 +50,7 @@ function getAllMiddlewares (params) { } function getMiddlewareByName (name) { - return APP.api.get(`${apiBase}/middlewares/${name}`) + return APP.api.get(`${apiBase}/middlewares/${encodeURIComponent(name)}`) .then(body => { console.log('Success -> HttpService -> getMiddlewareByName', body.data) return body.data diff --git a/webui/src/_services/TcpService.js b/webui/src/_services/TcpService.js index e5645e9b1..9132c8c19 100644 --- a/webui/src/_services/TcpService.js +++ b/webui/src/_services/TcpService.js @@ -14,7 +14,7 @@ function getAllRouters (params) { } function getRouterByName (name) { - return APP.api.get(`${apiBase}/routers/${name}`) + return APP.api.get(`${apiBase}/routers/${encodeURIComponent(name)}`) .then(body => { console.log('Success -> TcpService -> getRouterByName', body.data) return body.data @@ -32,7 +32,7 @@ function getAllServices (params) { } function getServiceByName (name) { - return APP.api.get(`${apiBase}/services/${name}`) + return APP.api.get(`${apiBase}/services/${encodeURIComponent(name)}`) .then(body => { console.log('Success -> TcpService -> getServiceByName', body.data) return body.data @@ -50,7 +50,7 @@ function getAllMiddlewares (params) { } function getMiddlewareByName (name) { - return APP.api.get(`${apiBase}/middlewares/${name}`) + return APP.api.get(`${apiBase}/middlewares/${encodeURIComponent(name)}`) .then(body => { console.log('Success -> TcpService -> getMiddlewareByName', body.data) return body.data diff --git a/webui/src/_services/UdpService.js b/webui/src/_services/UdpService.js index fd641d357..d0f8551da 100644 --- a/webui/src/_services/UdpService.js +++ b/webui/src/_services/UdpService.js @@ -14,7 +14,7 @@ function getAllRouters (params) { } function getRouterByName (name) { - return APP.api.get(`${apiBase}/routers/${name}`) + return APP.api.get(`${apiBase}/routers/${encodeURIComponent(name)}`) .then(body => { console.log('Success -> UdpService -> getRouterByName', body.data) return body.data @@ -32,7 +32,7 @@ function getAllServices (params) { } function getServiceByName (name) { - return APP.api.get(`${apiBase}/services/${name}`) + return APP.api.get(`${apiBase}/services/${encodeURIComponent(name)}`) .then(body => { console.log('Success -> UdpService -> getServiceByName', body.data) return body.data diff --git a/webui/src/pages/_commons/RouterDetail.vue b/webui/src/pages/_commons/RouterDetail.vue index 1fec28292..c93a5ccfe 100644 --- a/webui/src/pages/_commons/RouterDetail.vue +++ b/webui/src/pages/_commons/RouterDetail.vue @@ -279,7 +279,7 @@ export default { return data.service } - return `${data.service}@${data.provider}` + return `${encodeURIComponent(data.service)}@${data.provider}` } }, created () { From f4f3dbe1f5f9c8602df5a23ac57d19df5e68b148 Mon Sep 17 00:00:00 2001 From: Matthieu W Date: Thu, 25 Jan 2024 15:12:05 +0100 Subject: [PATCH 17/36] Update version comment in quick-start.md --- docs/content/getting-started/quick-start.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/getting-started/quick-start.md b/docs/content/getting-started/quick-start.md index a33eb4b22..122ff530d 100644 --- a/docs/content/getting-started/quick-start.md +++ b/docs/content/getting-started/quick-start.md @@ -19,7 +19,7 @@ version: '3' services: reverse-proxy: - # The official v2 Traefik docker image + # The official v3 Traefik docker image image: traefik:v3.0 # Enables the web UI and tells Traefik to listen to docker command: --api.insecure=true --providers.docker From 3174c69c66fab7f6906192eeba1bb2f4ccc96c96 Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Fri, 26 Jan 2024 01:44:05 +0100 Subject: [PATCH 18/36] Adds weight on ServersLoadBalancer --- .../dynamic-configuration/docker-labels.yml | 1 + .../reference/dynamic-configuration/file.toml | 2 + .../reference/dynamic-configuration/file.yaml | 2 + .../reference/dynamic-configuration/kv-ref.md | 2 + docs/content/routing/services/index.md | 30 +++++++++++ integration/docker_test.go | 51 +++++++++++++++++++ integration/fixtures/wrr_server.toml | 35 +++++++++++++ integration/resources/compose/docker.yml | 11 ++++ integration/simple_test.go | 43 ++++++++++++++++ pkg/config/dynamic/http_config.go | 1 + pkg/config/dynamic/zz_generated.deepcopy.go | 9 +++- pkg/server/service/service.go | 2 +- 12 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 integration/fixtures/wrr_server.toml diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index df6db2ff0..7044d13d0 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -178,6 +178,7 @@ - "traefik.http.services.service02.loadbalancer.sticky.cookie.secure=true" - "traefik.http.services.service02.loadbalancer.server.port=foobar" - "traefik.http.services.service02.loadbalancer.server.scheme=foobar" +- "traefik.http.services.service02.loadbalancer.server.weight=42" - "traefik.tcp.middlewares.tcpmiddleware01.ipallowlist.sourcerange=foobar, foobar" - "traefik.tcp.middlewares.tcpmiddleware02.ipwhitelist.sourcerange=foobar, foobar" - "traefik.tcp.middlewares.tcpmiddleware03.inflightconn.amount=42" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 541affb9c..7d771c6a2 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -58,9 +58,11 @@ [[http.services.Service02.loadBalancer.servers]] url = "foobar" + weight = 42 [[http.services.Service02.loadBalancer.servers]] url = "foobar" + weight = 42 [http.services.Service02.loadBalancer.healthCheck] scheme = "foobar" mode = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 4e0019e42..24df9aada 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -65,7 +65,9 @@ http: maxAge: 42 servers: - url: foobar + weight: 42 - url: foobar + weight: 42 healthCheck: scheme: foobar mode: foobar diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 6d80b13b8..cc0334567 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -240,7 +240,9 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/services/Service02/loadBalancer/passHostHeader` | `true` | | `traefik/http/services/Service02/loadBalancer/responseForwarding/flushInterval` | `42s` | | `traefik/http/services/Service02/loadBalancer/servers/0/url` | `foobar` | +| `traefik/http/services/Service02/loadBalancer/servers/0/weight` | `42` | | `traefik/http/services/Service02/loadBalancer/servers/1/url` | `foobar` | +| `traefik/http/services/Service02/loadBalancer/servers/1/weight` | `42` | | `traefik/http/services/Service02/loadBalancer/serversTransport` | `foobar` | | `traefik/http/services/Service02/loadBalancer/sticky/cookie/httpOnly` | `true` | | `traefik/http/services/Service02/loadBalancer/sticky/cookie/maxAge` | `42` | diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index 7658195e1..7ac5ce78c 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -143,6 +143,36 @@ The `url` option point to a specific instance. url = "http://private-ip-server-1/" ``` +The `weight` option allows for weighted load balancing on the servers. + +??? example "A Service with Two Servers with Weight -- Using the [File Provider](../../providers/file.md)" + + ```yaml tab="YAML" + ## Dynamic configuration + http: + services: + my-service: + loadBalancer: + servers: + - url: "http://private-ip-server-1/" + weight: 2 + - url: "http://private-ip-server-2/" + weight: 1 + + ``` + + ```toml tab="TOML" + ## Dynamic configuration + [http.services] + [http.services.my-service.loadBalancer] + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-1/" + weight = 2 + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-2/" + weight = 1 + ``` + #### Load-balancing For now, only round robin load balancing is supported: diff --git a/integration/docker_test.go b/integration/docker_test.go index 1a767ff6f..daa6fa06b 100644 --- a/integration/docker_test.go +++ b/integration/docker_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "io" "net/http" + "strings" "testing" "time" @@ -55,6 +56,56 @@ func (s *DockerSuite) TestSimpleConfiguration() { require.NoError(s.T(), err) } +func (s *DockerSuite) TestWRRServer() { + tempObjects := struct { + DockerHost string + DefaultRule string + }{ + DockerHost: s.getDockerHost(), + DefaultRule: "Host(`{{ normalize .Name }}.docker.localhost`)", + } + + file := s.adaptFile("fixtures/docker/simple.toml", tempObjects) + + s.composeUp() + + s.traefikCmd(withConfigFile(file)) + + whoami1IP := s.getComposeServiceIP("wrr-server") + whoami2IP := s.getComposeServiceIP("wrr-server2") + + // Expected a 404 as we did not configure anything + err := try.GetRequest("http://127.0.0.1:8000/", 500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound)) + require.NoError(s.T(), err) + + err = try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains("wrr-server")) + require.NoError(s.T(), err) + + repartition := map[string]int{} + for i := 0; i < 4; i++ { + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil) + req.Host = "my.wrr.host" + require.NoError(s.T(), err) + + response, err := http.DefaultClient.Do(req) + require.NoError(s.T(), err) + assert.Equal(s.T(), http.StatusOK, response.StatusCode) + + body, err := io.ReadAll(response.Body) + require.NoError(s.T(), err) + + if strings.Contains(string(body), whoami1IP) { + repartition[whoami1IP]++ + } + if strings.Contains(string(body), whoami2IP) { + repartition[whoami2IP]++ + } + } + + assert.Equal(s.T(), 3, repartition[whoami1IP]) + assert.Equal(s.T(), 1, repartition[whoami2IP]) +} + func (s *DockerSuite) TestDefaultDockerContainers() { tempObjects := struct { DockerHost string diff --git a/integration/fixtures/wrr_server.toml b/integration/fixtures/wrr_server.toml new file mode 100644 index 000000000..d1735986d --- /dev/null +++ b/integration/fixtures/wrr_server.toml @@ -0,0 +1,35 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[api] + insecure = true + +[log] + level = "DEBUG" + noColor = true + +[entryPoints] + + [entryPoints.web] + address = ":8000" + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.router] + service = "service1" + rule = "Path(`/whoami`)" + +[http.services] + + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "{{ .Server1 }}" + weight = 3 + [[http.services.service1.loadBalancer.servers]] + url = "{{ .Server2 }}" + diff --git a/integration/resources/compose/docker.yml b/integration/resources/compose/docker.yml index e594ec87c..b16571a4b 100644 --- a/integration/resources/compose/docker.yml +++ b/integration/resources/compose/docker.yml @@ -36,3 +36,14 @@ services: labels: traefik.http.Routers.Super.Rule: Host(`my.super.host`) traefik.http.Services.powpow.LoadBalancer.server.Port: 2375 + + wrr-server: + image: traefik/whoami + labels: + traefik.http.Routers.wrr-server.Rule: Host(`my.wrr.host`) + traefik.http.Services.wrr-server.LoadBalancer.server.Weight: 4 + wrr-server2: + image: traefik/whoami + labels: + traefik.http.Routers.wrr-server.Rule: Host(`my.wrr.host`) + traefik.http.Services.wrr-server.LoadBalancer.server.Weight: 1 diff --git a/integration/simple_test.go b/integration/simple_test.go index 94b4ec45e..d2c3be077 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -809,6 +809,49 @@ func (s *SimpleSuite) TestUDPServiceConfigErrors() { require.NoError(s.T(), err) } +func (s *SimpleSuite) TestWRRServer() { + s.createComposeProject("base") + + s.composeUp() + defer s.composeDown() + + whoami1IP := s.getComposeServiceIP("whoami1") + whoami2IP := s.getComposeServiceIP("whoami2") + + file := s.adaptFile("fixtures/wrr_server.toml", struct { + Server1 string + Server2 string + }{Server1: "http://" + whoami1IP, Server2: "http://" + whoami2IP}) + + s.traefikCmd(withConfigFile(file)) + + err := try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains("service1")) + require.NoError(s.T(), err) + + repartition := map[string]int{} + for i := 0; i < 4; i++ { + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil) + require.NoError(s.T(), err) + + response, err := http.DefaultClient.Do(req) + require.NoError(s.T(), err) + assert.Equal(s.T(), http.StatusOK, response.StatusCode) + + body, err := io.ReadAll(response.Body) + require.NoError(s.T(), err) + + if strings.Contains(string(body), whoami1IP) { + repartition[whoami1IP]++ + } + if strings.Contains(string(body), whoami2IP) { + repartition[whoami2IP]++ + } + } + + assert.Equal(s.T(), 3, repartition[whoami1IP]) + assert.Equal(s.T(), 1, repartition[whoami2IP]) +} + func (s *SimpleSuite) TestWRR() { s.createComposeProject("base") diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index 0783d5592..541f61b79 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -227,6 +227,7 @@ func (r *ResponseForwarding) SetDefaults() { // Server holds the server configuration. type Server struct { URL string `json:"url,omitempty" toml:"url,omitempty" yaml:"url,omitempty" label:"-"` + Weight *int `json:"weight,omitempty" toml:"weight,omitempty" yaml:"weight,omitempty" label:"weight"` Scheme string `json:"-" toml:"-" yaml:"-" file:"-"` Port string `json:"-" toml:"-" yaml:"-" file:"-"` } diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 3969d144d..85b513b2a 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -1128,6 +1128,11 @@ func (in *RouterTLSConfig) DeepCopy() *RouterTLSConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Server) DeepCopyInto(out *Server) { *out = *in + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int) + **out = **in + } return } @@ -1180,7 +1185,9 @@ func (in *ServersLoadBalancer) DeepCopyInto(out *ServersLoadBalancer) { if in.Servers != nil { in, out := &in.Servers, &out.Servers *out = make([]Server, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.HealthCheck != nil { in, out := &in.HealthCheck, &out.HealthCheck diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 55fcb9fa2..2d71791a8 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -319,7 +319,7 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName proxy = tracingMiddle.NewService(ctx, serviceName, proxy) - lb.Add(proxyName, proxy, nil) + lb.Add(proxyName, proxy, server.Weight) // servers are considered UP by default. info.UpdateServerStatus(target.String(), runtime.StatusUp) From d37ea3e88256b130126efd16d9c59465c95e08fe Mon Sep 17 00:00:00 2001 From: Fahrzin Hemmati Date: Mon, 29 Jan 2024 01:58:05 -0800 Subject: [PATCH 19/36] Add ResponseCode to CircuitBreaker --- docs/content/middlewares/http/circuitbreaker.md | 7 +++++++ .../reference/dynamic-configuration/docker-labels.yml | 1 + docs/content/reference/dynamic-configuration/file.toml | 1 + docs/content/reference/dynamic-configuration/file.yaml | 1 + docs/content/reference/dynamic-configuration/kv-ref.md | 1 + pkg/config/dynamic/middlewares.go | 4 ++++ pkg/config/label/label_test.go | 4 ++++ pkg/middlewares/circuitbreaker/circuit_breaker.go | 6 ++++-- pkg/provider/kv/kv_test.go | 2 ++ 9 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/content/middlewares/http/circuitbreaker.md b/docs/content/middlewares/http/circuitbreaker.md index be7b1422d..c1ead36b6 100644 --- a/docs/content/middlewares/http/circuitbreaker.md +++ b/docs/content/middlewares/http/circuitbreaker.md @@ -85,6 +85,7 @@ At specified intervals (`checkPeriod`), the circuit breaker evaluates `expressio ### Open While open, the fallback mechanism takes over the normal service calls for a duration of `FallbackDuration`. +The fallback mechanism returns a `HTTP 503` (or `ResponseCode`) to the client. After this duration, it enters the recovering state. ### Recovering @@ -179,3 +180,9 @@ The duration for which the circuit breaker will wait before trying to recover (f _Optional, Default="10s"_ The duration for which the circuit breaker will try to recover (as soon as it is in recovering state). + +### `ResponseCode` + +_Optional, Default="503"_ + +The status code that the circuit breaker will return while it is in the open state. diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 7044d13d0..3d1eb62f5 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -16,6 +16,7 @@ - "traefik.http.middlewares.middleware05.circuitbreaker.expression=foobar" - "traefik.http.middlewares.middleware05.circuitbreaker.fallbackduration=42s" - "traefik.http.middlewares.middleware05.circuitbreaker.recoveryduration=42s" +- "traefik.http.middlewares.middleware05.circuitbreaker.responsecode=42" - "traefik.http.middlewares.middleware06.compress=true" - "traefik.http.middlewares.middleware06.compress.excludedcontenttypes=foobar, foobar" - "traefik.http.middlewares.middleware06.compress.includedcontenttypes=foobar, foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 7d771c6a2..b4565206b 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -137,6 +137,7 @@ checkPeriod = "42s" fallbackDuration = "42s" recoveryDuration = "42s" + responseCode = 42 [http.middlewares.Middleware06] [http.middlewares.Middleware06.compress] excludedContentTypes = ["foobar", "foobar"] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 24df9aada..430e8cab1 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -142,6 +142,7 @@ http: checkPeriod: 42s fallbackDuration: 42s recoveryDuration: 42s + responseCode: 42 Middleware06: compress: excludedContentTypes: diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index cc0334567..6709fe5a9 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -20,6 +20,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/middlewares/Middleware05/circuitBreaker/expression` | `foobar` | | `traefik/http/middlewares/Middleware05/circuitBreaker/fallbackDuration` | `42s` | | `traefik/http/middlewares/Middleware05/circuitBreaker/recoveryDuration` | `42s` | +| `traefik/http/middlewares/Middleware05/circuitBreaker/responseCode` | `42` | | `traefik/http/middlewares/Middleware06/compress/excludedContentTypes/0` | `foobar` | | `traefik/http/middlewares/Middleware06/compress/excludedContentTypes/1` | `foobar` | | `traefik/http/middlewares/Middleware06/compress/includedContentTypes/0` | `foobar` | diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index a2c1794eb..fd1c20d33 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -1,6 +1,7 @@ package dynamic import ( + "net/http" "time" ptypes "github.com/traefik/paerser/types" @@ -141,6 +142,8 @@ type CircuitBreaker struct { FallbackDuration ptypes.Duration `json:"fallbackDuration,omitempty" toml:"fallbackDuration,omitempty" yaml:"fallbackDuration,omitempty" export:"true"` // RecoveryDuration is the duration for which the circuit breaker will try to recover (as soon as it is in recovering state). RecoveryDuration ptypes.Duration `json:"recoveryDuration,omitempty" toml:"recoveryDuration,omitempty" yaml:"recoveryDuration,omitempty" export:"true"` + // ResponseCode is the status code that the circuit breaker will return while it is in the open state. + ResponseCode int `json:"responseCode,omitempty" toml:"responseCode,omitempty" yaml:"responseCode,omitempty" export:"true"` } // SetDefaults sets the default values on a RateLimit. @@ -148,6 +151,7 @@ func (c *CircuitBreaker) SetDefaults() { c.CheckPeriod = ptypes.Duration(100 * time.Millisecond) c.FallbackDuration = ptypes.Duration(10 * time.Second) c.RecoveryDuration = ptypes.Duration(10 * time.Second) + c.ResponseCode = http.StatusServiceUnavailable } // +k8s:deepcopy-gen=true diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 28606cdbc..79f3df14b 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -30,6 +30,7 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware4.circuitbreaker.checkperiod": "1s", "traefik.HTTP.Middlewares.Middleware4.circuitbreaker.fallbackduration": "1s", "traefik.HTTP.Middlewares.Middleware4.circuitbreaker.recoveryduration": "1s", + "traefik.HTTP.Middlewares.Middleware4.circuitbreaker.responsecode": "403", "traefik.http.middlewares.Middleware5.digestauth.headerfield": "foobar", "traefik.http.middlewares.Middleware5.digestauth.realm": "foobar", "traefik.http.middlewares.Middleware5.digestauth.removeheader": "true", @@ -496,6 +497,7 @@ func TestDecodeConfiguration(t *testing.T) { CheckPeriod: ptypes.Duration(time.Second), FallbackDuration: ptypes.Duration(time.Second), RecoveryDuration: ptypes.Duration(time.Second), + ResponseCode: 403, }, }, "Middleware5": { @@ -996,6 +998,7 @@ func TestEncodeConfiguration(t *testing.T) { CheckPeriod: ptypes.Duration(time.Second), FallbackDuration: ptypes.Duration(time.Second), RecoveryDuration: ptypes.Duration(time.Second), + ResponseCode: 404, }, }, "Middleware5": { @@ -1206,6 +1209,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware4.CircuitBreaker.CheckPeriod": "1000000000", "traefik.HTTP.Middlewares.Middleware4.CircuitBreaker.FallbackDuration": "1000000000", "traefik.HTTP.Middlewares.Middleware4.CircuitBreaker.RecoveryDuration": "1000000000", + "traefik.HTTP.Middlewares.Middleware4.CircuitBreaker.ResponseCode": "404", "traefik.HTTP.Middlewares.Middleware5.DigestAuth.HeaderField": "foobar", "traefik.HTTP.Middlewares.Middleware5.DigestAuth.Realm": "foobar", "traefik.HTTP.Middlewares.Middleware5.DigestAuth.RemoveHeader": "true", diff --git a/pkg/middlewares/circuitbreaker/circuit_breaker.go b/pkg/middlewares/circuitbreaker/circuit_breaker.go index 3fa06ba23..ba377ea3d 100644 --- a/pkg/middlewares/circuitbreaker/circuit_breaker.go +++ b/pkg/middlewares/circuitbreaker/circuit_breaker.go @@ -30,12 +30,14 @@ func New(ctx context.Context, next http.Handler, confCircuitBreaker dynamic.Circ logger.Debug().Msg("Creating middleware") logger.Debug().Msgf("Setting up with expression: %s", expression) + responseCode := confCircuitBreaker.ResponseCode + cbOpts := []cbreaker.Option{ cbreaker.Fallback(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { tracing.SetStatusErrorf(req.Context(), "blocked by circuit-breaker (%q)", expression) - rw.WriteHeader(http.StatusServiceUnavailable) + rw.WriteHeader(responseCode) - if _, err := rw.Write([]byte(http.StatusText(http.StatusServiceUnavailable))); err != nil { + if _, err := rw.Write([]byte(http.StatusText(responseCode))); err != nil { log.Ctx(req.Context()).Error().Err(err).Send() } })), diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index cfe901037..8f63b5cb1 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -171,6 +171,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/middlewares/Middleware04/circuitBreaker/checkPeriod": "1s", "traefik/http/middlewares/Middleware04/circuitBreaker/fallbackDuration": "1s", "traefik/http/middlewares/Middleware04/circuitBreaker/recoveryDuration": "1s", + "traefik/http/middlewares/Middleware04/circuitBreaker/responseCode": "404", "traefik/http/middlewares/Middleware07/errors/status/0": "foobar", "traefik/http/middlewares/Middleware07/errors/status/1": "foobar", "traefik/http/middlewares/Middleware07/errors/service": "foobar", @@ -392,6 +393,7 @@ func Test_buildConfiguration(t *testing.T) { CheckPeriod: ptypes.Duration(time.Second), FallbackDuration: ptypes.Duration(time.Second), RecoveryDuration: ptypes.Duration(time.Second), + ResponseCode: 404, }, }, "Middleware05": { From ef0e9c6f058c4b4087f0a1d52d03ec16a81c5631 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Mon, 29 Jan 2024 15:10:05 +0100 Subject: [PATCH 20/36] Update go-acme/lego to v4.15.0 --- docs/content/https/acme.md | 4 +- go.mod | 90 ++++++++--------- go.sum | 196 ++++++++++++++++++++----------------- 3 files changed, 155 insertions(+), 135 deletions(-) diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index b614beedd..f423b23db 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -313,7 +313,7 @@ For complete details, refer to your provider's _Additional configuration_ link. | [ACME DNS](https://github.com/joohoi/acme-dns) | `acme-dns` | `ACME_DNS_API_BASE`, `ACME_DNS_STORAGE_PATH` | [Additional configuration](https://go-acme.github.io/lego/dns/acme-dns) | | [Alibaba Cloud](https://www.alibabacloud.com) | `alidns` | `ALICLOUD_ACCESS_KEY`, `ALICLOUD_SECRET_KEY`, `ALICLOUD_REGION_ID` | [Additional configuration](https://go-acme.github.io/lego/dns/alidns) | | [all-inkl](https://all-inkl.com) | `allinkl` | `ALL_INKL_LOGIN`, `ALL_INKL_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/allinkl) | -| [ArvanCloud](https://www.arvancloud.ir/en) | `arvancloud` | `ARVANCLOUD_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/arvancloud) | +| [ArvanCloud](https://www.arvancloud.ir/en) | `arvancloud` | `ARVANCLOUD_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/arvancloud) | | [Auroradns](https://www.pcextreme.com/dns-health-checks) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` | [Additional configuration](https://go-acme.github.io/lego/dns/auroradns) | | [Autodns](https://www.internetx.com/domains/autodns/) | `autodns` | `AUTODNS_API_USER`, `AUTODNS_API_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/autodns) | | [Azure](https://azure.microsoft.com/services/dns/) (DEPRECATED) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP`, `[AZURE_METADATA_ENDPOINT]` | [Additional configuration](https://go-acme.github.io/lego/dns/azure) | @@ -361,6 +361,7 @@ For complete details, refer to your provider's _Additional configuration_ link. | [Hetzner](https://hetzner.com) | `hetzner` | `HETZNER_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/hetzner) | | [hosting.de](https://www.hosting.de) | `hostingde` | `HOSTINGDE_API_KEY`, `HOSTINGDE_ZONE_NAME` | [Additional configuration](https://go-acme.github.io/lego/dns/hostingde) | | [Hosttech](https://www.hosttech.eu) | `hosttech` | `HOSTTECH_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/hosttech) | +| [http.net](https://www.http.net/) | `httpnet` | `HTTPNET_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/httpnet) | | [Hurricane Electric](https://dns.he.net) | `hurricane` | `HURRICANE_TOKENS` [^6] | [Additional configuration](https://go-acme.github.io/lego/dns/hurricane) | | [HyperOne](https://www.hyperone.com) | `hyperone` | `HYPERONE_PASSPORT_LOCATION`, `HYPERONE_LOCATION_ID` | [Additional configuration](https://go-acme.github.io/lego/dns/hyperone) | | [IBM Cloud (SoftLayer)](https://www.ibm.com/cloud/) | `ibmcloud` | `SOFTLAYER_USERNAME`, `SOFTLAYER_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/ibmcloud) | @@ -426,6 +427,7 @@ For complete details, refer to your provider's _Additional configuration_ link. | [VK Cloud](https://mcs.mail.ru/) | `vkcloud` | `VK_CLOUD_PASSWORD`, `VK_CLOUD_PROJECT_ID`, `VK_CLOUD_USERNAME` | [Additional configuration](https://go-acme.github.io/lego/dns/vkcloud) | | [Vscale](https://vscale.io/) | `vscale` | `VSCALE_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/vscale) | | [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/vultr) | +| [Webnames](https://www.webnames.ru/) | `webnames` | `WEBNAMES_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/webnames) | | [Websupport](https://websupport.sk) | `websupport` | `WEBSUPPORT_API_KEY`, `WEBSUPPORT_SECRET` | [Additional configuration](https://go-acme.github.io/lego/dns/websupport) | | [WEDOS](https://www.wedos.com) | `wedos` | `WEDOS_USERNAME`, `WEDOS_WAPI_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/wedos) | | [Yandex 360](https://360.yandex.ru) | `yandex360` | `YANDEX360_OAUTH_TOKEN`, `YANDEX360_ORG_ID` | [Additional configuration](https://go-acme.github.io/lego/dns/yandex360) | diff --git a/go.mod b/go.mod index d5a59d96f..9bf2bd1ed 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/fatih/structs v1.1.0 github.com/fsnotify/fsnotify v1.7.0 github.com/gambol99/go-marathon v0.0.0-20180614232016-99a156b96fb2 - github.com/go-acme/lego/v4 v4.14.0 + github.com/go-acme/lego/v4 v4.15.0 github.com/go-kit/kit v0.10.1-0.20200915143503-439c4d2ed3ea github.com/golang/protobuf v1.5.3 github.com/google/go-github/v28 v28.1.1 @@ -39,7 +39,7 @@ require ( github.com/kvtools/valkeyrie v1.0.0 github.com/kvtools/zookeeper v1.0.2 github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f - github.com/miekg/dns v1.1.55 + github.com/miekg/dns v1.1.58 github.com/mitchellh/copystructure v1.0.0 github.com/mitchellh/hashstructure v1.0.0 github.com/mitchellh/mapstructure v1.5.0 @@ -67,11 +67,11 @@ require ( github.com/vulcand/predicate v1.2.0 go.elastic.co/apm v1.13.1 go.elastic.co/apm/module/apmot v1.13.1 - golang.org/x/mod v0.12.0 - golang.org/x/net v0.17.0 - golang.org/x/text v0.13.0 - golang.org/x/time v0.3.0 - golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 + golang.org/x/mod v0.14.0 + golang.org/x/net v0.20.0 + golang.org/x/text v0.14.0 + golang.org/x/time v0.5.0 + golang.org/x/tools v0.17.0 google.golang.org/grpc v1.59.0 gopkg.in/DataDog/dd-trace-go.v1 v1.56.1 gopkg.in/yaml.v3 v3.0.1 @@ -97,8 +97,8 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.24 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect + github.com/Azure/go-autorest/autorest v0.11.29 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect @@ -125,25 +125,26 @@ require ( github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c // indirect - github.com/aws/aws-sdk-go-v2 v1.20.3 // indirect - github.com/aws/aws-sdk-go-v2/config v1.18.28 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.13.27 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.40 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.34 // indirect - github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2 // indirect - github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 // indirect - github.com/aws/smithy-go v1.14.2 // indirect + github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect + github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect + github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 // indirect + github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect + github.com/aws/smithy-go v1.19.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/civo/civogo v0.3.11 // indirect - github.com/cloudflare/cloudflare-go v0.70.0 // indirect + github.com/cloudflare/cloudflare-go v0.86.0 // indirect github.com/containerd/containerd v1.7.11 // indirect github.com/containerd/log v0.1.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect @@ -164,20 +165,22 @@ require ( github.com/elastic/go-windows v1.0.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect - github.com/exoscale/egoscale v0.100.1 // indirect + github.com/exoscale/egoscale v0.102.3 // indirect github.com/fatih/color v1.15.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.0.1 // indirect - github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.19.14 // indirect - github.com/go-resty/resty/v2 v2.7.0 // indirect + github.com/go-resty/resty/v2 v2.11.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-zookeeper/zk v1.0.3 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -197,7 +200,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-retryablehttp v0.7.4 // indirect + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/serf v0.10.1 // indirect @@ -218,10 +221,9 @@ require ( github.com/kylelemons/godebug v1.1.0 // indirect github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect github.com/labbsr0x/goh v1.0.1 // indirect - github.com/linode/linodego v1.17.2 // indirect - github.com/liquidweb/go-lwApi v0.0.5 // indirect + github.com/linode/linodego v1.28.0 // indirect github.com/liquidweb/liquidweb-cli v0.6.9 // indirect - github.com/liquidweb/liquidweb-go v1.6.3 // indirect + github.com/liquidweb/liquidweb-go v1.6.4 // indirect github.com/looplab/fsm v0.1.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -244,13 +246,14 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect github.com/nrdcg/auroradns v1.1.0 // indirect + github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect github.com/nrdcg/desec v0.7.0 // indirect github.com/nrdcg/dnspod-go v0.4.0 // indirect github.com/nrdcg/freemyip v0.2.0 // indirect - github.com/nrdcg/goinwx v0.8.2 // indirect + github.com/nrdcg/goinwx v0.10.0 // indirect github.com/nrdcg/namesilo v0.2.1 // indirect github.com/nrdcg/nodion v0.1.0 // indirect - github.com/nrdcg/porkbun v0.2.0 // indirect + github.com/nrdcg/porkbun v0.3.0 // indirect github.com/nzdjb/go-metaname v1.0.0 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect @@ -260,7 +263,7 @@ require ( github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 // indirect github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect github.com/outcaste-io/ristretto v0.2.3 // indirect - github.com/ovh/go-ovh v1.4.1 // indirect + github.com/ovh/go-ovh v1.4.3 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -276,15 +279,14 @@ require ( github.com/sacloud/iaas-api-go v1.11.1 // indirect github.com/sacloud/packages-go v0.0.9 // indirect github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect - github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17 // indirect + github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 // indirect github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect github.com/segmentio/fasthash v1.0.3 // indirect github.com/shirou/gopsutil/v3 v3.23.11 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.2.0 // indirect - github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 // indirect github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect - github.com/softlayer/softlayer-go v1.1.2 // indirect + github.com/softlayer/softlayer-go v1.1.3 // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -294,8 +296,8 @@ require ( github.com/tinylib/msgp v1.1.8 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect - github.com/transip/gotransip/v6 v6.20.0 // indirect - github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c // indirect + github.com/transip/gotransip/v6 v6.23.0 // indirect + github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect github.com/vinyldns/go-vinyldns v0.9.16 // indirect github.com/vultr/govultr/v2 v2.17.2 // indirect github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect @@ -315,12 +317,12 @@ require ( go.uber.org/zap v1.21.0 // indirect go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect - golang.org/x/crypto v0.14.0 // indirect + golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect - golang.org/x/oauth2 v0.11.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.13.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.128.0 // indirect google.golang.org/appengine v1.6.7 // indirect @@ -330,7 +332,7 @@ require ( google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/ns1/ns1-go.v2 v2.7.6 // indirect + gopkg.in/ns1/ns1-go.v2 v2.7.13 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect diff --git a/go.sum b/go.sum index dfa028007..267221b7c 100644 --- a/go.sum +++ b/go.sum @@ -70,20 +70,23 @@ github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -github.com/Azure/go-autorest/autorest v0.11.24 h1:1fIGgHKqVm54KIPT+q8Zmd1QlVsmHqeUGso5qm2BqqE= github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= +github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk= github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4= github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= @@ -176,39 +179,36 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.44.327 h1:ZS8oO4+7MOBLhkdwIhgtVeDzCeWOlTfKJS7EgggbIEY= github.com/aws/aws-sdk-go v1.44.327/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/aws/aws-sdk-go-v2 v1.19.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= -github.com/aws/aws-sdk-go-v2 v1.20.3 h1:lgeKmAZhlj1JqN43bogrM75spIvYnRxqTAh1iupu1yE= -github.com/aws/aws-sdk-go-v2 v1.20.3/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M= -github.com/aws/aws-sdk-go-v2/config v1.18.28 h1:TINEaKyh1Td64tqFvn09iYpKiWjmHYrG1fa91q2gnqw= -github.com/aws/aws-sdk-go-v2/config v1.18.28/go.mod h1:nIL+4/8JdAuNHEjn/gPEXqtnS02Q3NXB/9Z7o5xE4+A= -github.com/aws/aws-sdk-go-v2/credentials v1.13.27 h1:dz0yr/yR1jweAnsCx+BmjerUILVPQ6FS5AwF/OyG1kA= -github.com/aws/aws-sdk-go-v2/credentials v1.13.27/go.mod h1:syOqAek45ZXZp29HlnRS/BNgMIW6uiRmeuQsz4Qh2UE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 h1:kP3Me6Fy3vdi+9uHd7YLr6ewPxRL+PU6y15urfTaamU= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5/go.mod h1:Gj7tm95r+QsDoN2Fhuz/3npQvcZbkEf5mL70n3Xfluc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35/go.mod h1:ipR5PvpSPqIqL5Mi82BxLnfMkHVbmco8kUwO2xrCi0M= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.40 h1:CXceCS9BrDInRc74GDCQ8Qyk/Gp9VLdK+Rlve+zELSE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.40/go.mod h1:5kKmFhLeOVy6pwPDpDNA6/hK/d6URC98pqDDqHgdBx4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29/go.mod h1:M/eUABlDbw2uVrdAn+UsI6M727qp2fxkp8K0ejcBDUY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.34 h1:B+nZtd22cbko5+793hg7LEaTeLMiZwlgCLUrN5Y0uzg= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.34/go.mod h1:RZP0scceAyhMIQ9JvFp7HvkpcgqjL4l/4C+7RAeGbuM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 h1:8r5m1BoAWkn0TDC34lUculryf7nUF25EgIMdjvGCkgo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36/go.mod h1:Rmw2M1hMVTwiUhjwMoIBFWFJMhvJbct06sSidxInkhY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29/go.mod h1:fDbkK4o7fpPXWn8YAPmTieAMuB9mk/VgvW64uaUqxd4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.34 h1:JwvXk+1ePAD9xkFHprhHYqwsxLDcbNFsPI1IAT2sPS0= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.34/go.mod h1:ytsF+t+FApY2lFnN51fJKPhH6ICKOPXKEcwwgmJEdWI= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2 h1:PwNeYoonBzmTdCztKiiutws3U24KrnDBuabzRfIlZY4= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2/go.mod h1:gQhLZrTEath4zik5ixIe6axvgY5jJrgSBDJ360Fxnco= -github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4 h1:p4mTxJfCAyiTT4Wp6p/mOPa6j5MqCSRGot8qZwFs+Z0= -github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4/go.mod h1:VBLWpaHvhQNeu7N9rMEf00SWeOONb/HvaDUxe/7b44k= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 h1:sWDv7cMITPcZ21QdreULwxOOAmE05JjEsT6fCDtDA9k= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.13/go.mod h1:DfX0sWuT46KpcqbMhJ9QWtxAIP1VozkDWf8VAkByjYY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 h1:BFubHS/xN5bjl818QaroN6mQdjneYQ+AOx44KNXlyH4= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13/go.mod h1:BzqsVVFduubEmzrVtUFQQIQdFqvUItF8XUq2EnS8Wog= -github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 h1:e5mnydVdCVWxP+5rPAGi2PYxC7u2OZgH1ypC114H04U= -github.com/aws/aws-sdk-go-v2/service/sts v1.19.3/go.mod h1:yVGZA1CPkmUhBdA039jXNJJG7/6t+G+EBWmFq23xqnY= -github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ= -github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= +github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o= +github.com/aws/aws-sdk-go-v2/config v1.26.6/go.mod h1:uKU6cnDmYCvJ+pxO9S4cWDb2yWWIH5hra+32hVh1MI4= +github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8= +github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 h1:LvWkxBi/bsWHqj3bFTUuDLl4OAlbaM1HDZ9YPhj5+jg= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0/go.mod h1:35MKNS46RX7Lb9EIFP2bPy3WrJu+bxU6QgLis8K1aa4= +github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 h1:f3hBZWtpn9clZGXJoqahQeec9ZPZnu22g8pg+zNyif0= +github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0/go.mod h1:8qqfpG4mug2JLlEyWPSFhEGvJiaZ9iPmMDDMYc5Xtas= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -245,8 +245,8 @@ github.com/civo/civogo v0.3.11 h1:mON/fyrV946Sbk6paRtOSGsN+asCgCmHCgArf5xmGxM= github.com/civo/civogo v0.3.11/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/cloudflare-go v0.70.0 h1:4opGbUygM8DjirUuaz23jn3akuAcnOCEx+0nQtQEcFo= -github.com/cloudflare/cloudflare-go v0.70.0/go.mod h1:VW6GuazkaZ4xEDkFt24lkXQUsE8q7BiGqDniC2s8WEM= +github.com/cloudflare/cloudflare-go v0.86.0 h1:jEKN5VHNYNYtfDL2lUFLTRo+nOVNPFxpXTstVx0rqHI= +github.com/cloudflare/cloudflare-go v0.86.0/go.mod h1:wYW/5UP02TUfBToa/yKbQHV+r6h1NnJ1Je7XjuGM4Jw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -375,8 +375,8 @@ github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/exoscale/egoscale v0.100.1 h1:iXsV1Ei7daqe/6FYSCSDyrFs1iUG1l1X9qNh2uMw6z0= -github.com/exoscale/egoscale v0.100.1/go.mod h1:BAb9p4rmyU+Wl400CJZO5270H2sXtdsZjLcm5xMKkz4= +github.com/exoscale/egoscale v0.102.3 h1:DYqN2ipoLKpiFoprRGQkp2av/Ze7sUYYlGhi1N62tfY= +github.com/exoscale/egoscale v0.102.3/go.mod h1:RPf2Gah6up+6kAEayHTQwqapzXlm93f0VQas/UEGU5c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= @@ -407,8 +407,8 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/go-acme/lego/v4 v4.14.0 h1:/skZoRHgVh0d2RK7l1g3Ch8HqeqP9LB8ZEjLdGEpcDE= -github.com/go-acme/lego/v4 v4.14.0/go.mod h1:zjmvNCDLGz7GrC1OqdVpVmZFKSRabEDtWbdzmcpBsGo= +github.com/go-acme/lego/v4 v4.15.0 h1:A7MHEU3b+TDFqhC/HmzMJnzPbyeaYvMZQBbqgvbThhU= +github.com/go-acme/lego/v4 v4.15.0/go.mod h1:eeGhjW4zWT7Ccqa3sY7ayEqFLCAICx+mXgkMHKIkLxg= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= @@ -416,8 +416,8 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= -github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.1-0.20200915143503-439c4d2ed3ea h1:CnEQOUv4ilElSwFB9g/lVmz206oLE4aNZDYngIY1Gvg= @@ -459,8 +459,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= -github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= +github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -473,6 +473,8 @@ github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5 github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= github.com/gobuffalo/flect v0.2.3/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc= github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -549,7 +551,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -651,8 +652,8 @@ github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= -github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= +github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= @@ -813,15 +814,13 @@ github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7 github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/linode/linodego v1.17.2 h1:b32dj4662PGG5P9qVa6nBezccWdqgukndlMIuPGq1CQ= -github.com/linode/linodego v1.17.2/go.mod h1:C2iyT3Vg2O2sPxkWka4XAQ5WSUtm5LmTZ3Adw43Ra7Q= +github.com/linode/linodego v1.28.0 h1:lzxxJebsYg5cCWRNDLyL2StW3sfMyAwf/FYfxFjFrlk= +github.com/linode/linodego v1.28.0/go.mod h1:5oAsx+uinHtVo6U77nXXXtox7MWzUW6aEkTOKXxA9uo= github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= -github.com/liquidweb/go-lwApi v0.0.5 h1:CT4cdXzJXmo0bon298kS7NeSk+Gt8/UHpWBBol1NGCA= -github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM= github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ= -github.com/liquidweb/liquidweb-go v1.6.3 h1:NVHvcnX3eb3BltiIoA+gLYn15nOpkYkdizOEYGSKrk4= -github.com/liquidweb/liquidweb-go v1.6.3/go.mod h1:SuXXp+thr28LnjEw18AYtWwIbWMHSUiajPQs8T9c/Rc= +github.com/liquidweb/liquidweb-go v1.6.4 h1:6S0m3hHSpiLqGD7AFSb7lH/W/qr1wx+tKil9fgIbjMc= +github.com/liquidweb/liquidweb-go v1.6.4/go.mod h1:B934JPIIcdA+uTq2Nz5PgOtG6CuCaEvQKe/Ge/5GgZ4= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/looplab/fsm v0.1.0 h1:Qte7Zdn/5hBNbXzP7yxVU4OIFHWXBovyTT2LaBTyC20= github.com/looplab/fsm v0.1.0/go.mod h1:m2VaOfDHxqXBBMgc26m6yUOwkFn8H2AlJDE+jd/uafI= @@ -882,8 +881,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= -github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34= github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -945,20 +944,22 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uY github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo= github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk= +github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 h1:qpB3wZR4+MPK92cTC9zZPnndkJgDgPvQqPUAgVc1NXU= +github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9/go.mod h1:HUoHXDrFvidN1NK9Wb/mZKNOfDNutKkzF2Pg71M9hHA= github.com/nrdcg/desec v0.7.0 h1:iuGhi4pstF3+vJWwt292Oqe2+AsSPKDynQna/eu1fDs= github.com/nrdcg/desec v0.7.0/go.mod h1:e1uRqqKv1mJdd5+SQROAhmy75lKMphLzWIuASLkpeFY= github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U= github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= github.com/nrdcg/freemyip v0.2.0 h1:/GscavT4GVqAY13HExl5UyoB4wlchv6Cg5NYDGsUoJ8= github.com/nrdcg/freemyip v0.2.0/go.mod h1:HjF0Yz0lSb37HD2ihIyGz9esyGcxbCrrGFLPpKevbx4= -github.com/nrdcg/goinwx v0.8.2 h1:RmjiHlEA+lzi3toXyPSaE6hWnBQ0+G+1u7w8C6Fpp4g= -github.com/nrdcg/goinwx v0.8.2/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4= +github.com/nrdcg/goinwx v0.10.0 h1:6W630bjDxQD6OuXKqrFRYVpTt0G/9GXXm3CeOrN0zJM= +github.com/nrdcg/goinwx v0.10.0/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4= github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg= github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw= github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms= -github.com/nrdcg/porkbun v0.2.0 h1:ghaqPtIKcffba99epWFkK3VWf6TKJT9WMXMgaTqv95Y= -github.com/nrdcg/porkbun v0.2.0/go.mod h1:i0uLMn9ItFsLsSQIAeEu1wQ9/+6EvX1eQw15hulMMRw= +github.com/nrdcg/porkbun v0.3.0 h1:jnRV7j2zd3hmh+tSDOGetJyy3+WklaMxbs7HtTTmWMs= +github.com/nrdcg/porkbun v0.3.0/go.mod h1:jh1DKz96jGHW+NCdG3AmTbbnQeBlNUz1KeSgeN/cBVw= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -1016,8 +1017,8 @@ github.com/oracle/oci-go-sdk v24.3.0+incompatible h1:x4mcfb4agelf1O4/1/auGlZ1lr9 github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0= github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac= -github.com/ovh/go-ovh v1.4.1 h1:VBGa5wMyQtTP7Zb+w97zRCh9sLtM/2YKRyy+MEJmWaM= -github.com/ovh/go-ovh v1.4.1/go.mod h1:6bL6pPyUT7tBfI0pqOegJgRjgjuO+mOo+MyXd1EEC0M= +github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0= +github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= @@ -1140,8 +1141,8 @@ github.com/sacloud/packages-go v0.0.9/go.mod h1:k+EEUMF2LlncjbNIJNOqLyZ9wjTESPIW github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17 h1:1WuWJu7/e8SqK+uQl7lfk/N/oMZTL2NE/TJsNKRNMc4= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 h1:wJrcTdddKOI8TFxs8cemnhKP2EmKy3yfUKHj3ZdfzYo= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg= @@ -1158,8 +1159,6 @@ github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsB github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 h1:ZTzdx88+AcnjqUfJwnz89UBrMSBQ1NEysg9u5d+dU9c= -github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04/go.mod h1:5KS21fpch8TIMyAUv/qQqTa3GZfBDYgjaZbd2KXKYfg= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -1174,8 +1173,8 @@ github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA= github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= -github.com/softlayer/softlayer-go v1.1.2 h1:rUSSGCyaxymvTOsaFjwr+cGxA8muw3xg2LSrIMNcN/c= -github.com/softlayer/softlayer-go v1.1.2/go.mod h1:hvAbzGH4LRXA6yXY8BNx99yoqZ7urfDdtl9mvBf0G+g= +github.com/softlayer/softlayer-go v1.1.3 h1:dfFzt5eOKIAyB/b78fHMyDu5ICx0ZtxL9NRhBlf831A= +github.com/softlayer/softlayer-go v1.1.3/go.mod h1:Pc7F57OgUKaAam7TtpqkUeqL7QyKknfiUI4R49h41/U= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -1249,8 +1248,8 @@ github.com/traefik/paerser v0.2.0 h1:zqCLGSXoNlcBd+mzqSCLjon/I6phqIjeJL2xFB2ysgQ github.com/traefik/paerser v0.2.0/go.mod h1:afzaVcgF8A+MpTnPG4wBr4whjanCSYA6vK5RwaYVtRc= github.com/traefik/yaegi v0.15.1 h1:YA5SbaL6HZA0Exh9T/oArRHqGN2HQ+zgmCY7dkoTXu4= github.com/traefik/yaegi v0.15.1/go.mod h1:AVRxhaI2G+nUsaM1zyktzwXn69G3t/AuTDrCiTds9p0= -github.com/transip/gotransip/v6 v6.20.0 h1:AuvwyOZ51f2brzMbTqlRy/wmaM3kF7Vx5Wds8xcDflY= -github.com/transip/gotransip/v6 v6.20.0/go.mod h1:nzv9eN2tdsUrm5nG5ZX6AugYIU4qgsMwIn2c0EZLk8c= +github.com/transip/gotransip/v6 v6.23.0 h1:PsTdjortrEZ8IFFifEryzjVjOy9SgK4ahlnhKBBIQgA= +github.com/transip/gotransip/v6 v6.23.0/go.mod h1:nzv9eN2tdsUrm5nG5ZX6AugYIU4qgsMwIn2c0EZLk8c= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= @@ -1260,8 +1259,8 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= -github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c h1:mKnW6IGLw7uXu6DL6RitufZWcXS6hCnauXRUFof7rKM= -github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c/go.mod h1:F4UyVEmq4/m5lAmx+GccrxyRCXmnBjzUL09JLTQFp94= +github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a h1:w4PK5/N9kq8PfNxBv8a5t1bqlYRrVT7XzT7iTPTtiPk= +github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a/go.mod h1:Xwz7o+ExFtxR/i0aJDnTXuiccQJlOxDgNe6FsZC4TzQ= github.com/unrolled/render v1.0.2 h1:dGS3EmChQP3yOi1YeFNO/Dx+MbWZhdvhQJTXochM5bs= github.com/unrolled/render v1.0.2/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM= github.com/unrolled/secure v1.0.9 h1:BWRuEb1vDrBFFDdbCnKkof3gZ35I/bnHGyt0LB0TNyQ= @@ -1389,9 +1388,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1437,8 +1439,9 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1490,15 +1493,17 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1511,8 +1516,8 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= -golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1526,8 +1531,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1625,11 +1630,14 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1637,8 +1645,11 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1651,8 +1662,11 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1660,8 +1674,9 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1734,8 +1749,9 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1903,8 +1919,8 @@ gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/ns1/ns1-go.v2 v2.7.6 h1:mCPl7q0jbIGACXvGBljAuuApmKZo3rRi4tlRIEbMvjA= -gopkg.in/ns1/ns1-go.v2 v2.7.6/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk= +gopkg.in/ns1/ns1-go.v2 v2.7.13 h1:r07CLALg18f/L1KIK1ZJdbirBV349UtYT1rDWGjnaTk= +gopkg.in/ns1/ns1-go.v2 v2.7.13/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= From 18203f57d2bd5d2f48ab443a227927bef6e1b86e Mon Sep 17 00:00:00 2001 From: Liam van der Viven <62619917+liamvdv@users.noreply.github.com> Date: Mon, 29 Jan 2024 17:08:05 +0100 Subject: [PATCH 21/36] Add support for sending DogStatsD metrics over Unix Socket --- docs/content/observability/metrics/datadog.md | 2 + docs/mkdocs.yml | 4 ++ pkg/metrics/datadog.go | 27 ++++++++++--- pkg/metrics/datadog_test.go | 39 +++++++++++++++++++ 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/docs/content/observability/metrics/datadog.md b/docs/content/observability/metrics/datadog.md index 00a5d6c9f..134b726df 100644 --- a/docs/content/observability/metrics/datadog.md +++ b/docs/content/observability/metrics/datadog.md @@ -27,6 +27,8 @@ _Required, Default="127.0.0.1:8125"_ Address instructs exporter to send metrics to datadog-agent at this address. +This address can be a Unix Domain Socket (UDS) address with the following form: `unix:///path/to/datadog.socket`. + ```yaml tab="File (YAML)" metrics: datadog: diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6ba875a3f..0b7ffaa2c 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -153,7 +153,11 @@ nav: - 'Access Logs': 'observability/access-logs.md' - 'Metrics': - 'Overview': 'observability/metrics/overview.md' + - 'Datadog': 'observability/metrics/datadog.md' + - 'InfluxDB2': 'observability/metrics/influxdb2.md' - 'OpenTelemetry': 'observability/metrics/opentelemetry.md' + - 'Prometheus': 'observability/metrics/prometheus.md' + - 'StatsD': 'observability/metrics/statsd.md' - 'Tracing': - 'Overview': 'observability/tracing/overview.md' - 'OpenTelemetry': 'observability/tracing/opentelemetry.md' diff --git a/pkg/metrics/datadog.go b/pkg/metrics/datadog.go index b09e3407a..fcb8be6d2 100644 --- a/pkg/metrics/datadog.go +++ b/pkg/metrics/datadog.go @@ -2,6 +2,7 @@ package metrics import ( "context" + "strings" "time" "github.com/go-kit/kit/metrics/dogstatsd" @@ -16,6 +17,8 @@ var ( datadogLoopCancelFunc context.CancelFunc ) +const unixAddressPrefix = "unix://" + // Metric names consistent with https://github.com/DataDog/integrations-extras/pull/64 const ( ddConfigReloadsName = "config.reload.total" @@ -99,10 +102,7 @@ func RegisterDatadog(ctx context.Context, config *types.Datadog) Registry { } func initDatadogClient(ctx context.Context, config *types.Datadog) { - address := config.Address - if len(address) == 0 { - address = "localhost:8125" - } + network, address := parseDatadogAddress(config.Address) ctx, datadogLoopCancelFunc = context.WithCancel(ctx) @@ -110,10 +110,27 @@ func initDatadogClient(ctx context.Context, config *types.Datadog) { ticker := time.NewTicker(time.Duration(config.PushInterval)) defer ticker.Stop() - datadogClient.SendLoop(ctx, ticker.C, "udp", address) + datadogClient.SendLoop(ctx, ticker.C, network, address) }) } +func parseDatadogAddress(address string) (string, string) { + network := "udp" + + var addr string + switch { + case strings.HasPrefix(address, unixAddressPrefix): + network = "unix" + addr = address[len(unixAddressPrefix):] + case address != "": + addr = address + default: + addr = "localhost:8125" + } + + return network, addr +} + // StopDatadog stops the Datadog metrics pusher. func StopDatadog() { if datadogLoopCancelFunc != nil { diff --git a/pkg/metrics/datadog_test.go b/pkg/metrics/datadog_test.go index 6a53bf065..cd4b99433 100644 --- a/pkg/metrics/datadog_test.go +++ b/pkg/metrics/datadog_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stvp/go-udp-testing" ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v3/pkg/types" @@ -39,6 +40,44 @@ func TestDatadogWithPrefix(t *testing.T) { testDatadogRegistry(t, "testPrefix", datadogRegistry) } +func TestDatadog_parseDatadogAddress(t *testing.T) { + tests := []struct { + desc string + address string + expNetwork string + expAddress string + }{ + { + desc: "empty address", + expNetwork: "udp", + expAddress: "localhost:8125", + }, + { + desc: "udp address", + address: "127.0.0.1:8080", + expNetwork: "udp", + expAddress: "127.0.0.1:8080", + }, + { + desc: "unix address", + address: "unix:///path/to/datadog.socket", + expNetwork: "unix", + expAddress: "/path/to/datadog.socket", + }, + } + + for _, test := range tests { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + gotNetwork, gotAddress := parseDatadogAddress(test.address) + assert.Equal(t, test.expNetwork, gotNetwork) + assert.Equal(t, test.expAddress, gotAddress) + }) + } +} + func testDatadogRegistry(t *testing.T, metricsPrefix string, datadogRegistry Registry) { t.Helper() From 40de310927507c8c2e24b0ac6d32091e3c167e9b Mon Sep 17 00:00:00 2001 From: Romain Date: Mon, 29 Jan 2024 17:32:05 +0100 Subject: [PATCH 22/36] Reintroduce dropped v2 dynamic config Co-authored-by: Baptiste Mayelle --- .golangci.yml | 5 +- docs/content/middlewares/http/contenttype.md | 13 + docs/content/middlewares/http/headers.md | 40 +++ docs/content/middlewares/http/stripprefix.md | 69 +++++ .../dynamic-configuration/docker-labels.yml | 9 + .../reference/dynamic-configuration/file.toml | 11 + .../reference/dynamic-configuration/file.yaml | 13 +- .../kubernetes-crd-definition-v1.yml | 69 ++++- .../reference/dynamic-configuration/kv-ref.md | 12 +- .../traefik.io_ingressroutetcps.yaml | 12 + .../traefik.io_middlewares.yaml | 44 +++- .../traefik.io_middlewaretcps.yaml | 7 +- .../traefik.io_tlsoptions.yaml | 6 + docs/content/routing/services/index.md | 40 +++ integration/fixtures/k8s/01-traefik-crd.yml | 69 ++++- pkg/config/dynamic/middlewares.go | 48 +++- pkg/config/dynamic/tcp_config.go | 8 + pkg/config/dynamic/zz_generated.deepcopy.go | 67 ++++- pkg/config/label/label_test.go | 44 +++- pkg/middlewares/auth/forward.go | 17 +- pkg/middlewares/contenttype/content_type.go | 16 +- .../contenttype/content_type_test.go | 3 +- pkg/middlewares/stripprefix/strip_prefix.go | 27 +- .../stripprefix/strip_prefix_test.go | 3 + .../kubernetes/crd/client_mock_test.go | 196 -------------- .../kubernetes/crd/fixtures/tcp/services.yml | 19 +- .../crd/fixtures/tcp/with_externalname.yml | 2 +- .../tcp/with_externalname_with_port.yml | 2 +- .../tcp/with_externalname_without_ports.yml | 2 +- .../fixtures/tcp/with_native_service_lb.yml | 2 +- .../kubernetes/crd/fixtures/udp/services.yml | 19 +- .../crd/fixtures/udp/with_externalname.yml | 2 +- .../udp/with_externalname_service.yml | 2 +- .../udp/with_externalname_with_port.yml | 2 +- .../udp/with_externalname_without_ports.yml | 2 +- .../fixtures/udp/with_native_service_lb.yml | 2 +- ..._default_tls_options_default_namespace.yml | 2 +- .../crd/fixtures/with_default_tls_store.yml | 2 +- pkg/provider/kubernetes/crd/kubernetes.go | 9 +- pkg/provider/kubernetes/crd/kubernetes_tcp.go | 4 + .../kubernetes/crd/kubernetes_test.go | 247 ++++++++++-------- .../crd/traefikio/v1alpha1/ingressroutetcp.go | 7 + .../crd/traefikio/v1alpha1/middleware.go | 3 + .../crd/traefikio/v1alpha1/middlewaretcp.go | 4 + .../crd/traefikio/v1alpha1/tlsoption.go | 5 + .../v1alpha1/zz_generated.deepcopy.go | 19 +- pkg/provider/kv/kv_test.go | 21 +- pkg/redactor/redactor_config_test.go | 2 +- pkg/server/middleware/middlewares.go | 5 +- pkg/server/service/tcp/service.go | 23 ++ pkg/tls/tls.go | 3 + pkg/tls/tlsmanager.go | 7 + pkg/tls/zz_generated.deepcopy.go | 5 + 53 files changed, 880 insertions(+), 392 deletions(-) delete mode 100644 pkg/provider/kubernetes/crd/client_mock_test.go diff --git a/.golangci.yml b/.golangci.yml index 08685510f..626a19225 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -161,7 +161,10 @@ linters-settings: - len - suite-extra-assert-call - suite-thelper - + staticcheck: + checks: + - all + - -SA1019 linters: enable-all: true disable: diff --git a/docs/content/middlewares/http/contenttype.md b/docs/content/middlewares/http/contenttype.md index f08bccb39..fd3759edf 100644 --- a/docs/content/middlewares/http/contenttype.md +++ b/docs/content/middlewares/http/contenttype.md @@ -52,3 +52,16 @@ http: [http.middlewares] [http.middlewares.autodetect.contentType] ``` + +## Configuration Options + +### `autoDetect` + +!!! warning + + `autoDetect` option is deprecated and should not be used. + Moreover, it is redundant with an empty ContentType middleware declaration. + +`autoDetect` specifies whether to let the `Content-Type` header, +if it has not been set by the backend, +be automatically set to a value derived from the contents of the response. diff --git a/docs/content/middlewares/http/headers.md b/docs/content/middlewares/http/headers.md index 7c4599b18..d0cb63672 100644 --- a/docs/content/middlewares/http/headers.md +++ b/docs/content/middlewares/http/headers.md @@ -314,11 +314,43 @@ The `allowedHosts` option lists fully qualified domain names that are allowed. The `hostsProxyHeaders` option is a set of header keys that may hold a proxied hostname value for the request. +### `sslRedirect` + +!!! warning + + Deprecated in favor of [EntryPoint redirection](../../routing/entrypoints.md#redirection) or the [RedirectScheme middleware](./redirectscheme.md). + +The `sslRedirect` only allow HTTPS requests when set to `true`. + +### `sslTemporaryRedirect` + +!!! warning + + Deprecated in favor of [EntryPoint redirection](../../routing/entrypoints.md#redirection) or the [RedirectScheme middleware](./redirectscheme.md). + +Set `sslTemporaryRedirect` to `true` to force an SSL redirection using a 302 (instead of a 301). + +### `sslHost` + +!!! warning + + Deprecated in favor of the [RedirectRegex middleware](./redirectregex.md). + +The `sslHost` option is the host name that is used to redirect HTTP requests to HTTPS. + ### `sslProxyHeaders` The `sslProxyHeaders` option is set of header keys with associated values that would indicate a valid HTTPS request. It can be useful when using other proxies (example: `"X-Forwarded-Proto": "https"`). +### `sslForceHost` + +!!! warning + + Deprecated in favor of the [RedirectRegex middleware](./redirectregex.md). + +Set `sslForceHost` to `true` and set `sslHost` to force requests to use `SSLHost` regardless of whether they already use SSL. + ### `stsSeconds` The `stsSeconds` is the max-age of the `Strict-Transport-Security` header. @@ -370,6 +402,14 @@ The `publicKey` implements HPKP to prevent MITM attacks with forged certificates The `referrerPolicy` allows sites to control whether browsers forward the `Referer` header to other sites. +### `featurePolicy` + +!!! warning + + Deprecated in favor of [`permissionsPolicy`](#permissionsPolicy) + +The `featurePolicy` allows sites to control browser features. + ### `permissionsPolicy` The `permissionsPolicy` allows sites to control browser features. diff --git a/docs/content/middlewares/http/stripprefix.md b/docs/content/middlewares/http/stripprefix.md index 5da2543b2..b84442321 100644 --- a/docs/content/middlewares/http/stripprefix.md +++ b/docs/content/middlewares/http/stripprefix.md @@ -76,3 +76,72 @@ For instance, `/products` also matches `/products/shoes` and `/products/shirts`. If your backend is serving assets (e.g., images or JavaScript files), it can use the `X-Forwarded-Prefix` header to properly construct relative URLs. Using the previous example, the backend should return `/products/shoes/image.png` (and not `/image.png`, which Traefik would likely not be able to associate with the same backend). + +### `forceSlash` + +_Optional, Default=true_ + +!!! warning + + `forceSlash` option is deprecated and should not be used. + +The `forceSlash` option ensures the resulting stripped path is not the empty string, by replacing it with `/` when necessary. + +??? info "Behavior examples" + + - `forceSlash=true` + + | Path | Prefix to strip | Result | + |------------|-----------------|--------| + | `/` | `/` | `/` | + | `/foo` | `/foo` | `/` | + | `/foo/` | `/foo` | `/` | + | `/foo/` | `/foo/` | `/` | + | `/bar` | `/foo` | `/bar` | + | `/foo/bar` | `/foo` | `/bar` | + + - `forceSlash=false` + + | Path | Prefix to strip | Result | + |------------|-----------------|--------| + | `/` | `/` | empty | + | `/foo` | `/foo` | empty | + | `/foo/` | `/foo` | `/` | + | `/foo/` | `/foo/` | empty | + | `/bar` | `/foo` | `/bar` | + | `/foo/bar` | `/foo` | `/bar` | + +```yaml tab="Docker" +labels: + - "traefik.http.middlewares.example.stripprefix.prefixes=/foobar" + - "traefik.http.middlewares.example.stripprefix.forceSlash=false" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: example +spec: + stripPrefix: + prefixes: + - "/foobar" + forceSlash: false +``` + +```yaml tab="File (YAML)" +http: + middlewares: + example: + stripPrefix: + prefixes: + - "/foobar" + forceSlash: false +``` + +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.example.stripPrefix] + prefixes = ["/foobar"] + forceSlash = false +``` diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 3d1eb62f5..7f3fec5ad 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -22,6 +22,7 @@ - "traefik.http.middlewares.middleware06.compress.includedcontenttypes=foobar, foobar" - "traefik.http.middlewares.middleware06.compress.minresponsebodybytes=42" - "traefik.http.middlewares.middleware07.contenttype=true" +- "traefik.http.middlewares.middleware07.contenttype.autodetect=true" - "traefik.http.middlewares.middleware08.digestauth.headerfield=foobar" - "traefik.http.middlewares.middleware08.digestauth.realm=foobar" - "traefik.http.middlewares.middleware08.digestauth.removeheader=true" @@ -36,6 +37,7 @@ - "traefik.http.middlewares.middleware10.forwardauth.authresponseheaders=foobar, foobar" - "traefik.http.middlewares.middleware10.forwardauth.authresponseheadersregex=foobar" - "traefik.http.middlewares.middleware10.forwardauth.tls.ca=foobar" +- "traefik.http.middlewares.middleware10.forwardauth.tls.caoptional=true" - "traefik.http.middlewares.middleware10.forwardauth.tls.cert=foobar" - "traefik.http.middlewares.middleware10.forwardauth.tls.insecureskipverify=true" - "traefik.http.middlewares.middleware10.forwardauth.tls.key=foobar" @@ -59,6 +61,7 @@ - "traefik.http.middlewares.middleware12.headers.customrequestheaders.name1=foobar" - "traefik.http.middlewares.middleware12.headers.customresponseheaders.name0=foobar" - "traefik.http.middlewares.middleware12.headers.customresponseheaders.name1=foobar" +- "traefik.http.middlewares.middleware12.headers.featurepolicy=foobar" - "traefik.http.middlewares.middleware12.headers.forcestsheader=true" - "traefik.http.middlewares.middleware12.headers.framedeny=true" - "traefik.http.middlewares.middleware12.headers.hostsproxyheaders=foobar, foobar" @@ -66,8 +69,12 @@ - "traefik.http.middlewares.middleware12.headers.permissionspolicy=foobar" - "traefik.http.middlewares.middleware12.headers.publickey=foobar" - "traefik.http.middlewares.middleware12.headers.referrerpolicy=foobar" +- "traefik.http.middlewares.middleware12.headers.sslforcehost=true" +- "traefik.http.middlewares.middleware12.headers.sslhost=foobar" - "traefik.http.middlewares.middleware12.headers.sslproxyheaders.name0=foobar" - "traefik.http.middlewares.middleware12.headers.sslproxyheaders.name1=foobar" +- "traefik.http.middlewares.middleware12.headers.sslredirect=true" +- "traefik.http.middlewares.middleware12.headers.ssltemporaryredirect=true" - "traefik.http.middlewares.middleware12.headers.stsincludesubdomains=true" - "traefik.http.middlewares.middleware12.headers.stspreload=true" - "traefik.http.middlewares.middleware12.headers.stsseconds=42" @@ -127,6 +134,7 @@ - "traefik.http.middlewares.middleware22.replacepathregex.replacement=foobar" - "traefik.http.middlewares.middleware23.retry.attempts=42" - "traefik.http.middlewares.middleware23.retry.initialinterval=42s" +- "traefik.http.middlewares.middleware24.stripprefix.forceslash=true" - "traefik.http.middlewares.middleware24.stripprefix.prefixes=foobar, foobar" - "traefik.http.middlewares.middleware25.stripprefixregex.regex=foobar, foobar" - "traefik.http.routers.router0.entrypoints=foobar, foobar" @@ -214,6 +222,7 @@ - "traefik.tcp.services.tcpservice01.loadbalancer.proxyprotocol=true" - "traefik.tcp.services.tcpservice01.loadbalancer.proxyprotocol.version=42" - "traefik.tcp.services.tcpservice01.loadbalancer.serverstransport=foobar" +- "traefik.tcp.services.tcpservice01.loadbalancer.terminationdelay=42" - "traefik.tcp.services.tcpservice01.loadbalancer.server.port=foobar" - "traefik.tcp.services.tcpservice01.loadbalancer.server.tls=true" - "traefik.udp.routers.udprouter0.entrypoints=foobar, foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index b4565206b..d3cf0eaf2 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -145,6 +145,7 @@ minResponseBodyBytes = 42 [http.middlewares.Middleware07] [http.middlewares.Middleware07.contentType] + autoDetect = true [http.middlewares.Middleware08] [http.middlewares.Middleware08.digestAuth] users = ["foobar", "foobar"] @@ -170,6 +171,7 @@ cert = "foobar" key = "foobar" insecureSkipVerify = true + caOptional = true [http.middlewares.Middleware11] [http.middlewares.Middleware11.grpcWeb] allowOrigins = ["foobar", "foobar"] @@ -199,6 +201,11 @@ referrerPolicy = "foobar" permissionsPolicy = "foobar" isDevelopment = true + featurePolicy = "foobar" + sslRedirect = true + sslTemporaryRedirect = true + sslHost = "foobar" + sslForceHost = true [http.middlewares.Middleware12.headers.customRequestHeaders] name0 = "foobar" name1 = "foobar" @@ -298,6 +305,7 @@ [http.middlewares.Middleware24] [http.middlewares.Middleware24.stripPrefix] prefixes = ["foobar", "foobar"] + forceSlash = true [http.middlewares.Middleware25] [http.middlewares.Middleware25.stripPrefixRegex] regex = ["foobar", "foobar"] @@ -395,6 +403,7 @@ [tcp.services.TCPService01] [tcp.services.TCPService01.loadBalancer] serversTransport = "foobar" + terminationDelay = 42 [tcp.services.TCPService01.loadBalancer.proxyProtocol] version = 42 @@ -514,6 +523,7 @@ curvePreferences = ["foobar", "foobar"] sniStrict = true alpnProtocols = ["foobar", "foobar"] + preferServerCipherSuites = true [tls.options.Options0.clientAuth] caFiles = ["foobar", "foobar"] clientAuthType = "foobar" @@ -524,6 +534,7 @@ curvePreferences = ["foobar", "foobar"] sniStrict = true alpnProtocols = ["foobar", "foobar"] + preferServerCipherSuites = true [tls.options.Options1.clientAuth] caFiles = ["foobar", "foobar"] clientAuthType = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 430e8cab1..fdba1c332 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -153,7 +153,8 @@ http: - foobar minResponseBodyBytes: 42 Middleware07: - contentType: {} + contentType: + autoDetect: true Middleware08: digestAuth: users: @@ -178,6 +179,7 @@ http: cert: foobar key: foobar insecureSkipVerify: true + caOptional: true trustForwardHeader: true authResponseHeaders: - foobar @@ -243,6 +245,11 @@ http: referrerPolicy: foobar permissionsPolicy: foobar isDevelopment: true + featurePolicy: foobar + sslRedirect: true + sslTemporaryRedirect: true + sslHost: foobar + sslForceHost: true Middleware13: ipAllowList: sourceRange: @@ -347,6 +354,7 @@ http: prefixes: - foobar - foobar + forceSlash: true Middleware25: stripPrefixRegex: regex: @@ -464,6 +472,7 @@ tcp: - address: foobar tls: true serversTransport: foobar + terminationDelay: 42 TCPService02: weighted: services: @@ -584,6 +593,7 @@ tls: alpnProtocols: - foobar - foobar + preferServerCipherSuites: true Options1: minVersion: foobar maxVersion: foobar @@ -602,6 +612,7 @@ tls: alpnProtocols: - foobar - foobar + preferServerCipherSuites: true stores: Store0: defaultCertificate: diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index 966a45177..34631cbe4 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -393,6 +393,18 @@ spec: between Traefik and your servers. Can only be used on a Kubernetes Service. type: string + terminationDelay: + description: 'TerminationDelay defines the deadline that + the proxy sets, after one of its connected peers indicates + it has closed the writing capability of its connection, + to close the reading capability as well, hence fully + terminating the connection. It is a duration in milliseconds, + defaulting to 100. A negative value means an infinite + deadline (i.e. the reading capability is never closed). + Deprecated: TerminationDelay is not supported APIVersion + traefik.io/v1, please use ServersTransport to configure + the TerminationDelay instead.' + type: integer tls: description: TLS determines whether to use TLS when dialing with the backend. @@ -779,9 +791,17 @@ spec: type: object contentType: description: ContentType holds the content-type middleware configuration. - This middleware sets the `Content-Type` header value to the media - type detected from the response content, when it is not set by the - backend. + This middleware exists to enable the correct behavior until at least + the default one can be changed in a future version. + properties: + autoDetect: + description: 'AutoDetect specifies whether to let the `Content-Type` + header, if it has not been set by the backend, be automatically + set to a value derived from the contents of the response. Deprecated: + AutoDetect option is deprecated, Content-Type middleware is + only meant to be used to enable the content-type detection, + please remove any usage of this option.' + type: boolean type: object digestAuth: description: 'DigestAuth holds the digest auth middleware configuration. @@ -972,6 +992,10 @@ spec: description: TLS defines the configuration used to secure the connection to the authentication server. properties: + caOptional: + description: 'Deprecated: TLS client authentication is a server + side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634).' + type: boolean caSecret: description: CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate. @@ -1090,6 +1114,10 @@ spec: description: CustomResponseHeaders defines the header names and values to apply to the response. type: object + featurePolicy: + description: 'Deprecated: FeaturePolicy option is deprecated, + please use PermissionsPolicy instead.' + type: string forceSTSHeader: description: ForceSTSHeader defines whether to add the STS header even when the connection is HTTP. @@ -1125,6 +1153,14 @@ spec: value. This allows sites to control whether browsers forward the Referer header to other sites. type: string + sslForceHost: + description: 'Deprecated: SSLForceHost option is deprecated, please + use RedirectRegex instead.' + type: boolean + sslHost: + description: 'Deprecated: SSLHost option is deprecated, please + use RedirectRegex instead.' + type: string sslProxyHeaders: additionalProperties: type: string @@ -1133,6 +1169,14 @@ spec: useful when using other proxies (example: "X-Forwarded-Proto": "https").' type: object + sslRedirect: + description: 'Deprecated: SSLRedirect option is deprecated, please + use EntryPoint redirection or RedirectScheme instead.' + type: boolean + sslTemporaryRedirect: + description: 'Deprecated: SSLTemporaryRedirect option is deprecated, + please use EntryPoint redirection or RedirectScheme instead.' + type: boolean stsIncludeSubdomains: description: STSIncludeSubdomains defines whether the includeSubDomains directive is appended to the Strict-Transport-Security header. @@ -1504,6 +1548,12 @@ spec: This middleware removes the specified prefixes from the URL path. More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/stripprefix/' properties: + forceSlash: + description: 'Deprecated: ForceSlash option is deprecated, please + remove any usage of this option. ForceSlash ensures that the + resulting stripped path is not the empty string, by replacing + it with / when necessary. Default: true.' + type: boolean prefixes: description: Prefixes defines the prefixes to strip from the request URL. @@ -1578,7 +1628,9 @@ spec: type: integer type: object ipAllowList: - description: IPAllowList defines the IPAllowList middleware configuration. + description: 'IPAllowList defines the IPAllowList middleware configuration. + This middleware accepts/refuses connections based on the client + IP. More info: https://doc.traefik.io/traefik/v3.0/middlewares/tcp/ipallowlist/' properties: sourceRange: description: SourceRange defines the allowed IPs (or ranges of @@ -1589,7 +1641,8 @@ spec: type: object ipWhiteList: description: 'IPWhiteList defines the IPWhiteList middleware configuration. - Deprecated: please use IPAllowList instead.' + This middleware accepts/refuses connections based on the client + IP. Deprecated: please use IPAllowList instead. More info: https://doc.traefik.io/traefik/v3.0/middlewares/tcp/ipwhitelist/' properties: sourceRange: description: SourceRange defines the allowed IPs (or ranges of @@ -1940,6 +1993,12 @@ spec: will accept. Possible values: VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13. Default: VersionTLS10.' type: string + preferServerCipherSuites: + description: 'PreferServerCipherSuites defines whether the server + chooses a cipher suite among his own instead of among the client''s. + It is enabled automatically when minVersion or maxVersion is set. + Deprecated: https://github.com/golang/go/issues/45430' + type: boolean sniStrict: description: SniStrict defines whether Traefik allows connections from clients connections that do not specify a server_name extension. diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 6709fe5a9..254801508 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -26,7 +26,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/middlewares/Middleware06/compress/includedContentTypes/0` | `foobar` | | `traefik/http/middlewares/Middleware06/compress/includedContentTypes/1` | `foobar` | | `traefik/http/middlewares/Middleware06/compress/minResponseBodyBytes` | `42` | -| `traefik/http/middlewares/Middleware07/contentType` | `` | +| `traefik/http/middlewares/Middleware07/contentType/autoDetect` | `true` | | `traefik/http/middlewares/Middleware08/digestAuth/headerField` | `foobar` | | `traefik/http/middlewares/Middleware08/digestAuth/realm` | `foobar` | | `traefik/http/middlewares/Middleware08/digestAuth/removeHeader` | `true` | @@ -46,6 +46,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeaders/1` | `foobar` | | `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeadersRegex` | `foobar` | | `traefik/http/middlewares/Middleware10/forwardAuth/tls/ca` | `foobar` | +| `traefik/http/middlewares/Middleware10/forwardAuth/tls/caOptional` | `true` | | `traefik/http/middlewares/Middleware10/forwardAuth/tls/cert` | `foobar` | | `traefik/http/middlewares/Middleware10/forwardAuth/tls/insecureSkipVerify` | `true` | | `traefik/http/middlewares/Middleware10/forwardAuth/tls/key` | `foobar` | @@ -76,6 +77,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/middlewares/Middleware12/headers/customRequestHeaders/name1` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/customResponseHeaders/name0` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/customResponseHeaders/name1` | `foobar` | +| `traefik/http/middlewares/Middleware12/headers/featurePolicy` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/forceSTSHeader` | `true` | | `traefik/http/middlewares/Middleware12/headers/frameDeny` | `true` | | `traefik/http/middlewares/Middleware12/headers/hostsProxyHeaders/0` | `foobar` | @@ -84,8 +86,12 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/middlewares/Middleware12/headers/permissionsPolicy` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/publicKey` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/referrerPolicy` | `foobar` | +| `traefik/http/middlewares/Middleware12/headers/sslForceHost` | `true` | +| `traefik/http/middlewares/Middleware12/headers/sslHost` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/sslProxyHeaders/name0` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/sslProxyHeaders/name1` | `foobar` | +| `traefik/http/middlewares/Middleware12/headers/sslRedirect` | `true` | +| `traefik/http/middlewares/Middleware12/headers/sslTemporaryRedirect` | `true` | | `traefik/http/middlewares/Middleware12/headers/stsIncludeSubdomains` | `true` | | `traefik/http/middlewares/Middleware12/headers/stsPreload` | `true` | | `traefik/http/middlewares/Middleware12/headers/stsSeconds` | `42` | @@ -149,6 +155,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/middlewares/Middleware22/replacePathRegex/replacement` | `foobar` | | `traefik/http/middlewares/Middleware23/retry/attempts` | `42` | | `traefik/http/middlewares/Middleware23/retry/initialInterval` | `42s` | +| `traefik/http/middlewares/Middleware24/stripPrefix/forceSlash` | `true` | | `traefik/http/middlewares/Middleware24/stripPrefix/prefixes/0` | `foobar` | | `traefik/http/middlewares/Middleware24/stripPrefix/prefixes/1` | `foobar` | | `traefik/http/middlewares/Middleware25/stripPrefixRegex/regex/0` | `foobar` | @@ -342,6 +349,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/tcp/services/TCPService01/loadBalancer/servers/1/address` | `foobar` | | `traefik/tcp/services/TCPService01/loadBalancer/servers/1/tls` | `true` | | `traefik/tcp/services/TCPService01/loadBalancer/serversTransport` | `foobar` | +| `traefik/tcp/services/TCPService01/loadBalancer/terminationDelay` | `42` | | `traefik/tcp/services/TCPService02/weighted/services/0/name` | `foobar` | | `traefik/tcp/services/TCPService02/weighted/services/0/weight` | `42` | | `traefik/tcp/services/TCPService02/weighted/services/1/name` | `foobar` | @@ -365,6 +373,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/tls/options/Options0/curvePreferences/1` | `foobar` | | `traefik/tls/options/Options0/maxVersion` | `foobar` | | `traefik/tls/options/Options0/minVersion` | `foobar` | +| `traefik/tls/options/Options0/preferServerCipherSuites` | `true` | | `traefik/tls/options/Options0/sniStrict` | `true` | | `traefik/tls/options/Options1/alpnProtocols/0` | `foobar` | | `traefik/tls/options/Options1/alpnProtocols/1` | `foobar` | @@ -377,6 +386,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/tls/options/Options1/curvePreferences/1` | `foobar` | | `traefik/tls/options/Options1/maxVersion` | `foobar` | | `traefik/tls/options/Options1/minVersion` | `foobar` | +| `traefik/tls/options/Options1/preferServerCipherSuites` | `true` | | `traefik/tls/options/Options1/sniStrict` | `true` | | `traefik/tls/stores/Store0/defaultCertificate/certFile` | `foobar` | | `traefik/tls/stores/Store0/defaultCertificate/keyFile` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutetcps.yaml b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutetcps.yaml index 0f95de8d2..6a68b0b0a 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutetcps.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutetcps.yaml @@ -116,6 +116,18 @@ spec: between Traefik and your servers. Can only be used on a Kubernetes Service. type: string + terminationDelay: + description: 'TerminationDelay defines the deadline that + the proxy sets, after one of its connected peers indicates + it has closed the writing capability of its connection, + to close the reading capability as well, hence fully + terminating the connection. It is a duration in milliseconds, + defaulting to 100. A negative value means an infinite + deadline (i.e. the reading capability is never closed). + Deprecated: TerminationDelay is not supported APIVersion + traefik.io/v1, please use ServersTransport to configure + the TerminationDelay instead.' + type: integer tls: description: TLS determines whether to use TLS when dialing with the backend. diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index b7d8c6b52..0e49f9092 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -190,9 +190,17 @@ spec: type: object contentType: description: ContentType holds the content-type middleware configuration. - This middleware sets the `Content-Type` header value to the media - type detected from the response content, when it is not set by the - backend. + This middleware exists to enable the correct behavior until at least + the default one can be changed in a future version. + properties: + autoDetect: + description: 'AutoDetect specifies whether to let the `Content-Type` + header, if it has not been set by the backend, be automatically + set to a value derived from the contents of the response. Deprecated: + AutoDetect option is deprecated, Content-Type middleware is + only meant to be used to enable the content-type detection, + please remove any usage of this option.' + type: boolean type: object digestAuth: description: 'DigestAuth holds the digest auth middleware configuration. @@ -383,6 +391,10 @@ spec: description: TLS defines the configuration used to secure the connection to the authentication server. properties: + caOptional: + description: 'Deprecated: TLS client authentication is a server + side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634).' + type: boolean caSecret: description: CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate. @@ -501,6 +513,10 @@ spec: description: CustomResponseHeaders defines the header names and values to apply to the response. type: object + featurePolicy: + description: 'Deprecated: FeaturePolicy option is deprecated, + please use PermissionsPolicy instead.' + type: string forceSTSHeader: description: ForceSTSHeader defines whether to add the STS header even when the connection is HTTP. @@ -536,6 +552,14 @@ spec: value. This allows sites to control whether browsers forward the Referer header to other sites. type: string + sslForceHost: + description: 'Deprecated: SSLForceHost option is deprecated, please + use RedirectRegex instead.' + type: boolean + sslHost: + description: 'Deprecated: SSLHost option is deprecated, please + use RedirectRegex instead.' + type: string sslProxyHeaders: additionalProperties: type: string @@ -544,6 +568,14 @@ spec: useful when using other proxies (example: "X-Forwarded-Proto": "https").' type: object + sslRedirect: + description: 'Deprecated: SSLRedirect option is deprecated, please + use EntryPoint redirection or RedirectScheme instead.' + type: boolean + sslTemporaryRedirect: + description: 'Deprecated: SSLTemporaryRedirect option is deprecated, + please use EntryPoint redirection or RedirectScheme instead.' + type: boolean stsIncludeSubdomains: description: STSIncludeSubdomains defines whether the includeSubDomains directive is appended to the Strict-Transport-Security header. @@ -915,6 +947,12 @@ spec: This middleware removes the specified prefixes from the URL path. More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/stripprefix/' properties: + forceSlash: + description: 'Deprecated: ForceSlash option is deprecated, please + remove any usage of this option. ForceSlash ensures that the + resulting stripped path is not the empty string, by replacing + it with / when necessary. Default: true.' + type: boolean prefixes: description: Prefixes defines the prefixes to strip from the request URL. diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewaretcps.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewaretcps.yaml index 616e48b9c..f72579c65 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewaretcps.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewaretcps.yaml @@ -46,7 +46,9 @@ spec: type: integer type: object ipAllowList: - description: IPAllowList defines the IPAllowList middleware configuration. + description: 'IPAllowList defines the IPAllowList middleware configuration. + This middleware accepts/refuses connections based on the client + IP. More info: https://doc.traefik.io/traefik/v3.0/middlewares/tcp/ipallowlist/' properties: sourceRange: description: SourceRange defines the allowed IPs (or ranges of @@ -57,7 +59,8 @@ spec: type: object ipWhiteList: description: 'IPWhiteList defines the IPWhiteList middleware configuration. - Deprecated: please use IPAllowList instead.' + This middleware accepts/refuses connections based on the client + IP. Deprecated: please use IPAllowList instead. More info: https://doc.traefik.io/traefik/v3.0/middlewares/tcp/ipwhitelist/' properties: sourceRange: description: SourceRange defines the allowed IPs (or ranges of diff --git a/docs/content/reference/dynamic-configuration/traefik.io_tlsoptions.yaml b/docs/content/reference/dynamic-configuration/traefik.io_tlsoptions.yaml index 925e4c025..ae829b34a 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_tlsoptions.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_tlsoptions.yaml @@ -86,6 +86,12 @@ spec: will accept. Possible values: VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13. Default: VersionTLS10.' type: string + preferServerCipherSuites: + description: 'PreferServerCipherSuites defines whether the server + chooses a cipher suite among his own instead of among the client''s. + It is enabled automatically when minVersion or maxVersion is set. + Deprecated: https://github.com/golang/go/issues/45430' + type: boolean sniStrict: description: SniStrict defines whether Traefik allows connections from clients connections that do not specify a server_name extension. diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index 7ac5ce78c..57dd2466a 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -1617,6 +1617,46 @@ Below are the available options for the PROXY protocol: version = 1 ``` +#### Termination Delay + +!!! warning + + Deprecated in favor of [`serversTransport.terminationDelay`](#terminationdelay). + Please note that if any `serversTransport` configuration on the servers load balancer is found, + it will take precedence over the servers load balancer `terminationDelay` value, + even if the `serversTransport.terminationDelay` is undefined. + +As a proxy between a client and a server, it can happen that either side (e.g. client side) decides to terminate its writing capability on the connection (i.e. issuance of a FIN packet). +The proxy needs to propagate that intent to the other side, and so when that happens, it also does the same on its connection with the other side (e.g. backend side). + +However, if for some reason (bad implementation, or malicious intent) the other side does not eventually do the same as well, +the connection would stay half-open, which would lock resources for however long. + +To that end, as soon as the proxy enters this termination sequence, it sets a deadline on fully terminating the connections on both sides. + +The termination delay controls that deadline. +It is a duration in milliseconds, defaulting to 100. +A negative value means an infinite deadline (i.e. the connection is never fully terminated by the proxy itself). + +??? example "A Service with a termination delay -- Using the [File Provider](../../providers/file.md)" + + ```yaml tab="YAML" + ## Dynamic configuration + tcp: + services: + my-service: + loadBalancer: + terminationDelay: 200 + ``` + + ```toml tab="TOML" + ## Dynamic configuration + [tcp.services] + [tcp.services.my-service.loadBalancer] + [[tcp.services.my-service.loadBalancer]] + terminationDelay = 200 + ``` + ### Weighted Round Robin The Weighted Round Robin (alias `WRR`) load-balancer of services is in charge of balancing the requests between multiple services based on provided weights. diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 966a45177..34631cbe4 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -393,6 +393,18 @@ spec: between Traefik and your servers. Can only be used on a Kubernetes Service. type: string + terminationDelay: + description: 'TerminationDelay defines the deadline that + the proxy sets, after one of its connected peers indicates + it has closed the writing capability of its connection, + to close the reading capability as well, hence fully + terminating the connection. It is a duration in milliseconds, + defaulting to 100. A negative value means an infinite + deadline (i.e. the reading capability is never closed). + Deprecated: TerminationDelay is not supported APIVersion + traefik.io/v1, please use ServersTransport to configure + the TerminationDelay instead.' + type: integer tls: description: TLS determines whether to use TLS when dialing with the backend. @@ -779,9 +791,17 @@ spec: type: object contentType: description: ContentType holds the content-type middleware configuration. - This middleware sets the `Content-Type` header value to the media - type detected from the response content, when it is not set by the - backend. + This middleware exists to enable the correct behavior until at least + the default one can be changed in a future version. + properties: + autoDetect: + description: 'AutoDetect specifies whether to let the `Content-Type` + header, if it has not been set by the backend, be automatically + set to a value derived from the contents of the response. Deprecated: + AutoDetect option is deprecated, Content-Type middleware is + only meant to be used to enable the content-type detection, + please remove any usage of this option.' + type: boolean type: object digestAuth: description: 'DigestAuth holds the digest auth middleware configuration. @@ -972,6 +992,10 @@ spec: description: TLS defines the configuration used to secure the connection to the authentication server. properties: + caOptional: + description: 'Deprecated: TLS client authentication is a server + side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634).' + type: boolean caSecret: description: CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate. @@ -1090,6 +1114,10 @@ spec: description: CustomResponseHeaders defines the header names and values to apply to the response. type: object + featurePolicy: + description: 'Deprecated: FeaturePolicy option is deprecated, + please use PermissionsPolicy instead.' + type: string forceSTSHeader: description: ForceSTSHeader defines whether to add the STS header even when the connection is HTTP. @@ -1125,6 +1153,14 @@ spec: value. This allows sites to control whether browsers forward the Referer header to other sites. type: string + sslForceHost: + description: 'Deprecated: SSLForceHost option is deprecated, please + use RedirectRegex instead.' + type: boolean + sslHost: + description: 'Deprecated: SSLHost option is deprecated, please + use RedirectRegex instead.' + type: string sslProxyHeaders: additionalProperties: type: string @@ -1133,6 +1169,14 @@ spec: useful when using other proxies (example: "X-Forwarded-Proto": "https").' type: object + sslRedirect: + description: 'Deprecated: SSLRedirect option is deprecated, please + use EntryPoint redirection or RedirectScheme instead.' + type: boolean + sslTemporaryRedirect: + description: 'Deprecated: SSLTemporaryRedirect option is deprecated, + please use EntryPoint redirection or RedirectScheme instead.' + type: boolean stsIncludeSubdomains: description: STSIncludeSubdomains defines whether the includeSubDomains directive is appended to the Strict-Transport-Security header. @@ -1504,6 +1548,12 @@ spec: This middleware removes the specified prefixes from the URL path. More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/stripprefix/' properties: + forceSlash: + description: 'Deprecated: ForceSlash option is deprecated, please + remove any usage of this option. ForceSlash ensures that the + resulting stripped path is not the empty string, by replacing + it with / when necessary. Default: true.' + type: boolean prefixes: description: Prefixes defines the prefixes to strip from the request URL. @@ -1578,7 +1628,9 @@ spec: type: integer type: object ipAllowList: - description: IPAllowList defines the IPAllowList middleware configuration. + description: 'IPAllowList defines the IPAllowList middleware configuration. + This middleware accepts/refuses connections based on the client + IP. More info: https://doc.traefik.io/traefik/v3.0/middlewares/tcp/ipallowlist/' properties: sourceRange: description: SourceRange defines the allowed IPs (or ranges of @@ -1589,7 +1641,8 @@ spec: type: object ipWhiteList: description: 'IPWhiteList defines the IPWhiteList middleware configuration. - Deprecated: please use IPAllowList instead.' + This middleware accepts/refuses connections based on the client + IP. Deprecated: please use IPAllowList instead. More info: https://doc.traefik.io/traefik/v3.0/middlewares/tcp/ipwhitelist/' properties: sourceRange: description: SourceRange defines the allowed IPs (or ranges of @@ -1940,6 +1993,12 @@ spec: will accept. Possible values: VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13. Default: VersionTLS10.' type: string + preferServerCipherSuites: + description: 'PreferServerCipherSuites defines whether the server + chooses a cipher suite among his own instead of among the client''s. + It is enabled automatically when minVersion or maxVersion is set. + Deprecated: https://github.com/golang/go/issues/45430' + type: boolean sniStrict: description: SniStrict defines whether Traefik allows connections from clients connections that do not specify a server_name extension. diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index fd1c20d33..a9be47306 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -6,7 +6,6 @@ import ( ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v3/pkg/ip" - "github.com/traefik/traefik/v3/pkg/types" ) // +k8s:deepcopy-gen=true @@ -55,9 +54,13 @@ type GrpcWeb struct { // +k8s:deepcopy-gen=true // ContentType holds the content-type middleware configuration. -// This middleware sets the `Content-Type` header value to the media type detected from the response content, -// when it is not set by the backend. -type ContentType struct{} +// This middleware exists to enable the correct behavior until at least the default one can be changed in a future version. +type ContentType struct { + // AutoDetect specifies whether to let the `Content-Type` header, if it has not been set by the backend, + // be automatically set to a value derived from the contents of the response. + // Deprecated: AutoDetect option is deprecated, Content-Type middleware is only meant to be used to enable the content-type detection, please remove any usage of this option. + AutoDetect *bool `json:"autoDetect,omitempty" toml:"autoDetect,omitempty" yaml:"autoDetect,omitempty" export:"true"` +} // +k8s:deepcopy-gen=true @@ -218,7 +221,7 @@ type ForwardAuth struct { // Address defines the authentication server address. Address string `json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"` // TLS defines the configuration used to secure the connection to the authentication server. - TLS *types.ClientTLS `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"` + TLS *ClientTLS `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"` // TrustForwardHeader defines whether to trust (ie: forward) all X-Forwarded-* headers. TrustForwardHeader bool `json:"trustForwardHeader,omitempty" toml:"trustForwardHeader,omitempty" yaml:"trustForwardHeader,omitempty" export:"true"` // AuthResponseHeaders defines the list of headers to copy from the authentication server response and set on forwarded request, replacing any existing conflicting headers. @@ -235,6 +238,20 @@ type ForwardAuth struct { // +k8s:deepcopy-gen=true +// ClientTLS holds TLS specific configurations as client +// CA, Cert and Key can be either path or file contents. +// TODO: remove this struct when CAOptional option will be removed. +type ClientTLS struct { + CA string `description:"TLS CA" json:"ca,omitempty" toml:"ca,omitempty" yaml:"ca,omitempty"` + Cert string `description:"TLS cert" json:"cert,omitempty" toml:"cert,omitempty" yaml:"cert,omitempty"` + Key string `description:"TLS key" json:"key,omitempty" toml:"key,omitempty" yaml:"key,omitempty" loggable:"false"` + InsecureSkipVerify bool `description:"TLS insecure skip verify" json:"insecureSkipVerify,omitempty" toml:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty" export:"true"` + // Deprecated: TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634). + CAOptional *bool `description:"TLS CA.Optional" json:"caOptional,omitempty" toml:"caOptional,omitempty" yaml:"caOptional,omitempty" export:"true"` +} + +// +k8s:deepcopy-gen=true + // Headers holds the headers middleware configuration. // This middleware manages the requests and responses headers. // More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/headers/#customrequestheaders @@ -303,6 +320,17 @@ type Headers struct { // If you would like your development environment to mimic production with complete Host blocking, SSL redirects, // and STS headers, leave this as false. IsDevelopment bool `json:"isDevelopment,omitempty" toml:"isDevelopment,omitempty" yaml:"isDevelopment,omitempty" export:"true"` + + // Deprecated: FeaturePolicy option is deprecated, please use PermissionsPolicy instead. + FeaturePolicy *string `json:"featurePolicy,omitempty" toml:"featurePolicy,omitempty" yaml:"featurePolicy,omitempty" export:"true"` + // Deprecated: SSLRedirect option is deprecated, please use EntryPoint redirection or RedirectScheme instead. + SSLRedirect *bool `json:"sslRedirect,omitempty" toml:"sslRedirect,omitempty" yaml:"sslRedirect,omitempty" export:"true"` + // Deprecated: SSLTemporaryRedirect option is deprecated, please use EntryPoint redirection or RedirectScheme instead. + SSLTemporaryRedirect *bool `json:"sslTemporaryRedirect,omitempty" toml:"sslTemporaryRedirect,omitempty" yaml:"sslTemporaryRedirect,omitempty" export:"true"` + // Deprecated: SSLHost option is deprecated, please use RedirectRegex instead. + SSLHost *string `json:"sslHost,omitempty" toml:"sslHost,omitempty" yaml:"sslHost,omitempty"` + // Deprecated: SSLForceHost option is deprecated, please use RedirectRegex instead. + SSLForceHost *bool `json:"sslForceHost,omitempty" toml:"sslForceHost,omitempty" yaml:"sslForceHost,omitempty" export:"true"` } // HasCustomHeadersDefined checks to see if any of the custom header elements have been set. @@ -327,6 +355,10 @@ func (h *Headers) HasCorsHeadersDefined() bool { func (h *Headers) HasSecureHeadersDefined() bool { return h != nil && (len(h.AllowedHosts) != 0 || len(h.HostsProxyHeaders) != 0 || + (h.SSLRedirect != nil && *h.SSLRedirect) || + (h.SSLTemporaryRedirect != nil && *h.SSLTemporaryRedirect) || + (h.SSLForceHost != nil && *h.SSLForceHost) || + (h.SSLHost != nil && *h.SSLHost != "") || len(h.SSLProxyHeaders) != 0 || h.STSSeconds != 0 || h.STSIncludeSubdomains || @@ -340,6 +372,7 @@ func (h *Headers) HasSecureHeadersDefined() bool { h.ContentSecurityPolicy != "" || h.PublicKey != "" || h.ReferrerPolicy != "" || + (h.FeaturePolicy != nil && *h.FeaturePolicy != "") || h.PermissionsPolicy != "" || h.IsDevelopment) } @@ -557,6 +590,11 @@ type Retry struct { type StripPrefix struct { // Prefixes defines the prefixes to strip from the request URL. Prefixes []string `json:"prefixes,omitempty" toml:"prefixes,omitempty" yaml:"prefixes,omitempty" export:"true"` + + // Deprecated: ForceSlash option is deprecated, please remove any usage of this option. + // ForceSlash ensures that the resulting stripped path is not the empty string, by replacing it with / when necessary. + // Default: true. + ForceSlash *bool `json:"forceSlash,omitempty" toml:"forceSlash,omitempty" yaml:"forceSlash,omitempty" export:"true"` } // +k8s:deepcopy-gen=true diff --git a/pkg/config/dynamic/tcp_config.go b/pkg/config/dynamic/tcp_config.go index 4bf82364f..db3484973 100644 --- a/pkg/config/dynamic/tcp_config.go +++ b/pkg/config/dynamic/tcp_config.go @@ -86,6 +86,14 @@ type TCPServersLoadBalancer struct { ProxyProtocol *ProxyProtocol `json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` Servers []TCPServer `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server" export:"true"` ServersTransport string `json:"serversTransport,omitempty" toml:"serversTransport,omitempty" yaml:"serversTransport,omitempty" export:"true"` + + // TerminationDelay, corresponds to the deadline that the proxy sets, after one + // of its connected peers indicates it has closed the writing capability of its + // connection, to close the reading capability as well, hence fully terminating the + // connection. It is a duration in milliseconds, defaulting to 100. A negative value + // means an infinite deadline (i.e. the reading capability is never closed). + // Deprecated: use ServersTransport to configure the TerminationDelay instead. + TerminationDelay *int `json:"terminationDelay,omitempty" toml:"terminationDelay,omitempty" yaml:"terminationDelay,omitempty" export:"true"` } // Mergeable tells if the given service is mergeable. diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 85b513b2a..f04de4275 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -124,6 +124,27 @@ func (in *CircuitBreaker) DeepCopy() *CircuitBreaker { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientTLS) DeepCopyInto(out *ClientTLS) { + *out = *in + if in.CAOptional != nil { + in, out := &in.CAOptional, &out.CAOptional + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientTLS. +func (in *ClientTLS) DeepCopy() *ClientTLS { + if in == nil { + return nil + } + out := new(ClientTLS) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Compress) DeepCopyInto(out *Compress) { *out = *in @@ -219,6 +240,11 @@ func (in Configurations) DeepCopy() Configurations { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ContentType) DeepCopyInto(out *ContentType) { *out = *in + if in.AutoDetect != nil { + in, out := &in.AutoDetect, &out.AutoDetect + *out = new(bool) + **out = **in + } return } @@ -316,8 +342,8 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) { *out = *in if in.TLS != nil { in, out := &in.TLS, &out.TLS - *out = new(types.ClientTLS) - **out = **in + *out = new(ClientTLS) + (*in).DeepCopyInto(*out) } if in.AuthResponseHeaders != nil { in, out := &in.AuthResponseHeaders, &out.AuthResponseHeaders @@ -534,6 +560,31 @@ func (in *Headers) DeepCopyInto(out *Headers) { (*out)[key] = val } } + if in.FeaturePolicy != nil { + in, out := &in.FeaturePolicy, &out.FeaturePolicy + *out = new(string) + **out = **in + } + if in.SSLRedirect != nil { + in, out := &in.SSLRedirect, &out.SSLRedirect + *out = new(bool) + **out = **in + } + if in.SSLTemporaryRedirect != nil { + in, out := &in.SSLTemporaryRedirect, &out.SSLTemporaryRedirect + *out = new(bool) + **out = **in + } + if in.SSLHost != nil { + in, out := &in.SSLHost, &out.SSLHost + *out = new(string) + **out = **in + } + if in.SSLForceHost != nil { + in, out := &in.SSLForceHost, &out.SSLForceHost + *out = new(bool) + **out = **in + } return } @@ -794,7 +845,7 @@ func (in *Middleware) DeepCopyInto(out *Middleware) { if in.ContentType != nil { in, out := &in.ContentType, &out.ContentType *out = new(ContentType) - **out = **in + (*in).DeepCopyInto(*out) } if in.GrpcWeb != nil { in, out := &in.GrpcWeb, &out.GrpcWeb @@ -1360,6 +1411,11 @@ func (in *StripPrefix) DeepCopyInto(out *StripPrefix) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.ForceSlash != nil { + in, out := &in.ForceSlash, &out.ForceSlash + *out = new(bool) + **out = **in + } return } @@ -1650,6 +1706,11 @@ func (in *TCPServersLoadBalancer) DeepCopyInto(out *TCPServersLoadBalancer) { *out = make([]TCPServer, len(*in)) copy(*out, *in) } + if in.TerminationDelay != nil { + in, out := &in.TerminationDelay, &out.TerminationDelay + *out = new(int) + **out = **in + } return } diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 79f3df14b..c6ffb1534 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -9,9 +9,11 @@ import ( "github.com/stretchr/testify/require" ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v3/pkg/config/dynamic" - "github.com/traefik/traefik/v3/pkg/types" ) +func Bool(v bool) *bool { return &v } +func String(v string) *string { return &v } + func TestDecodeConfiguration(t *testing.T) { labels := map[string]string{ "traefik.http.middlewares.Middleware0.addprefix.prefix": "foobar", @@ -43,6 +45,7 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.middlewares.Middleware7.forwardauth.authresponseheaders": "foobar, fiibar", "traefik.http.middlewares.Middleware7.forwardauth.authrequestheaders": "foobar, fiibar", "traefik.http.middlewares.Middleware7.forwardauth.tls.ca": "foobar", + "traefik.http.middlewares.Middleware7.forwardauth.tls.caoptional": "true", "traefik.http.middlewares.Middleware7.forwardauth.tls.cert": "foobar", "traefik.http.middlewares.Middleware7.forwardauth.tls.insecureskipverify": "true", "traefik.http.middlewares.Middleware7.forwardauth.tls.key": "foobar", @@ -71,9 +74,14 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.middlewares.Middleware8.headers.isdevelopment": "true", "traefik.http.middlewares.Middleware8.headers.publickey": "foobar", "traefik.http.middlewares.Middleware8.headers.referrerpolicy": "foobar", + "traefik.http.middlewares.Middleware8.headers.featurepolicy": "foobar", "traefik.http.middlewares.Middleware8.headers.permissionspolicy": "foobar", + "traefik.http.middlewares.Middleware8.headers.sslforcehost": "true", + "traefik.http.middlewares.Middleware8.headers.sslhost": "foobar", "traefik.http.middlewares.Middleware8.headers.sslproxyheaders.name0": "foobar", "traefik.http.middlewares.Middleware8.headers.sslproxyheaders.name1": "foobar", + "traefik.http.middlewares.Middleware8.headers.sslredirect": "true", + "traefik.http.middlewares.Middleware8.headers.ssltemporaryredirect": "true", "traefik.http.middlewares.Middleware8.headers.stsincludesubdomains": "true", "traefik.http.middlewares.Middleware8.headers.stspreload": "true", "traefik.http.middlewares.Middleware8.headers.stsseconds": "42", @@ -124,6 +132,7 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.middlewares.Middleware16.retry.attempts": "42", "traefik.http.middlewares.Middleware16.retry.initialinterval": "1s", "traefik.http.middlewares.Middleware17.stripprefix.prefixes": "foobar, fiibar", + "traefik.http.middlewares.Middleware17.stripprefix.forceslash": "true", "traefik.http.middlewares.Middleware18.stripprefixregex.regex": "foobar, fiibar", "traefik.http.middlewares.Middleware19.compress.minresponsebodybytes": "42", "traefik.http.middlewares.Middleware20.plugin.tomato.aaa": "foo1", @@ -194,9 +203,11 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.tcp.routers.Router1.tls.options": "foo", "traefik.tcp.routers.Router1.tls.passthrough": "false", "traefik.tcp.services.Service0.loadbalancer.server.Port": "42", + "traefik.tcp.services.Service0.loadbalancer.TerminationDelay": "42", "traefik.tcp.services.Service0.loadbalancer.proxyProtocol.version": "42", "traefik.tcp.services.Service0.loadbalancer.serversTransport": "foo", "traefik.tcp.services.Service1.loadbalancer.server.Port": "42", + "traefik.tcp.services.Service1.loadbalancer.TerminationDelay": "42", "traefik.tcp.services.Service1.loadbalancer.proxyProtocol": "true", "traefik.tcp.services.Service1.loadbalancer.serversTransport": "foo", @@ -261,6 +272,7 @@ func TestDecodeConfiguration(t *testing.T) { Port: "42", }, }, + TerminationDelay: func(i int) *int { return &i }(42), ProxyProtocol: &dynamic.ProxyProtocol{Version: 42}, ServersTransport: "foo", }, @@ -272,6 +284,7 @@ func TestDecodeConfiguration(t *testing.T) { Port: "42", }, }, + TerminationDelay: func(i int) *int { return &i }(42), ProxyProtocol: &dynamic.ProxyProtocol{Version: 2}, ServersTransport: "foo", }, @@ -459,6 +472,7 @@ func TestDecodeConfiguration(t *testing.T) { "foobar", "fiibar", }, + ForceSlash: Bool(true), }, }, "Middleware18": { @@ -525,11 +539,12 @@ func TestDecodeConfiguration(t *testing.T) { "Middleware7": { ForwardAuth: &dynamic.ForwardAuth{ Address: "foobar", - TLS: &types.ClientTLS{ + TLS: &dynamic.ClientTLS{ CA: "foobar", Cert: "foobar", Key: "foobar", InsecureSkipVerify: true, + CAOptional: Bool(true), }, TrustForwardHeader: true, AuthResponseHeaders: []string{ @@ -583,10 +598,14 @@ func TestDecodeConfiguration(t *testing.T) { "foobar", "fiibar", }, + SSLRedirect: Bool(true), + SSLTemporaryRedirect: Bool(true), + SSLHost: String("foobar"), SSLProxyHeaders: map[string]string{ "name0": "foobar", "name1": "foobar", }, + SSLForceHost: Bool(true), STSSeconds: 42, STSIncludeSubdomains: true, STSPreload: true, @@ -599,6 +618,7 @@ func TestDecodeConfiguration(t *testing.T) { ContentSecurityPolicy: "foobar", PublicKey: "foobar", ReferrerPolicy: "foobar", + FeaturePolicy: String("foobar"), PermissionsPolicy: "foobar", IsDevelopment: true, }, @@ -758,6 +778,7 @@ func TestEncodeConfiguration(t *testing.T) { }, }, ServersTransport: "foo", + TerminationDelay: func(i int) *int { return &i }(42), }, }, "Service1": { @@ -768,6 +789,7 @@ func TestEncodeConfiguration(t *testing.T) { }, }, ServersTransport: "foo", + TerminationDelay: func(i int) *int { return &i }(42), }, }, }, @@ -952,6 +974,7 @@ func TestEncodeConfiguration(t *testing.T) { "foobar", "fiibar", }, + ForceSlash: Bool(true), }, }, "Middleware18": { @@ -1026,11 +1049,12 @@ func TestEncodeConfiguration(t *testing.T) { "Middleware7": { ForwardAuth: &dynamic.ForwardAuth{ Address: "foobar", - TLS: &types.ClientTLS{ + TLS: &dynamic.ClientTLS{ CA: "foobar", Cert: "foobar", Key: "foobar", InsecureSkipVerify: true, + CAOptional: Bool(true), }, TrustForwardHeader: true, AuthResponseHeaders: []string{ @@ -1084,10 +1108,14 @@ func TestEncodeConfiguration(t *testing.T) { "foobar", "fiibar", }, + SSLRedirect: Bool(true), + SSLTemporaryRedirect: Bool(true), + SSLHost: String("foobar"), SSLProxyHeaders: map[string]string{ "name0": "foobar", "name1": "foobar", }, + SSLForceHost: Bool(true), STSSeconds: 42, STSIncludeSubdomains: true, STSPreload: true, @@ -1100,6 +1128,7 @@ func TestEncodeConfiguration(t *testing.T) { ContentSecurityPolicy: "foobar", PublicKey: "foobar", ReferrerPolicy: "foobar", + FeaturePolicy: String("foobar"), PermissionsPolicy: "foobar", IsDevelopment: true, }, @@ -1222,6 +1251,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.AuthResponseHeaders": "foobar, fiibar", "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.AuthRequestHeaders": "foobar, fiibar", "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.CA": "foobar", + "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.CAOptional": "true", "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Cert": "foobar", "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.InsecureSkipVerify": "true", "traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Key": "foobar", @@ -1250,9 +1280,14 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware8.Headers.IsDevelopment": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.PublicKey": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.ReferrerPolicy": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.FeaturePolicy": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.PermissionsPolicy": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.SSLForceHost": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.SSLHost": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.SSLProxyHeaders.name0": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.SSLProxyHeaders.name1": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.SSLRedirect": "true", + "traefik.HTTP.Middlewares.Middleware8.Headers.SSLTemporaryRedirect": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.STSIncludeSubdomains": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.STSPreload": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.STSSeconds": "42", @@ -1304,6 +1339,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware16.Retry.Attempts": "42", "traefik.HTTP.Middlewares.Middleware16.Retry.InitialInterval": "1000000000", "traefik.HTTP.Middlewares.Middleware17.StripPrefix.Prefixes": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware17.StripPrefix.ForceSlash": "true", "traefik.HTTP.Middlewares.Middleware18.StripPrefixRegex.Regex": "foobar, fiibar", "traefik.HTTP.Middlewares.Middleware19.Compress.MinResponseBodyBytes": "42", "traefik.HTTP.Middlewares.Middleware20.Plugin.tomato.aaa": "foo1", @@ -1373,9 +1409,11 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.TCP.Services.Service0.LoadBalancer.server.Port": "42", "traefik.TCP.Services.Service0.LoadBalancer.server.TLS": "false", "traefik.TCP.Services.Service0.LoadBalancer.ServersTransport": "foo", + "traefik.TCP.Services.Service0.LoadBalancer.TerminationDelay": "42", "traefik.TCP.Services.Service1.LoadBalancer.server.Port": "42", "traefik.TCP.Services.Service1.LoadBalancer.server.TLS": "false", "traefik.TCP.Services.Service1.LoadBalancer.ServersTransport": "foo", + "traefik.TCP.Services.Service1.LoadBalancer.TerminationDelay": "42", "traefik.UDP.Routers.Router0.EntryPoints": "foobar, fiibar", "traefik.UDP.Routers.Router0.Service": "foobar", diff --git a/pkg/middlewares/auth/forward.go b/pkg/middlewares/auth/forward.go index 6e3b3ac84..daa2dbd27 100644 --- a/pkg/middlewares/auth/forward.go +++ b/pkg/middlewares/auth/forward.go @@ -15,6 +15,7 @@ import ( "github.com/traefik/traefik/v3/pkg/middlewares" "github.com/traefik/traefik/v3/pkg/middlewares/connectionheader" "github.com/traefik/traefik/v3/pkg/tracing" + "github.com/traefik/traefik/v3/pkg/types" "github.com/vulcand/oxy/v2/forward" "github.com/vulcand/oxy/v2/utils" "go.opentelemetry.io/otel/trace" @@ -53,7 +54,8 @@ type forwardAuth struct { // NewForward creates a forward auth middleware. func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAuth, name string) (http.Handler, error) { - middlewares.GetLogger(ctx, name, typeNameForward).Debug().Msg("Creating middleware") + logger := middlewares.GetLogger(ctx, name, typeNameForward) + logger.Debug().Msg("Creating middleware") addAuthCookiesToResponse := make(map[string]struct{}) for _, cookieName := range config.AddAuthCookiesToResponse { @@ -79,7 +81,18 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu } if config.TLS != nil { - tlsConfig, err := config.TLS.CreateTLSConfig(ctx) + if config.TLS.CAOptional != nil { + logger.Warn().Msg("CAOptional option is deprecated, TLS client authentication is a server side option, please remove any usage of this option.") + } + + clientTLS := &types.ClientTLS{ + CA: config.TLS.CA, + Cert: config.TLS.Cert, + Key: config.TLS.Key, + InsecureSkipVerify: config.TLS.InsecureSkipVerify, + } + + tlsConfig, err := clientTLS.CreateTLSConfig(ctx) if err != nil { return nil, fmt.Errorf("unable to create client TLS configuration: %w", err) } diff --git a/pkg/middlewares/contenttype/content_type.go b/pkg/middlewares/contenttype/content_type.go index 763f7587e..4d9af4a73 100644 --- a/pkg/middlewares/contenttype/content_type.go +++ b/pkg/middlewares/contenttype/content_type.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/middlewares" ) @@ -18,8 +19,19 @@ type contentType struct { } // New creates a new handler. -func New(ctx context.Context, next http.Handler, name string) (http.Handler, error) { - middlewares.GetLogger(ctx, name, typeName).Debug().Msg("Creating middleware") +func New(ctx context.Context, next http.Handler, config dynamic.ContentType, name string) (http.Handler, error) { + logger := middlewares.GetLogger(ctx, name, typeName) + logger.Debug().Msg("Creating middleware") + + if config.AutoDetect != nil { + logger.Warn().Msg("AutoDetect option is deprecated, Content-Type middleware is only meant to be used to enable the content-type detection, please remove any usage of this option.") + + // Disable content-type detection (idempotent). + if !*config.AutoDetect { + return next, nil + } + } + return &contentType{next: next, name: name}, nil } diff --git a/pkg/middlewares/contenttype/content_type_test.go b/pkg/middlewares/contenttype/content_type_test.go index 24facb0d2..9f311b5bc 100644 --- a/pkg/middlewares/contenttype/content_type_test.go +++ b/pkg/middlewares/contenttype/content_type_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/testhelpers" ) @@ -60,7 +61,7 @@ func TestAutoDetection(t *testing.T) { if test.autoDetect { var err error - next, err = New(context.Background(), next, "foo-content-type") + next, err = New(context.Background(), next, dynamic.ContentType{}, "foo-content-type") require.NoError(t, err) } diff --git a/pkg/middlewares/stripprefix/strip_prefix.go b/pkg/middlewares/stripprefix/strip_prefix.go index 18717875e..8483f5100 100644 --- a/pkg/middlewares/stripprefix/strip_prefix.go +++ b/pkg/middlewares/stripprefix/strip_prefix.go @@ -21,15 +21,27 @@ type stripPrefix struct { next http.Handler prefixes []string name string + + // Deprecated: Must be removed (breaking), the default behavior must be forceSlash=false + forceSlash bool } // New creates a new strip prefix middleware. func New(ctx context.Context, next http.Handler, config dynamic.StripPrefix, name string) (http.Handler, error) { - middlewares.GetLogger(ctx, name, typeName).Debug().Msg("Creating middleware") + logger := middlewares.GetLogger(ctx, name, typeName) + logger.Debug().Msg("Creating middleware") + + if config.ForceSlash != nil { + logger.Warn().Msgf("`ForceSlash` option is deprecated, please remove any usage of this option.") + } + // Handle default value (here because of deprecation and the removal of setDefault). + forceSlash := config.ForceSlash != nil && *config.ForceSlash + return &stripPrefix{ - prefixes: config.Prefixes, - next: next, - name: name, + prefixes: config.Prefixes, + next: next, + name: name, + forceSlash: forceSlash, }, nil } @@ -58,6 +70,13 @@ func (s *stripPrefix) serveRequest(rw http.ResponseWriter, req *http.Request, pr } func (s *stripPrefix) getPrefixStripped(urlPath, prefix string) string { + if s.forceSlash { + // Only for compatibility reason with the previous behavior, + // but the previous behavior is wrong. + // This needs to be removed in the next breaking version. + return "/" + strings.TrimPrefix(strings.TrimPrefix(urlPath, prefix), "/") + } + return ensureLeadingSlash(strings.TrimPrefix(urlPath, prefix)) } diff --git a/pkg/middlewares/stripprefix/strip_prefix_test.go b/pkg/middlewares/stripprefix/strip_prefix_test.go index 612a282f1..cb2c58826 100644 --- a/pkg/middlewares/stripprefix/strip_prefix_test.go +++ b/pkg/middlewares/stripprefix/strip_prefix_test.go @@ -146,6 +146,9 @@ func TestStripPrefix(t *testing.T) { requestURI = r.RequestURI }) + pointer := func(v bool) *bool { return &v } + test.config.ForceSlash = pointer(false) + handler, err := New(context.Background(), next, test.config, "foo-strip-prefix") require.NoError(t, err) diff --git a/pkg/provider/kubernetes/crd/client_mock_test.go b/pkg/provider/kubernetes/crd/client_mock_test.go deleted file mode 100644 index 548966123..000000000 --- a/pkg/provider/kubernetes/crd/client_mock_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package crd - -import ( - "fmt" - "os" - "path/filepath" - - traefikv1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" - "github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s" - corev1 "k8s.io/api/core/v1" - kscheme "k8s.io/client-go/kubernetes/scheme" -) - -var _ Client = (*clientMock)(nil) - -func init() { - // required by k8s.MustParseYaml - err := traefikv1alpha1.AddToScheme(kscheme.Scheme) - if err != nil { - panic(err) - } -} - -type clientMock struct { - services []*corev1.Service - secrets []*corev1.Secret - endpoints []*corev1.Endpoints - - apiServiceError error - apiSecretError error - apiEndpointsError error - - ingressRoutes []*traefikv1alpha1.IngressRoute - ingressRouteTCPs []*traefikv1alpha1.IngressRouteTCP - ingressRouteUDPs []*traefikv1alpha1.IngressRouteUDP - middlewares []*traefikv1alpha1.Middleware - middlewareTCPs []*traefikv1alpha1.MiddlewareTCP - tlsOptions []*traefikv1alpha1.TLSOption - tlsStores []*traefikv1alpha1.TLSStore - traefikServices []*traefikv1alpha1.TraefikService - serversTransports []*traefikv1alpha1.ServersTransport - serversTransportTCPs []*traefikv1alpha1.ServersTransportTCP - - watchChan chan interface{} -} - -func newClientMock(paths ...string) clientMock { - var c clientMock - - for _, path := range paths { - yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path)) - if err != nil { - panic(err) - } - - k8sObjects := k8s.MustParseYaml(yamlContent) - for _, obj := range k8sObjects { - switch o := obj.(type) { - case *corev1.Service: - c.services = append(c.services, o) - case *corev1.Endpoints: - c.endpoints = append(c.endpoints, o) - case *traefikv1alpha1.IngressRoute: - c.ingressRoutes = append(c.ingressRoutes, o) - case *traefikv1alpha1.IngressRouteTCP: - c.ingressRouteTCPs = append(c.ingressRouteTCPs, o) - case *traefikv1alpha1.IngressRouteUDP: - c.ingressRouteUDPs = append(c.ingressRouteUDPs, o) - case *traefikv1alpha1.Middleware: - c.middlewares = append(c.middlewares, o) - case *traefikv1alpha1.MiddlewareTCP: - c.middlewareTCPs = append(c.middlewareTCPs, o) - case *traefikv1alpha1.TraefikService: - c.traefikServices = append(c.traefikServices, o) - case *traefikv1alpha1.TLSOption: - c.tlsOptions = append(c.tlsOptions, o) - case *traefikv1alpha1.ServersTransport: - c.serversTransports = append(c.serversTransports, o) - case *traefikv1alpha1.ServersTransportTCP: - c.serversTransportTCPs = append(c.serversTransportTCPs, o) - case *traefikv1alpha1.TLSStore: - c.tlsStores = append(c.tlsStores, o) - case *corev1.Secret: - c.secrets = append(c.secrets, o) - default: - panic(fmt.Sprintf("Unknown runtime object %+v %T", o, o)) - } - } - } - - return c -} - -func (c clientMock) GetIngressRoutes() []*traefikv1alpha1.IngressRoute { - return c.ingressRoutes -} - -func (c clientMock) GetIngressRouteTCPs() []*traefikv1alpha1.IngressRouteTCP { - return c.ingressRouteTCPs -} - -func (c clientMock) GetIngressRouteUDPs() []*traefikv1alpha1.IngressRouteUDP { - return c.ingressRouteUDPs -} - -func (c clientMock) GetMiddlewares() []*traefikv1alpha1.Middleware { - return c.middlewares -} - -func (c clientMock) GetMiddlewareTCPs() []*traefikv1alpha1.MiddlewareTCP { - return c.middlewareTCPs -} - -func (c clientMock) GetTraefikService(namespace, name string) (*traefikv1alpha1.TraefikService, bool, error) { - for _, svc := range c.traefikServices { - if svc.Namespace == namespace && svc.Name == name { - return svc, true, nil - } - } - - return nil, false, nil -} - -func (c clientMock) GetTraefikServices() []*traefikv1alpha1.TraefikService { - return c.traefikServices -} - -func (c clientMock) GetTLSOptions() []*traefikv1alpha1.TLSOption { - return c.tlsOptions -} - -func (c clientMock) GetTLSStores() []*traefikv1alpha1.TLSStore { - return c.tlsStores -} - -func (c clientMock) GetServersTransports() []*traefikv1alpha1.ServersTransport { - return c.serversTransports -} - -func (c clientMock) GetServersTransportTCPs() []*traefikv1alpha1.ServersTransportTCP { - return c.serversTransportTCPs -} - -func (c clientMock) GetTLSOption(namespace, name string) (*traefikv1alpha1.TLSOption, bool, error) { - for _, option := range c.tlsOptions { - if option.Namespace == namespace && option.Name == name { - return option, true, nil - } - } - - return nil, false, nil -} - -func (c clientMock) GetService(namespace, name string) (*corev1.Service, bool, error) { - if c.apiServiceError != nil { - return nil, false, c.apiServiceError - } - - for _, service := range c.services { - if service.Namespace == namespace && service.Name == name { - return service, true, nil - } - } - return nil, false, c.apiServiceError -} - -func (c clientMock) GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error) { - if c.apiEndpointsError != nil { - return nil, false, c.apiEndpointsError - } - - for _, endpoints := range c.endpoints { - if endpoints.Namespace == namespace && endpoints.Name == name { - return endpoints, true, nil - } - } - - return &corev1.Endpoints{}, false, nil -} - -func (c clientMock) GetSecret(namespace, name string) (*corev1.Secret, bool, error) { - if c.apiSecretError != nil { - return nil, false, c.apiSecretError - } - - for _, secret := range c.secrets { - if secret.Namespace == namespace && secret.Name == name { - return secret, true, nil - } - } - return nil, false, nil -} - -func (c clientMock) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) { - return c.watchChan, nil -} diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml index 8c18ffdc8..1c49644f2 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml @@ -166,7 +166,7 @@ subsets: apiVersion: v1 kind: Service metadata: - name: external-svc + name: external-svc-tcp namespace: default spec: externalName: external.domain @@ -176,7 +176,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: external.service.with.port + name: external.service.with.port.tcp namespace: default spec: externalName: external.domain @@ -186,19 +186,6 @@ spec: protocol: TCP port: 80 ---- -apiVersion: v1 -kind: Service -metadata: - name: external.service.without.port - namespace: default -spec: - externalName: external.domain - type: ExternalName - ports: - - name: http - protocol: TCP - --- apiVersion: v1 kind: Service @@ -266,7 +253,7 @@ metadata: apiVersion: v1 kind: Service metadata: - name: native-svc + name: native-svc-tcp namespace: default spec: diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname.yml index 34a105c34..3fc47607d 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname.yml @@ -11,5 +11,5 @@ spec: routes: - match: HostSNI(`foo.com`) services: - - name: external-svc + - name: external-svc-tcp port: 8000 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_with_port.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_with_port.yml index f31764a3e..980a8cb96 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_with_port.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_with_port.yml @@ -11,5 +11,5 @@ spec: routes: - match: HostSNI(`foo.com`) services: - - name: external.service.with.port + - name: external.service.with.port.tcp port: 80 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_without_ports.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_without_ports.yml index 1065d1380..8c0ba1a99 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_without_ports.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_without_ports.yml @@ -11,4 +11,4 @@ spec: routes: - match: HostSNI(`foo.com`) services: - - name: external-svc + - name: external-svc-tcp diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_native_service_lb.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_native_service_lb.yml index f95202ec0..c2dbfca31 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/with_native_service_lb.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_native_service_lb.yml @@ -11,6 +11,6 @@ spec: routes: - match: HostSNI(`foo.com`) services: - - name: native-svc + - name: native-svc-tcp port: 8000 nativeLB: true diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/services.yml b/pkg/provider/kubernetes/crd/fixtures/udp/services.yml index adaa7272e..90d75d059 100644 --- a/pkg/provider/kubernetes/crd/fixtures/udp/services.yml +++ b/pkg/provider/kubernetes/crd/fixtures/udp/services.yml @@ -150,7 +150,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: external-svc + name: external-svc-udp namespace: default spec: externalName: external.domain @@ -160,7 +160,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: external.service.with.port + name: external.service.with.port.udp namespace: default spec: externalName: external.domain @@ -170,19 +170,6 @@ spec: protocol: TCP port: 80 ---- -apiVersion: v1 -kind: Service -metadata: - name: external.service.without.port - namespace: default -spec: - externalName: external.domain - type: ExternalName - ports: - - name: http - protocol: TCP - --- kind: Endpoints apiVersion: v1 @@ -225,7 +212,7 @@ metadata: apiVersion: v1 kind: Service metadata: - name: native-svc + name: native-svc-udp namespace: default spec: diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname.yml b/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname.yml index dbd919422..c0beba368 100644 --- a/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname.yml +++ b/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname.yml @@ -10,5 +10,5 @@ spec: routes: - services: - - name: external-svc + - name: external-svc-udp port: 8000 diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_service.yml b/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_service.yml index 302332c0c..d642b55c9 100644 --- a/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_service.yml +++ b/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_service.yml @@ -10,5 +10,5 @@ spec: routes: - services: - - name: external.service.with.port + - name: external.service.with.port.udp port: 80 diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_with_port.yml b/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_with_port.yml index 302332c0c..d642b55c9 100644 --- a/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_with_port.yml +++ b/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_with_port.yml @@ -10,5 +10,5 @@ spec: routes: - services: - - name: external.service.with.port + - name: external.service.with.port.udp port: 80 diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_without_ports.yml b/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_without_ports.yml index 2a30279bb..606f2029b 100644 --- a/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_without_ports.yml +++ b/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_without_ports.yml @@ -10,4 +10,4 @@ spec: routes: - services: - - name: external-svc + - name: external-svc-udp diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/with_native_service_lb.yml b/pkg/provider/kubernetes/crd/fixtures/udp/with_native_service_lb.yml index 6942f106b..3390bbac2 100644 --- a/pkg/provider/kubernetes/crd/fixtures/udp/with_native_service_lb.yml +++ b/pkg/provider/kubernetes/crd/fixtures/udp/with_native_service_lb.yml @@ -10,6 +10,6 @@ spec: routes: - services: - - name: native-svc + - name: native-svc-udp port: 8000 nativeLB: true diff --git a/pkg/provider/kubernetes/crd/fixtures/with_default_tls_options_default_namespace.yml b/pkg/provider/kubernetes/crd/fixtures/with_default_tls_options_default_namespace.yml index ec84b589d..b477eefcb 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_default_tls_options_default_namespace.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_default_tls_options_default_namespace.yml @@ -40,7 +40,7 @@ spec: apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: - name: test.route + name: test.route.default namespace: default spec: diff --git a/pkg/provider/kubernetes/crd/fixtures/with_default_tls_store.yml b/pkg/provider/kubernetes/crd/fixtures/with_default_tls_store.yml index 36de8bd50..902a5fc2c 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_default_tls_store.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_default_tls_store.yml @@ -23,7 +23,7 @@ data: apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: - name: test.route + name: test.route.default namespace: default spec: diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 45b6634af..50a5595f8 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -735,7 +735,7 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traef return forwardAuth, nil } - forwardAuth.TLS = &types.ClientTLS{ + forwardAuth.TLS = &dynamic.ClientTLS{ InsecureSkipVerify: auth.TLS.InsecureSkipVerify, } @@ -756,6 +756,8 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traef forwardAuth.TLS.Key = authSecretKey } + forwardAuth.TLS.CAOptional = auth.TLS.CAOptional + return forwardAuth, nil } @@ -1008,8 +1010,9 @@ func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options CAFiles: clientCAs, ClientAuthType: tlsOption.Spec.ClientAuth.ClientAuthType, }, - SniStrict: tlsOption.Spec.SniStrict, - ALPNProtocols: alpnProtocols, + SniStrict: tlsOption.Spec.SniStrict, + ALPNProtocols: alpnProtocols, + PreferServerCipherSuites: tlsOption.Spec.PreferServerCipherSuites, } } diff --git a/pkg/provider/kubernetes/crd/kubernetes_tcp.go b/pkg/provider/kubernetes/crd/kubernetes_tcp.go index d5fb434a3..f41fe125e 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_tcp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go @@ -204,6 +204,10 @@ func (p *Provider) createLoadBalancerServerTCP(client Client, parentNamespace st } } + if service.ServersTransport == "" && service.TerminationDelay != nil { + tcpService.LoadBalancer.TerminationDelay = service.TerminationDelay + } + if service.ServersTransport != "" { tcpService.LoadBalancer.ServersTransport, err = p.makeTCPServersTransportKey(parentNamespace, service.ServersTransport) if err != nil { diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index a6d2c64ca..c851140c9 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" kubefake "k8s.io/client-go/kubernetes/fake" + kscheme "k8s.io/client-go/kubernetes/scheme" ) var _ provider.Provider = (*Provider)(nil) @@ -30,6 +31,14 @@ var _ provider.Provider = (*Provider)(nil) func Int(v int) *int { return &v } func Bool(v bool) *bool { return &v } +func init() { + // required by k8s.MustParseYaml + err := traefikv1alpha1.AddToScheme(kscheme.Scheme) + if err != nil { + panic(err) + } +} + func TestLoadIngressRouteTCPs(t *testing.T) { testCases := []struct { desc string @@ -1035,6 +1044,7 @@ func TestLoadIngressRouteTCPs(t *testing.T) { Services: map[string]*dynamic.TCPService{ "default-test.route-fdd3e9338e47a45efefc": { LoadBalancer: &dynamic.TCPServersLoadBalancer{ + TerminationDelay: Int(500), Servers: []dynamic.TCPServer{ { Address: "10.10.0.1:8000", @@ -1568,6 +1578,23 @@ func TestLoadIngressRouteTCPs(t *testing.T) { return } + k8sObjects, crdObjects := readResources(t, test.paths) + + kubeClient := kubefake.NewSimpleClientset(k8sObjects...) + crdClient := traefikcrdfake.NewSimpleClientset(crdObjects...) + + client := newClientImpl(kubeClient, crdClient) + + stopCh := make(chan struct{}) + + eventCh, err := client.WatchAll(nil, stopCh) + require.NoError(t, err) + + if k8sObjects != nil || crdObjects != nil { + // just wait for the first event + <-eventCh + } + p := Provider{ IngressClass: test.ingressClass, AllowCrossNamespace: true, @@ -1575,8 +1602,7 @@ func TestLoadIngressRouteTCPs(t *testing.T) { AllowEmptyServices: test.allowEmptyServices, } - clientMock := newClientMock(test.paths...) - conf := p.loadConfigurationFromCRD(context.Background(), clientMock) + conf := p.loadConfigurationFromCRD(context.Background(), client) assert.Equal(t, test.expected, conf) }) } @@ -3063,6 +3089,15 @@ func TestLoadIngressRoutes(t *testing.T) { Options: "default-foo", }, }, + "default-test-route-default-6b204d94623b3df4370c": { + EntryPoints: []string{"web"}, + Service: "default-test-route-default-6b204d94623b3df4370c", + Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", + Priority: 12, + TLS: &dynamic.RouterTLSConfig{ + Options: "default-foo", + }, + }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ @@ -3082,6 +3117,22 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, }, + "default-test-route-default-6b204d94623b3df4370c": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, }, ServersTransports: map[string]*dynamic.ServersTransport{}, }, @@ -3602,7 +3653,7 @@ func TestLoadIngressRoutes(t *testing.T) { "default-forwardauth": { ForwardAuth: &dynamic.ForwardAuth{ Address: "test.com", - TLS: &types.ClientTLS{ + TLS: &dynamic.ClientTLS{ CA: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----", Cert: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----", Key: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----", @@ -4017,6 +4068,13 @@ func TestLoadIngressRoutes(t *testing.T) { Priority: 12, TLS: &dynamic.RouterTLSConfig{}, }, + "default-test-route-default-6b204d94623b3df4370c": { + EntryPoints: []string{"web"}, + Service: "default-test-route-default-6b204d94623b3df4370c", + Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", + Priority: 12, + TLS: &dynamic.RouterTLSConfig{}, + }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ @@ -4036,6 +4094,22 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, }, + "default-test-route-default-6b204d94623b3df4370c": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, }, ServersTransports: map[string]*dynamic.ServersTransport{}, }, @@ -4521,6 +4595,23 @@ func TestLoadIngressRoutes(t *testing.T) { return } + k8sObjects, crdObjects := readResources(t, test.paths) + + kubeClient := kubefake.NewSimpleClientset(k8sObjects...) + crdClient := traefikcrdfake.NewSimpleClientset(crdObjects...) + + client := newClientImpl(kubeClient, crdClient) + + stopCh := make(chan struct{}) + + eventCh, err := client.WatchAll(nil, stopCh) + require.NoError(t, err) + + if k8sObjects != nil || crdObjects != nil { + // just wait for the first event + <-eventCh + } + p := Provider{ IngressClass: test.ingressClass, AllowCrossNamespace: test.allowCrossNamespace, @@ -4528,8 +4619,7 @@ func TestLoadIngressRoutes(t *testing.T) { AllowEmptyServices: test.allowEmptyServices, } - clientMock := newClientMock(test.paths...) - conf := p.loadConfigurationFromCRD(context.Background(), clientMock) + conf := p.loadConfigurationFromCRD(context.Background(), client) assert.Equal(t, test.expected, conf) }) } @@ -5016,6 +5106,23 @@ func TestLoadIngressRouteUDPs(t *testing.T) { return } + k8sObjects, crdObjects := readResources(t, test.paths) + + kubeClient := kubefake.NewSimpleClientset(k8sObjects...) + crdClient := traefikcrdfake.NewSimpleClientset(crdObjects...) + + client := newClientImpl(kubeClient, crdClient) + + stopCh := make(chan struct{}) + + eventCh, err := client.WatchAll(nil, stopCh) + require.NoError(t, err) + + if k8sObjects != nil || crdObjects != nil { + // just wait for the first event + <-eventCh + } + p := Provider{ IngressClass: test.ingressClass, AllowCrossNamespace: true, @@ -5023,8 +5130,7 @@ func TestLoadIngressRouteUDPs(t *testing.T) { AllowEmptyServices: test.allowEmptyServices, } - clientMock := newClientMock(test.paths...) - conf := p.loadConfigurationFromCRD(context.Background(), clientMock) + conf := p.loadConfigurationFromCRD(context.Background(), client) assert.Equal(t, test.expected, conf) }) } @@ -6435,43 +6541,7 @@ func TestCrossNamespace(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - var k8sObjects []runtime.Object - var crdObjects []runtime.Object - for _, path := range test.paths { - yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path)) - if err != nil { - panic(err) - } - - objects := k8s.MustParseYaml(yamlContent) - for _, obj := range objects { - switch o := obj.(type) { - case *corev1.Service, *corev1.Endpoints, *corev1.Secret: - k8sObjects = append(k8sObjects, o) - case *traefikv1alpha1.IngressRoute: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.IngressRouteTCP: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.IngressRouteUDP: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.Middleware: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.MiddlewareTCP: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.TraefikService: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.TLSOption: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.TLSStore: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.ServersTransport: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.ServersTransportTCP: - crdObjects = append(crdObjects, o) - default: - } - } - } + k8sObjects, crdObjects := readResources(t, test.paths) kubeClient := kubefake.NewSimpleClientset(k8sObjects...) crdClient := traefikcrdfake.NewSimpleClientset(crdObjects...) @@ -6742,37 +6812,7 @@ func TestExternalNameService(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - var k8sObjects []runtime.Object - var crdObjects []runtime.Object - for _, path := range test.paths { - yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path)) - if err != nil { - panic(err) - } - - objects := k8s.MustParseYaml(yamlContent) - for _, obj := range objects { - switch o := obj.(type) { - case *corev1.Service, *corev1.Endpoints, *corev1.Secret: - k8sObjects = append(k8sObjects, o) - case *traefikv1alpha1.IngressRoute: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.IngressRouteTCP: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.IngressRouteUDP: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.Middleware: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.TraefikService: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.TLSOption: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.TLSStore: - crdObjects = append(crdObjects, o) - default: - } - } - } + k8sObjects, crdObjects := readResources(t, test.paths) kubeClient := kubefake.NewSimpleClientset(k8sObjects...) crdClient := traefikcrdfake.NewSimpleClientset(crdObjects...) @@ -6955,37 +6995,7 @@ func TestNativeLB(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - var k8sObjects []runtime.Object - var crdObjects []runtime.Object - for _, path := range test.paths { - yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path)) - if err != nil { - panic(err) - } - - objects := k8s.MustParseYaml(yamlContent) - for _, obj := range objects { - switch o := obj.(type) { - case *corev1.Service, *corev1.Endpoints, *corev1.Secret: - k8sObjects = append(k8sObjects, o) - case *traefikv1alpha1.IngressRoute: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.IngressRouteTCP: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.IngressRouteUDP: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.Middleware: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.TraefikService: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.TLSOption: - crdObjects = append(crdObjects, o) - case *traefikv1alpha1.TLSStore: - crdObjects = append(crdObjects, o) - default: - } - } - } + k8sObjects, crdObjects := readResources(t, test.paths) kubeClient := kubefake.NewSimpleClientset(k8sObjects...) crdClient := traefikcrdfake.NewSimpleClientset(crdObjects...) @@ -7071,3 +7081,28 @@ func TestCreateBasicAuthCredentials(t *testing.T) { assert.Equal(t, "$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", hashedPassword) assert.True(t, auth.CheckSecret("test2", hashedPassword)) } + +func readResources(t *testing.T, paths []string) ([]runtime.Object, []runtime.Object) { + t.Helper() + + var k8sObjects []runtime.Object + var crdObjects []runtime.Object + for _, path := range paths { + yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path)) + if err != nil { + panic(err) + } + + objects := k8s.MustParseYaml(yamlContent) + for _, obj := range objects { + switch obj.GetObjectKind().GroupVersionKind().Group { + case "traefik.io": + crdObjects = append(crdObjects, obj) + default: + k8sObjects = append(k8sObjects, obj) + } + } + } + + return k8sObjects, crdObjects +} diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go index 5cfd69d4a..9ff6897db 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroutetcp.go @@ -72,6 +72,13 @@ type ServiceTCP struct { Port intstr.IntOrString `json:"port"` // Weight defines the weight used when balancing requests between multiple Kubernetes Service. Weight *int `json:"weight,omitempty"` + // TerminationDelay defines the deadline that the proxy sets, after one of its connected peers indicates + // it has closed the writing capability of its connection, to close the reading capability as well, + // hence fully terminating the connection. + // It is a duration in milliseconds, defaulting to 100. + // A negative value means an infinite deadline (i.e. the reading capability is never closed). + // Deprecated: TerminationDelay is not supported APIVersion traefik.io/v1, please use ServersTransport to configure the TerminationDelay instead. + TerminationDelay *int `json:"terminationDelay,omitempty"` // ProxyProtocol defines the PROXY protocol configuration. // More info: https://doc.traefik.io/traefik/v3.0/routing/services/#proxy-protocol ProxyProtocol *dynamic.ProxyProtocol `json:"proxyProtocol,omitempty"` diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go index 1378dc85d..f4ade76a0 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go @@ -171,6 +171,9 @@ type ClientTLS struct { CertSecret string `json:"certSecret,omitempty"` // InsecureSkipVerify defines whether the server certificates should be validated. InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` + + // Deprecated: TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634). + CAOptional *bool `json:"caOptional,omitempty"` } // +k8s:deepcopy-gen=true diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middlewaretcp.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middlewaretcp.go index d15402a26..f58736ae8 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middlewaretcp.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middlewaretcp.go @@ -26,9 +26,13 @@ type MiddlewareTCPSpec struct { // InFlightConn defines the InFlightConn middleware configuration. InFlightConn *dynamic.TCPInFlightConn `json:"inFlightConn,omitempty"` // IPWhiteList defines the IPWhiteList middleware configuration. + // This middleware accepts/refuses connections based on the client IP. // Deprecated: please use IPAllowList instead. + // More info: https://doc.traefik.io/traefik/v3.0/middlewares/tcp/ipwhitelist/ IPWhiteList *dynamic.TCPIPWhiteList `json:"ipWhiteList,omitempty"` // IPAllowList defines the IPAllowList middleware configuration. + // This middleware accepts/refuses connections based on the client IP. + // More info: https://doc.traefik.io/traefik/v3.0/middlewares/tcp/ipallowlist/ IPAllowList *dynamic.TCPIPAllowList `json:"ipAllowList,omitempty"` } diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go index f8132c138..8b330c468 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go @@ -44,6 +44,11 @@ type TLSOptionSpec struct { // ALPNProtocols defines the list of supported application level protocols for the TLS handshake, in order of preference. // More info: https://doc.traefik.io/traefik/v3.0/https/tls/#alpn-protocols ALPNProtocols []string `json:"alpnProtocols,omitempty"` + + // PreferServerCipherSuites defines whether the server chooses a cipher suite among his own instead of among the client's. + // It is enabled automatically when minVersion or maxVersion is set. + // Deprecated: https://github.com/golang/go/issues/45430 + PreferServerCipherSuites *bool `json:"preferServerCipherSuites,omitempty"` } // +k8s:deepcopy-gen=true diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go index 40c9b979d..547f0de4e 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go @@ -146,6 +146,11 @@ func (in *ClientAuth) DeepCopy() *ClientAuth { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClientTLS) DeepCopyInto(out *ClientTLS) { *out = *in + if in.CAOptional != nil { + in, out := &in.CAOptional, &out.CAOptional + *out = new(bool) + **out = **in + } return } @@ -213,7 +218,7 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) { if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(ClientTLS) - **out = **in + (*in).DeepCopyInto(*out) } if in.AddAuthCookiesToResponse != nil { in, out := &in.AddAuthCookiesToResponse, &out.AddAuthCookiesToResponse @@ -777,7 +782,7 @@ func (in *MiddlewareSpec) DeepCopyInto(out *MiddlewareSpec) { if in.ContentType != nil { in, out := &in.ContentType, &out.ContentType *out = new(dynamic.ContentType) - **out = **in + (*in).DeepCopyInto(*out) } if in.GrpcWeb != nil { in, out := &in.GrpcWeb, &out.GrpcWeb @@ -1318,6 +1323,11 @@ func (in *ServiceTCP) DeepCopyInto(out *ServiceTCP) { *out = new(int) **out = **in } + if in.TerminationDelay != nil { + in, out := &in.TerminationDelay, &out.TerminationDelay + *out = new(int) + **out = **in + } if in.ProxyProtocol != nil { in, out := &in.ProxyProtocol, &out.ProxyProtocol *out = new(dynamic.ProxyProtocol) @@ -1517,6 +1527,11 @@ func (in *TLSOptionSpec) DeepCopyInto(out *TLSOptionSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.PreferServerCipherSuites != nil { + in, out := &in.PreferServerCipherSuites, &out.PreferServerCipherSuites + *out = new(bool) + **out = **in + } return } diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index 8f63b5cb1..438871031 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -15,6 +15,9 @@ import ( "github.com/traefik/traefik/v3/pkg/types" ) +func Bool(v bool) *bool { return &v } +func String(v string) *string { return &v } + func Test_buildConfiguration(t *testing.T) { provider := newProviderMock(mapToPairs(map[string]string{ "traefik/http/routers/Router0/entryPoints/0": "foobar", @@ -79,6 +82,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/middlewares/Middleware08/forwardAuth/tls/key": "foobar", "traefik/http/middlewares/Middleware08/forwardAuth/tls/insecureSkipVerify": "true", "traefik/http/middlewares/Middleware08/forwardAuth/tls/ca": "foobar", + "traefik/http/middlewares/Middleware08/forwardAuth/tls/caOptional": "true", "traefik/http/middlewares/Middleware08/forwardAuth/tls/cert": "foobar", "traefik/http/middlewares/Middleware08/forwardAuth/address": "foobar", "traefik/http/middlewares/Middleware08/forwardAuth/trustForwardHeader": "true", @@ -105,8 +109,12 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/middlewares/Middleware09/headers/accessControlAllowOriginListRegex/1": "foobar", "traefik/http/middlewares/Middleware09/headers/contentTypeNosniff": "true", "traefik/http/middlewares/Middleware09/headers/accessControlAllowCredentials": "true", + "traefik/http/middlewares/Middleware09/headers/featurePolicy": "foobar", "traefik/http/middlewares/Middleware09/headers/permissionsPolicy": "foobar", "traefik/http/middlewares/Middleware09/headers/forceSTSHeader": "true", + "traefik/http/middlewares/Middleware09/headers/sslRedirect": "true", + "traefik/http/middlewares/Middleware09/headers/sslHost": "foobar", + "traefik/http/middlewares/Middleware09/headers/sslForceHost": "true", "traefik/http/middlewares/Middleware09/headers/sslProxyHeaders/name1": "foobar", "traefik/http/middlewares/Middleware09/headers/sslProxyHeaders/name0": "foobar", "traefik/http/middlewares/Middleware09/headers/allowedHosts/0": "foobar", @@ -125,6 +133,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/middlewares/Middleware09/headers/addVaryHeader": "true", "traefik/http/middlewares/Middleware09/headers/hostsProxyHeaders/0": "foobar", "traefik/http/middlewares/Middleware09/headers/hostsProxyHeaders/1": "foobar", + "traefik/http/middlewares/Middleware09/headers/sslTemporaryRedirect": "true", "traefik/http/middlewares/Middleware09/headers/customBrowserXSSValue": "foobar", "traefik/http/middlewares/Middleware09/headers/referrerPolicy": "foobar", "traefik/http/middlewares/Middleware09/headers/accessControlExposeHeaders/0": "foobar", @@ -201,6 +210,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/middlewares/Middleware18/retry/attempts": "42", "traefik/http/middlewares/Middleware19/stripPrefix/prefixes/0": "foobar", "traefik/http/middlewares/Middleware19/stripPrefix/prefixes/1": "foobar", + "traefik/http/middlewares/Middleware19/stripPrefix/forceSlash": "true", "traefik/tcp/routers/TCPRouter0/entryPoints/0": "foobar", "traefik/tcp/routers/TCPRouter0/entryPoints/1": "foobar", "traefik/tcp/routers/TCPRouter0/service": "foobar", @@ -227,6 +237,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/tcp/routers/TCPRouter1/tls/passthrough": "true", "traefik/tcp/routers/TCPRouter1/tls/options": "foobar", "traefik/tcp/routers/TCPRouter1/tls/certResolver": "foobar", + "traefik/tcp/services/TCPService01/loadBalancer/terminationDelay": "42", "traefik/tcp/services/TCPService01/loadBalancer/servers/0/address": "foobar", "traefik/tcp/services/TCPService01/loadBalancer/servers/1/address": "foobar", "traefik/tcp/services/TCPService02/weighted/services/0/name": "foobar", @@ -371,6 +382,7 @@ func Test_buildConfiguration(t *testing.T) { "foobar", "foobar", }, + ForceSlash: Bool(true), }, }, "Middleware00": { @@ -404,11 +416,12 @@ func Test_buildConfiguration(t *testing.T) { "Middleware08": { ForwardAuth: &dynamic.ForwardAuth{ Address: "foobar", - TLS: &types.ClientTLS{ + TLS: &dynamic.ClientTLS{ CA: "foobar", Cert: "foobar", Key: "foobar", InsecureSkipVerify: true, + CAOptional: Bool(true), }, TrustForwardHeader: true, AuthResponseHeaders: []string{ @@ -581,10 +594,14 @@ func Test_buildConfiguration(t *testing.T) { "foobar", "foobar", }, + SSLRedirect: Bool(true), + SSLTemporaryRedirect: Bool(true), + SSLHost: String("foobar"), SSLProxyHeaders: map[string]string{ "name1": "foobar", "name0": "foobar", }, + SSLForceHost: Bool(true), STSSeconds: 42, STSIncludeSubdomains: true, STSPreload: true, @@ -597,6 +614,7 @@ func Test_buildConfiguration(t *testing.T) { ContentSecurityPolicy: "foobar", PublicKey: "foobar", ReferrerPolicy: "foobar", + FeaturePolicy: String("foobar"), PermissionsPolicy: "foobar", IsDevelopment: true, }, @@ -757,6 +775,7 @@ func Test_buildConfiguration(t *testing.T) { Services: map[string]*dynamic.TCPService{ "TCPService01": { LoadBalancer: &dynamic.TCPServersLoadBalancer{ + TerminationDelay: func(v int) *int { return &v }(42), Servers: []dynamic.TCPServer{ {Address: "foobar"}, {Address: "foobar"}, diff --git a/pkg/redactor/redactor_config_test.go b/pkg/redactor/redactor_config_test.go index c3ef9ef9d..fe90db3d3 100644 --- a/pkg/redactor/redactor_config_test.go +++ b/pkg/redactor/redactor_config_test.go @@ -263,7 +263,7 @@ func init() { }, ForwardAuth: &dynamic.ForwardAuth{ Address: "127.0.0.1", - TLS: &types.ClientTLS{ + TLS: &dynamic.ClientTLS{ CA: "ca.pem", Cert: "cert.pem", Key: "cert.pem", diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index 72da39663..655c1a047 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -184,7 +184,7 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( return nil, badConf } middleware = func(next http.Handler) (http.Handler, error) { - return contenttype.New(ctx, next, middlewareName) + return contenttype.New(ctx, next, *config.ContentType, middlewareName) } } @@ -240,7 +240,8 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( // IPWhiteList if config.IPWhiteList != nil { - log.Warn().Msg("IPWhiteList is deprecated, please use IPAllowList instead.") + qualifiedName := provider.GetQualifiedName(ctx, middlewareName) + log.Warn().Msgf("Middleware %q of type IPWhiteList is deprecated, please use IPAllowList instead.", qualifiedName) if middleware != nil { return nil, badConf diff --git a/pkg/server/service/tcp/service.go b/pkg/server/service/tcp/service.go index bbc19b8cd..64c173a51 100644 --- a/pkg/server/service/tcp/service.go +++ b/pkg/server/service/tcp/service.go @@ -13,6 +13,7 @@ import ( "github.com/traefik/traefik/v3/pkg/logs" "github.com/traefik/traefik/v3/pkg/server/provider" "github.com/traefik/traefik/v3/pkg/tcp" + "golang.org/x/net/proxy" ) // Manager is the TCPHandlers factory. @@ -53,6 +54,10 @@ func (m *Manager) BuildTCP(rootCtx context.Context, serviceName string) (tcp.Han case conf.LoadBalancer != nil: loadBalancer := tcp.NewWRRLoadBalancer() + if conf.LoadBalancer.TerminationDelay != nil { + log.Ctx(ctx).Warn().Msgf("Service %q load balancer uses `TerminationDelay`, but this option is deprecated, please use ServersTransport configuration instead.", serviceName) + } + if len(conf.LoadBalancer.ServersTransport) > 0 { conf.LoadBalancer.ServersTransport = provider.GetQualifiedName(ctx, conf.LoadBalancer.ServersTransport) } @@ -72,6 +77,14 @@ func (m *Manager) BuildTCP(rootCtx context.Context, serviceName string) (tcp.Han return nil, err } + // Handle TerminationDelay deprecated option. + if conf.LoadBalancer.ServersTransport == "" && conf.LoadBalancer.TerminationDelay != nil { + dialer = &dialerWrapper{ + Dialer: dialer, + terminationDelay: time.Duration(*conf.LoadBalancer.TerminationDelay), + } + } + handler, err := tcp.NewProxy(server.Address, conf.LoadBalancer.ProxyProtocol, dialer) if err != nil { srvLogger.Error().Err(err).Msg("Failed to create server") @@ -113,3 +126,13 @@ func shuffle[T any](values []T, r *rand.Rand) []T { return shuffled } + +// dialerWrapper is only used to handle TerminationDelay deprecated option on TCPServersLoadBalancer. +type dialerWrapper struct { + proxy.Dialer + terminationDelay time.Duration +} + +func (d dialerWrapper) TerminationDelay() time.Duration { + return d.terminationDelay +} diff --git a/pkg/tls/tls.go b/pkg/tls/tls.go index fa12cbc85..5b1827c97 100644 --- a/pkg/tls/tls.go +++ b/pkg/tls/tls.go @@ -25,6 +25,9 @@ type Options struct { ClientAuth ClientAuth `json:"clientAuth,omitempty" toml:"clientAuth,omitempty" yaml:"clientAuth,omitempty"` SniStrict bool `json:"sniStrict,omitempty" toml:"sniStrict,omitempty" yaml:"sniStrict,omitempty" export:"true"` ALPNProtocols []string `json:"alpnProtocols,omitempty" toml:"alpnProtocols,omitempty" yaml:"alpnProtocols,omitempty" export:"true"` + + // Deprecated: https://github.com/golang/go/issues/45430 + PreferServerCipherSuites *bool `json:"preferServerCipherSuites,omitempty" toml:"preferServerCipherSuites,omitempty" yaml:"preferServerCipherSuites,omitempty" export:"true"` } // SetDefaults sets the default values for an Options struct. diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go index 790745d98..b07cfe507 100644 --- a/pkg/tls/tlsmanager.go +++ b/pkg/tls/tlsmanager.go @@ -68,6 +68,13 @@ func (m *Manager) UpdateConfigs(ctx context.Context, stores map[string]Store, co defer m.lock.Unlock() m.configs = configs + for optionName, option := range m.configs { + // Handle `PreferServerCipherSuites` depreciation + if option.PreferServerCipherSuites != nil { + log.Ctx(ctx).Warn().Msgf("TLSOption %q uses `PreferServerCipherSuites` option, but this option is deprecated and ineffective, please remove this option.", optionName) + } + } + m.storesConfig = stores m.certs = certs diff --git a/pkg/tls/zz_generated.deepcopy.go b/pkg/tls/zz_generated.deepcopy.go index 26461ab0d..59ffcb2bf 100644 --- a/pkg/tls/zz_generated.deepcopy.go +++ b/pkg/tls/zz_generated.deepcopy.go @@ -116,6 +116,11 @@ func (in *Options) DeepCopyInto(out *Options) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.PreferServerCipherSuites != nil { + in, out := &in.PreferServerCipherSuites, &out.PreferServerCipherSuites + *out = new(bool) + **out = **in + } return } From 4d539273ad71ba1b6c96c8d2f4a30ad177d99906 Mon Sep 17 00:00:00 2001 From: Asad Rizvi Date: Mon, 29 Jan 2024 19:50:05 +0200 Subject: [PATCH 23/36] docs: include ECS as supported backend --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dc52334eb..8a017443e 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ _(But if you'd rather configure some of your routes manually, Traefik supports t - [Kubernetes](https://doc.traefik.io/traefik/providers/kubernetes-crd/) - [Marathon](https://doc.traefik.io/traefik/providers/marathon/) - [Rancher](https://doc.traefik.io/traefik/providers/rancher/) (Metadata) +- [ECS](https://doc.traefik.io/traefik/providers/ecs/) - [File](https://doc.traefik.io/traefik/providers/file/) ## Quickstart From d02be003ab9d37d19cf8e95c58124e7e31b371c6 Mon Sep 17 00:00:00 2001 From: Aofei Sheng Date: Tue, 30 Jan 2024 21:56:05 +0800 Subject: [PATCH 24/36] Add SO_REUSEPORT support for EntryPoints --- .../reference/static-configuration/cli-ref.md | 3 + .../reference/static-configuration/env-ref.md | 3 + .../reference/static-configuration/file.toml | 1 + .../reference/static-configuration/file.yaml | 1 + docs/content/routing/entrypoints.md | 73 +++++++++++++++++++ go.mod | 2 +- pkg/config/static/entrypoints.go | 1 + .../server_entrypoint_listenconfig_other.go | 15 ++++ ...rver_entrypoint_listenconfig_other_test.go | 44 +++++++++++ .../server_entrypoint_listenconfig_unix.go | 44 +++++++++++ ...point_listenconfig_unix_sockopt_freebsd.go | 7 ++ ...rypoint_listenconfig_unix_sockopt_other.go | 7 ++ ...erver_entrypoint_listenconfig_unix_test.go | 56 ++++++++++++++ pkg/server/server_entrypoint_tcp.go | 3 +- pkg/server/server_entrypoint_tcp_http3.go | 3 +- pkg/server/server_entrypoint_udp.go | 9 +-- pkg/udp/conn.go | 14 +++- pkg/udp/conn_test.go | 31 ++------ pkg/udp/proxy_test.go | 5 +- 19 files changed, 279 insertions(+), 43 deletions(-) create mode 100644 pkg/server/server_entrypoint_listenconfig_other.go create mode 100644 pkg/server/server_entrypoint_listenconfig_other_test.go create mode 100644 pkg/server/server_entrypoint_listenconfig_unix.go create mode 100644 pkg/server/server_entrypoint_listenconfig_unix_sockopt_freebsd.go create mode 100644 pkg/server/server_entrypoint_listenconfig_unix_sockopt_other.go create mode 100644 pkg/server/server_entrypoint_listenconfig_unix_test.go diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 2f655ccb1..d2a5db058 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -180,6 +180,9 @@ Trust all. (Default: ```false```) `--entrypoints..proxyprotocol.trustedips`: Trust only selected IPs. +`--entrypoints..reuseport`: +Enables EntryPoints from the same or different processes listening on the same TCP/UDP port. (Default: ```false```) + `--entrypoints..transport.keepalivemaxrequests`: Maximum number of requests before closing a keep-alive connection. (Default: ```0```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index fdf733783..cc813b154 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -180,6 +180,9 @@ Trust all. (Default: ```false```) `TRAEFIK_ENTRYPOINTS__PROXYPROTOCOL_TRUSTEDIPS`: Trust only selected IPs. +`TRAEFIK_ENTRYPOINTS__REUSEPORT`: +Enables EntryPoints from the same or different processes listening on the same TCP/UDP port. (Default: ```false```) + `TRAEFIK_ENTRYPOINTS__TRANSPORT_KEEPALIVEMAXREQUESTS`: Maximum number of requests before closing a keep-alive connection. (Default: ```0```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 8f9326416..79226cb70 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -30,6 +30,7 @@ [entryPoints] [entryPoints.EntryPoint0] address = "foobar" + reusePort = true asDefault = true [entryPoints.EntryPoint0.transport] keepAliveMaxTime = "42s" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 1739759cb..45092911f 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -35,6 +35,7 @@ tcpServersTransport: entryPoints: EntryPoint0: address: foobar + reusePort: true asDefault: true transport: lifeCycle: diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index cd79c3cfd..aa92aa5c9 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -233,6 +233,79 @@ If both TCP and UDP are wanted for the same port, two entryPoints definitions ar 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. +### ReusePort + +_Optional, Default=false_ + +The `ReusePort` option enables EntryPoints from the same or different processes +listening on the same TCP/UDP port by utilizing the `SO_REUSEPORT` socket option. +It also allows the kernel to act like a load balancer to distribute incoming +connections between entry points. + +For example, you can use it with the [transport.lifeCycle](#lifecycle) to do +canary deployments against Traefik itself. Like upgrading Traefik version or +reloading the static configuration without any service downtime. + +!!! warning "Supported platforms" + + The `ReusePort` option currently works only on Linux, FreeBSD, OpenBSD and Darwin. + It will be ignored on other platforms. + + There is a known bug in the Linux kernel that may cause unintended TCP connection failures when using the `ReusePort` option. + For more details, see https://lwn.net/Articles/853637/. + +??? example "Listen on the same port" + + ```yaml tab="File (yaml)" + entryPoints: + web: + address: ":80" + reusePort: true + ``` + + ```toml tab="File (TOML)" + [entryPoints.web] + address = ":80" + reusePort = true + ``` + + ```bash tab="CLI" + --entrypoints.web.address=:80 + --entrypoints.web.reusePort=true + ``` + + Now it is possible to run multiple Traefik processes with the same EntryPoint configuration. + +??? example "Listen on the same port but bind to a different host" + + ```yaml tab="File (yaml)" + entryPoints: + web: + address: ":80" + reusePort: true + privateWeb: + address: "192.168.1.2:80" + reusePort: true + ``` + + ```toml tab="File (TOML)" + [entryPoints.web] + address = ":80" + reusePort = true + [entryPoints.privateWeb] + address = "192.168.1.2:80" + reusePort = true + ``` + + ```bash tab="CLI" + --entrypoints.web.address=:80 + --entrypoints.web.reusePort=true + --entrypoints.privateWeb.address=192.168.1.2:80 + --entrypoints.privateWeb.reusePort=true + ``` + + Requests to `192.168.1.2:80` will only be handled by routers that have `privateWeb` as the entry point. + ### AsDefault _Optional, Default=false_ diff --git a/go.mod b/go.mod index 3685c0c5a..b4e712b51 100644 --- a/go.mod +++ b/go.mod @@ -80,6 +80,7 @@ require ( golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/mod v0.13.0 golang.org/x/net v0.17.0 + golang.org/x/sys v0.15.0 golang.org/x/text v0.13.0 golang.org/x/time v0.3.0 golang.org/x/tools v0.14.0 @@ -315,7 +316,6 @@ require ( golang.org/x/arch v0.4.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/oauth2 v0.13.0 // indirect - golang.org/x/sys v0.15.0 // indirect golang.org/x/term v0.13.0 // indirect google.golang.org/api v0.128.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index 2c1187ac5..d7b155dbe 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -12,6 +12,7 @@ import ( // EntryPoint holds the entry point configuration. type EntryPoint struct { Address string `description:"Entry point address." json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"` + ReusePort bool `description:"Enables EntryPoints from the same or different processes listening on the same TCP/UDP port." json:"reusePort,omitempty" toml:"reusePort,omitempty" yaml:"reusePort,omitempty"` AsDefault bool `description:"Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined." json:"asDefault,omitempty" toml:"asDefault,omitempty" yaml:"asDefault,omitempty"` Transport *EntryPointsTransport `description:"Configures communication between clients and Traefik." json:"transport,omitempty" toml:"transport,omitempty" yaml:"transport,omitempty" export:"true"` ProxyProtocol *ProxyProtocol `description:"Proxy-Protocol configuration." json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` diff --git a/pkg/server/server_entrypoint_listenconfig_other.go b/pkg/server/server_entrypoint_listenconfig_other.go new file mode 100644 index 000000000..199012284 --- /dev/null +++ b/pkg/server/server_entrypoint_listenconfig_other.go @@ -0,0 +1,15 @@ +//go:build !(linux || freebsd || openbsd || darwin) + +package server + +import ( + "net" + + "github.com/traefik/traefik/v3/pkg/config/static" +) + +// newListenConfig creates a new net.ListenConfig for the given configuration of +// the entry point. +func newListenConfig(configuration *static.EntryPoint) (lc net.ListenConfig) { + return +} diff --git a/pkg/server/server_entrypoint_listenconfig_other_test.go b/pkg/server/server_entrypoint_listenconfig_other_test.go new file mode 100644 index 000000000..f2c736d26 --- /dev/null +++ b/pkg/server/server_entrypoint_listenconfig_other_test.go @@ -0,0 +1,44 @@ +//go:build !(linux || freebsd || openbsd || darwin) + +package server + +import ( + "context" + "net" + "testing" + + "github.com/stretchr/testify/require" + "github.com/traefik/traefik/v3/pkg/config/static" +) + +func TestNewListenConfig(t *testing.T) { + ep := static.EntryPoint{Address: ":0"} + listenConfig := newListenConfig(&ep) + require.Nil(t, listenConfig.Control) + require.Zero(t, listenConfig.KeepAlive) + + l1, err := listenConfig.Listen(context.Background(), "tcp", ep.Address) + require.NoError(t, err) + require.NotNil(t, l1) + defer l1.Close() + + l2, err := listenConfig.Listen(context.Background(), "tcp", l1.Addr().String()) + require.Error(t, err) + require.ErrorContains(t, err, "address already in use") + require.Nil(t, l2) + + ep = static.EntryPoint{Address: ":0", ReusePort: true} + listenConfig = newListenConfig(&ep) + require.Nil(t, listenConfig.Control) + require.Zero(t, listenConfig.KeepAlive) + + l3, err := listenConfig.Listen(context.Background(), "tcp", ep.Address) + require.NoError(t, err) + require.NotNil(t, l3) + defer l3.Close() + + l4, err := listenConfig.Listen(context.Background(), "tcp", l3.Addr().String()) + require.Error(t, err) + require.ErrorContains(t, err, "address already in use") + require.Nil(t, l4) +} diff --git a/pkg/server/server_entrypoint_listenconfig_unix.go b/pkg/server/server_entrypoint_listenconfig_unix.go new file mode 100644 index 000000000..7e43f4f7d --- /dev/null +++ b/pkg/server/server_entrypoint_listenconfig_unix.go @@ -0,0 +1,44 @@ +//go:build linux || freebsd || openbsd || darwin + +package server + +import ( + "fmt" + "net" + "syscall" + + "github.com/traefik/traefik/v3/pkg/config/static" + "golang.org/x/sys/unix" +) + +// newListenConfig creates a new net.ListenConfig for the given configuration of +// the entry point. +func newListenConfig(configuration *static.EntryPoint) (lc net.ListenConfig) { + if configuration != nil && configuration.ReusePort { + lc.Control = controlReusePort + } + return +} + +// controlReusePort is a net.ListenConfig.Control function that enables SO_REUSEPORT +// on the socket. +func controlReusePort(network, address string, c syscall.RawConn) error { + var setSockOptErr error + err := c.Control(func(fd uintptr) { + // Note that net.ListenConfig enables unix.SO_REUSEADDR by default, + // as seen in https://go.dev/src/net/sockopt_linux.go. Therefore, no + // additional action is required to enable it here. + + setSockOptErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unixSOREUSEPORT, 1) + if setSockOptErr != nil { + return + } + }) + if err != nil { + return fmt.Errorf("control: %w", err) + } + if setSockOptErr != nil { + return fmt.Errorf("setsockopt: %w", setSockOptErr) + } + return nil +} diff --git a/pkg/server/server_entrypoint_listenconfig_unix_sockopt_freebsd.go b/pkg/server/server_entrypoint_listenconfig_unix_sockopt_freebsd.go new file mode 100644 index 000000000..e20b61c48 --- /dev/null +++ b/pkg/server/server_entrypoint_listenconfig_unix_sockopt_freebsd.go @@ -0,0 +1,7 @@ +//go:build freebsd + +package server + +import "golang.org/x/sys/unix" + +const unixSOREUSEPORT = unix.SO_REUSEPORT_LB diff --git a/pkg/server/server_entrypoint_listenconfig_unix_sockopt_other.go b/pkg/server/server_entrypoint_listenconfig_unix_sockopt_other.go new file mode 100644 index 000000000..306dd8bd2 --- /dev/null +++ b/pkg/server/server_entrypoint_listenconfig_unix_sockopt_other.go @@ -0,0 +1,7 @@ +//go:build linux || openbsd || darwin + +package server + +import "golang.org/x/sys/unix" + +const unixSOREUSEPORT = unix.SO_REUSEPORT diff --git a/pkg/server/server_entrypoint_listenconfig_unix_test.go b/pkg/server/server_entrypoint_listenconfig_unix_test.go new file mode 100644 index 000000000..a5f7dda0a --- /dev/null +++ b/pkg/server/server_entrypoint_listenconfig_unix_test.go @@ -0,0 +1,56 @@ +//go:build linux || freebsd || openbsd || darwin + +package server + +import ( + "context" + "net" + "testing" + + "github.com/stretchr/testify/require" + "github.com/traefik/traefik/v3/pkg/config/static" +) + +func TestNewListenConfig(t *testing.T) { + ep := static.EntryPoint{Address: ":0"} + listenConfig := newListenConfig(&ep) + require.Nil(t, listenConfig.Control) + require.Zero(t, listenConfig.KeepAlive) + + l1, err := listenConfig.Listen(context.Background(), "tcp", ep.Address) + require.NoError(t, err) + require.NotNil(t, l1) + defer l1.Close() + + l2, err := listenConfig.Listen(context.Background(), "tcp", l1.Addr().String()) + require.Error(t, err) + require.ErrorContains(t, err, "address already in use") + require.Nil(t, l2) + + ep = static.EntryPoint{Address: ":0", ReusePort: true} + listenConfig = newListenConfig(&ep) + require.NotNil(t, listenConfig.Control) + require.Zero(t, listenConfig.KeepAlive) + + l3, err := listenConfig.Listen(context.Background(), "tcp", ep.Address) + require.NoError(t, err) + require.NotNil(t, l3) + defer l3.Close() + + l4, err := listenConfig.Listen(context.Background(), "tcp", l3.Addr().String()) + require.NoError(t, err) + require.NotNil(t, l4) + defer l4.Close() + + _, l3Port, err := net.SplitHostPort(l3.Addr().String()) + require.NoError(t, err) + l5, err := listenConfig.Listen(context.Background(), "tcp", "127.0.0.1:"+l3Port) + require.NoError(t, err) + require.NotNil(t, l5) + defer l5.Close() + + l6, err := listenConfig.Listen(context.Background(), "tcp", l1.Addr().String()) + require.Error(t, err) + require.ErrorContains(t, err, "address already in use") + require.Nil(t, l6) +} diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index e4d2892a8..c7d33abb7 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -460,7 +460,8 @@ func buildProxyProtocolListener(ctx context.Context, entryPoint *static.EntryPoi } func buildListener(ctx context.Context, entryPoint *static.EntryPoint) (net.Listener, error) { - listener, err := net.Listen("tcp", entryPoint.GetAddress()) + listenConfig := newListenConfig(entryPoint) + listener, err := listenConfig.Listen(ctx, "tcp", entryPoint.GetAddress()) if err != nil { return nil, fmt.Errorf("error opening listener: %w", err) } diff --git a/pkg/server/server_entrypoint_tcp_http3.go b/pkg/server/server_entrypoint_tcp_http3.go index 188be8026..827d6638b 100644 --- a/pkg/server/server_entrypoint_tcp_http3.go +++ b/pkg/server/server_entrypoint_tcp_http3.go @@ -33,7 +33,8 @@ func newHTTP3Server(ctx context.Context, configuration *static.EntryPoint, https return nil, errors.New("advertised port must be greater than or equal to zero") } - conn, err := net.ListenPacket("udp", configuration.GetAddress()) + listenConfig := newListenConfig(configuration) + conn, err := listenConfig.ListenPacket(ctx, "udp", configuration.GetAddress()) if err != nil { return nil, fmt.Errorf("starting listener: %w", err) } diff --git a/pkg/server/server_entrypoint_udp.go b/pkg/server/server_entrypoint_udp.go index 347b169db..08ed4fb1f 100644 --- a/pkg/server/server_entrypoint_udp.go +++ b/pkg/server/server_entrypoint_udp.go @@ -3,7 +3,6 @@ package server import ( "context" "fmt" - "net" "sync" "time" @@ -87,12 +86,8 @@ type UDPEntryPoint struct { // NewUDPEntryPoint returns a UDP entry point. func NewUDPEntryPoint(cfg *static.EntryPoint) (*UDPEntryPoint, error) { - addr, err := net.ResolveUDPAddr("udp", cfg.GetAddress()) - if err != nil { - return nil, err - } - - listener, err := udp.Listen("udp", addr, time.Duration(cfg.UDP.Timeout)) + listenConfig := newListenConfig(cfg) + listener, err := udp.Listen(listenConfig, "udp", cfg.GetAddress(), time.Duration(cfg.UDP.Timeout)) if err != nil { return nil, err } diff --git a/pkg/udp/conn.go b/pkg/udp/conn.go index 5753a9c13..6b6054b69 100644 --- a/pkg/udp/conn.go +++ b/pkg/udp/conn.go @@ -1,7 +1,9 @@ package udp import ( + "context" "errors" + "fmt" "io" "net" "sync" @@ -33,18 +35,22 @@ type Listener struct { } // Listen creates a new listener. -func Listen(network string, laddr *net.UDPAddr, timeout time.Duration) (*Listener, error) { +func Listen(listenConfig net.ListenConfig, network, address string, timeout time.Duration) (*Listener, error) { if timeout <= 0 { return nil, errors.New("timeout should be greater than zero") } - conn, err := net.ListenUDP(network, laddr) + packetConn, err := listenConfig.ListenPacket(context.Background(), network, address) if err != nil { - return nil, err + return nil, fmt.Errorf("listen packet: %w", err) + } + pConn, ok := packetConn.(*net.UDPConn) + if !ok { + return nil, errors.New("packet conn is not an UDPConn") } l := &Listener{ - pConn: conn, + pConn: pConn, acceptCh: make(chan *Conn), conns: make(map[string]*Conn), accepting: true, diff --git a/pkg/udp/conn_test.go b/pkg/udp/conn_test.go index 44e748474..3c8cc0a7f 100644 --- a/pkg/udp/conn_test.go +++ b/pkg/udp/conn_test.go @@ -14,10 +14,7 @@ import ( ) func TestConsecutiveWrites(t *testing.T) { - addr, err := net.ResolveUDPAddr("udp", ":0") - require.NoError(t, err) - - ln, err := Listen("udp", addr, 3*time.Second) + ln, err := Listen(net.ListenConfig{}, "udp", ":0", 3*time.Second) require.NoError(t, err) defer func() { err := ln.Close() @@ -75,11 +72,7 @@ func TestConsecutiveWrites(t *testing.T) { } func TestListenNotBlocking(t *testing.T) { - addr, err := net.ResolveUDPAddr("udp", ":0") - - require.NoError(t, err) - - ln, err := Listen("udp", addr, 3*time.Second) + ln, err := Listen(net.ListenConfig{}, "udp", ":0", 3*time.Second) require.NoError(t, err) defer func() { err := ln.Close() @@ -165,10 +158,7 @@ func TestListenNotBlocking(t *testing.T) { } func TestListenWithZeroTimeout(t *testing.T) { - addr, err := net.ResolveUDPAddr("udp", ":0") - require.NoError(t, err) - - _, err = Listen("udp", addr, 0) + _, err := Listen(net.ListenConfig{}, "udp", ":0", 0) assert.Error(t, err) } @@ -183,10 +173,7 @@ func TestTimeoutWithoutRead(t *testing.T) { func testTimeout(t *testing.T, withRead bool) { t.Helper() - addr, err := net.ResolveUDPAddr("udp", ":0") - require.NoError(t, err) - - ln, err := Listen("udp", addr, 3*time.Second) + ln, err := Listen(net.ListenConfig{}, "udp", ":0", 3*time.Second) require.NoError(t, err) defer func() { err := ln.Close() @@ -227,10 +214,7 @@ func testTimeout(t *testing.T, withRead bool) { } func TestShutdown(t *testing.T) { - addr, err := net.ResolveUDPAddr("udp", ":0") - require.NoError(t, err) - - l, err := Listen("udp", addr, 3*time.Second) + l, err := Listen(net.ListenConfig{}, "udp", ":0", 3*time.Second) require.NoError(t, err) go func() { @@ -331,10 +315,7 @@ func TestReadLoopMaxDataSize(t *testing.T) { doneCh := make(chan struct{}) - addr, err := net.ResolveUDPAddr("udp", ":0") - require.NoError(t, err) - - l, err := Listen("udp", addr, 3*time.Second) + l, err := Listen(net.ListenConfig{}, "udp", ":0", 3*time.Second) require.NoError(t, err) defer func() { diff --git a/pkg/udp/proxy_test.go b/pkg/udp/proxy_test.go index b3ce2ec2c..3b5703875 100644 --- a/pkg/udp/proxy_test.go +++ b/pkg/udp/proxy_test.go @@ -96,10 +96,7 @@ func TestProxy_ServeUDP_MaxDataSize(t *testing.T) { func newServer(t *testing.T, addr string, handler Handler) { t.Helper() - addrL, err := net.ResolveUDPAddr("udp", addr) - require.NoError(t, err) - - listener, err := Listen("udp", addrL, 3*time.Second) + listener, err := Listen(net.ListenConfig{}, "udp", addr, 3*time.Second) require.NoError(t, err) for { From 8b77f0c2ddf79b9972b0fd8db3d027d553d0f4be Mon Sep 17 00:00:00 2001 From: Romain Date: Tue, 30 Jan 2024 16:28:05 +0100 Subject: [PATCH 25/36] Remove observability for internal resources --- cmd/traefik/traefik.go | 14 +- docs/content/migration/v2-to-v3.md | 10 ++ docs/content/observability/access-logs.md | 20 +++ .../content/observability/metrics/overview.md | 22 +++ docs/content/observability/overview.md | 42 ++++++ .../content/observability/tracing/overview.md | 22 ++- .../reference/static-configuration/cli-ref.md | 9 ++ .../reference/static-configuration/env-ref.md | 9 ++ .../reference/static-configuration/file.toml | 3 + .../reference/static-configuration/file.yaml | 3 + docs/mkdocs.yml | 1 + integration/access_log_test.go | 82 ++++++++-- .../access_log_base.toml} | 0 .../fixtures/access_log/access_log_ping.toml | 30 ++++ integration/fixtures/throttling/simple.toml | 1 + .../tracing/simple-opentelemetry.toml | 6 + integration/log_rotation_test.go | 2 +- integration/simple_test.go | 4 + integration/tracing_test.go | 61 ++++++++ pkg/config/static/static_config.go | 3 +- pkg/muxer/http/mux.go | 19 ++- .../testdata/anonymized-static-config.json | 2 +- pkg/server/middleware/chainbuilder.go | 63 -------- pkg/server/middleware/middlewares.go | 3 + pkg/server/middleware/observability.go | 140 ++++++++++++++++++ pkg/server/router/router.go | 67 ++++++--- pkg/server/router/router_test.go | 138 +---------------- pkg/server/routerfactory.go | 27 ++-- pkg/server/routerfactory_test.go | 16 +- pkg/server/server.go | 40 ++--- pkg/server/server_signals.go | 6 +- pkg/server/service/managerfactory.go | 9 +- pkg/server/service/service.go | 29 ++-- pkg/tracing/tracing.go | 5 + pkg/types/logs.go | 1 + pkg/types/metrics.go | 2 + 36 files changed, 594 insertions(+), 317 deletions(-) create mode 100644 docs/content/observability/overview.md rename integration/fixtures/{access_log_config.toml => access_log/access_log_base.toml} (100%) create mode 100644 integration/fixtures/access_log/access_log_ping.toml delete mode 100644 pkg/server/middleware/chainbuilder.go create mode 100644 pkg/server/middleware/observability.go diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 1274e5104..0ddcceb00 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -193,10 +193,13 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err tsProviders := initTailscaleProviders(staticConfiguration, &providerAggregator) - // Metrics + // Observability metricRegistries := registerMetricClients(staticConfiguration.Metrics) metricsRegistry := metrics.NewMultiRegistry(metricRegistries) + accessLog := setupAccessLog(staticConfiguration.AccessLog) + tracer, tracerCloser := setupTracing(staticConfiguration.Tracing) + observabilityMgr := middleware.NewObservabilityMgr(*staticConfiguration, metricsRegistry, accessLog, tracer, tracerCloser) // Entrypoints @@ -263,14 +266,11 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err roundTripperManager := service.NewRoundTripperManager(spiffeX509Source) dialerManager := tcp.NewDialerManager(spiffeX509Source) acmeHTTPHandler := getHTTPChallengeHandler(acmeProviders, httpChallengeProvider) - managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, metricsRegistry, roundTripperManager, acmeHTTPHandler) + managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, observabilityMgr, roundTripperManager, acmeHTTPHandler) // Router factory - accessLog := setupAccessLog(staticConfiguration.AccessLog) - tracer, tracerCloser := setupTracing(staticConfiguration.Tracing) - chainBuilder := middleware.NewChainBuilder(metricsRegistry, accessLog, tracer) - routerFactory := server.NewRouterFactory(*staticConfiguration, managerFactory, tlsManager, chainBuilder, pluginBuilder, metricsRegistry, dialerManager) + routerFactory := server.NewRouterFactory(*staticConfiguration, managerFactory, tlsManager, observabilityMgr, pluginBuilder, dialerManager) // Watcher @@ -351,7 +351,7 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err } }) - return server.NewServer(routinesPool, serverEntryPointsTCP, serverEntryPointsUDP, watcher, chainBuilder, accessLog, tracerCloser), nil + return server.NewServer(routinesPool, serverEntryPointsTCP, serverEntryPointsUDP, watcher, observabilityMgr), nil } func getHTTPChallengeHandler(acmeProviders []*acme.Provider, httpChallengeProvider http.Handler) http.Handler { diff --git a/docs/content/migration/v2-to-v3.md b/docs/content/migration/v2-to-v3.md index b9f7c5102..08a45d741 100644 --- a/docs/content/migration/v2-to-v3.md +++ b/docs/content/migration/v2-to-v3.md @@ -693,3 +693,13 @@ Here are two possible transition strategies: This allows continued compatibility with the existing infrastructure. Please check the [OpenTelemetry Tracing provider documention](../observability/tracing/opentelemetry.md) for more information. + +#### Internal Resources Observability (AccessLogs, Metrics and Tracing) + +In v3, observability for internal routers or services (e.g.: `ping@internal`) is disabled by default. +To enable it one should use the new `addInternals` option for AccessLogs, Metrics or Tracing. +Please take a look at the observability documentation for more information: + +- [AccessLogs](../observability/access-logs.md#addinternals) +- [Metrics](../observability/metrics/overview.md#addinternals) +- [AccessLogs](../observability/tracing/overview.md#addinternals) diff --git a/docs/content/observability/access-logs.md b/docs/content/observability/access-logs.md index dac71805f..beb114db1 100644 --- a/docs/content/observability/access-logs.md +++ b/docs/content/observability/access-logs.md @@ -26,6 +26,26 @@ accessLog: {} --accesslog=true ``` +### `addInternals` + +_Optional, Default="false"_ + +Enables accessLogs for internal resources. + +```yaml tab="File (YAML)" +accesslog: + addInternals: true +``` + +```toml tab="File (TOML)" +[accesslog] + addInternals = true +``` + +```bash tab="CLI" +--accesslog.addinternals +``` + ### `filePath` By default access logs are written to the standard output. diff --git a/docs/content/observability/metrics/overview.md b/docs/content/observability/metrics/overview.md index 1d6f04b15..f968e77e6 100644 --- a/docs/content/observability/metrics/overview.md +++ b/docs/content/observability/metrics/overview.md @@ -14,6 +14,28 @@ Traefik supports these metrics backends: Traefik Proxy hosts an official Grafana dashboard for both [on-premises](https://grafana.com/grafana/dashboards/17346) and [Kubernetes](https://grafana.com/grafana/dashboards/17347) deployments. +## Common Options + +### `addInternals` + +_Optional, Default="false"_ + +Enables metrics for internal resources. + +```yaml tab="File (YAML)" +metrics: + addInternals: true +``` + +```toml tab="File (TOML)" +[metrics] + addInternals = true +``` + +```bash tab="CLI" +--metrics.addinternals +``` + ## Global Metrics | Metric | Type | [Labels](#labels) | Description | diff --git a/docs/content/observability/overview.md b/docs/content/observability/overview.md new file mode 100644 index 000000000..2de429b4e --- /dev/null +++ b/docs/content/observability/overview.md @@ -0,0 +1,42 @@ +--- +title: "Traefik Observability Overview" +description: "Traefik provides Logs, Access Logs, Metrics and Tracing. Read the full documentation to get started." +--- + +# Overview + +Traefik's Observability system +{: .subtitle } + +## Logs + +Traefik logs informs about everything that happens within Traefik (startup, configuration, events, shutdown, and so on). + +Read the [Logs documentation](./logs.md) to learn how to configure it. + +## Access Logs + +Access logs are a key part of observability in Traefik. + +They are providing valuable insights about incoming traffic, and allow to monitor it. +The access logs record detailed information about each request received by Traefik, +including the source IP address, requested URL, response status code, and more. + +Read the [Access Logs documentation](./access-logs.md) to learn how to configure it. + +## Metrics + +Traefik offers a metrics feature that provides valuable insights about the performance and usage. +These metrics include the number of requests received, the requests duration, and more. + +Traefik supports these metrics systems: Prometheus, Datadog, InfluxDB 2.X, and StatsD. + +Read the [Metrics documentation](./metrics/overview.md) to learn how to configure it. + +## Tracing + +The Traefik tracing system allows developers to gain deep visibility into the flow of requests through their infrastructure. + +Traefik supports these tracing with OpenTelemetry. + +Read the [Tracing documentation](./tracing/overview.md) to learn how to configure it. diff --git a/docs/content/observability/tracing/overview.md b/docs/content/observability/tracing/overview.md index 18b8c4f3e..52cbb483a 100644 --- a/docs/content/observability/tracing/overview.md +++ b/docs/content/observability/tracing/overview.md @@ -14,10 +14,8 @@ Traefik uses [OpenTelemetry](https://opentelemetry.io/ "Link to website of OTel" Please check our dedicated [OTel docs](./opentelemetry.md) to learn more. - ## Configuration - To enable the tracing: ```yaml tab="File (YAML)" @@ -34,6 +32,26 @@ tracing: {} ### Common Options +#### `addInternals` + +_Optional, Default="false"_ + +Enables tracing for internal resources. + +```yaml tab="File (YAML)" +tracing: + addInternals: true +``` + +```toml tab="File (TOML)" +[tracing] + addInternals = true +``` + +```bash tab="CLI" +--tracing.addinternals +``` + #### `serviceName` _Required, Default="traefik"_ diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index d2a5db058..a3bd3d391 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -6,6 +6,9 @@ THIS FILE MUST NOT BE EDITED BY HAND `--accesslog`: Access log settings. (Default: ```false```) +`--accesslog.addinternals`: +Enables access log for internal services (ping, dashboard, etc...). (Default: ```false```) + `--accesslog.bufferingsize`: Number of access log lines to process in a buffered way. (Default: ```0```) @@ -267,6 +270,9 @@ Maximum size in megabytes of the log file before it gets rotated. (Default: ```0 `--log.nocolor`: When using the 'common' format, disables the colorized output. (Default: ```false```) +`--metrics.addinternals`: +Enables metrics for internal services (ping, dashboard, etc...). (Default: ```false```) + `--metrics.datadog`: Datadog metrics exporter type. (Default: ```false```) @@ -993,6 +999,9 @@ Defines the allowed SPIFFE trust domain. `--tracing`: OpenTracing configuration. (Default: ```false```) +`--tracing.addinternals`: +Enables tracing for internal services (ping, dashboard, etc...). (Default: ```false```) + `--tracing.globalattributes.`: Defines additional attributes (key:value) on all spans. diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index cc813b154..79cc1797d 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -6,6 +6,9 @@ THIS FILE MUST NOT BE EDITED BY HAND `TRAEFIK_ACCESSLOG`: Access log settings. (Default: ```false```) +`TRAEFIK_ACCESSLOG_ADDINTERNALS`: +Enables access log for internal services (ping, dashboard, etc...). (Default: ```false```) + `TRAEFIK_ACCESSLOG_BUFFERINGSIZE`: Number of access log lines to process in a buffered way. (Default: ```0```) @@ -267,6 +270,9 @@ Maximum size in megabytes of the log file before it gets rotated. (Default: ```0 `TRAEFIK_LOG_NOCOLOR`: When using the 'common' format, disables the colorized output. (Default: ```false```) +`TRAEFIK_METRICS_ADDINTERNALS`: +Enables metrics for internal services (ping, dashboard, etc...). (Default: ```false```) + `TRAEFIK_METRICS_DATADOG`: Datadog metrics exporter type. (Default: ```false```) @@ -993,6 +999,9 @@ Defines the allowed SPIFFE trust domain. `TRAEFIK_TRACING`: OpenTracing configuration. (Default: ```false```) +`TRAEFIK_TRACING_ADDINTERNALS`: +Enables tracing for internal services (ping, dashboard, etc...). (Default: ```false```) + `TRAEFIK_TRACING_GLOBALATTRIBUTES_`: Defines additional attributes (key:value) on all spans. diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 79226cb70..e2a459ae6 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -277,6 +277,7 @@ disableDashboardAd = true [metrics] + addInternals = true [metrics.prometheus] buckets = [42.0, 42.0] addEntryPointsLabels = true @@ -351,6 +352,7 @@ filePath = "foobar" format = "foobar" bufferingSize = 42 + addInternals = true [accessLog.filters] statusCodes = ["foobar", "foobar"] retryAttempts = true @@ -369,6 +371,7 @@ [tracing] serviceName = "foobar" sampleRate = 42.0 + addInternals = true [tracing.headers] name0 = "foobar" name1 = "foobar" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 45092911f..2779e0997 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -308,6 +308,7 @@ api: debug: true disableDashboardAd: true metrics: + addInternals: true prometheus: buckets: - 42 @@ -399,6 +400,7 @@ accessLog: name0: foobar name1: foobar bufferingSize: 42 + addInternals: true tracing: serviceName: foobar headers: @@ -408,6 +410,7 @@ tracing: name0: foobar name1: foobar sampleRate: 42 + addInternals: true otlp: grpc: endpoint: foobar diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 0b7ffaa2c..bbb3751ae 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -149,6 +149,7 @@ nav: - 'API': 'operations/api.md' - 'Ping': 'operations/ping.md' - 'Observability': + - 'Overview': 'observability/overview.md' - 'Logs': 'observability/logs.md' - 'Access Logs': 'observability/access-logs.md' - 'Metrics': diff --git a/integration/access_log_test.go b/integration/access_log_test.go index 8e2c9581f..b5e4ec1ef 100644 --- a/integration/access_log_test.go +++ b/integration/access_log_test.go @@ -61,7 +61,7 @@ func (s *AccessLogSuite) TestAccessLog() { ensureWorkingDirectoryIsClean() // Start Traefik - s.traefikCmd(withConfigFile("fixtures/access_log_config.toml")) + s.traefikCmd(withConfigFile("fixtures/access_log/access_log_base.toml")) defer func() { traefikLog, err := os.ReadFile(traefikTestLogFile) @@ -130,7 +130,7 @@ func (s *AccessLogSuite) TestAccessLogAuthFrontend() { } // Start Traefik - s.traefikCmd(withConfigFile("fixtures/access_log_config.toml")) + s.traefikCmd(withConfigFile("fixtures/access_log/access_log_base.toml")) s.checkStatsForLogFile() @@ -194,7 +194,7 @@ func (s *AccessLogSuite) TestAccessLogDigestAuthMiddleware() { } // Start Traefik - s.traefikCmd(withConfigFile("fixtures/access_log_config.toml")) + s.traefikCmd(withConfigFile("fixtures/access_log/access_log_base.toml")) s.checkStatsForLogFile() @@ -304,7 +304,7 @@ func (s *AccessLogSuite) TestAccessLogFrontendRedirect() { } // Start Traefik - s.traefikCmd(withConfigFile("fixtures/access_log_config.toml")) + s.traefikCmd(withConfigFile("fixtures/access_log/access_log_base.toml")) s.checkStatsForLogFile() @@ -410,7 +410,7 @@ func (s *AccessLogSuite) TestAccessLogRateLimit() { } // Start Traefik - s.traefikCmd(withConfigFile("fixtures/access_log_config.toml")) + s.traefikCmd(withConfigFile("fixtures/access_log/access_log_base.toml")) s.checkStatsForLogFile() @@ -454,7 +454,7 @@ func (s *AccessLogSuite) TestAccessLogBackendNotFound() { } // Start Traefik - s.traefikCmd(withConfigFile("fixtures/access_log_config.toml")) + s.traefikCmd(withConfigFile("fixtures/access_log/access_log_base.toml")) s.waitForTraefik("server1") @@ -494,7 +494,7 @@ func (s *AccessLogSuite) TestAccessLogFrontendAllowlist() { } // Start Traefik - s.traefikCmd(withConfigFile("fixtures/access_log_config.toml")) + s.traefikCmd(withConfigFile("fixtures/access_log/access_log_base.toml")) s.checkStatsForLogFile() @@ -534,7 +534,7 @@ func (s *AccessLogSuite) TestAccessLogAuthFrontendSuccess() { } // Start Traefik - s.traefikCmd(withConfigFile("fixtures/access_log_config.toml")) + s.traefikCmd(withConfigFile("fixtures/access_log/access_log_base.toml")) s.checkStatsForLogFile() @@ -575,7 +575,7 @@ func (s *AccessLogSuite) TestAccessLogPreflightHeadersMiddleware() { } // Start Traefik - s.traefikCmd(withConfigFile("fixtures/access_log_config.toml")) + s.traefikCmd(withConfigFile("fixtures/access_log/access_log_base.toml")) s.checkStatsForLogFile() @@ -603,6 +603,56 @@ func (s *AccessLogSuite) TestAccessLogPreflightHeadersMiddleware() { s.checkNoOtherTraefikProblems() } +func (s *AccessLogSuite) TestAccessLogDisabledForInternals() { + ensureWorkingDirectoryIsClean() + + file := s.adaptFile("fixtures/access_log/access_log_ping.toml", struct{}{}) + + // Start Traefik. + s.traefikCmd(withConfigFile(file)) + + defer func() { + traefikLog, err := os.ReadFile(traefikTestLogFile) + require.NoError(s.T(), err) + log.Info().Msg(string(traefikLog)) + }() + + // waitForTraefik makes at least one call to the rawdata api endpoint, + // but the logs for this endpoint are ignored in checkAccessLogOutput. + s.waitForTraefik("customPing") + + s.checkStatsForLogFile() + + // Verify Traefik started OK. + s.checkTraefikStarted() + + // Make some requests on the internal ping router. + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/ping", nil) + require.NoError(s.T(), err) + + err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK), try.HasBody()) + require.NoError(s.T(), err) + err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK), try.HasBody()) + require.NoError(s.T(), err) + + // Make some requests on the custom ping router. + req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/ping", nil) + require.NoError(s.T(), err) + + err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK), try.HasBody()) + require.NoError(s.T(), err) + err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK), try.HasBody()) + require.NoError(s.T(), err) + + // Verify access.log output as expected. + count := s.checkAccessLogOutput() + + require.Equal(s.T(), 0, count) + + // Verify no other Traefik problems. + s.checkNoOtherTraefikProblems() +} + func (s *AccessLogSuite) checkNoOtherTraefikProblems() { traefikLog, err := os.ReadFile(traefikTestLogFile) require.NoError(s.T(), err) @@ -612,6 +662,8 @@ func (s *AccessLogSuite) checkNoOtherTraefikProblems() { } func (s *AccessLogSuite) checkAccessLogOutput() int { + s.T().Helper() + lines := s.extractLines() count := 0 for i, line := range lines { @@ -624,6 +676,8 @@ func (s *AccessLogSuite) checkAccessLogOutput() int { } func (s *AccessLogSuite) checkAccessLogExactValuesOutput(values []accessLogValue) int { + s.T().Helper() + lines := s.extractLines() count := 0 for i, line := range lines { @@ -641,6 +695,8 @@ func (s *AccessLogSuite) checkAccessLogExactValuesOutput(values []accessLogValue } func (s *AccessLogSuite) extractLines() []string { + s.T().Helper() + accessLog, err := os.ReadFile(traefikTestAccessLogFile) require.NoError(s.T(), err) @@ -656,6 +712,8 @@ func (s *AccessLogSuite) extractLines() []string { } func (s *AccessLogSuite) checkStatsForLogFile() { + s.T().Helper() + err := try.Do(1*time.Second, func() error { if _, errStat := os.Stat(traefikTestLogFile); errStat != nil { return fmt.Errorf("could not get stats for log file: %w", errStat) @@ -671,6 +729,8 @@ func ensureWorkingDirectoryIsClean() { } func (s *AccessLogSuite) checkTraefikStarted() []byte { + s.T().Helper() + traefikLog, err := os.ReadFile(traefikTestLogFile) require.NoError(s.T(), err) if len(traefikLog) > 0 { @@ -680,6 +740,8 @@ func (s *AccessLogSuite) checkTraefikStarted() []byte { } func (s *BaseSuite) CheckAccessLogFormat(line string, i int) { + s.T().Helper() + results, err := accesslog.ParseAccessLog(line) require.NoError(s.T(), err) assert.Len(s.T(), results, 14) @@ -692,6 +754,8 @@ func (s *BaseSuite) CheckAccessLogFormat(line string, i int) { } func (s *AccessLogSuite) checkAccessLogExactValues(line string, i int, v accessLogValue) { + s.T().Helper() + results, err := accesslog.ParseAccessLog(line) require.NoError(s.T(), err) assert.Len(s.T(), results, 14) diff --git a/integration/fixtures/access_log_config.toml b/integration/fixtures/access_log/access_log_base.toml similarity index 100% rename from integration/fixtures/access_log_config.toml rename to integration/fixtures/access_log/access_log_base.toml diff --git a/integration/fixtures/access_log/access_log_ping.toml b/integration/fixtures/access_log/access_log_ping.toml new file mode 100644 index 000000000..4e85b93bc --- /dev/null +++ b/integration/fixtures/access_log/access_log_ping.toml @@ -0,0 +1,30 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "ERROR" + filePath = "traefik.log" + +[accessLog] + filePath = "access.log" + +[entryPoints] + [entryPoints.web] + address = ":8000" + +[api] + insecure = true + +[ping] + +[providers] + [providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## +[http.routers] + [http.routers.customPing] + entryPoints = ["web"] + rule = "PathPrefix(`/ping`)" + service = "ping@internal" diff --git a/integration/fixtures/throttling/simple.toml b/integration/fixtures/throttling/simple.toml index 668d4988b..cd98c44f1 100644 --- a/integration/fixtures/throttling/simple.toml +++ b/integration/fixtures/throttling/simple.toml @@ -19,5 +19,6 @@ insecure = true [metrics] + addInternals = true [metrics.prometheus] buckets = [0.1,0.3,1.2,5.0] diff --git a/integration/fixtures/tracing/simple-opentelemetry.toml b/integration/fixtures/tracing/simple-opentelemetry.toml index 77d779800..0599bb18d 100644 --- a/integration/fixtures/tracing/simple-opentelemetry.toml +++ b/integration/fixtures/tracing/simple-opentelemetry.toml @@ -9,6 +9,8 @@ [api] insecure = true +[ping] + [entryPoints] [entryPoints.web] address = ":8000" @@ -47,6 +49,10 @@ Service = "service3" Middlewares = ["retry", "basic-auth"] Rule = "Path(`/auth`)" + [http.routers.customPing] + entryPoints = ["web"] + rule = "PathPrefix(`/ping`)" + service = "ping@internal" [http.middlewares] [http.middlewares.retry.retry] diff --git a/integration/log_rotation_test.go b/integration/log_rotation_test.go index 28d72f8cd..269d17d69 100644 --- a/integration/log_rotation_test.go +++ b/integration/log_rotation_test.go @@ -57,7 +57,7 @@ func (s *LogRotationSuite) TearDownSuite() { func (s *LogRotationSuite) TestAccessLogRotation() { // Start Traefik - cmd, _ := s.cmdTraefik(withConfigFile("fixtures/access_log_config.toml")) + cmd, _ := s.cmdTraefik(withConfigFile("fixtures/access_log/access_log_base.toml")) defer s.displayTraefikLogFile(traefikTestLogFile) // Verify Traefik started ok diff --git a/integration/simple_test.go b/integration/simple_test.go index d2c3be077..8e2f14ec1 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -287,6 +287,10 @@ func (s *SimpleSuite) TestMetricsPrometheusDefaultEntryPoint() { err = try.GetRequest("http://127.0.0.1:8080/metrics", 1*time.Second, try.BodyContains("_service_")) require.NoError(s.T(), err) + + // No metrics for internals. + err = try.GetRequest("http://127.0.0.1:8080/metrics", 1*time.Second, try.BodyNotContains("router=\"api@internal\"", "service=\"api@internal\"")) + require.NoError(s.T(), err) } func (s *SimpleSuite) TestMetricsPrometheusTwoRoutersOneService() { diff --git a/integration/tracing_test.go b/integration/tracing_test.go index c94f3c58b..ef92015bd 100644 --- a/integration/tracing_test.go +++ b/integration/tracing_test.go @@ -414,6 +414,67 @@ func (s *TracingSuite) TestOpentelemetryAuth() { s.checkTraceContent(contains) } +func (s *TracingSuite) TestNoInternals() { + file := s.adaptFile("fixtures/tracing/simple-opentelemetry.toml", TracingTemplate{ + WhoamiIP: s.whoamiIP, + WhoamiPort: s.whoamiPort, + IP: s.otelCollectorIP, + IsHTTP: true, + }) + + s.traefikCmd(withConfigFile(file)) + + // wait for traefik + err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", time.Second, try.BodyContains("basic-auth")) + require.NoError(s.T(), err) + + err = try.GetRequest("http://127.0.0.1:8000/ratelimit", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + require.NoError(s.T(), err) + + err = try.GetRequest("http://127.0.0.1:8000/ping", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + require.NoError(s.T(), err) + err = try.GetRequest("http://127.0.0.1:8080/ping", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + require.NoError(s.T(), err) + + baseURL, err := url.Parse("http://" + s.tempoIP + ":3200/api/search") + require.NoError(s.T(), err) + + req := &http.Request{ + Method: http.MethodGet, + URL: baseURL, + } + // Wait for traces to be available. + time.Sleep(10 * time.Second) + resp, err := try.Response(req, 5*time.Second) + require.NoError(s.T(), err) + + out := &TraceResponse{} + content, err := io.ReadAll(resp.Body) + require.NoError(s.T(), err) + err = json.Unmarshal(content, &out) + require.NoError(s.T(), err) + + s.NotEmptyf(len(out.Traces), "expected at least one trace") + + for _, t := range out.Traces { + baseURL, err := url.Parse("http://" + s.tempoIP + ":3200/api/traces/" + t.TraceID) + require.NoError(s.T(), err) + + req := &http.Request{ + Method: http.MethodGet, + URL: baseURL, + } + + resp, err := try.Response(req, 5*time.Second) + require.NoError(s.T(), err) + + content, err := io.ReadAll(resp.Body) + require.NoError(s.T(), err) + + require.NotContains(s.T(), content, "@internal") + } +} + func (s *TracingSuite) checkTraceContent(expectedJSON []map[string]string) { s.T().Helper() diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index bb5778671..19d6ea24c 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -197,6 +197,7 @@ type Tracing struct { Headers map[string]string `description:"Defines additional headers to be sent with the payloads." json:"headers,omitempty" toml:"headers,omitempty" yaml:"headers,omitempty" export:"true"` GlobalAttributes map[string]string `description:"Defines additional attributes (key:value) on all spans." json:"globalAttributes,omitempty" toml:"globalAttributes,omitempty" yaml:"globalAttributes,omitempty" export:"true"` SampleRate float64 `description:"Sets the rate between 0.0 and 1.0 of requests to trace." json:"sampleRate,omitempty" toml:"sampleRate,omitempty" yaml:"sampleRate,omitempty" export:"true"` + AddInternals bool `description:"Enables tracing for internal services (ping, dashboard, etc...)." json:"addInternals,omitempty" toml:"addInternals,omitempty" yaml:"addInternals,omitempty" export:"true"` OTLP *opentelemetry.Config `description:"Settings for OpenTelemetry." json:"otlp,omitempty" toml:"otlp,omitempty" yaml:"otlp,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` } @@ -218,7 +219,7 @@ type Providers struct { KubernetesIngress *ingress.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` KubernetesCRD *crd.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesCRD,omitempty" toml:"kubernetesCRD,omitempty" yaml:"kubernetesCRD,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` KubernetesGateway *gateway.Provider `description:"Enable Kubernetes gateway api provider with default settings." json:"kubernetesGateway,omitempty" toml:"kubernetesGateway,omitempty" yaml:"kubernetesGateway,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - Rest *rest.Provider ` description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Rest *rest.Provider `description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` ConsulCatalog *consulcatalog.ProviderBuilder `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` Nomad *nomad.ProviderBuilder `description:"Enable Nomad backend with default settings." json:"nomad,omitempty" toml:"nomad,omitempty" yaml:"nomad,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` Ecs *ecs.Provider `description:"Enable AWS ECS backend with default settings." json:"ecs,omitempty" toml:"ecs,omitempty" yaml:"ecs,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` diff --git a/pkg/muxer/http/mux.go b/pkg/muxer/http/mux.go index 33e700697..1d84a1433 100644 --- a/pkg/muxer/http/mux.go +++ b/pkg/muxer/http/mux.go @@ -12,9 +12,10 @@ import ( // Muxer handles routing with rules. type Muxer struct { - routes routes - parser predicate.Parser - parserV2 predicate.Parser + routes routes + parser predicate.Parser + parserV2 predicate.Parser + defaultHandler http.Handler } // NewMuxer returns a new muxer instance. @@ -40,8 +41,9 @@ func NewMuxer() (*Muxer, error) { } return &Muxer{ - parser: parser, - parserV2: parserV2, + parser: parser, + parserV2: parserV2, + defaultHandler: http.NotFoundHandler(), }, nil } @@ -55,7 +57,12 @@ func (m *Muxer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } } - http.NotFoundHandler().ServeHTTP(rw, req) + m.defaultHandler.ServeHTTP(rw, req) +} + +// SetDefaultHandler sets the muxer default handler. +func (m *Muxer) SetDefaultHandler(handler http.Handler) { + m.defaultHandler = handler } // GetRulePriority computes the priority for a given rule. diff --git a/pkg/redactor/testdata/anonymized-static-config.json b/pkg/redactor/testdata/anonymized-static-config.json index 97e678c85..d756ba779 100644 --- a/pkg/redactor/testdata/anonymized-static-config.json +++ b/pkg/redactor/testdata/anonymized-static-config.json @@ -407,4 +407,4 @@ } } } -} +} \ No newline at end of file diff --git a/pkg/server/middleware/chainbuilder.go b/pkg/server/middleware/chainbuilder.go deleted file mode 100644 index 980d7a2b5..000000000 --- a/pkg/server/middleware/chainbuilder.go +++ /dev/null @@ -1,63 +0,0 @@ -package middleware - -import ( - "context" - - "github.com/containous/alice" - "github.com/rs/zerolog/log" - "github.com/traefik/traefik/v3/pkg/metrics" - "github.com/traefik/traefik/v3/pkg/middlewares/accesslog" - "github.com/traefik/traefik/v3/pkg/middlewares/capture" - metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics" - tracingMiddle "github.com/traefik/traefik/v3/pkg/middlewares/tracing" - "go.opentelemetry.io/otel/trace" -) - -// ChainBuilder Creates a middleware chain by entry point. It is used for middlewares that are created almost systematically and that need to be created before all others. -type ChainBuilder struct { - metricsRegistry metrics.Registry - accessLoggerMiddleware *accesslog.Handler - tracer trace.Tracer -} - -// NewChainBuilder Creates a new ChainBuilder. -func NewChainBuilder(metricsRegistry metrics.Registry, accessLoggerMiddleware *accesslog.Handler, tracer trace.Tracer) *ChainBuilder { - return &ChainBuilder{ - metricsRegistry: metricsRegistry, - accessLoggerMiddleware: accessLoggerMiddleware, - tracer: tracer, - } -} - -// Build a middleware chain by entry point. -func (c *ChainBuilder) Build(ctx context.Context, entryPointName string) alice.Chain { - chain := alice.New() - - if c.accessLoggerMiddleware != nil || c.metricsRegistry != nil && (c.metricsRegistry.IsEpEnabled() || c.metricsRegistry.IsRouterEnabled() || c.metricsRegistry.IsSvcEnabled()) { - chain = chain.Append(capture.Wrap) - } - - if c.accessLoggerMiddleware != nil { - chain = chain.Append(accesslog.WrapHandler(c.accessLoggerMiddleware)) - } - - if c.tracer != nil { - chain = chain.Append(tracingMiddle.WrapEntryPointHandler(ctx, c.tracer, entryPointName)) - } - - if c.metricsRegistry != nil && c.metricsRegistry.IsEpEnabled() { - metricsHandler := metricsMiddle.WrapEntryPointHandler(ctx, c.metricsRegistry, entryPointName) - chain = chain.Append(tracingMiddle.WrapMiddleware(ctx, metricsHandler)) - } - - return chain -} - -// Close accessLogger and tracer. -func (c *ChainBuilder) Close() { - if c.accessLoggerMiddleware != nil { - if err := c.accessLoggerMiddleware.Close(); err != nil { - log.Error().Err(err).Msg("Could not close the access log file") - } - } -} diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index 655c1a047..3065b16ae 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -387,6 +387,9 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName) } + // The tracing middleware is a NOOP if tracing is not setup on the middleware chain. + // Hence, regarding internal resources' observability deactivation, + // this would not enable tracing. return tracing.WrapMiddleware(ctx, middleware), nil } diff --git a/pkg/server/middleware/observability.go b/pkg/server/middleware/observability.go new file mode 100644 index 000000000..af65cf480 --- /dev/null +++ b/pkg/server/middleware/observability.go @@ -0,0 +1,140 @@ +package middleware + +import ( + "context" + "io" + "net/http" + "strings" + + "github.com/containous/alice" + "github.com/rs/zerolog/log" + "github.com/traefik/traefik/v3/pkg/config/static" + "github.com/traefik/traefik/v3/pkg/logs" + "github.com/traefik/traefik/v3/pkg/metrics" + "github.com/traefik/traefik/v3/pkg/middlewares/accesslog" + "github.com/traefik/traefik/v3/pkg/middlewares/capture" + metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics" + tracingMiddle "github.com/traefik/traefik/v3/pkg/middlewares/tracing" + "go.opentelemetry.io/otel/trace" +) + +// ObservabilityMgr is a manager for observability (AccessLogs, Metrics and Tracing) enablement. +type ObservabilityMgr struct { + config static.Configuration + accessLoggerMiddleware *accesslog.Handler + metricsRegistry metrics.Registry + tracer trace.Tracer + tracerCloser io.Closer +} + +// NewObservabilityMgr creates a new ObservabilityMgr. +func NewObservabilityMgr(config static.Configuration, metricsRegistry metrics.Registry, accessLoggerMiddleware *accesslog.Handler, tracer trace.Tracer, tracerCloser io.Closer) *ObservabilityMgr { + return &ObservabilityMgr{ + config: config, + metricsRegistry: metricsRegistry, + accessLoggerMiddleware: accessLoggerMiddleware, + tracer: tracer, + tracerCloser: tracerCloser, + } +} + +// BuildEPChain an observability middleware chain by entry point. +func (c *ObservabilityMgr) BuildEPChain(ctx context.Context, entryPointName string, resourceName string) alice.Chain { + chain := alice.New() + + if c == nil { + return chain + } + + if c.accessLoggerMiddleware != nil || c.metricsRegistry != nil && (c.metricsRegistry.IsEpEnabled() || c.metricsRegistry.IsRouterEnabled() || c.metricsRegistry.IsSvcEnabled()) { + if c.ShouldAddAccessLogs(resourceName) || c.ShouldAddMetrics(resourceName) { + chain = chain.Append(capture.Wrap) + } + } + + if c.accessLoggerMiddleware != nil && c.ShouldAddAccessLogs(resourceName) { + chain = chain.Append(accesslog.WrapHandler(c.accessLoggerMiddleware)) + chain = chain.Append(func(next http.Handler) (http.Handler, error) { + return accesslog.NewFieldHandler(next, logs.EntryPointName, entryPointName, accesslog.InitServiceFields), nil + }) + } + + if c.tracer != nil && c.ShouldAddTracing(resourceName) { + chain = chain.Append(tracingMiddle.WrapEntryPointHandler(ctx, c.tracer, entryPointName)) + } + + if c.metricsRegistry != nil && c.metricsRegistry.IsEpEnabled() && c.ShouldAddMetrics(resourceName) { + metricsHandler := metricsMiddle.WrapEntryPointHandler(ctx, c.metricsRegistry, entryPointName) + + if c.tracer != nil && c.ShouldAddTracing(resourceName) { + chain = chain.Append(tracingMiddle.WrapMiddleware(ctx, metricsHandler)) + } else { + chain = chain.Append(metricsHandler) + } + } + + return chain +} + +// ShouldAddAccessLogs returns whether the access logs should be enabled for the given resource. +func (c *ObservabilityMgr) ShouldAddAccessLogs(resourceName string) bool { + if c == nil { + return false + } + + return c.config.AccessLog != nil && (c.config.AccessLog.AddInternals || !strings.HasSuffix(resourceName, "@internal")) +} + +// ShouldAddMetrics returns whether the metrics should be enabled for the given resource. +func (c *ObservabilityMgr) ShouldAddMetrics(resourceName string) bool { + if c == nil { + return false + } + + return c.config.Metrics != nil && (c.config.Metrics.AddInternals || !strings.HasSuffix(resourceName, "@internal")) +} + +// ShouldAddTracing returns whether the tracing should be enabled for the given resource. +func (c *ObservabilityMgr) ShouldAddTracing(resourceName string) bool { + if c == nil { + return false + } + + return c.config.Tracing != nil && (c.config.Tracing.AddInternals || !strings.HasSuffix(resourceName, "@internal")) +} + +// MetricsRegistry is an accessor to the metrics registry. +func (c *ObservabilityMgr) MetricsRegistry() metrics.Registry { + if c == nil { + return nil + } + + return c.metricsRegistry +} + +// Close closes the accessLogger and tracer. +func (c *ObservabilityMgr) Close() { + if c == nil { + return + } + + if c.accessLoggerMiddleware != nil { + if err := c.accessLoggerMiddleware.Close(); err != nil { + log.Error().Err(err).Msg("Could not close the access log file") + } + } + + if c.tracerCloser != nil { + if err := c.tracerCloser.Close(); err != nil { + log.Error().Err(err).Msg("Could not close the tracer") + } + } +} + +func (c *ObservabilityMgr) RotateAccessLogs() error { + if c.accessLoggerMiddleware == nil { + return nil + } + + return c.accessLoggerMiddleware.Rotate() +} diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index bfa99cfb0..eae645383 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -10,7 +10,6 @@ import ( "github.com/rs/zerolog/log" "github.com/traefik/traefik/v3/pkg/config/runtime" "github.com/traefik/traefik/v3/pkg/logs" - "github.com/traefik/traefik/v3/pkg/metrics" "github.com/traefik/traefik/v3/pkg/middlewares/accesslog" "github.com/traefik/traefik/v3/pkg/middlewares/denyrouterrecursion" metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics" @@ -35,21 +34,19 @@ type serviceManager interface { type Manager struct { routerHandlers map[string]http.Handler serviceManager serviceManager - metricsRegistry metrics.Registry + observabilityMgr *middleware.ObservabilityMgr middlewaresBuilder middlewareBuilder - chainBuilder *middleware.ChainBuilder conf *runtime.Configuration tlsManager *tls.Manager } // NewManager creates a new Manager. -func NewManager(conf *runtime.Configuration, serviceManager serviceManager, middlewaresBuilder middlewareBuilder, chainBuilder *middleware.ChainBuilder, metricsRegistry metrics.Registry, tlsManager *tls.Manager) *Manager { +func NewManager(conf *runtime.Configuration, serviceManager serviceManager, middlewaresBuilder middlewareBuilder, observabilityMgr *middleware.ObservabilityMgr, tlsManager *tls.Manager) *Manager { return &Manager{ routerHandlers: make(map[string]http.Handler), serviceManager: serviceManager, - metricsRegistry: metricsRegistry, + observabilityMgr: observabilityMgr, middlewaresBuilder: middlewaresBuilder, - chainBuilder: chainBuilder, conf: conf, tlsManager: tlsManager, } @@ -73,49 +70,49 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t logger := log.Ctx(rootCtx).With().Str(logs.EntryPointName, entryPointName).Logger() ctx := logger.WithContext(rootCtx) - handler, err := m.buildEntryPointHandler(ctx, routers) + handler, err := m.buildEntryPointHandler(ctx, entryPointName, routers) if err != nil { logger.Error().Err(err).Send() continue } - handlerWithAccessLog, err := alice.New(func(next http.Handler) (http.Handler, error) { - return accesslog.NewFieldHandler(next, logs.EntryPointName, entryPointName, accesslog.InitServiceFields), nil - }).Then(handler) - if err != nil { - logger.Error().Err(err).Send() - entryPointHandlers[entryPointName] = handler - } else { - entryPointHandlers[entryPointName] = handlerWithAccessLog - } + entryPointHandlers[entryPointName] = handler } + // Create default handlers. for _, entryPointName := range entryPoints { logger := log.Ctx(rootCtx).With().Str(logs.EntryPointName, entryPointName).Logger() ctx := logger.WithContext(rootCtx) handler, ok := entryPointHandlers[entryPointName] - if !ok || handler == nil { - handler = BuildDefaultHTTPRouter() + if ok || handler != nil { + continue } - handlerWithMiddlewares, err := m.chainBuilder.Build(ctx, entryPointName).Then(handler) + handler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, "").Then(BuildDefaultHTTPRouter()) if err != nil { logger.Error().Err(err).Send() continue } - entryPointHandlers[entryPointName] = handlerWithMiddlewares + entryPointHandlers[entryPointName] = handler } return entryPointHandlers } -func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.RouterInfo) (http.Handler, error) { +func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName string, configs map[string]*runtime.RouterInfo) (http.Handler, error) { muxer, err := httpmuxer.NewMuxer() if err != nil { return nil, err } + defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, "defaultHandler").Then(http.NotFoundHandler()) + if err != nil { + return nil, err + } + + muxer.SetDefaultHandler(defaultHandler) + for routerName, routerConfig := range configs { logger := log.Ctx(ctx).With().Str(logs.RouterName, routerName).Logger() ctxRouter := logger.WithContext(provider.AddInContext(ctx, routerName)) @@ -131,6 +128,14 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string continue } + observabilityChain := m.observabilityMgr.BuildEPChain(ctx, entryPointName, routerConfig.Service) + handler, err = observabilityChain.Then(handler) + if err != nil { + routerConfig.AddError(err, true) + logger.Error().Err(err).Send() + continue + } + if err = muxer.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, handler); err != nil { routerConfig.AddError(err, true) logger.Error().Err(err).Send() @@ -167,6 +172,12 @@ func (m *Manager) buildRouterHandler(ctx context.Context, routerName string, rou return nil, err } + // Prevents from enabling observability for internal resources. + if !m.observabilityMgr.ShouldAddAccessLogs(provider.GetQualifiedName(ctx, routerConfig.Service)) { + m.routerHandlers[routerName] = handler + return m.routerHandlers[routerName], nil + } + handlerWithAccessLog, err := alice.New(func(next http.Handler) (http.Handler, error) { return accesslog.NewFieldHandler(next, accesslog.RouterName, routerName, nil), nil }).Then(handler) @@ -200,10 +211,20 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn chain := alice.New() + if m.observabilityMgr.MetricsRegistry() != nil && m.observabilityMgr.MetricsRegistry().IsRouterEnabled() && + m.observabilityMgr.ShouldAddMetrics(provider.GetQualifiedName(ctx, router.Service)) { + chain = chain.Append(metricsMiddle.WrapRouterHandler(ctx, m.observabilityMgr.MetricsRegistry(), routerName, provider.GetQualifiedName(ctx, router.Service))) + } + + // Prevents from enabling tracing for internal resources. + if !m.observabilityMgr.ShouldAddTracing(provider.GetQualifiedName(ctx, router.Service)) { + return chain.Extend(*mHandler).Then(sHandler) + } + chain = chain.Append(tracing.WrapRouterHandler(ctx, routerName, router.Rule, provider.GetQualifiedName(ctx, router.Service))) - if m.metricsRegistry != nil && m.metricsRegistry.IsRouterEnabled() { - metricsHandler := metricsMiddle.WrapRouterHandler(ctx, m.metricsRegistry, routerName, provider.GetQualifiedName(ctx, router.Service)) + if m.observabilityMgr.MetricsRegistry() != nil && m.observabilityMgr.MetricsRegistry().IsRouterEnabled() { + metricsHandler := metricsMiddle.WrapRouterHandler(ctx, m.observabilityMgr.MetricsRegistry(), routerName, provider.GetQualifiedName(ctx, router.Service)) chain = chain.Append(tracing.WrapMiddleware(ctx, metricsHandler)) } diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index 21e0de141..eb83ed25d 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -9,21 +9,15 @@ import ( "testing" "time" - "github.com/containous/alice" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ptypes "github.com/traefik/paerser/types" "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/config/runtime" - "github.com/traefik/traefik/v3/pkg/metrics" - "github.com/traefik/traefik/v3/pkg/middlewares/accesslog" - "github.com/traefik/traefik/v3/pkg/middlewares/capture" "github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator" "github.com/traefik/traefik/v3/pkg/server/middleware" "github.com/traefik/traefik/v3/pkg/server/service" "github.com/traefik/traefik/v3/pkg/testhelpers" "github.com/traefik/traefik/v3/pkg/tls" - "github.com/traefik/traefik/v3/pkg/types" ) func TestRouterManager_Get(t *testing.T) { @@ -319,10 +313,9 @@ func TestRouterManager_Get(t *testing.T) { roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) - chainBuilder := middleware.NewChainBuilder(nil, nil, nil) tlsManager := tls.NewManager() - routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager) handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false) @@ -341,126 +334,6 @@ func TestRouterManager_Get(t *testing.T) { } } -func TestAccessLog(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) - - t.Cleanup(func() { server.Close() }) - - testCases := []struct { - desc string - routersConfig map[string]*dynamic.Router - serviceConfig map[string]*dynamic.Service - middlewaresConfig map[string]*dynamic.Middleware - entryPoints []string - expected string - }{ - { - desc: "apply routerName in accesslog (first match)", - routersConfig: map[string]*dynamic.Router{ - "foo": { - EntryPoints: []string{"web"}, - Service: "foo-service", - Rule: "Host(`foo.bar`)", - }, - "bar": { - EntryPoints: []string{"web"}, - Service: "foo-service", - Rule: "Host(`bar.foo`)", - }, - }, - serviceConfig: map[string]*dynamic.Service{ - "foo-service": { - LoadBalancer: &dynamic.ServersLoadBalancer{ - Servers: []dynamic.Server{ - { - URL: server.URL, - }, - }, - }, - }, - }, - entryPoints: []string{"web"}, - expected: "foo", - }, - { - desc: "apply routerName in accesslog (second match)", - routersConfig: map[string]*dynamic.Router{ - "foo": { - EntryPoints: []string{"web"}, - Service: "foo-service", - Rule: "Host(`bar.foo`)", - }, - "bar": { - EntryPoints: []string{"web"}, - Service: "foo-service", - Rule: "Host(`foo.bar`)", - }, - }, - serviceConfig: map[string]*dynamic.Service{ - "foo-service": { - LoadBalancer: &dynamic.ServersLoadBalancer{ - Servers: []dynamic.Server{ - { - URL: server.URL, - }, - }, - }, - }, - }, - entryPoints: []string{"web"}, - expected: "bar", - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - rtConf := runtime.NewConfig(dynamic.Configuration{ - HTTP: &dynamic.HTTPConfiguration{ - Services: test.serviceConfig, - Routers: test.routersConfig, - Middlewares: test.middlewaresConfig, - }, - }) - - roundTripperManager := service.NewRoundTripperManager(nil) - roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) - serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager) - middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) - chainBuilder := middleware.NewChainBuilder(nil, nil, nil) - tlsManager := tls.NewManager() - - routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager) - - handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, false) - - w := httptest.NewRecorder() - req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil) - - accesslogger, err := accesslog.NewHandler(&types.AccessLog{ - Format: "json", - }) - require.NoError(t, err) - - reqHost := requestdecorator.New(nil) - - chain := alice.New() - chain = chain.Append(capture.Wrap) - chain = chain.Append(accesslog.WrapHandler(accesslogger)) - handler, err := chain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - reqHost.ServeHTTP(w, req, handlers["web"].ServeHTTP) - - data := accesslog.GetLogData(req) - require.NotNil(t, data) - - assert.Equal(t, test.expected, data.Core[accesslog.RouterName]) - })) - require.NoError(t, err) - - handler.ServeHTTP(w, req) - }) - } -} - func TestRuntimeConfiguration(t *testing.T) { testCases := []struct { desc string @@ -788,11 +661,10 @@ func TestRuntimeConfiguration(t *testing.T) { roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) - chainBuilder := middleware.NewChainBuilder(nil, nil, nil) tlsManager := tls.NewManager() tlsManager.UpdateConfigs(context.Background(), nil, test.tlsOptions, nil) - routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager) _ = routerManager.BuildHandlers(context.Background(), entryPoints, false) _ = routerManager.BuildHandlers(context.Background(), entryPoints, true) @@ -866,10 +738,9 @@ func TestProviderOnMiddlewares(t *testing.T) { roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) - chainBuilder := middleware.NewChainBuilder(nil, nil, nil) tlsManager := tls.NewManager() - routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager) _ = routerManager.BuildHandlers(context.Background(), entryPoints, false) @@ -935,10 +806,9 @@ func BenchmarkRouterServe(b *testing.B) { serviceManager := service.NewManager(rtConf.Services, nil, nil, staticRoundTripperGetter{res}) middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil) - chainBuilder := middleware.NewChainBuilder(nil, nil, nil) tlsManager := tls.NewManager() - routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, chainBuilder, metrics.NewVoidRegistry(), tlsManager) + routerManager := NewManager(rtConf, serviceManager, middlewaresBuilder, nil, tlsManager) handlers := routerManager.BuildHandlers(context.Background(), entryPoints, false) diff --git a/pkg/server/routerfactory.go b/pkg/server/routerfactory.go index d7a1bf68c..1ece7096a 100644 --- a/pkg/server/routerfactory.go +++ b/pkg/server/routerfactory.go @@ -6,7 +6,6 @@ import ( "github.com/rs/zerolog/log" "github.com/traefik/traefik/v3/pkg/config/runtime" "github.com/traefik/traefik/v3/pkg/config/static" - "github.com/traefik/traefik/v3/pkg/metrics" "github.com/traefik/traefik/v3/pkg/server/middleware" tcpmiddleware "github.com/traefik/traefik/v3/pkg/server/middleware/tcp" "github.com/traefik/traefik/v3/pkg/server/router" @@ -25,13 +24,12 @@ type RouterFactory struct { entryPointsTCP []string entryPointsUDP []string - managerFactory *service.ManagerFactory - metricsRegistry metrics.Registry + managerFactory *service.ManagerFactory pluginBuilder middleware.PluginsBuilder - chainBuilder *middleware.ChainBuilder - tlsManager *tls.Manager + observabilityMgr *middleware.ObservabilityMgr + tlsManager *tls.Manager dialerManager *tcp.DialerManager @@ -40,7 +38,7 @@ type RouterFactory struct { // NewRouterFactory creates a new RouterFactory. func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *service.ManagerFactory, tlsManager *tls.Manager, - chainBuilder *middleware.ChainBuilder, pluginBuilder middleware.PluginsBuilder, metricsRegistry metrics.Registry, dialerManager *tcp.DialerManager, + observabilityMgr *middleware.ObservabilityMgr, pluginBuilder middleware.PluginsBuilder, dialerManager *tcp.DialerManager, ) *RouterFactory { var entryPointsTCP, entryPointsUDP []string for name, cfg := range staticConfiguration.EntryPoints { @@ -58,14 +56,13 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory * } return &RouterFactory{ - entryPointsTCP: entryPointsTCP, - entryPointsUDP: entryPointsUDP, - managerFactory: managerFactory, - metricsRegistry: metricsRegistry, - tlsManager: tlsManager, - chainBuilder: chainBuilder, - pluginBuilder: pluginBuilder, - dialerManager: dialerManager, + entryPointsTCP: entryPointsTCP, + entryPointsUDP: entryPointsUDP, + managerFactory: managerFactory, + observabilityMgr: observabilityMgr, + tlsManager: tlsManager, + pluginBuilder: pluginBuilder, + dialerManager: dialerManager, } } @@ -83,7 +80,7 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, f.pluginBuilder) - routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, f.chainBuilder, f.metricsRegistry, f.tlsManager) + routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, f.observabilityMgr, f.tlsManager) handlersNonTLS := routerManager.BuildHandlers(ctx, f.entryPointsTCP, false) handlersTLS := routerManager.BuildHandlers(ctx, f.entryPointsTCP, true) diff --git a/pkg/server/routerfactory_test.go b/pkg/server/routerfactory_test.go index b935df89e..ebd923f62 100644 --- a/pkg/server/routerfactory_test.go +++ b/pkg/server/routerfactory_test.go @@ -9,7 +9,6 @@ import ( "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/config/runtime" "github.com/traefik/traefik/v3/pkg/config/static" - "github.com/traefik/traefik/v3/pkg/metrics" "github.com/traefik/traefik/v3/pkg/server/middleware" "github.com/traefik/traefik/v3/pkg/server/service" "github.com/traefik/traefik/v3/pkg/tcp" @@ -51,12 +50,12 @@ func TestReuseService(t *testing.T) { roundTripperManager := service.NewRoundTripperManager(nil) roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) - managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil) + managerFactory := service.NewManagerFactory(staticConfig, nil, nil, roundTripperManager, nil) tlsManager := tls.NewManager() dialerManager := tcp.NewDialerManager(nil) dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}}) - factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(nil, nil, nil), nil, metrics.NewVoidRegistry(), dialerManager) + factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, nil, nil, dialerManager) entryPointsHandlers, _ := factory.CreateRouters(runtime.NewConfig(dynamic.Configuration{HTTP: dynamicConfigs})) @@ -189,12 +188,13 @@ func TestServerResponseEmptyBackend(t *testing.T) { roundTripperManager := service.NewRoundTripperManager(nil) roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) - managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil) + managerFactory := service.NewManagerFactory(staticConfig, nil, nil, roundTripperManager, nil) tlsManager := tls.NewManager() dialerManager := tcp.NewDialerManager(nil) dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}}) - factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(nil, nil, nil), nil, metrics.NewVoidRegistry(), dialerManager) + observabiltyMgr := middleware.NewObservabilityMgr(staticConfig, nil, nil, nil, nil) + factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, observabiltyMgr, nil, dialerManager) entryPointsHandlers, _ := factory.CreateRouters(runtime.NewConfig(dynamic.Configuration{HTTP: test.config(testServer.URL)})) @@ -232,14 +232,12 @@ func TestInternalServices(t *testing.T) { roundTripperManager := service.NewRoundTripperManager(nil) roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) - managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil) + managerFactory := service.NewManagerFactory(staticConfig, nil, nil, roundTripperManager, nil) tlsManager := tls.NewManager() - voidRegistry := metrics.NewVoidRegistry() - dialerManager := tcp.NewDialerManager(nil) dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}}) - factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, middleware.NewChainBuilder(voidRegistry, nil, nil), nil, voidRegistry, dialerManager) + factory := NewRouterFactory(staticConfig, managerFactory, tlsManager, nil, nil, dialerManager) entryPointsHandlers, _ := factory.CreateRouters(runtime.NewConfig(dynamic.Configuration{HTTP: dynamicConfigs})) diff --git a/pkg/server/server.go b/pkg/server/server.go index 0f4379f04..9c96bdcc6 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -3,47 +3,39 @@ package server import ( "context" "errors" - "io" "os" "os/signal" "time" "github.com/rs/zerolog/log" "github.com/traefik/traefik/v3/pkg/metrics" - "github.com/traefik/traefik/v3/pkg/middlewares/accesslog" "github.com/traefik/traefik/v3/pkg/safe" "github.com/traefik/traefik/v3/pkg/server/middleware" ) // Server is the reverse-proxy/load-balancer engine. type Server struct { - watcher *ConfigurationWatcher - tcpEntryPoints TCPEntryPoints - udpEntryPoints UDPEntryPoints - chainBuilder *middleware.ChainBuilder - - accessLoggerMiddleware *accesslog.Handler + watcher *ConfigurationWatcher + tcpEntryPoints TCPEntryPoints + udpEntryPoints UDPEntryPoints + observabilityMgr *middleware.ObservabilityMgr signals chan os.Signal stopChan chan bool routinesPool *safe.Pool - - tracerCloser io.Closer } // NewServer returns an initialized Server. -func NewServer(routinesPool *safe.Pool, entryPoints TCPEntryPoints, entryPointsUDP UDPEntryPoints, watcher *ConfigurationWatcher, chainBuilder *middleware.ChainBuilder, accessLoggerMiddleware *accesslog.Handler, tracerCloser io.Closer) *Server { +func NewServer(routinesPool *safe.Pool, entryPoints TCPEntryPoints, entryPointsUDP UDPEntryPoints, watcher *ConfigurationWatcher, observabilityMgr *middleware.ObservabilityMgr) *Server { srv := &Server{ - watcher: watcher, - tcpEntryPoints: entryPoints, - chainBuilder: chainBuilder, - accessLoggerMiddleware: accessLoggerMiddleware, - signals: make(chan os.Signal, 1), - stopChan: make(chan bool, 1), - routinesPool: routinesPool, - udpEntryPoints: entryPointsUDP, - tracerCloser: tracerCloser, + watcher: watcher, + tcpEntryPoints: entryPoints, + observabilityMgr: observabilityMgr, + signals: make(chan os.Signal, 1), + stopChan: make(chan bool, 1), + routinesPool: routinesPool, + udpEntryPoints: entryPointsUDP, } srv.configureSignals() @@ -105,13 +97,7 @@ func (s *Server) Close() { close(s.stopChan) - s.chainBuilder.Close() - - if s.tracerCloser != nil { - if err := s.tracerCloser.Close(); err != nil { - log.Error().Err(err).Msg("Could not close the tracer") - } - } + s.observabilityMgr.Close() cancel() } diff --git a/pkg/server/server_signals.go b/pkg/server/server_signals.go index 0987abb30..c582fce5c 100644 --- a/pkg/server/server_signals.go +++ b/pkg/server/server_signals.go @@ -24,10 +24,8 @@ func (s *Server) listenSignals(ctx context.Context) { if sig == syscall.SIGUSR1 { log.Info().Msgf("Closing and re-opening log files for rotation: %+v", sig) - if s.accessLoggerMiddleware != nil { - if err := s.accessLoggerMiddleware.Rotate(); err != nil { - log.Error().Err(err).Msg("Error rotating access log") - } + if err := s.observabilityMgr.RotateAccessLogs(); err != nil { + log.Error().Err(err).Msg("Error rotating access log") } } } diff --git a/pkg/server/service/managerfactory.go b/pkg/server/service/managerfactory.go index 524ae01d9..abff2d5a6 100644 --- a/pkg/server/service/managerfactory.go +++ b/pkg/server/service/managerfactory.go @@ -10,11 +10,12 @@ import ( "github.com/traefik/traefik/v3/pkg/config/static" "github.com/traefik/traefik/v3/pkg/metrics" "github.com/traefik/traefik/v3/pkg/safe" + "github.com/traefik/traefik/v3/pkg/server/middleware" ) // ManagerFactory a factory of service manager. type ManagerFactory struct { - metricsRegistry metrics.Registry + observabilityMgr *middleware.ObservabilityMgr roundTripperManager *RoundTripperManager @@ -29,9 +30,9 @@ type ManagerFactory struct { } // NewManagerFactory creates a new ManagerFactory. -func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *safe.Pool, metricsRegistry metrics.Registry, roundTripperManager *RoundTripperManager, acmeHTTPHandler http.Handler) *ManagerFactory { +func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *safe.Pool, observabilityMgr *middleware.ObservabilityMgr, roundTripperManager *RoundTripperManager, acmeHTTPHandler http.Handler) *ManagerFactory { factory := &ManagerFactory{ - metricsRegistry: metricsRegistry, + observabilityMgr: observabilityMgr, routinesPool: routinesPool, roundTripperManager: roundTripperManager, acmeHTTPHandler: acmeHTTPHandler, @@ -72,7 +73,7 @@ func NewManagerFactory(staticConfiguration static.Configuration, routinesPool *s // Build creates a service manager. func (f *ManagerFactory) Build(configuration *runtime.Configuration) *InternalHandlers { - svcManager := NewManager(configuration.Services, f.metricsRegistry, f.routinesPool, f.roundTripperManager) + svcManager := NewManager(configuration.Services, f.observabilityMgr, f.routinesPool, f.roundTripperManager) var apiHandler http.Handler if f.api != nil { diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 2d71791a8..b01d2327c 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -20,12 +20,12 @@ import ( "github.com/traefik/traefik/v3/pkg/config/runtime" "github.com/traefik/traefik/v3/pkg/healthcheck" "github.com/traefik/traefik/v3/pkg/logs" - "github.com/traefik/traefik/v3/pkg/metrics" "github.com/traefik/traefik/v3/pkg/middlewares/accesslog" metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics" tracingMiddle "github.com/traefik/traefik/v3/pkg/middlewares/tracing" "github.com/traefik/traefik/v3/pkg/safe" "github.com/traefik/traefik/v3/pkg/server/cookie" + "github.com/traefik/traefik/v3/pkg/server/middleware" "github.com/traefik/traefik/v3/pkg/server/provider" "github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/failover" "github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/mirror" @@ -42,7 +42,7 @@ type RoundTripperGetter interface { // Manager The service manager. type Manager struct { routinePool *safe.Pool - metricsRegistry metrics.Registry + observabilityMgr *middleware.ObservabilityMgr bufferPool httputil.BufferPool roundTripperManager RoundTripperGetter @@ -53,10 +53,10 @@ type Manager struct { } // NewManager creates a new Manager. -func NewManager(configs map[string]*runtime.ServiceInfo, metricsRegistry metrics.Registry, routinePool *safe.Pool, roundTripperManager RoundTripperGetter) *Manager { +func NewManager(configs map[string]*runtime.ServiceInfo, observabilityMgr *middleware.ObservabilityMgr, routinePool *safe.Pool, roundTripperManager RoundTripperGetter) *Manager { return &Manager{ routinePool: routinePool, - metricsRegistry: metricsRegistry, + observabilityMgr: observabilityMgr, bufferPool: newBufferPool(), roundTripperManager: roundTripperManager, services: make(map[string]http.Handler), @@ -302,12 +302,17 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName proxy := buildSingleHostProxy(target, passHostHeader, time.Duration(flushInterval), roundTripper, m.bufferPool) - proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceURL, target.String(), nil) - proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceAddr, target.Host, nil) - proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceName, serviceName, accesslog.AddServiceFields) + // Prevents from enabling observability for internal resources. - if m.metricsRegistry != nil && m.metricsRegistry.IsSvcEnabled() { - metricsHandler := metricsMiddle.WrapServiceHandler(ctx, m.metricsRegistry, serviceName) + if m.observabilityMgr.ShouldAddAccessLogs(provider.GetQualifiedName(ctx, serviceName)) { + proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceURL, target.String(), nil) + proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceAddr, target.Host, nil) + proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceName, serviceName, accesslog.AddServiceFields) + } + + if m.observabilityMgr.MetricsRegistry() != nil && m.observabilityMgr.MetricsRegistry().IsSvcEnabled() && + m.observabilityMgr.ShouldAddMetrics(provider.GetQualifiedName(ctx, serviceName)) { + metricsHandler := metricsMiddle.WrapServiceHandler(ctx, m.observabilityMgr.MetricsRegistry(), serviceName) proxy, err = alice.New(). Append(tracingMiddle.WrapMiddleware(ctx, metricsHandler)). @@ -317,7 +322,9 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName } } - proxy = tracingMiddle.NewService(ctx, serviceName, proxy) + if m.observabilityMgr.ShouldAddTracing(provider.GetQualifiedName(ctx, serviceName)) { + proxy = tracingMiddle.NewService(ctx, serviceName, proxy) + } lb.Add(proxyName, proxy, server.Weight) @@ -330,7 +337,7 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName if service.HealthCheck != nil { m.healthCheckers[serviceName] = healthcheck.NewServiceHealthChecker( ctx, - m.metricsRegistry, + m.observabilityMgr.MetricsRegistry(), service.HealthCheck, lb, info, diff --git a/pkg/tracing/tracing.go b/pkg/tracing/tracing.go index 6bb382920..79c6cf5d3 100644 --- a/pkg/tracing/tracing.go +++ b/pkg/tracing/tracing.go @@ -42,6 +42,11 @@ func NewTracing(conf *static.Tracing) (trace.Tracer, io.Closer, error) { // TracerFromContext extracts the trace.Tracer from the given context. func TracerFromContext(ctx context.Context) trace.Tracer { + // Prevent picking trace.noopSpan tracer. + if !trace.SpanContextFromContext(ctx).IsValid() { + return nil + } + span := trace.SpanFromContext(ctx) if span != nil && span.TracerProvider() != nil { return span.TracerProvider().Tracer("github.com/traefik/traefik") diff --git a/pkg/types/logs.go b/pkg/types/logs.go index 657e56102..e0a50ec71 100644 --- a/pkg/types/logs.go +++ b/pkg/types/logs.go @@ -45,6 +45,7 @@ type AccessLog struct { Filters *AccessLogFilters `description:"Access log filters, used to keep only specific access logs." json:"filters,omitempty" toml:"filters,omitempty" yaml:"filters,omitempty" export:"true"` Fields *AccessLogFields `description:"AccessLogFields." json:"fields,omitempty" toml:"fields,omitempty" yaml:"fields,omitempty" export:"true"` BufferingSize int64 `description:"Number of access log lines to process in a buffered way." json:"bufferingSize,omitempty" toml:"bufferingSize,omitempty" yaml:"bufferingSize,omitempty" export:"true"` + AddInternals bool `description:"Enables access log for internal services (ping, dashboard, etc...)." json:"addInternals,omitempty" toml:"addInternals,omitempty" yaml:"addInternals,omitempty" export:"true"` } // SetDefaults sets the default values. diff --git a/pkg/types/metrics.go b/pkg/types/metrics.go index 665333630..571666e23 100644 --- a/pkg/types/metrics.go +++ b/pkg/types/metrics.go @@ -10,6 +10,8 @@ import ( // Metrics provides options to expose and send Traefik metrics to different third party monitoring systems. type Metrics struct { + AddInternals bool `description:"Enables metrics for internal services (ping, dashboard, etc...)." json:"addInternals,omitempty" toml:"addInternals,omitempty" yaml:"addInternals,omitempty" export:"true"` + Prometheus *Prometheus `description:"Prometheus metrics exporter type." json:"prometheus,omitempty" toml:"prometheus,omitempty" yaml:"prometheus,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` Datadog *Datadog `description:"Datadog metrics exporter type." json:"datadog,omitempty" toml:"datadog,omitempty" yaml:"datadog,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` StatsD *Statsd `description:"StatsD metrics exporter type." json:"statsD,omitempty" toml:"statsD,omitempty" yaml:"statsD,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` From 9be523d7720361283d89e80754a5ee85ae24eb60 Mon Sep 17 00:00:00 2001 From: Pascal Hofmann Date: Tue, 30 Jan 2024 16:44:05 +0100 Subject: [PATCH 26/36] Support for cross-namespace references / GatewayAPI ReferenceGrants --- integration/k8s_conformance_test.go | 8 +- pkg/provider/kubernetes/gateway/client.go | 36 +- .../kubernetes/gateway/client_mock_test.go | 29 +- .../fixtures/referencegrant/for_secret.yml | 78 ++++ .../referencegrant/for_secret_missing.yml | 64 +++ .../for_secret_not_matching_from.yml | 78 ++++ .../for_secret_not_matching_to.yml | 79 ++++ pkg/provider/kubernetes/gateway/kubernetes.go | 198 ++++++-- .../kubernetes/gateway/kubernetes_test.go | 435 ++++++++++++++++++ pkg/provider/kubernetes/k8s/parser.go | 2 +- 10 files changed, 940 insertions(+), 67 deletions(-) create mode 100644 pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret.yml create mode 100644 pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret_missing.yml create mode 100644 pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret_not_matching_from.yml create mode 100644 pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret_not_matching_to.yml diff --git a/integration/k8s_conformance_test.go b/integration/k8s_conformance_test.go index f5c869f6c..88be63c28 100644 --- a/integration/k8s_conformance_test.go +++ b/integration/k8s_conformance_test.go @@ -153,17 +153,16 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() { RequiredConsecutiveSuccesses: 0, }, SupportedFeatures: sets.New[ksuite.SupportedFeature](). - Insert(ksuite.GatewayCoreFeatures.UnsortedList()...), + Insert(ksuite.GatewayCoreFeatures.UnsortedList()...). + Insert(ksuite.ReferenceGrantCoreFeatures.UnsortedList()...), EnableAllSupportedFeatures: false, RunTest: *k8sConformanceRunTest, // Until the feature are all supported, following tests are skipped. SkipTests: []string{ "HTTPExactPathMatching", "HTTPRouteHostnameIntersection", - "GatewaySecretReferenceGrantAllInNamespace", "HTTPRouteListenerHostnameMatching", "HTTPRouteRequestHeaderModifier", - "GatewaySecretInvalidReferenceGrant", "GatewayClassObservedGenerationBump", "HTTPRouteInvalidNonExistentBackendRef", "GatewayWithAttachedRoutes", @@ -171,14 +170,11 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() { "HTTPRouteDisallowedKind", "HTTPRouteInvalidReferenceGrant", "HTTPRouteObservedGenerationBump", - "GatewayInvalidRouteKind", "TLSRouteSimpleSameNamespace", "TLSRouteInvalidReferenceGrant", "HTTPRouteInvalidCrossNamespaceParentRef", "HTTPRouteInvalidParentRefNotMatchingSectionName", - "GatewaySecretReferenceGrantSpecific", "GatewayModifyListeners", - "GatewaySecretMissingReferenceGrant", "GatewayInvalidTLSConfiguration", "HTTPRouteInvalidCrossNamespaceBackendRef", "HTTPRouteMatchingAcrossRoutes", diff --git a/pkg/provider/kubernetes/gateway/client.go b/pkg/provider/kubernetes/gateway/client.go index 709be079d..dc957e966 100644 --- a/pkg/provider/kubernetes/gateway/client.go +++ b/pkg/provider/kubernetes/gateway/client.go @@ -19,6 +19,7 @@ import ( "k8s.io/client-go/tools/clientcmd" gatev1 "sigs.k8s.io/gateway-api/apis/v1" gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatev1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" gateclientset "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned" gateinformers "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions" ) @@ -34,13 +35,7 @@ func (reh *resourceEventHandler) OnAdd(obj interface{}, isInInitialList bool) { } func (reh *resourceEventHandler) OnUpdate(oldObj, newObj interface{}) { - switch oldObj.(type) { - case *gatev1.GatewayClass: - // Skip update for gateway classes. We only manage addition or deletion for this cluster-wide resource. - return - default: - eventHandlerFunc(reh.ev, newObj) - } + eventHandlerFunc(reh.ev, newObj) } func (reh *resourceEventHandler) OnDelete(obj interface{}) { @@ -59,6 +54,7 @@ type Client interface { GetHTTPRoutes(namespaces []string) ([]*gatev1.HTTPRoute, error) GetTCPRoutes(namespaces []string) ([]*gatev1alpha2.TCPRoute, error) GetTLSRoutes(namespaces []string) ([]*gatev1alpha2.TLSRoute, error) + GetReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error) GetService(namespace, name string) (*corev1.Service, bool, error) GetSecret(namespace, name string) (*corev1.Secret, bool, error) GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error) @@ -189,9 +185,6 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (< return nil, err } - // TODO manage Reference Policy - // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy - for _, ns := range namespaces { factoryGateway := gateinformers.NewSharedInformerFactoryWithOptions(c.csGateway, resyncPeriod, gateinformers.WithNamespace(ns)) _, err = factoryGateway.Gateway().V1().Gateways().Informer().AddEventHandler(eventHandler) @@ -210,6 +203,10 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (< if err != nil { return nil, err } + _, err = factoryGateway.Gateway().V1beta1().ReferenceGrants().Informer().AddEventHandler(eventHandler) + if err != nil { + return nil, err + } factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns)) _, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler) @@ -363,6 +360,21 @@ func (c *clientWrapper) GetTLSRoutes(namespaces []string) ([]*gatev1alpha2.TLSRo return tlsRoutes, nil } +func (c *clientWrapper) GetReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error) { + if !c.isWatchedNamespace(namespace) { + log.Warn().Msgf("Failed to get ReferenceGrants: %q is not within watched namespaces", namespace) + + return nil, fmt.Errorf("failed to get ReferenceGrants: namespace %s is not within watched namespaces", namespace) + } + + referenceGrants, err := c.factoriesGateway[c.lookupNamespace(namespace)].Gateway().V1beta1().ReferenceGrants().Lister().ReferenceGrants(namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + return referenceGrants, nil +} + func (c *clientWrapper) GetGateways() []*gatev1.Gateway { var result []*gatev1.Gateway @@ -388,7 +400,7 @@ func (c *clientWrapper) UpdateGatewayClassStatus(gatewayClass *gatev1.GatewayCla var newConditions []metav1.Condition for _, cond := range gc.Status.Conditions { // No update for identical condition. - if cond.Type == condition.Type && cond.Status == condition.Status { + if cond.Type == condition.Type && cond.Status == condition.Status && cond.ObservedGeneration == condition.ObservedGeneration { return nil } @@ -470,7 +482,7 @@ func conditionsEquals(conditionsA, conditionsB []metav1.Condition) bool { for _, conditionA := range conditionsA { for _, conditionB := range conditionsB { if conditionA.Type == conditionB.Type { - if conditionA.Reason != conditionB.Reason || conditionA.Status != conditionB.Status || conditionA.Message != conditionB.Message { + if conditionA.Reason != conditionB.Reason || conditionA.Status != conditionB.Status || conditionA.Message != conditionB.Message || conditionA.ObservedGeneration != conditionB.ObservedGeneration { return false } conditionMatches++ diff --git a/pkg/provider/kubernetes/gateway/client_mock_test.go b/pkg/provider/kubernetes/gateway/client_mock_test.go index 01611de40..b19dd9609 100644 --- a/pkg/provider/kubernetes/gateway/client_mock_test.go +++ b/pkg/provider/kubernetes/gateway/client_mock_test.go @@ -12,6 +12,7 @@ import ( kscheme "k8s.io/client-go/kubernetes/scheme" gatev1 "sigs.k8s.io/gateway-api/apis/v1" gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatev1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) var _ Client = (*clientMock)(nil) @@ -23,6 +24,11 @@ func init() { panic(err) } + err = gatev1beta1.AddToScheme(kscheme.Scheme) + if err != nil { + panic(err) + } + err = gatev1.AddToScheme(kscheme.Scheme) if err != nil { panic(err) @@ -39,11 +45,12 @@ type clientMock struct { apiSecretError error apiEndpointsError error - gatewayClasses []*gatev1.GatewayClass - gateways []*gatev1.Gateway - httpRoutes []*gatev1.HTTPRoute - tcpRoutes []*gatev1alpha2.TCPRoute - tlsRoutes []*gatev1alpha2.TLSRoute + gatewayClasses []*gatev1.GatewayClass + gateways []*gatev1.Gateway + httpRoutes []*gatev1.HTTPRoute + tcpRoutes []*gatev1alpha2.TCPRoute + tlsRoutes []*gatev1alpha2.TLSRoute + referenceGrants []*gatev1beta1.ReferenceGrant watchChan chan interface{} } @@ -78,6 +85,8 @@ func newClientMock(paths ...string) clientMock { c.tcpRoutes = append(c.tcpRoutes, o) case *gatev1alpha2.TLSRoute: c.tlsRoutes = append(c.tlsRoutes, o) + case *gatev1beta1.ReferenceGrant: + c.referenceGrants = append(c.referenceGrants, o) default: panic(fmt.Sprintf("Unknown runtime object %+v %T", o, o)) } @@ -190,6 +199,16 @@ func (c clientMock) GetTLSRoutes(namespaces []string) ([]*gatev1alpha2.TLSRoute, return tlsRoutes, nil } +func (c clientMock) GetReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error) { + var referenceGrants []*gatev1beta1.ReferenceGrant + for _, referenceGrant := range c.referenceGrants { + if inNamespace(referenceGrant.ObjectMeta, namespace) { + referenceGrants = append(referenceGrants, referenceGrant) + } + } + return referenceGrants, nil +} + func (c clientMock) GetService(namespace, name string) (*corev1.Service, bool, error) { if c.apiServiceError != nil { return nil, false, c.apiServiceError diff --git a/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret.yml b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret.yml new file mode 100644 index 000000000..ba5f1a9fb --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret.yml @@ -0,0 +1,78 @@ +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: secret-from-default + namespace: secret-namespace +spec: + from: + - group: gateway.networking.k8s.io + kind: Gateway + namespace: default + to: + - group: "" + kind: Secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: supersecret + namespace: secret-namespace + +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= + +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: tls + protocol: TLS + port: 9000 + hostname: foo.example.com + tls: + mode: Terminate # Default mode + certificateRefs: + - kind: Secret + name: supersecret + namespace: secret-namespace + group: "" + allowedRoutes: + kinds: + - kind: TCPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +kind: TCPRoute +apiVersion: gateway.networking.k8s.io/v1alpha2 +metadata: + name: tcp-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + rules: + - backendRefs: + - name: whoamitcp + port: 9000 + weight: 1 + kind: Service + group: "" diff --git a/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret_missing.yml b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret_missing.yml new file mode 100644 index 000000000..660ff18b6 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret_missing.yml @@ -0,0 +1,64 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: supersecret + namespace: secret-namespace + +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= + +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: tls + protocol: TLS + port: 9000 + hostname: foo.example.com + tls: + mode: Terminate # Default mode + certificateRefs: + - kind: Secret + name: supersecret + namespace: secret-namespace + group: "" + allowedRoutes: + kinds: + - kind: TCPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +kind: TCPRoute +apiVersion: gateway.networking.k8s.io/v1alpha2 +metadata: + name: tcp-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + rules: + - backendRefs: + - name: whoamitcp + port: 9000 + weight: 1 + kind: Service + group: "" diff --git a/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret_not_matching_from.yml b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret_not_matching_from.yml new file mode 100644 index 000000000..47aba19d8 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret_not_matching_from.yml @@ -0,0 +1,78 @@ +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: secret-from-default + namespace: secret-namespace +spec: + from: + - group: gateway.networking.k8s.io + kind: Gateway + namespace: differentnamespace + to: + - group: "" + kind: Secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: supersecret + namespace: secret-namespace + +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= + +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: tls + protocol: TLS + port: 9000 + hostname: foo.example.com + tls: + mode: Terminate # Default mode + certificateRefs: + - kind: Secret + name: supersecret + namespace: secret-namespace + group: "" + allowedRoutes: + kinds: + - kind: TCPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +kind: TCPRoute +apiVersion: gateway.networking.k8s.io/v1alpha2 +metadata: + name: tcp-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + rules: + - backendRefs: + - name: whoamitcp + port: 9000 + weight: 1 + kind: Service + group: "" diff --git a/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret_not_matching_to.yml b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret_not_matching_to.yml new file mode 100644 index 000000000..e08fa7a42 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_secret_not_matching_to.yml @@ -0,0 +1,79 @@ +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: secret-from-default + namespace: secret-namespace +spec: + from: + - group: gateway.networking.k8s.io + kind: Gateway + namespace: default + to: + - group: "" + kind: Secret + name: differentsecret +--- +apiVersion: v1 +kind: Secret +metadata: + name: supersecret + namespace: secret-namespace + +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= + +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: tls + protocol: TLS + port: 9000 + hostname: foo.example.com + tls: + mode: Terminate # Default mode + certificateRefs: + - kind: Secret + name: supersecret + namespace: secret-namespace + group: "" + allowedRoutes: + kinds: + - kind: TCPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +kind: TCPRoute +apiVersion: gateway.networking.k8s.io/v1alpha2 +metadata: + name: tcp-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + rules: + - backendRefs: + - name: whoamitcp + port: 9000 + weight: 1 + kind: Service + group: "" diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index c632cde73..724d5b841 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -34,11 +34,14 @@ import ( "k8s.io/utils/ptr" "k8s.io/utils/strings/slices" gatev1 "sigs.k8s.io/gateway-api/apis/v1" + gatev1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) const ( providerName = "kubernetesgateway" + groupCore = "core" + kindGateway = "Gateway" kindTraefikService = "TraefikService" kindHTTPRoute = "HTTPRoute" @@ -348,6 +351,7 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * Name: listener.Name, SupportedKinds: []gatev1.RouteGroupKind{}, Conditions: []metav1.Condition{}, + // AttachedRoutes: 0 TODO Set to number of Routes associated with a Listener regardless of Gateway or Route status } supportedKinds, conditions := supportedRouteKinds(listener.Protocol) @@ -356,9 +360,8 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * continue } - listenerStatuses[i].SupportedKinds = supportedKinds - routeKinds, conditions := getAllowedRouteKinds(gateway, listener, supportedKinds) + listenerStatuses[i].SupportedKinds = routeKinds if len(conditions) > 0 { listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, conditions...) continue @@ -474,7 +477,7 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * certificateRef := listener.TLS.CertificateRefs[0] if certificateRef.Kind == nil || *certificateRef.Kind != "Secret" || - certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != "core") { + certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) { // update "ResolvedRefs" status true with "InvalidCertificateRef" reason listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionResolvedRefs), @@ -482,43 +485,74 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * ObservedGeneration: gateway.Generation, LastTransitionTime: metav1.Now(), Reason: string(gatev1.ListenerReasonInvalidCertificateRef), - Message: fmt.Sprintf("Unsupported TLS CertificateRef group/kind: %v/%v", certificateRef.Group, certificateRef.Kind), + Message: fmt.Sprintf("Unsupported TLS CertificateRef group/kind: %s/%s", groupToString(certificateRef.Group), kindToString(certificateRef.Kind)), }) continue } - // TODO Support ReferencePolicy to support cross namespace references. + certificateNamespace := gateway.Namespace if certificateRef.Namespace != nil && string(*certificateRef.Namespace) != gateway.Namespace { - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.ListenerReasonInvalidCertificateRef), - Message: "Cross namespace secrets are not supported", - }) - - continue + certificateNamespace = string(*certificateRef.Namespace) } - configKey := gateway.Namespace + "/" + string(certificateRef.Name) - if _, tlsExists := tlsConfigs[configKey]; !tlsExists { - tlsConf, err := getTLS(client, certificateRef.Name, gateway.Namespace) + if certificateNamespace != gateway.Namespace { + referenceGrants, err := client.GetReferenceGrants(certificateNamespace) if err != nil { - // update "ResolvedRefs" status true with "InvalidCertificateRef" reason listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionResolvedRefs), Status: metav1.ConditionFalse, ObservedGeneration: gateway.Generation, LastTransitionTime: metav1.Now(), - Reason: string(gatev1.ListenerReasonInvalidCertificateRef), - Message: fmt.Sprintf("Error while retrieving certificate: %v", err), + Reason: string(gatev1.ListenerReasonRefNotPermitted), + Message: fmt.Sprintf("Cannot find any ReferenceGrant: %v", err), + }) + continue + } + + referenceGrants = filterReferenceGrantsFrom(referenceGrants, "gateway.networking.k8s.io", "Gateway", gateway.Namespace) + referenceGrants = filterReferenceGrantsTo(referenceGrants, groupCore, "Secret", string(certificateRef.Name)) + if len(referenceGrants) == 0 { + listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ + Type: string(gatev1.ListenerConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: gateway.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.ListenerReasonRefNotPermitted), + Message: "Required ReferenceGrant for cross namespace secret reference is missing", }) continue } + } + configKey := certificateNamespace + "/" + string(certificateRef.Name) + if _, tlsExists := tlsConfigs[configKey]; !tlsExists { + tlsConf, err := getTLS(client, certificateRef.Name, certificateNamespace) + if err != nil { + // update "ResolvedRefs" status false with "InvalidCertificateRef" reason + // update "Programmed" status false with "Invalid" reason + listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, + metav1.Condition{ + Type: string(gatev1.ListenerConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: gateway.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.ListenerReasonInvalidCertificateRef), + Message: fmt.Sprintf("Error while retrieving certificate: %v", err), + }, + metav1.Condition{ + Type: string(gatev1.ListenerConditionProgrammed), + Status: metav1.ConditionFalse, + ObservedGeneration: gateway.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.ListenerReasonInvalid), + Message: fmt.Sprintf("Error while retrieving certificate: %v", err), + }, + ) + + continue + } tlsConfigs[configKey] = tlsConf } } @@ -548,15 +582,32 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses [ var result error for i, listener := range listenerStatuses { if len(listener.Conditions) == 0 { - // GatewayConditionReady "Ready", GatewayConditionReason "ListenerReady" - listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{ - Type: string(gatev1.ListenerReasonAccepted), - Status: metav1.ConditionTrue, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: "ListenerReady", - Message: "No error found", - }) + listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, + metav1.Condition{ + Type: string(gatev1.ListenerConditionAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: gateway.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.ListenerReasonAccepted), + Message: "No error found", + }, + metav1.Condition{ + Type: string(gatev1.ListenerConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: gateway.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.ListenerReasonResolvedRefs), + Message: "No error found", + }, + metav1.Condition{ + Type: string(gatev1.ListenerConditionProgrammed), + Status: metav1.ConditionTrue, + ObservedGeneration: gateway.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.ListenerReasonProgrammed), + Message: "No error found", + }, + ) continue } @@ -565,6 +616,7 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses [ result = multierror.Append(result, errors.New(condition.Message)) } } + gatewayStatus.Listeners = listenerStatuses if result != nil { // GatewayConditionReady "Ready", GatewayConditionReason "ListenersNotValid" @@ -580,8 +632,6 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses [ return gatewayStatus, result } - gatewayStatus.Listeners = listenerStatuses - gatewayStatus.Conditions = append(gatewayStatus.Conditions, // update "Accepted" status with "Accepted" reason metav1.Condition{ @@ -656,7 +706,7 @@ func getAllowedRouteKinds(gateway *gatev1.Gateway, listener gatev1.Listener, sup } var ( - routeKinds []gatev1.RouteGroupKind + routeKinds = []gatev1.RouteGroupKind{} conditions []metav1.Condition ) @@ -672,12 +722,12 @@ func getAllowedRouteKinds(gateway *gatev1.Gateway, listener gatev1.Listener, sup if !isSupported { conditions = append(conditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionAccepted), - Status: metav1.ConditionTrue, + Type: string(gatev1.ListenerConditionResolvedRefs), + Status: metav1.ConditionFalse, ObservedGeneration: gateway.Generation, LastTransitionTime: metav1.Now(), Reason: string(gatev1.ListenerReasonInvalidRouteKinds), - Message: fmt.Sprintf("Listener protocol %q does not support RouteGroupKind %v/%s", listener.Protocol, routeKind.Group, routeKind.Kind), + Message: fmt.Sprintf("Listener protocol %q does not support RouteGroupKind %s/%s", listener.Protocol, groupToString(routeKind.Group), routeKind.Kind), }) continue } @@ -712,7 +762,7 @@ func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, li routes, err := client.GetHTTPRoutes(namespaces) if err != nil { - // update "ResolvedRefs" status true with "InvalidRoutesRef" reason + // update "ResolvedRefs" status true with "RefNotPermitted" reason return []metav1.Condition{{ Type: string(gatev1.ListenerConditionResolvedRefs), Status: metav1.ConditionFalse, @@ -757,7 +807,7 @@ func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, li for _, routeRule := range route.Spec.Rules { rule, err := extractRule(routeRule, hostRule) if err != nil { - // update "ResolvedRefs" status true with "DroppedRoutes" reason + // update "ResolvedRefs" status true with "UnsupportedPathOrHeaderType" reason conditions = append(conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionResolvedRefs), Status: metav1.ConditionFalse, @@ -1048,7 +1098,7 @@ func gatewayTLSRouteToTCPConf(ctx context.Context, ep string, listener gatev1.Li Type: string(gatev1.GatewayClassConditionStatusAccepted), Status: metav1.ConditionFalse, ObservedGeneration: gateway.Generation, - Reason: string(gatev1.ListenerConditionConflicted), + Reason: string(gatev1.ListenerReasonHostnameConflict), Message: fmt.Sprintf("No hostname match between listener: %v and route: %v", listener.Hostname, route.Spec.Hostnames), LastTransitionTime: metav1.Now(), }) @@ -1059,7 +1109,7 @@ func gatewayTLSRouteToTCPConf(ctx context.Context, ep string, listener gatev1.Li rule, err := hostSNIRule(hostnames) if err != nil { - // update "ResolvedRefs" status true with "DroppedRoutes" reason + // update "ResolvedRefs" status true with "InvalidHostnames" reason conditions = append(conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionResolvedRefs), Status: metav1.ConditionFalse, @@ -1111,7 +1161,7 @@ func gatewayTLSRouteToTCPConf(ctx context.Context, ep string, listener gatev1.Li wrrService, subServices, err := loadTCPServices(client, route.Namespace, routeRule.BackendRefs) if err != nil { - // update "ResolvedRefs" status true with "DroppedRoutes" reason + // update "ResolvedRefs" status true with "InvalidBackendRefs" reason conditions = append(conditions, metav1.Condition{ Type: string(gatev1.ListenerConditionResolvedRefs), Status: metav1.ConditionFalse, @@ -1526,7 +1576,7 @@ func loadServices(client Client, namespace string, backendRefs []gatev1.HTTPBack continue } - if *backendRef.Group != "" && *backendRef.Group != "core" && *backendRef.Kind != "Service" { + if *backendRef.Group != "" && *backendRef.Group != groupCore && *backendRef.Kind != "Service" { return nil, nil, fmt.Errorf("unsupported HTTPBackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name) } @@ -1649,7 +1699,7 @@ func loadTCPServices(client Client, namespace string, backendRefs []gatev1.Backe continue } - if *backendRef.Group != "" && *backendRef.Group != "core" && *backendRef.Kind != "Service" { + if *backendRef.Group != "" && *backendRef.Group != groupCore && *backendRef.Kind != "Service" { return nil, nil, fmt.Errorf("unsupported BackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name) } @@ -1880,3 +1930,65 @@ func makeListenerKey(l gatev1.Listener) string { return fmt.Sprintf("%s|%s|%d", l.Protocol, hostname, l.Port) } + +func filterReferenceGrantsFrom(referenceGrants []*gatev1beta1.ReferenceGrant, group, kind, namespace string) []*gatev1beta1.ReferenceGrant { + var matchingReferenceGrants []*gatev1beta1.ReferenceGrant + for _, referenceGrant := range referenceGrants { + if referenceGrantMatchesFrom(referenceGrant, group, kind, namespace) { + matchingReferenceGrants = append(matchingReferenceGrants, referenceGrant) + } + } + return matchingReferenceGrants +} + +func referenceGrantMatchesFrom(referenceGrant *gatev1beta1.ReferenceGrant, group, kind, namespace string) bool { + for _, from := range referenceGrant.Spec.From { + sanitizedGroup := string(from.Group) + if sanitizedGroup == "" { + sanitizedGroup = groupCore + } + if string(from.Namespace) != namespace || string(from.Kind) != kind || sanitizedGroup != group { + continue + } + return true + } + return false +} + +func filterReferenceGrantsTo(referenceGrants []*gatev1beta1.ReferenceGrant, group, kind, name string) []*gatev1beta1.ReferenceGrant { + var matchingReferenceGrants []*gatev1beta1.ReferenceGrant + for _, referenceGrant := range referenceGrants { + if referenceGrantMatchesTo(referenceGrant, group, kind, name) { + matchingReferenceGrants = append(matchingReferenceGrants, referenceGrant) + } + } + return matchingReferenceGrants +} + +func referenceGrantMatchesTo(referenceGrant *gatev1beta1.ReferenceGrant, group, kind, name string) bool { + for _, to := range referenceGrant.Spec.To { + sanitizedGroup := string(to.Group) + if sanitizedGroup == "" { + sanitizedGroup = groupCore + } + if string(to.Kind) != kind || sanitizedGroup != group || (to.Name != nil && string(*to.Name) != name) { + continue + } + return true + } + return false +} + +func groupToString(p *gatev1.Group) string { + if p == nil { + return "" + } + return string(*p) +} + +func kindToString(p *gatev1.Kind) string { + if p == nil { + return "" + } + return string(*p) +} diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index df3b95a0b..d5aeca18b 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -15,6 +15,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" gatev1 "sigs.k8s.io/gateway-api/apis/v1" + gatev1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) var _ provider.Provider = (*Provider)(nil) @@ -4628,6 +4629,196 @@ func TestLoadMixedRoutes(t *testing.T) { } } +func TestLoadRoutesWithReferenceGrants(t *testing.T) { + testCases := []struct { + desc string + ingressClass string + paths []string + expected *dynamic.Configuration + entryPoints map[string]Entrypoint + }{ + { + desc: "Empty", + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "Empty because ReferenceGrant for Secret is missing", + paths: []string{"services.yml", "referencegrant/for_secret_missing.yml"}, + entryPoints: map[string]Entrypoint{ + "tls": {Address: ":9000"}, + }, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "Empty because ReferenceGrant spec.from does not match", + paths: []string{"services.yml", "referencegrant/for_secret_not_matching_from.yml"}, + entryPoints: map[string]Entrypoint{ + "tls": {Address: ":9000"}, + }, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "Empty because ReferenceGrant spec.to does not match", + paths: []string{"services.yml", "referencegrant/for_secret_not_matching_to.yml"}, + entryPoints: map[string]Entrypoint{ + "tls": {Address: ":9000"}, + }, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "For Secret", + paths: []string{"services.yml", "referencegrant/for_secret.yml"}, + entryPoints: map[string]Entrypoint{ + "tls": {Address: ":9000"}, + }, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb": { + EntryPoints: []string{"tls"}, + Service: "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0", + Rule: "HostSNI(`*`)", + RuleSyntax: "v3", + TLS: &dynamic.RouterTCPTLSConfig{}, + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{ + "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0": { + Weighted: &dynamic.TCPWeightedRoundRobin{ + Services: []dynamic.TCPWRRService{{ + Name: "default-whoamitcp-9000", + Weight: func(i int) *int { return &i }(1), + }}, + }, + }, + "default-whoamitcp-9000": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "10.10.0.9:9000", + }, + { + Address: "10.10.0.10:9000", + }, + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{ + CertFile: types.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), + KeyFile: types.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"), + }, + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + if test.expected == nil { + return + } + + p := Provider{EntryPoints: test.entryPoints} + conf := p.loadConfigurationFromGateway(context.Background(), newClientMock(test.paths...)) + assert.Equal(t, test.expected, conf) + }) + } +} + func Test_hostRule(t *testing.T) { testCases := []struct { desc string @@ -5644,3 +5835,247 @@ func kindPtr(kind gatev1.Kind) *gatev1.Kind { func pathMatchTypePtr(p gatev1.PathMatchType) *gatev1.PathMatchType { return &p } func headerMatchTypePtr(h gatev1.HeaderMatchType) *gatev1.HeaderMatchType { return &h } + +func Test_referenceGrantMatchesFrom(t *testing.T) { + testCases := []struct { + desc string + referenceGrant gatev1beta1.ReferenceGrant + group string + kind string + namespace string + expectedResult bool + }{ + { + desc: "matches", + referenceGrant: gatev1beta1.ReferenceGrant{ + Spec: gatev1beta1.ReferenceGrantSpec{ + From: []gatev1beta1.ReferenceGrantFrom{ + { + Group: "correct-group", + Kind: "correct-kind", + Namespace: "correct-namespace", + }, + }, + }, + }, + group: "correct-group", + kind: "correct-kind", + namespace: "correct-namespace", + expectedResult: true, + }, + { + desc: "empty group matches core", + referenceGrant: gatev1beta1.ReferenceGrant{ + Spec: gatev1beta1.ReferenceGrantSpec{ + From: []gatev1beta1.ReferenceGrantFrom{ + { + Group: "", + Kind: "correct-kind", + Namespace: "correct-namespace", + }, + }, + }, + }, + group: "core", + kind: "correct-kind", + namespace: "correct-namespace", + expectedResult: true, + }, + { + desc: "wrong group", + referenceGrant: gatev1beta1.ReferenceGrant{ + Spec: gatev1beta1.ReferenceGrantSpec{ + From: []gatev1beta1.ReferenceGrantFrom{ + { + Group: "wrong-group", + Kind: "correct-kind", + Namespace: "correct-namespace", + }, + }, + }, + }, + group: "correct-group", + kind: "correct-kind", + namespace: "correct-namespace", + expectedResult: false, + }, + { + desc: "wrong kind", + referenceGrant: gatev1beta1.ReferenceGrant{ + Spec: gatev1beta1.ReferenceGrantSpec{ + From: []gatev1beta1.ReferenceGrantFrom{ + { + Group: "correct-group", + Kind: "wrong-kind", + Namespace: "correct-namespace", + }, + }, + }, + }, + group: "correct-group", + kind: "correct-kind", + namespace: "correct-namespace", + expectedResult: false, + }, + { + desc: "wrong namespace", + referenceGrant: gatev1beta1.ReferenceGrant{ + Spec: gatev1beta1.ReferenceGrantSpec{ + From: []gatev1beta1.ReferenceGrantFrom{ + { + Group: "correct-group", + Kind: "correct-kind", + Namespace: "wrong-namespace", + }, + }, + }, + }, + group: "correct-group", + kind: "correct-kind", + namespace: "correct-namespace", + expectedResult: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, test.expectedResult, referenceGrantMatchesFrom(&test.referenceGrant, test.group, test.kind, test.namespace)) + }) + } +} + +func Test_referenceGrantMatchesTo(t *testing.T) { + testCases := []struct { + desc string + referenceGrant gatev1beta1.ReferenceGrant + group string + kind string + name string + expectedResult bool + }{ + { + desc: "matches", + referenceGrant: gatev1beta1.ReferenceGrant{ + Spec: gatev1beta1.ReferenceGrantSpec{ + To: []gatev1beta1.ReferenceGrantTo{ + { + Group: "correct-group", + Kind: "correct-kind", + Name: objectNamePtr("correct-name"), + }, + }, + }, + }, + group: "correct-group", + kind: "correct-kind", + name: "correct-name", + expectedResult: true, + }, + { + desc: "matches without name", + referenceGrant: gatev1beta1.ReferenceGrant{ + Spec: gatev1beta1.ReferenceGrantSpec{ + To: []gatev1beta1.ReferenceGrantTo{ + { + Group: "correct-group", + Kind: "correct-kind", + Name: nil, + }, + }, + }, + }, + group: "correct-group", + kind: "correct-kind", + name: "some-name", + expectedResult: true, + }, + { + desc: "empty group matches core", + referenceGrant: gatev1beta1.ReferenceGrant{ + Spec: gatev1beta1.ReferenceGrantSpec{ + To: []gatev1beta1.ReferenceGrantTo{ + { + Group: "", + Kind: "correct-kind", + Name: objectNamePtr("correct-name"), + }, + }, + }, + }, + group: "core", + kind: "correct-kind", + name: "correct-name", + expectedResult: true, + }, + { + desc: "wrong group", + referenceGrant: gatev1beta1.ReferenceGrant{ + Spec: gatev1beta1.ReferenceGrantSpec{ + To: []gatev1beta1.ReferenceGrantTo{ + { + Group: "wrong-group", + Kind: "correct-kind", + Name: objectNamePtr("correct-name"), + }, + }, + }, + }, + group: "correct-group", + kind: "correct-kind", + name: "correct-namespace", + expectedResult: false, + }, + { + desc: "wrong kind", + referenceGrant: gatev1beta1.ReferenceGrant{ + Spec: gatev1beta1.ReferenceGrantSpec{ + To: []gatev1beta1.ReferenceGrantTo{ + { + Group: "correct-group", + Kind: "wrong-kind", + Name: objectNamePtr("correct-name"), + }, + }, + }, + }, + group: "correct-group", + kind: "correct-kind", + name: "correct-name", + expectedResult: false, + }, + { + desc: "wrong name", + referenceGrant: gatev1beta1.ReferenceGrant{ + Spec: gatev1beta1.ReferenceGrantSpec{ + To: []gatev1beta1.ReferenceGrantTo{ + { + Group: "correct-group", + Kind: "correct-kind", + Name: objectNamePtr("wrong-name"), + }, + }, + }, + }, + group: "correct-group", + kind: "correct-kind", + name: "correct-name", + expectedResult: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, test.expectedResult, referenceGrantMatchesTo(&test.referenceGrant, test.group, test.kind, test.name)) + }) + } +} + +func objectNamePtr(objectName gatev1.ObjectName) *gatev1.ObjectName { + return &objectName +} diff --git a/pkg/provider/kubernetes/k8s/parser.go b/pkg/provider/kubernetes/k8s/parser.go index 769d5edf5..ddcd69b06 100644 --- a/pkg/provider/kubernetes/k8s/parser.go +++ b/pkg/provider/kubernetes/k8s/parser.go @@ -12,7 +12,7 @@ import ( // MustParseYaml parses a YAML to objects. func MustParseYaml(content []byte) []runtime.Object { - acceptedK8sTypes := regexp.MustCompile(`^(Namespace|Deployment|Endpoints|Service|Ingress|IngressRoute|IngressRouteTCP|IngressRouteUDP|Middleware|MiddlewareTCP|Secret|TLSOption|TLSStore|TraefikService|IngressClass|ServersTransport|ServersTransportTCP|GatewayClass|Gateway|HTTPRoute|TCPRoute|TLSRoute)$`) + acceptedK8sTypes := regexp.MustCompile(`^(Namespace|Deployment|Endpoints|Service|Ingress|IngressRoute|IngressRouteTCP|IngressRouteUDP|Middleware|MiddlewareTCP|Secret|TLSOption|TLSStore|TraefikService|IngressClass|ServersTransport|ServersTransportTCP|GatewayClass|Gateway|HTTPRoute|TCPRoute|TLSRoute|ReferenceGrant)$`) files := strings.Split(string(content), "---\n") retVal := make([]runtime.Object, 0, len(files)) From 85039e0d54ddd551f4f695b11cfd7ae138743990 Mon Sep 17 00:00:00 2001 From: Romain Date: Tue, 30 Jan 2024 17:32:05 +0100 Subject: [PATCH 27/36] Fix brotli response status code when compression is disabled --- pkg/middlewares/compress/brotli/brotli.go | 1 + pkg/middlewares/compress/brotli/brotli_test.go | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/middlewares/compress/brotli/brotli.go b/pkg/middlewares/compress/brotli/brotli.go index c95a5ff66..142eb78ee 100644 --- a/pkg/middlewares/compress/brotli/brotli.go +++ b/pkg/middlewares/compress/brotli/brotli.go @@ -138,6 +138,7 @@ func (r *responseWriter) Write(p []byte) (int, error) { // If we detect a contentEncoding, we know we are never going to compress. if r.rw.Header().Get(contentEncoding) != "" { r.compressionDisabled = true + r.rw.WriteHeader(r.statusCode) return r.rw.Write(p) } diff --git a/pkg/middlewares/compress/brotli/brotli_test.go b/pkg/middlewares/compress/brotli/brotli_test.go index 5e1bfeea7..445268f85 100644 --- a/pkg/middlewares/compress/brotli/brotli_test.go +++ b/pkg/middlewares/compress/brotli/brotli_test.go @@ -27,7 +27,7 @@ func Test_Vary(t *testing.T) { rw := httptest.NewRecorder() h.ServeHTTP(rw, req) - assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, http.StatusAccepted, rw.Code) assert.Equal(t, acceptEncoding, rw.Header().Get(vary)) } @@ -41,7 +41,7 @@ func Test_SmallBodyNoCompression(t *testing.T) { h.ServeHTTP(rw, req) // With less than 1024 bytes the response should not be compressed. - assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, http.StatusAccepted, rw.Code) assert.Empty(t, rw.Header().Get(contentEncoding)) assert.Equal(t, smallTestBody, rw.Body.Bytes()) } @@ -55,6 +55,7 @@ func Test_AlreadyCompressed(t *testing.T) { rw := httptest.NewRecorder() h.ServeHTTP(rw, req) + assert.Equal(t, http.StatusAccepted, rw.Code) assert.Equal(t, bigTestBody, rw.Body.Bytes()) } @@ -749,6 +750,7 @@ func newTestHandler(t *testing.T, body []byte) http.Handler { rw.Header().Set("Content-Encoding", "br") } + rw.WriteHeader(http.StatusAccepted) _, err := rw.Write(body) require.NoError(t, err) }), From 3ba3ca6eb05945d2f39159fff4591d772eb7e494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imm=C3=A1nuel!?= <21174107+immanuelfodor@users.noreply.github.com> Date: Wed, 31 Jan 2024 17:00:06 +0100 Subject: [PATCH 28/36] Fix the keepAlive options for the CLI examples --- docs/content/routing/entrypoints.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index c77c8e406..fe78191e0 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -616,7 +616,7 @@ The maximum number of requests Traefik can handle before sending a `Connection: ```bash tab="CLI" ## Static configuration --entryPoints.name.address=:8888 - --entryPoints.name.transport.keepAliveRequests=42 + --entryPoints.name.transport.keepAliveMaxRequests=42 ``` #### `keepAliveMaxTime` @@ -646,7 +646,7 @@ The maximum duration Traefik can handle requests before sending a `Connection: C ```bash tab="CLI" ## Static configuration --entryPoints.name.address=:8888 - --entryPoints.name.transport.keepAliveTime=42s + --entryPoints.name.transport.keepAliveMaxTime=42s ``` ### ProxyProtocol From f1104ada651454f013abfc09c2428880e8de365d Mon Sep 17 00:00:00 2001 From: Massimiliano D <126668030+mdeliatf@users.noreply.github.com> Date: Thu, 1 Feb 2024 10:52:07 +0100 Subject: [PATCH 29/36] Fixes the Header Button --- webui/src/statics/traefiklabs-hub-button-app/main-v1.js | 2 +- webui/src/statics/traefiklabs-hub-button-app/main-v1.js.map | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webui/src/statics/traefiklabs-hub-button-app/main-v1.js b/webui/src/statics/traefiklabs-hub-button-app/main-v1.js index 480bc15d9..9d36a8b62 100644 --- a/webui/src/statics/traefiklabs-hub-button-app/main-v1.js +++ b/webui/src/statics/traefiklabs-hub-button-app/main-v1.js @@ -1,3 +1,3 @@ /* eslint-disable */ -!function(){var e={110:function(e,t,n){"use strict";var r=n(441),a={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},l={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},o={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},i={};function u(e){return r.isMemo(e)?o:i[e.$$typeof]||a}i[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},i[r.Memo]=o;var s=Object.defineProperty,c=Object.getOwnPropertyNames,d=Object.getOwnPropertySymbols,f=Object.getOwnPropertyDescriptor,p=Object.getPrototypeOf,m=Object.prototype;e.exports=function e(t,n,r){if("string"!==typeof n){if(m){var a=p(n);a&&a!==m&&e(t,a,r)}var o=c(n);d&&(o=o.concat(d(n)));for(var i=u(t),h=u(n),g=0;g