diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 18af81a3b..51ebc4e83 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -52,12 +52,12 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 env: ImageOS: ${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.goarm }} with: @@ -65,7 +65,7 @@ jobs: check-latest: true - name: Artifact webui - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: webui.tar.gz diff --git a/.github/workflows/check_doc.yaml b/.github/workflows/check_doc.yaml index 5fea9809c..df4c31b6b 100644 --- a/.github/workflows/check_doc.yaml +++ b/.github/workflows/check_doc.yaml @@ -16,7 +16,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v5 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 @@ -28,7 +28,7 @@ jobs: run: ./docs/scripts/lint.sh docs - name: Setup python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' cache: 'pip' @@ -41,7 +41,7 @@ jobs: mkdocs build --strict - name: Setup ruby - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@90be1154f987f4dc0fe0dd0feedac9e473aa4ba8 # v1.286.0 with: ruby-version: '3.4' @@ -54,7 +54,7 @@ jobs: # Comes from https://github.com/gjtorikian/html-proofer?tab=readme-ov-file#caching-with-continuous-integration - name: Cache HTMLProofer - uses: actions/cache@v4 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: tmp/.htmlproofer key: ${{ runner.os }}-htmlproofer diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4ea4ab978..42c868570 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,17 +29,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: setup go - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 if: ${{ matrix.language == 'go' }} with: go-version-file: 'go.mod' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@38e701f46e33fb233075bf4238cb1e5d68e429e4 # v3.31.11 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -53,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@38e701f46e33fb233075bf4238cb1e5d68e429e4 # v3.31.11 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -66,6 +66,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@38e701f46e33fb233075bf4238cb1e5d68e429e4 # v3.31.11 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index 7bced37d4..bfeab83a0 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/experimental.yaml b/.github/workflows/experimental.yaml index ecc78f346..c46aa07d0 100644 --- a/.github/workflows/experimental.yaml +++ b/.github/workflows/experimental.yaml @@ -24,12 +24,12 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 env: ImageOS: ${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.goarm }} with: @@ -43,19 +43,19 @@ jobs: run: echo ${GITHUB_REF##*/} - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Artifact webui - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: webui.tar.gz diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f14abff88..207f23691 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -31,12 +31,12 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 env: # Ensure cache consistency on Linux, see https://github.com/actions/setup-go/pull/383 ImageOS: ${{ matrix.os }} @@ -45,7 +45,7 @@ jobs: check-latest: true - name: Artifact webui - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: webui.tar.gz @@ -64,7 +64,7 @@ jobs: echo "GORELEASER_CONFIG_FILE_PATH=$GORELEASER_CONFIG_FILE_PATH" >> $GITHUB_ENV - name: Build with goreleaser - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: distribution: goreleaser # 'latest', 'nightly', or a semver @@ -72,7 +72,7 @@ jobs: args: release --clean --timeout="90m" --config "${{ env.GORELEASER_CONFIG_FILE_PATH }}" - name: Artifact binaries - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: ${{ matrix.os }}-binaries path: | @@ -91,12 +91,12 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Artifact webui - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: webui.tar.gz @@ -113,7 +113,7 @@ jobs: echo "${TRAEFIKER_RSA}" | base64 --decode > ~/.ssh/traefiker_rsa - name: Download All Artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: path: dist/ pattern: "*-binaries" diff --git a/.github/workflows/sync-docker-images.yaml b/.github/workflows/sync-docker-images.yaml index 4352b20bb..2dcf4aab5 100644 --- a/.github/workflows/sync-docker-images.yaml +++ b/.github/workflows/sync-docker-images.yaml @@ -15,9 +15,9 @@ jobs: if: github.repository == 'traefik/traefik' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: imjasonh/setup-crane@v0.4 + - uses: imjasonh/setup-crane@31b88efe9de28ae0ffa220711af4b60be9435f6e # v0.4 - name: Sync run: | diff --git a/.github/workflows/template-webui.yaml b/.github/workflows/template-webui.yaml index 465dd04d5..6af4f9918 100644 --- a/.github/workflows/template-webui.yaml +++ b/.github/workflows/template-webui.yaml @@ -11,7 +11,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 @@ -19,7 +19,7 @@ jobs: run: corepack enable - name: Setup node - uses: actions/setup-node@v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: webui/.nvmrc cache: yarn @@ -42,7 +42,7 @@ jobs: tar czvf webui.tar.gz ./webui/static/ - name: Artifact webui - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: webui.tar.gz path: webui.tar.gz diff --git a/.github/workflows/test-gateway-api-conformance.yaml b/.github/workflows/test-gateway-api-conformance.yaml index 64340057c..cfe2da412 100644 --- a/.github/workflows/test-gateway-api-conformance.yaml +++ b/.github/workflows/test-gateway-api-conformance.yaml @@ -23,12 +23,12 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 1e48d45bd..39e802e71 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -21,12 +21,12 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true @@ -39,14 +39,14 @@ jobs: run: make binary-linux-amd64 - name: Save go cache build - uses: actions/cache/save@v4 + uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: | ~/.cache/go-build key: ${{ runner.os }}-go-build-cache-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} - name: Artifact traefik binary - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: traefik path: ./dist/linux/amd64/traefik @@ -65,18 +65,18 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true - name: Download traefik binary - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: traefik path: ./dist/linux/amd64/ @@ -85,7 +85,7 @@ jobs: run: chmod +x ./dist/linux/amd64/traefik - name: Restore go cache build - uses: actions/cache/restore@v4 + uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: | ~/.cache/go-build @@ -93,7 +93,7 @@ jobs: - name: Generate go test Slice id: test_split - uses: hashicorp-forge/go-test-split-action@v2.0.0 + uses: hashicorp-forge/go-test-split-action@ddb2685fb140c29505663b405af7eb2cd953dd13 # v2.0.1 with: packages: ./integration total: ${{ matrix.parallel }} diff --git a/.github/workflows/test-knative-conformance.yaml b/.github/workflows/test-knative-conformance.yaml index b7f5b6a1c..2ca5584a2 100644 --- a/.github/workflows/test-knative-conformance.yaml +++ b/.github/workflows/test-knative-conformance.yaml @@ -23,17 +23,17 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} - name: Set up KO - uses: ko-build/setup-ko@v0.6 + uses: ko-build/setup-ko@ace48d793556083a76f1e3e6068850c1f4a369aa # v0.6 env: KO_DOCKER_REPO: ko.local diff --git a/.github/workflows/test-unit.yaml b/.github/workflows/test-unit.yaml index 73b0dd425..bdb6003cc 100644 --- a/.github/workflows/test-unit.yaml +++ b/.github/workflows/test-unit.yaml @@ -21,12 +21,12 @@ jobs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true @@ -48,12 +48,12 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true @@ -68,7 +68,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 @@ -76,7 +76,7 @@ jobs: run: corepack enable - name: Set up Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v4 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: webui/.nvmrc cache: 'yarn' diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index eacfd356e..1c245f4d5 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -18,18 +18,18 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true - name: golangci-lint - uses: golangci/golangci-lint-action@v7 + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: version: "${{ env.GOLANGCI_LINT_VERSION }}" @@ -39,12 +39,12 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true @@ -61,12 +61,12 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v6 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: ${{ env.GO_VERSION }} check-latest: true diff --git a/.golangci.yml b/.golangci.yml index d49dcdcae..fa489d956 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -82,6 +82,7 @@ linters: toolchain-pattern: go1\.\d+\.\d+$ tool-forbidden: true go-version-pattern: ^1\.\d+(\.0)?$ + replace-local: true replace-allow-list: - github.com/abbot/go-http-auth - github.com/gorilla/mux diff --git a/Makefile b/Makefile index 7f361cfb2..24a2b6fc9 100644 --- a/Makefile +++ b/Makefile @@ -103,7 +103,7 @@ test-integration: #? test-gateway-api-conformance: Run the Gateway API conformance tests test-gateway-api-conformance: build-image-dirty # In case of a new Minor/Major version, the traefikVersion needs to be updated. - GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -tags gatewayAPIConformance -test.run GatewayAPIConformanceSuite -traefikVersion="v3.6" $(TESTFLAGS) + GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -tags gatewayAPIConformance -test.run GatewayAPIConformanceSuite -traefikVersion="v3.7" $(TESTFLAGS) .PHONY: test-knative-conformance #? test-knative-conformance: Run the Knative conformance tests diff --git a/docs/Makefile b/docs/Makefile index 501cbdbad..9a2e41ff3 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -21,7 +21,7 @@ docs: docs-clean docs-image docs-lint docs-build docs-verify # Writer Mode: build and serve docs on http://localhost:8000 with livereload .PHONY: docs-serve docs-serve: docs-image - docker run $(DOCKER_RUN_DOC_OPTS) $(TRAEFIK_DOCS_BUILD_IMAGE) mkdocs serve + docker run $(DOCKER_RUN_DOC_OPTS) $(TRAEFIK_DOCS_BUILD_IMAGE) mkdocs serve -a 0.0.0.0:8000 ## Pull image for doc building .PHONY: docs-pull-images diff --git a/docs/content/contributing/maintainers.md b/docs/content/contributing/maintainers.md index a7c3bbe50..a86671181 100644 --- a/docs/content/contributing/maintainers.md +++ b/docs/content/contributing/maintainers.md @@ -23,6 +23,7 @@ description: "Traefik Proxy is an open source software with a thriving community * Simon Delicata [@sdelicata](https://github.com/sdelicata) * Baptiste Mayelle [@youkoulayley](https://github.com/youkoulayley) * Jesper Noordsij [@jnoordsij](https://github.com/jnoordsij) +* Gina Adzani [@gndz07](https://github.com/gndz07) ## Past Maintainers diff --git a/docs/content/expose/docker.md b/docs/content/expose/docker/advanced.md similarity index 54% rename from docs/content/expose/docker.md rename to docs/content/expose/docker/advanced.md index 0e6de8cc6..088daa10f 100644 --- a/docs/content/expose/docker.md +++ b/docs/content/expose/docker/advanced.md @@ -1,252 +1,29 @@ -# Exposing Services with Traefik on Docker +# Exposing Services with Traefik on Docker - Advanced -This guide will help you expose your services securely through Traefik Proxy using Docker. We'll cover routing HTTP and HTTPS traffic, implementing TLS, adding middlewares, Let's Encrypt integration, and sticky sessions. +This guide builds on the concepts and setup from the [Basic Guide](basic.md). Make sure you've completed the basic guide and have a working Traefik setup with Docker before proceeding. + +In this advanced guide, you'll learn how to enhance your Traefik deployment with: + +- **Middlewares** for security headers and access control +- **Let's Encrypt** for automated certificate management +- **Sticky sessions** for stateful applications +- **Multi-layer routing** for hierarchical routing with a complex authentication based routing example ## Prerequisites +- Completed the [Basic Guide](basic.md) - Docker and Docker Compose installed -- Basic understanding of Docker concepts -- Traefik deployed using the Traefik Docker Setup guide - -## Expose Your First HTTP Service - -Let's expose a simple HTTP service using the [whoami](https://hub.docker.com/r/traefik/whoami) application. This will demonstrate basic routing to a backend service. - -First, create a `docker-compose.yml` file: - -```yaml -services: - traefik: - image: "traefik:v3.4" - container_name: "traefik" - restart: unless-stopped - security_opt: - - no-new-privileges:true - networks: - - proxy - command: - - "--providers.docker=true" - - "--providers.docker.exposedbydefault=false" - - "--providers.docker.network=proxy" - - "--entryPoints.web.address=:80" - ports: - - "80:80" - - "8080:8080" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock:ro" - - whoami: - image: "traefik/whoami" - restart: unless-stopped - networks: - - proxy - labels: - - "traefik.enable=true" - - "traefik.http.routers.whoami.rule=Host(`whoami.docker.localhost`)" - - "traefik.http.routers.whoami.entrypoints=web" - -networks: - proxy: - name: proxy -``` - -Save this as `docker-compose.yml` and start the services: - -```bash -docker compose up -d -``` - -### Verify Your Service - -Your service is now available at http://whoami.docker.localhost/. Test that it works: - -```bash -curl -H "Host: whoami.docker.localhost" http://localhost/ -``` - -You should see output similar to: - -```bash -Hostname: whoami -IP: 127.0.0.1 -IP: ::1 -IP: 172.18.0.3 -IP: fe80::215:5dff:fe00:c9e -RemoteAddr: 172.18.0.2:55108 -GET / HTTP/1.1 -Host: whoami.docker.localhost -User-Agent: curl/7.68.0 -Accept: */* -Accept-Encoding: gzip -X-Forwarded-For: 172.18.0.1 -X-Forwarded-Host: whoami.docker.localhost -X-Forwarded-Port: 80 -X-Forwarded-Proto: http -X-Forwarded-Server: 5789f594e7d5 -X-Real-Ip: 172.18.0.1 -``` - -This confirms that Traefik is successfully routing requests to your whoami application. - -## Add Routing Rules - -Now we'll enhance our routing by directing traffic to different services based on [URL paths](../reference/routing-configuration/http/routing/rules-and-priority.md#path-pathprefix-and-pathregexp). This is useful for API versioning, frontend/backend separation, or organizing microservices. - -Update your `docker-compose.yml` to add another service: - -```yaml -# ... - -# New service - whoami-api: - image: "traefik/whoami" - networks: - - proxy - container_name: "whoami-api" - environment: - - WHOAMI_NAME=API Service - labels: - - "traefik.enable=true" - # Path-based routing - - "traefik.http.routers.whoami-api.rule=Host(`whoami.docker.localhost`) && PathPrefix(`/api`)" - - "traefik.http.routers.whoami-api.entrypoints=web" -``` - -Apply the changes: - -```bash -docker compose up -d -``` - -### Test the Path-Based Routing - -Verify that different paths route to different services: - -```bash -# Root path should go to the main whoami service -curl -H "Host: whoami.docker.localhost" http://localhost/ - -# /api path should go to the whoami-api service -curl -H "Host: whoami.docker.localhost" http://localhost/api -``` - -For the `/api` requests, you should see the response showing "API Service" in the environment variables section, confirming that your path-based routing is working correctly. - -## Enable TLS - -Let's secure our service with HTTPS by adding TLS. We'll start with a self-signed certificate for local development. - -### Create a Self-Signed Certificate - -Generate a self-signed certificate: - -```bash -mkdir -p certs -openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout certs/local.key -out certs/local.crt \ - -subj "/CN=*.docker.localhost" -``` - -Create a directory for dynamic configuration and add a TLS configuration file: - -```bash -mkdir -p dynamic -cat > dynamic/tls.yml << EOF -tls: - certificates: - - certFile: /certs/local.crt - keyFile: /certs/local.key -EOF -``` - -Update your `docker-compose.yml` file with the following changes: - -```yaml -services: - traefik: - image: "traefik:v3.4" - container_name: "traefik" - restart: unless-stopped - security_opt: - - no-new-privileges:true - networks: - - proxy - command: - - "--api.insecure=false" - - "--api.dashboard=true" - - "--providers.docker=true" - - "--providers.docker.exposedbydefault=false" - - "--providers.docker.network=proxy" - - "--providers.file.directory=/etc/traefik/dynamic" - - "--entryPoints.web.address=:80" - - "--entryPoints.websecure.address=:443" - - "--entryPoints.websecure.http.tls=true" - ports: - - "80:80" - - "443:443" - - "8080:8080" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock:ro" - # Add the following volumes - - "./certs:/certs:ro" - - "./dynamic:/etc/traefik/dynamic:ro" - labels: - - "traefik.enable=true" - - "traefik.http.routers.dashboard.rule=Host(`dashboard.docker.localhost`)" - - "traefik.http.routers.dashboard.entrypoints=websecure" - - "traefik.http.routers.dashboard.service=api@internal" - # Add the following label - - "traefik.http.routers.dashboard.tls=true" - - whoami: - image: "traefik/whoami" - restart: unless-stopped - networks: - - proxy - labels: - - "traefik.enable=true" - - "traefik.http.routers.whoami.rule=Host(`whoami.docker.localhost`)" - - "traefik.http.routers.whoami.entrypoints=websecure" - # Add the following label - - "traefik.http.routers.whoami.tls=true" - - whoami-api: - image: "traefik/whoami" - container_name: "whoami-api" - restart: unless-stopped - networks: - - proxy - environment: - - WHOAMI_NAME=API Service - labels: - - "traefik.enable=true" - - "traefik.http.routers.whoami-api.rule=Host(`whoami.docker.localhost`) && PathPrefix(`/api`)" - - "traefik.http.routers.whoami-api.entrypoints=websecure" - # Add the following label - - "traefik.http.routers.whoami-api.tls=true" - -networks: - proxy: - name: proxy -``` - -Apply the changes: - -```bash -docker compose up -d -``` - -Your browser can access https://whoami.docker.localhost/ for the service. You'll need to accept the security warning for the self-signed certificate. +- Working Traefik setup from the basic guide ## Add Middlewares -Middlewares allow you to modify requests or responses as they pass through Traefik. Let's add two useful middlewares: [Headers](../reference/routing-configuration/http/middlewares/headers.md) for security and [IP allowlisting](../reference/routing-configuration/http/middlewares/ipallowlist.md) for access control. +Middlewares allow you to modify requests or responses as they pass through Traefik. Let's add two useful middlewares: [Headers](../../reference/routing-configuration/http/middlewares/headers.md) for security and [IP allowlisting](../../reference/routing-configuration/http/middlewares/ipallowlist.md) for access control. Add the following labels to your whoami service in `docker-compose.yml`: ```yaml labels: - + # Secure Headers Middleware - "traefik.http.middlewares.secure-headers.headers.frameDeny=true" - "traefik.http.middlewares.secure-headers.headers.sslRedirect=true" @@ -255,10 +32,10 @@ labels: - "traefik.http.middlewares.secure-headers.headers.stsIncludeSubdomains=true" - "traefik.http.middlewares.secure-headers.headers.stsPreload=true" - "traefik.http.middlewares.secure-headers.headers.stsSeconds=31536000" - + # IP Allowlist Middleware - "traefik.http.middlewares.ip-allowlist.ipallowlist.sourceRange=127.0.0.1/32,192.168.0.0/16,10.0.0.0/8" - + # Apply middlewares to whoami router - "traefik.http.routers.whoami.middlewares=secure-headers,ip-allowlist" ``` @@ -288,7 +65,7 @@ curl -k -I -H "Host: whoami.docker.localhost" https://localhost/ In the response headers, you should see security headers set by the middleware: -- `X-Frame-Options: DENY` +- `X-Frame-Options: DENY` - `X-Content-Type-Options: nosniff` - `X-XSS-Protection: 1; mode=block` - `Strict-Transport-Security` with the appropriate settings @@ -438,28 +215,191 @@ You should see different `Hostname` values in these responses, as each request i !!! important "Browser Testing" When testing in browsers, you need to use the same browser session to maintain the cookie. The cookie is set with `httpOnly` and `secure` flags for security, so it will only be sent over HTTPS connections and won't be accessible via JavaScript. -For more advanced configuration options, see the [reference documentation](../reference/routing-configuration/http/load-balancing/service.md). +For more advanced configuration options, see the [reference documentation](../../reference/routing-configuration/http/load-balancing/service.md). + +## Multi-Layer Routing + +Multi-layer routing enables hierarchical relationships between routers, where parent routers can process requests through middleware before child routers make final routing decisions. This is particularly useful for authentication-based routing or staged middleware application. + +!!! info "Provider Requirement" + Multi-layer routing requires the File provider, as Docker labels do not support the `parentRefs` field. However, you can use **both Docker and File providers together** - Docker labels for service discovery and File configuration for multi-layer routing. + +### Setup Multi-Layer Routing with Docker + +To use multi-layer routing with Docker, you need to enable the File provider alongside the Docker provider. + +Update your Traefik service in `docker-compose.yml`: + +```yaml +services: + traefik: + image: "traefik:latest" + container_name: "traefik" + restart: unless-stopped + security_opt: + - no-new-privileges:true + networks: + - proxy + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--providers.docker.network=proxy" + - "--providers.file.directory=/etc/traefik/dynamic" # Enable File provider + - "--entryPoints.web.address=:80" + - "--entryPoints.websecure.address=:443" + - "--entryPoints.websecure.http.tls=true" + ports: + - "80:80" + - "443:443" + - "8080:8080" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + - "./dynamic:/etc/traefik/dynamic:ro" # Mount directory for dynamic config +``` + +### Authentication-Based Routing Example + +Let's create a multi-layer routing setup where a parent router authenticates requests, and child routers direct traffic based on user roles. + +First, keep your Docker services defined with labels as usual: + +```yaml +# In docker-compose.yml +services: + # ... traefik service from above ... + + # Mock authentication service that adds X-User-Role header + auth-service: + image: "traefik/whoami" + networks: + - proxy + environment: + - WHOAMI_NAME=Auth Service + labels: + - "traefik.enable=true" + - "traefik.http.services.auth-service.loadbalancer.server.port=80" + + # Admin backend service + admin-backend: + image: "traefik/whoami" + networks: + - proxy + environment: + - WHOAMI_NAME=Admin Backend + labels: + - "traefik.enable=true" + - "traefik.http.services.admin-backend.loadbalancer.server.port=80" + + # User backend service + user-backend: + image: "traefik/whoami" + networks: + - proxy + environment: + - WHOAMI_NAME=User Backend + labels: + - "traefik.enable=true" + - "traefik.http.services.user-backend.loadbalancer.server.port=80" +``` + +Now create the multi-layer routing configuration in a file. Create `dynamic/mlr.yml`: + +```yaml +http: + routers: + # Parent router with authentication middleware + api-parent: + rule: "Host(`api.docker.localhost`) && PathPrefix(`/api`)" + middlewares: + - auth-middleware + entryPoints: + - websecure + # Note: No service and no TLS config - this is a parent router + + # Child router for admin users + api-admin: + rule: "HeadersRegexp(`X-Auth-User`, `admin`)" + service: admin-backend@docker # Reference Docker service + parentRefs: + - api-parent@file # Explicit reference to parent in file provider + + # Child router for regular users + api-user: + rule: "HeadersRegexp(`X-Auth-User`, `user`)" + service: user-backend@docker # Reference Docker service + parentRefs: + - api-parent@file # Explicit reference to parent in file provider + + middlewares: + auth-middleware: + basicAuth: + users: + - "admin:$apr1$DmXR3Add$wfdbGw6RWIhFb0ffXMM4d0" + - "user:$apr1$GJtcIY1o$mSLdsWYeXpPHVsxGDqadI." + headerField: X-Auth-User +``` + +!!! note "Generating Password Hashes" + The password hashes above are generated using `htpasswd`. To create your own user credentials: + + ```bash + # Using htpasswd (Apache utils) + htpasswd -nb admin yourpassword + ``` + +!!! important "Cross-Provider References" + Notice the `@docker` suffix on service names and the `@file` suffix in `parentRefs`. When using the File provider to orchestrate multi-layer routing with Docker services: + + - Use `service-name@docker` to reference Docker services + - Use `parent-name@file` in `parentRefs` to reference the parent router in the File provider + + The `@provider` suffix tells Traefik which provider namespace to look in for the resource. + +Apply the changes: + +```bash +docker compose up -d +``` + +### Test Multi-Layer Routing + +Test the routing behavior: + +```bash +# Request goes through parent router → auth middleware → admin child router +curl -k -u admin:test -H "Host: api.docker.localhost" https://localhost/api +``` + +You should see the response from the admin-backend service when authenticating as `admin`. Try with `user:test` credentials to reach the user-backend service instead. + +### How It Works + +1. **Request arrives** at `api.docker.localhost/api` +2. **Parent router** (`api-parent`) matches based on host and path +3. **BasicAuth middleware** authenticates the user and sets the `X-Auth-User` header with the username +4. **Child router** (`api-admin` or `api-user`) matches based on the header value +5. **Request forwarded** to the appropriate Docker service + +For more details about multi-layer routing, see the [Multi-Layer Routing documentation](../../reference/routing-configuration/http/routing/multi-layer-routing.md). ## Conclusion -In this guide, you've learned how to: +In this advanced guide, you've learned how to: -- Expose HTTP services through Traefik in Docker -- Set up path-based routing to direct traffic to different backend services -- Secure your services with TLS using self-signed certificates - Add security with middlewares like secure headers and IP allow listing - Automate certificate management with Let's Encrypt - Implement sticky sessions for stateful applications +- Setup multi-layer routing for authentication-based routing -These fundamental capabilities provide a solid foundation for exposing any application through Traefik Proxy in Docker. Each of these can be further customized to meet your specific requirements. +These advanced capabilities allow you to build production-ready Traefik deployments with Docker. Each of these can be further customized to meet your specific requirements. ### Next Steps -Now that you understand the basics of exposing services with Traefik Proxy, you might want to explore: +Now that you've mastered both basic and advanced Traefik features with Docker, you might want to explore: -- [Advanced routing options](../reference/routing-configuration/http/routing/rules-and-priority.md) like query parameter matching, header-based routing, and more -- [Additional middlewares](../reference/routing-configuration/http/middlewares/overview.md) for authentication, rate limiting, and request modifications -- [Observability features](../reference/install-configuration/observability/metrics.md) for monitoring and debugging your Traefik deployment -- [TCP services](../reference/routing-configuration/tcp/service.md) for exposing TCP services -- [UDP services](../reference/routing-configuration/udp/service.md) for exposing UDP services -- [Docker provider documentation](../reference/install-configuration/providers/docker.md) for more details about the Docker integration +- [Advanced routing options](../../reference/routing-configuration/http/routing/rules-and-priority.md) like query parameter matching, header-based routing, and more +- [Additional middlewares](../../reference/routing-configuration/http/middlewares/overview.md) for authentication, rate limiting, and request modifications +- [Observability features](../../reference/install-configuration/observability/metrics.md) for monitoring and debugging your Traefik deployment +- [TCP services](../../reference/routing-configuration/tcp/service.md) for exposing TCP services +- [UDP services](../../reference/routing-configuration/udp/service.md) for exposing UDP services +- [Docker provider documentation](../../reference/install-configuration/providers/docker.md) for more details about the Docker integration diff --git a/docs/content/expose/docker/basic.md b/docs/content/expose/docker/basic.md new file mode 100644 index 000000000..4db874c62 --- /dev/null +++ b/docs/content/expose/docker/basic.md @@ -0,0 +1,250 @@ +# Exposing Services with Traefik on Docker - Basic + +This guide will help you get started with exposing your services through Traefik Proxy using Docker. You'll learn the fundamentals of routing HTTP traffic, setting up path-based routing, and securing your services with TLS. + +## Prerequisites + +- Docker and Docker Compose installed +- Basic understanding of Docker concepts +- Traefik deployed using the [Traefik Docker Setup guide](../../setup/docker.md) + +## Expose Your First HTTP Service + +Let's expose a simple HTTP service using the [whoami](https://hub.docker.com/r/traefik/whoami) application. This will demonstrate basic routing to a backend service. + +First, create a `docker-compose.yml` file: + +```yaml +services: + traefik: + image: "traefik:v3.4" + container_name: "traefik" + restart: unless-stopped + security_opt: + - no-new-privileges:true + networks: + - proxy + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--providers.docker.network=proxy" + - "--entryPoints.web.address=:80" + ports: + - "80:80" + - "8080:8080" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + + whoami: + image: "traefik/whoami" + restart: unless-stopped + networks: + - proxy + labels: + - "traefik.enable=true" + - "traefik.http.routers.whoami.rule=Host(`whoami.docker.localhost`)" + - "traefik.http.routers.whoami.entrypoints=web" + +networks: + proxy: + name: proxy +``` + +Save this as `docker-compose.yml` and start the services: + +```bash +docker compose up -d +``` + +### Verify Your Service + +Your service is now available at http://whoami.docker.localhost/. Test that it works: + +```bash +curl -H "Host: whoami.docker.localhost" http://localhost/ +``` + +You should see output similar to: + +```bash +Hostname: whoami +IP: 127.0.0.1 +IP: ::1 +IP: 172.18.0.3 +IP: fe80::215:5dff:fe00:c9e +RemoteAddr: 172.18.0.2:55108 +GET / HTTP/1.1 +Host: whoami.docker.localhost +User-Agent: curl/7.68.0 +Accept: */* +Accept-Encoding: gzip +X-Forwarded-For: 172.18.0.1 +X-Forwarded-Host: whoami.docker.localhost +X-Forwarded-Port: 80 +X-Forwarded-Proto: http +X-Forwarded-Server: 5789f594e7d5 +X-Real-Ip: 172.18.0.1 +``` + +This confirms that Traefik is successfully routing requests to your whoami application. + +## Add Routing Rules + +Now we'll enhance our routing by directing traffic to different services based on [URL paths](../../reference/routing-configuration/http/routing/rules-and-priority.md#path-pathprefix-and-pathregexp). This is useful for API versioning, frontend/backend separation, or organizing microservices. + +Update your `docker-compose.yml` to add another service: + +```yaml +# ... + +# New service + whoami-api: + image: "traefik/whoami" + networks: + - proxy + container_name: "whoami-api" + environment: + - WHOAMI_NAME=API Service + labels: + - "traefik.enable=true" + # Path-based routing + - "traefik.http.routers.whoami-api.rule=Host(`whoami.docker.localhost`) && PathPrefix(`/api`)" + - "traefik.http.routers.whoami-api.entrypoints=web" +``` + +Apply the changes: + +```bash +docker compose up -d +``` + +### Test the Path-Based Routing + +Verify that different paths route to different services: + +```bash +# Root path should go to the main whoami service +curl -H "Host: whoami.docker.localhost" http://localhost/ + +# /api path should go to the whoami-api service +curl -H "Host: whoami.docker.localhost" http://localhost/api +``` + +For the `/api` requests, you should see the response showing "API Service" in the environment variables section, confirming that your path-based routing is working correctly. + +## Enable TLS + +Let's secure our service with HTTPS by adding TLS. We'll start with a self-signed certificate for local development. + +### Create a Self-Signed Certificate + +Generate a self-signed certificate: + +```bash +mkdir -p certs +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout certs/local.key -out certs/local.crt \ + -subj "/CN=*.docker.localhost" +``` + +Create a directory for dynamic configuration and add a TLS configuration file: + +```bash +mkdir -p dynamic +cat > dynamic/tls.yml << EOF +tls: + certificates: + - certFile: /certs/local.crt + keyFile: /certs/local.key +EOF +``` + +Update your `docker-compose.yml` file with the following changes: + +```yaml +services: + traefik: + image: "traefik:v3.4" + container_name: "traefik" + restart: unless-stopped + security_opt: + - no-new-privileges:true + networks: + - proxy + command: + - "--api.insecure=false" + - "--api.dashboard=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--providers.docker.network=proxy" + - "--providers.file.directory=/etc/traefik/dynamic" + - "--entryPoints.web.address=:80" + - "--entryPoints.websecure.address=:443" + - "--entryPoints.websecure.http.tls=true" + ports: + - "80:80" + - "443:443" + - "8080:8080" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + # Add the following volumes + - "./certs:/certs:ro" + - "./dynamic:/etc/traefik/dynamic:ro" + labels: + - "traefik.enable=true" + - "traefik.http.routers.dashboard.rule=Host(`dashboard.docker.localhost`)" + - "traefik.http.routers.dashboard.entrypoints=websecure" + - "traefik.http.routers.dashboard.service=api@internal" + # Add the following label + - "traefik.http.routers.dashboard.tls=true" + + whoami: + image: "traefik/whoami" + restart: unless-stopped + networks: + - proxy + labels: + - "traefik.enable=true" + - "traefik.http.routers.whoami.rule=Host(`whoami.docker.localhost`)" + - "traefik.http.routers.whoami.entrypoints=websecure" + # Add the following label + - "traefik.http.routers.whoami.tls=true" + + whoami-api: + image: "traefik/whoami" + container_name: "whoami-api" + restart: unless-stopped + networks: + - proxy + environment: + - WHOAMI_NAME=API Service + labels: + - "traefik.enable=true" + - "traefik.http.routers.whoami-api.rule=Host(`whoami.docker.localhost`) && PathPrefix(`/api`)" + - "traefik.http.routers.whoami-api.entrypoints=websecure" + # Add the following label + - "traefik.http.routers.whoami-api.tls=true" + +networks: + proxy: + name: proxy +``` + +Apply the changes: + +```bash +docker compose up -d +``` + +Your browser can access https://whoami.docker.localhost/ for the service. You'll need to accept the security warning for the self-signed certificate. + +## Next Steps + +Now that you've mastered the basics of exposing services with Traefik on Docker, you're ready to explore more advanced features like middlewares, Let's Encrypt certificates, sticky sessions, and multi-layer routing. + +Continue to the [Advanced Guide](advanced.md) to learn about: + +- Adding middlewares for security and access control +- Generating certificates with Let's Encrypt +- Configuring sticky sessions for stateful applications +- Setting up multi-layer routing for authentication-based routing diff --git a/docs/content/expose/kubernetes.md b/docs/content/expose/kubernetes/advanced.md similarity index 59% rename from docs/content/expose/kubernetes.md rename to docs/content/expose/kubernetes/advanced.md index 7abac99e6..64f5e78b7 100644 --- a/docs/content/expose/kubernetes.md +++ b/docs/content/expose/kubernetes/advanced.md @@ -1,432 +1,25 @@ -# Exposing Services with Traefik on Kubernetes +# Exposing Services with Traefik on Kubernetes - Advanced -This guide will help you expose your services securely through Traefik Proxy on Kubernetes. We'll cover routing HTTP and HTTPS traffic, implementing TLS, adding security middleware, and configuring sticky sessions. For routing, this guide gives you two options: +This guide builds on the concepts and setup from the [Basic Guide](basic.md). Make sure you've completed the basic guide and have a working Traefik setup with Kubernetes before proceeding. -- [Gateway API](../reference/routing-configuration/kubernetes/gateway-api.md) -- [IngressRoute](../reference/routing-configuration/kubernetes/crd/http/ingressroute.md) +In this advanced guide, you'll learn how to enhance your Traefik deployment with: -Feel free to choose the one that fits your needs best. +- **Middlewares** for security headers and access control +- **Let's Encrypt** for automated certificate management (IngressRoute) +- **cert-manager** for automated certificate management (Gateway API) +- **Sticky sessions** for stateful applications +- **Multi-layer routing** for hierarchical routing with complex authentication scenarios (IngressRoute only) ## Prerequisites +- Completed the [Basic Guide](basic.md) - A Kubernetes cluster with Traefik Proxy installed - `kubectl` configured to interact with your cluster -- Traefik deployed using the Traefik Kubernetes Setup guide - -## Expose Your First HTTP Service - -Let's expose a simple HTTP service using the [whoami](https://github.com/traefik/whoami) application. This will demonstrate basic routing to a backend service. - -First, create the deployment and service: - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: whoami - namespace: default -spec: - replicas: 2 - selector: - matchLabels: - app: whoami - template: - metadata: - labels: - app: whoami - spec: - containers: - - name: whoami - image: traefik/whoami - ports: - - containerPort: 80 ---- -apiVersion: v1 -kind: Service -metadata: - name: whoami - namespace: default -spec: - selector: - app: whoami - ports: - - port: 80 -``` - -Save this as `whoami.yaml` and apply it: - -```bash -kubectl apply -f whoami.yaml -``` - -Now, let's create routes using either Gateway API or IngressRoute. - -### Using Gateway API - -```yaml -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: whoami - namespace: default -spec: - parentRefs: - - name: traefik-gateway # This Gateway is automatically created by Traefik - hostnames: - - "whoami.docker.localhost" - rules: - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: whoami - port: 80 -``` - -Save this as `whoami-route.yaml` and apply it: - -```bash -kubectl apply -f whoami-route.yaml -``` - -### Using IngressRoute - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: whoami - namespace: default -spec: - entryPoints: - - web - routes: - - match: Host(`whoami.docker.localhost`) - kind: Rule - services: - - name: whoami - port: 80 -``` - -Save this as `whoami-ingressroute.yaml` and apply it: - -```bash -kubectl apply -f whoami-ingressroute.yaml -``` - -### Verify Your Service - -Your service is now available at http://whoami.docker.localhost/. Test that it works: - -```bash -curl -H "Host: whoami.docker.localhost" http://localhost/ -``` - -!!! info - Make sure to remove the `ports.web.redirections` block from the `values.yaml` file if you followed the Kubernetes Setup Guide to install Traefik otherwise you will be redirected to the HTTPS entrypoint: - - ```yaml - redirections: - entryPoint: - to: websecure - ``` - -You should see output similar to: - -```bash -Hostname: whoami-6d5d964cb-8pv4k -IP: 127.0.0.1 -IP: ::1 -IP: 10.42.0.18 -IP: fe80::d4c0:3bff:fe20:b0a3 -RemoteAddr: 10.42.0.17:39872 -GET / HTTP/1.1 -Host: whoami.docker.localhost -User-Agent: curl/7.68.0 -Accept: */* -Accept-Encoding: gzip -X-Forwarded-For: 10.42.0.1 -X-Forwarded-Host: whoami.docker.localhost -X-Forwarded-Port: 80 -X-Forwarded-Proto: http -X-Forwarded-Server: traefik-76cbd5b89c-rx5xn -X-Real-Ip: 10.42.0.1 -``` - -This confirms that Traefik is successfully routing requests to your whoami application. - -## Add Routing Rules - -Now we'll enhance our routing by directing traffic to different services based on URL paths. This is useful for API versioning, frontend/backend separation, or organizing microservices. - -First, deploy a second service to represent an API: - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: whoami-api - namespace: default -spec: - replicas: 1 - selector: - matchLabels: - app: whoami-api - template: - metadata: - labels: - app: whoami-api - spec: - containers: - - name: whoami - image: traefik/whoami - env: - - name: WHOAMI_NAME - value: "API Service" - ports: - - containerPort: 80 ---- -apiVersion: v1 -kind: Service -metadata: - name: whoami-api - namespace: default -spec: - selector: - app: whoami-api - ports: - - port: 80 -``` - -Save this as `whoami-api.yaml` and apply it: - -```bash -kubectl apply -f whoami-api.yaml -``` - -Now set up path-based routing: - -### Gateway API with Path Rules - -Update your existing `HTTPRoute` to include path-based routing: - -```yaml -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: whoami - namespace: default -spec: - parentRefs: - - name: traefik-gateway - hostnames: - - "whoami.docker.localhost" - rules: - - matches: - - path: - type: PathPrefix - value: /api - backendRefs: - - name: whoami-api - port: 80 - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: whoami - port: 80 -``` - -Update the file `whoami-route.yaml` and apply it: - -```bash -kubectl apply -f whoami-route.yaml -``` - -### IngressRoute with Path Rules - -Update your existing IngressRoute to include path-based routing: - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: whoami - namespace: default -spec: - entryPoints: - - web - routes: - - match: Host(`whoami.docker.localhost`) && Path(`/api`) - kind: Rule - services: - - name: whoami-api - port: 80 - - match: Host(`whoami.docker.localhost`) - kind: Rule - services: - - name: whoami - port: 80 -``` - -Save this as `whoami-ingressroute.yaml` and apply it: - -```bash -kubectl apply -f whoami-ingressroute.yaml -``` - -### Test the Path-Based Routing - -Verify that different paths route to different services: - -```bash -# Root path should go to the main whoami service -curl -H "Host: whoami.docker.localhost" http://localhost/ - -# /api path should go to the whoami-api service -curl -H "Host: whoami.docker.localhost" http://localhost/api -``` - -For the `/api` requests, you should see the response showing "API Service" in the environment variables section, confirming that your path-based routing is working correctly: - -```bash -{"hostname":"whoami-api-67d97b4868-dvvll","ip":["127.0.0.1","::1","10.42.0.9","fe80::10aa:37ff:fe74:31f2"],"headers":{"Accept":["*/*"],"Accept-Encoding":["gzip"],"User-Agent":["curl/8.7.1"],"X-Forwarded-For":["10.42.0.1"],"X-Forwarded-Host":["whoami.docker.localhost"],"X-Forwarded-Port":["80"],"X-Forwarded-Proto":["http"],"X-Forwarded-Server":["traefik-669c479df8-vkj22"],"X-Real-Ip":["10.42.0.1"]},"url":"/api","host":"whoami.docker.localhost","method":"GET","name":"API Service","remoteAddr":"10.42.0.13:36592"} -``` - -## Enable TLS - -Let's secure our service with HTTPS by adding TLS. We'll start with a self-signed certificate for local development. - -### Create a Self-Signed Certificate - -Generate a self-signed certificate: - -```bash -openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout tls.key -out tls.crt \ - -subj "/CN=whoami.docker.localhost" -``` - -Create a TLS secret in Kubernetes: - -```bash -kubectl create secret tls whoami-tls --cert=tls.crt --key=tls.key -``` - -!!! important "Prerequisite for Gateway API with TLS" - Before using the Gateway API with TLS, you must define the `websecure` listener in your Traefik installation. This is typically done in your Helm values. - - Example configuration in `values.yaml`: - ```yaml - gateway: - listeners: - web: - port: 80 - protocol: HTTP - namespacePolicy: - from: All - websecure: - port: 443 - protocol: HTTPS - namespacePolicy: - from: All - mode: Terminate - certificateRefs: - - kind: Secret - name: local-selfsigned-tls - group: "" - ``` - - See the Traefik Kubernetes Setup Guide for complete installation details. - -### Gateway API with TLS - -Update your existing `HTTPRoute` to use the secured gateway listener: - -```yaml -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: whoami - namespace: default -spec: - parentRefs: - - name: traefik-gateway - sectionName: websecure # The HTTPS listener - hostnames: - - "whoami.docker.localhost" - rules: - - matches: - - path: - type: PathPrefix - value: /api - backendRefs: - - name: whoami-api - port: 80 - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: whoami - port: 80 -``` - -Update the file `whoami-route.yaml` and apply it: - -```bash -kubectl apply -f whoami-route.yaml -``` - -### IngressRoute with TLS - -Update your existing IngressRoute to use TLS: - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: whoami - namespace: default -spec: - entryPoints: - - websecure # Changed from 'web' to 'websecure' - routes: - - match: Host(`whoami.docker.localhost`) && Path(`/api`) - kind: Rule - services: - - name: whoami-api - port: 80 - - match: Host(`whoami.docker.localhost`) - kind: Rule - services: - - name: whoami - port: 80 - tls: - secretName: whoami-tls # Added TLS configuration -``` - -Update the file `whoami-ingressroute.yaml` and apply it: - -```bash -kubectl apply -f whoami-ingressroute.yaml -``` - -### Verify HTTPS Access - -Now you can access your service securely. Since we're using a self-signed certificate, you'll need to skip certificate verification: - -```bash -curl -k -H "Host: whoami.docker.localhost" https://localhost/ -``` - -Your browser can also access https://whoami.docker.localhost/ (you'll need to accept the security warning for the self-signed certificate). +- Working Traefik setup from the basic guide ## Add Middlewares -Middlewares allow you to modify requests or responses as they pass through Traefik. Let's add two useful middlewares: [Headers](../reference/routing-configuration/http/middlewares/headers.md) for security and [IP allowlisting](../reference/routing-configuration/http/middlewares/ipallowlist.md) for access control. +Middlewares allow you to modify requests or responses as they pass through Traefik. Let's add two useful middlewares: [Headers](../../reference/routing-configuration/http/middlewares/headers.md) for security and [IP allowlisting](../../reference/routing-configuration/http/middlewares/ipallowlist.md) for access control. ### Create Middlewares @@ -469,37 +62,6 @@ kubectl apply -f middlewares.yaml In Gateway API, you can apply middlewares using the `ExtensionRef` filter type. This is the preferred and standard way to use Traefik middlewares with Gateway API, as it integrates directly with the HTTPRoute specification. -First, make sure you have the same middlewares defined: - -```yaml -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: secure-headers - namespace: default -spec: - headers: - frameDeny: true - sslRedirect: true - browserXssFilter: true - contentTypeNosniff: true - stsIncludeSubdomains: true - stsPreload: true - stsSeconds: 31536000 ---- -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: ip-allowlist - namespace: default -spec: - ipAllowList: - sourceRange: - - 127.0.0.1/32 - - 10.0.0.0/8 # Typical cluster network range - - 192.168.0.0/16 # Common local network range -``` - Now, update your `HTTPRoute` to reference these middlewares using the `ExtensionRef` filter: ```yaml @@ -525,7 +87,7 @@ spec: group: traefik.io kind: Middleware name: secure-headers - - type: ExtensionRef + - type: ExtensionRef extensionRef: # IP AllowList Middleware Definition group: traefik.io kind: Middleware @@ -543,7 +105,7 @@ spec: group: traefik.io kind: Middleware name: secure-headers - - type: ExtensionRef + - type: ExtensionRef extensionRef: # IP AllowList Middleware Definition group: traefik.io kind: Middleware @@ -854,7 +416,7 @@ spec: group: traefik.io kind: Middleware name: secure-headers - - type: ExtensionRef + - type: ExtensionRef extensionRef: # IP AllowList Middleware Definition group: traefik.io kind: Middleware @@ -876,7 +438,7 @@ spec: group: traefik.io kind: Middleware name: secure-headers - - type: ExtensionRef + - type: ExtensionRef extensionRef: # IP AllowList Middleware Definition group: traefik.io kind: Middleware @@ -986,29 +548,283 @@ You should see different `Hostname` values in these responses, as each request i !!! important "Browser Testing" When testing in browsers, you need to use the same browser session to maintain the cookie. The cookie is set with `httpOnly` and `secure` flags for security, so it will only be sent over HTTPS connections and won't be accessible via JavaScript. -For more advanced configuration options, see the [reference documentation](../reference/routing-configuration/http/load-balancing/service.md). +For more advanced configuration options, see the [reference documentation](../../reference/routing-configuration/http/load-balancing/service.md). + +## Setup Multi-Layer Routing + +Multi-layer routing enables hierarchical relationships between routers, where parent routers can process requests through middleware before child routers make final routing decisions. This is particularly useful for authentication-based routing or staged middleware application. + +!!! info "IngressRoute Support" + Multi-layer routing is **natively supported** by Kubernetes IngressRoute (CRD) using the `spec.parentRefs` field. This feature is not available when using standard Kubernetes Ingress or Gateway API resources. + +### Authentication-Based Routing Example + +Let's create a multi-layer routing setup where a parent IngressRoute authenticates requests, and child IngressRoutes direct traffic based on user roles. + +!!! important "Parent Router Requirements" + Parent routers in multi-layer routing must not have a service defined. The child routers will handle the service selection based on their matching rules. Make sure all child IngressRoutes reference the parent correctly using `parentRefs`. + +First, deploy your backend services: + +```yaml +# whoami-backends.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: admin-backend + namespace: default +spec: + replicas: 2 + selector: + matchLabels: + app: admin-backend + template: + metadata: + labels: + app: admin-backend + spec: + containers: + - name: whoami + image: traefik/whoami + env: + - name: WHOAMI_NAME + value: "Admin Backend" + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: admin-backend + namespace: default +spec: + selector: + app: admin-backend + ports: + - port: 80 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: user-backend + namespace: default +spec: + replicas: 2 + selector: + matchLabels: + app: user-backend + template: + metadata: + labels: + app: user-backend + spec: + containers: + - name: whoami + image: traefik/whoami + env: + - name: WHOAMI_NAME + value: "User Backend" + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: user-backend + namespace: default +spec: + selector: + app: user-backend + ports: + - port: 80 +``` + +Apply the backend services: + +```bash +kubectl apply -f whoami-backends.yaml +``` + +Now create the middleware and IngressRoutes for multi-layer routing: + +```yaml +# mlr-ingressroute.yaml +apiVersion: v1 +kind: Secret +metadata: + name: auth-secret + namespace: default +type: Opaque +stringData: + users: | + admin:$apr1$DmXR3Add$wfdbGw6RWIhFb0ffXMM4d0 + user:$apr1$GJtcIY1o$mSLdsWYeXpPHVsxGDqadI. +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: auth-middleware + namespace: default +spec: + basicAuth: + secret: auth-secret + headerField: X-Auth-User +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: api-parent + namespace: default +spec: + entryPoints: + - websecure + routes: + - match: Host(`api.docker.localhost`) && PathPrefix(`/api`) + kind: Rule + middlewares: + - name: auth-middleware + # Note: No services and no TLS config - this is a parent IngressRoute +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: api-admin + namespace: default +spec: + parentRefs: + - name: api-parent + namespace: default # Optional, defaults to same namespace + routes: + - match: HeadersRegexp(`X-Auth-User`, `admin`) + kind: Rule + services: + - name: admin-backend + port: 80 +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: api-user + namespace: default +spec: + parentRefs: + - name: api-parent + namespace: default # Optional, defaults to same namespace + routes: + - match: HeadersRegexp(`X-Auth-User`, `user`) + kind: Rule + services: + - name: user-backend + port: 80 +``` + +!!! note "Generating Password Hashes" + The password hashes above are generated using `htpasswd`. To create your own user credentials: + + ```bash + # Using htpasswd (Apache utils) + htpasswd -nb admin yourpassword + ``` + +Apply the multi-layer routing configuration: + +```bash +kubectl apply -f mlr-ingressroute.yaml +``` + +### Test Multi-Layer Routing + +Test the routing behavior: + +```bash +# Request goes through parent router → auth middleware → admin child router +curl -k -u admin:test -H "Host: api.docker.localhost" https://localhost/api +``` + +You should see the response from the admin-backend service when authenticating as `admin`. Try with `user:test` credentials to reach the user-backend service instead. + +### How It Works + +1. **Request arrives** at `api.docker.localhost/api` +2. **Parent IngressRoute** (`api-parent`) matches based on host and path +3. **BasicAuth middleware** authenticates the user and sets the `X-Auth-User` header with the username +4. **Child IngressRoute** (`api-admin` or `api-user`) matches based on the header value +5. **Request forwarded** to the appropriate Kubernetes service + +### Cross-Namespace Parent References + +You can reference parent IngressRoutes in different namespaces by specifying the `namespace` field: + +```yaml +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: api-child + namespace: app-namespace +spec: + parentRefs: + - name: api-parent + namespace: shared-namespace # Parent in different namespace + routes: + - match: Path(`/child`) + kind: Rule + services: + - name: child-service + port: 80 +``` + +!!! important "Cross-Namespace Requirement" + To use cross-namespace parent references, you must enable the `allowCrossNamespace` option in your Traefik Helm values: + + ```yaml + providers: + kubernetesCRD: + allowCrossNamespace: true + ``` + +### Multiple Parent References + +Child IngressRoutes can reference multiple parent IngressRoutes: + +```yaml +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: api-child + namespace: default +spec: + parentRefs: + - name: parent-one + - name: parent-two + routes: + - match: Path(`/api`) + kind: Rule + services: + - name: child-service + port: 80 +``` + +For more details about multi-layer routing, see the [Multi-Layer Routing documentation](../../reference/routing-configuration/http/routing/multi-layer-routing.md). ## Conclusion -In this guide, you've learned how to: +In this advanced guide, you've learned how to: -- Expose HTTP services through Traefik in Kubernetes using both Gateway API and IngressRoute -- Set up path-based routing to direct traffic to different backend services -- Secure your services with TLS using self-signed certificates - Add security with middlewares like secure headers and IP allow listing -- Automate certificate management with Let's Encrypt +- Automate certificate management with Let's Encrypt (IngressRoute) and cert-manager (Gateway API) - Implement sticky sessions for stateful applications +- Setup multi-layer routing for authentication-based routing (IngressRoute only) -These fundamental capabilities provide a solid foundation for exposing any application through Traefik Proxy in Kubernetes. Each of these can be further customized to meet your specific requirements. +These advanced capabilities allow you to build production-ready Traefik deployments with Kubernetes. Each of these can be further customized to meet your specific requirements. ### Next Steps -Now that you understand the basics of exposing services with Traefik Proxy, you might want to explore: +Now that you've mastered both basic and advanced Traefik features with Kubernetes, you might want to explore: -- [Advanced routing options](../reference/routing-configuration/http/routing/rules-and-priority.md) like query parameter matching, header-based routing, and more -- [Additional middlewares](../reference/routing-configuration/http/middlewares/overview.md) for authentication, rate limiting, and request modifications -- [Observability features](../reference/install-configuration/observability/metrics.md) for monitoring and debugging your Traefik deployment -- [TCP services](../reference/routing-configuration/tcp/service.md) for exposing TCP services -- [UDP services](../reference/routing-configuration/udp/service.md) for exposing UDP services -- [Kubernetes Provider documentation](../reference/install-configuration/providers/kubernetes/kubernetes-crd.md) for more details about the Kubernetes integration. -- [Gateway API provider documentation](../reference/install-configuration/providers/kubernetes/kubernetes-gateway.md) for more details about the Gateway API integration. +- [Advanced routing options](../../reference/routing-configuration/http/routing/rules-and-priority.md) like query parameter matching, header-based routing, and more +- [Additional middlewares](../../reference/routing-configuration/http/middlewares/overview.md) for authentication, rate limiting, and request modifications +- [Observability features](../../reference/install-configuration/observability/metrics.md) for monitoring and debugging your Traefik deployment +- [TCP services](../../reference/routing-configuration/tcp/service.md) for exposing TCP services +- [UDP services](../../reference/routing-configuration/udp/service.md) for exposing UDP services +- [Kubernetes Provider documentation](../../reference/install-configuration/providers/kubernetes/kubernetes-crd.md) for more details about the Kubernetes integration +- [Gateway API provider documentation](../../reference/install-configuration/providers/kubernetes/kubernetes-gateway.md) for more details about the Gateway API integration diff --git a/docs/content/expose/kubernetes/basic.md b/docs/content/expose/kubernetes/basic.md new file mode 100644 index 000000000..86db8a6da --- /dev/null +++ b/docs/content/expose/kubernetes/basic.md @@ -0,0 +1,438 @@ +# Exposing Services with Traefik on Kubernetes - Basic + +This guide will help you get started with exposing your services through Traefik Proxy on Kubernetes. You'll learn the fundamentals of routing HTTP traffic, setting up path-based routing, and securing your services with TLS. + +For routing, this guide gives you two options: + +- [Gateway API](../../reference/routing-configuration/kubernetes/gateway-api.md) +- [IngressRoute](../../reference/routing-configuration/kubernetes/crd/http/ingressroute.md) + +Feel free to choose the one that fits your needs best. + +## Prerequisites + +- A Kubernetes cluster with Traefik Proxy installed +- `kubectl` configured to interact with your cluster +- Traefik deployed using the [Traefik Kubernetes Setup guide](../../setup/kubernetes.md) + +## Expose Your First HTTP Service + +Let's expose a simple HTTP service using the [whoami](https://github.com/traefik/whoami) application. This will demonstrate basic routing to a backend service. + +First, create the deployment and service: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: whoami + namespace: default +spec: + replicas: 2 + selector: + matchLabels: + app: whoami + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: traefik/whoami + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: default +spec: + selector: + app: whoami + ports: + - port: 80 +``` + +Save this as `whoami.yaml` and apply it: + +```bash +kubectl apply -f whoami.yaml +``` + +Now, let's create routes using either Gateway API or IngressRoute. + +### Using Gateway API + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: whoami + namespace: default +spec: + parentRefs: + - name: traefik-gateway # This Gateway is automatically created by Traefik + hostnames: + - "whoami.docker.localhost" + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: whoami + port: 80 +``` + +Save this as `whoami-route.yaml` and apply it: + +```bash +kubectl apply -f whoami-route.yaml +``` + +### Using IngressRoute + +```yaml +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: whoami + namespace: default +spec: + entryPoints: + - web + routes: + - match: Host(`whoami.docker.localhost`) + kind: Rule + services: + - name: whoami + port: 80 +``` + +Save this as `whoami-ingressroute.yaml` and apply it: + +```bash +kubectl apply -f whoami-ingressroute.yaml +``` + +### Verify Your Service + +Your service is now available at http://whoami.docker.localhost/. Test that it works: + +```bash +curl -H "Host: whoami.docker.localhost" http://localhost/ +``` + +!!! info + Make sure to remove the `ports.web.redirections` block from the `values.yaml` file if you followed the Kubernetes Setup Guide to install Traefik otherwise you will be redirected to the HTTPS entrypoint: + + ```yaml + redirections: + entryPoint: + to: websecure + ``` + +You should see output similar to: + +```bash +Hostname: whoami-6d5d964cb-8pv4k +IP: 127.0.0.1 +IP: ::1 +IP: 10.42.0.18 +IP: fe80::d4c0:3bff:fe20:b0a3 +RemoteAddr: 10.42.0.17:39872 +GET / HTTP/1.1 +Host: whoami.docker.localhost +User-Agent: curl/7.68.0 +Accept: */* +Accept-Encoding: gzip +X-Forwarded-For: 10.42.0.1 +X-Forwarded-Host: whoami.docker.localhost +X-Forwarded-Port: 80 +X-Forwarded-Proto: http +X-Forwarded-Server: traefik-76cbd5b89c-rx5xn +X-Real-Ip: 10.42.0.1 +``` + +This confirms that Traefik is successfully routing requests to your whoami application. + +## Add Routing Rules + +Now we'll enhance our routing by directing traffic to different services based on URL paths. This is useful for API versioning, frontend/backend separation, or organizing microservices. + +First, deploy a second service to represent an API: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: whoami-api + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: whoami-api + template: + metadata: + labels: + app: whoami-api + spec: + containers: + - name: whoami + image: traefik/whoami + env: + - name: WHOAMI_NAME + value: "API Service" + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami-api + namespace: default +spec: + selector: + app: whoami-api + ports: + - port: 80 +``` + +Save this as `whoami-api.yaml` and apply it: + +```bash +kubectl apply -f whoami-api.yaml +``` + +Now set up path-based routing: + +### Gateway API with Path Rules + +Update your existing `HTTPRoute` to include path-based routing: + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: whoami + namespace: default +spec: + parentRefs: + - name: traefik-gateway + hostnames: + - "whoami.docker.localhost" + rules: + - matches: + - path: + type: PathPrefix + value: /api + backendRefs: + - name: whoami-api + port: 80 + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: whoami + port: 80 +``` + +Update the file `whoami-route.yaml` and apply it: + +```bash +kubectl apply -f whoami-route.yaml +``` + +### IngressRoute with Path Rules + +Update your existing IngressRoute to include path-based routing: + +```yaml +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: whoami + namespace: default +spec: + entryPoints: + - web + routes: + - match: Host(`whoami.docker.localhost`) && Path(`/api`) + kind: Rule + services: + - name: whoami-api + port: 80 + - match: Host(`whoami.docker.localhost`) + kind: Rule + services: + - name: whoami + port: 80 +``` + +Save this as `whoami-ingressroute.yaml` and apply it: + +```bash +kubectl apply -f whoami-ingressroute.yaml +``` + +### Test the Path-Based Routing + +Verify that different paths route to different services: + +```bash +# Root path should go to the main whoami service +curl -H "Host: whoami.docker.localhost" http://localhost/ + +# /api path should go to the whoami-api service +curl -H "Host: whoami.docker.localhost" http://localhost/api +``` + +For the `/api` requests, you should see the response showing "API Service" in the environment variables section, confirming that your path-based routing is working correctly: + +```bash +{"hostname":"whoami-api-67d97b4868-dvvll","ip":["127.0.0.1","::1","10.42.0.9","fe80::10aa:37ff:fe74:31f2"],"headers":{"Accept":["*/*"],"Accept-Encoding":["gzip"],"User-Agent":["curl/8.7.1"],"X-Forwarded-For":["10.42.0.1"],"X-Forwarded-Host":["whoami.docker.localhost"],"X-Forwarded-Port":["80"],"X-Forwarded-Proto":["http"],"X-Forwarded-Server":["traefik-669c479df8-vkj22"],"X-Real-Ip":["10.42.0.1"]},"url":"/api","host":"whoami.docker.localhost","method":"GET","name":"API Service","remoteAddr":"10.42.0.13:36592"} +``` + +## Enable TLS + +Let's secure our service with HTTPS by adding TLS. We'll start with a self-signed certificate for local development. + +### Create a Self-Signed Certificate + +Generate a self-signed certificate: + +```bash +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout tls.key -out tls.crt \ + -subj "/CN=whoami.docker.localhost" +``` + +Create a TLS secret in Kubernetes: + +```bash +kubectl create secret tls whoami-tls --cert=tls.crt --key=tls.key +``` + +!!! important "Prerequisite for Gateway API with TLS" + Before using the Gateway API with TLS, you must define the `websecure` listener in your Traefik installation. This is typically done in your Helm values. + + Example configuration in `values.yaml`: + ```yaml + gateway: + listeners: + web: + port: 80 + protocol: HTTP + namespacePolicy: + from: All + websecure: + port: 443 + protocol: HTTPS + namespacePolicy: + from: All + mode: Terminate + certificateRefs: + - kind: Secret + name: local-selfsigned-tls + group: "" + ``` + + See the Traefik Kubernetes Setup Guide for complete installation details. + +### Gateway API with TLS + +Update your existing `HTTPRoute` to use the secured gateway listener: + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: whoami + namespace: default +spec: + parentRefs: + - name: traefik-gateway + sectionName: websecure # The HTTPS listener + hostnames: + - "whoami.docker.localhost" + rules: + - matches: + - path: + type: PathPrefix + value: /api + backendRefs: + - name: whoami-api + port: 80 + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: whoami + port: 80 +``` + +Update the file `whoami-route.yaml` and apply it: + +```bash +kubectl apply -f whoami-route.yaml +``` + +### IngressRoute with TLS + +Update your existing IngressRoute to use TLS: + +```yaml +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: whoami + namespace: default +spec: + entryPoints: + - websecure # Changed from 'web' to 'websecure' + routes: + - match: Host(`whoami.docker.localhost`) && Path(`/api`) + kind: Rule + services: + - name: whoami-api + port: 80 + - match: Host(`whoami.docker.localhost`) + kind: Rule + services: + - name: whoami + port: 80 + tls: + secretName: whoami-tls # Added TLS configuration +``` + +Update the file `whoami-ingressroute.yaml` and apply it: + +```bash +kubectl apply -f whoami-ingressroute.yaml +``` + +### Verify HTTPS Access + +Now you can access your service securely. Since we're using a self-signed certificate, you'll need to skip certificate verification: + +```bash +curl -k -H "Host: whoami.docker.localhost" https://localhost/ +``` + +Your browser can also access https://whoami.docker.localhost/ (you'll need to accept the security warning for the self-signed certificate). + +## Next Steps + +Now that you've mastered the basics of exposing services with Traefik on Kubernetes, you're ready to explore more advanced features like middlewares, Let's Encrypt certificates, sticky sessions, and multi-layer routing. + +Continue to the [Advanced Guide](advanced.md) to learn about: + +- Adding middlewares for security and access control +- Generating certificates with Let's Encrypt (IngressRoute) or cert-manager (Gateway API) +- Configuring sticky sessions for stateful applications +- Setting up multi-layer routing for authentication-based routing with IngressRoute diff --git a/docs/content/expose/overview.md b/docs/content/expose/overview.md index 641203a65..4563011ea 100644 --- a/docs/content/expose/overview.md +++ b/docs/content/expose/overview.md @@ -17,6 +17,100 @@ Following these guides, you'll learn how to: For detailed steps tailored to your environment, follow the guide for your platform: -- [Kubernetes](./kubernetes.md) -- [Docker](./docker.md) -- [Docker Swarm](./swarm.md) +- [Kubernetes](./kubernetes/basic.md) +- [Docker](./docker/basic.md) +- [Docker Swarm](./swarm/basic.md) + +## Advanced Use Cases + +### Exposing gRPC Services + +Traefik Proxy supports gRPC applications without requiring specific configuration. You can expose gRPC services using either HTTP (h2c) or HTTPS. + +??? example "Using HTTP (h2c)" + + For unencrypted gRPC communication, configure your service to use the `h2c://` protocol scheme: + + ```yaml + http: + routers: + grpc-router: + service: grpc-service + rule: Host(`grpc.example.com`) + + services: + grpc-service: + loadBalancer: + servers: + - url: h2c://backend:8080 + ``` + + !!! note + For providers with labels (Docker, Kubernetes), specify the scheme using: + `traefik.http.services..loadbalancer.server.scheme=h2c` + +??? example "Using HTTPS" + + For encrypted gRPC communication, use standard HTTPS URLs. Traefik will use HTTP/2 over TLS to communicate with your gRPC backend: + + ```yaml + http: + routers: + grpc-router: + service: grpc-service + rule: Host(`grpc.example.com`) + tls: {} + + services: + grpc-service: + loadBalancer: + servers: + - url: https://backend:8080 + ``` + + Traefik handles the protocol negotiation automatically. Configure TLS certificates for your backends using [ServersTransport](../reference/routing-configuration/http/load-balancing/serverstransport.md) if needed. + +### Exposing WebSocket Services + +Traefik Proxy supports WebSocket (WS) and WebSocket Secure (WSS) connections out of the box. No special configuration is required beyond standard HTTP routing. + +??? example "Basic WebSocket" + + Configure a router and service pointing to your WebSocket server. Traefik automatically detects and handles the WebSocket upgrade: + + ```yaml + http: + routers: + websocket-router: + rule: Host(`ws.example.com`) + service: websocket-service + + services: + websocket-service: + loadBalancer: + servers: + - url: http://websocket-backend:8000 + ``` + +??? example "WebSocket Secure (WSS)" + + For encrypted WebSocket connections, enable TLS on your router. Clients connect using `wss://` while you can choose whether backends use encrypted or unencrypted connections: + + ```yaml + http: + routers: + websocket-secure-router: + rule: Host(`wss.example.com`) + service: websocket-service + tls: {} + + services: + websocket-service: + loadBalancer: + servers: + - url: http://websocket-backend:8000 # SSL termination at Traefik + # OR + # - url: https://websocket-backend:8443 # End-to-end encryption + ``` + + Traefik preserves WebSocket headers including `Origin`, `Sec-WebSocket-Key`, and `Sec-WebSocket-Version`. Use the [Headers middleware](../reference/routing-configuration/http/middlewares/headers.md) if you need to modify headers for origin checking or other requirements. diff --git a/docs/content/expose/swarm.md b/docs/content/expose/swarm/advanced.md similarity index 51% rename from docs/content/expose/swarm.md rename to docs/content/expose/swarm/advanced.md index 67a01663a..efe93df3d 100644 --- a/docs/content/expose/swarm.md +++ b/docs/content/expose/swarm/advanced.md @@ -1,186 +1,23 @@ -# Exposing Services with Traefik on Docker Swarm +# Exposing Services with Traefik on Docker Swarm - Advanced -This guide will help you expose your services securely through Traefik Proxy using Docker Swarm. We'll cover routing HTTP and HTTPS traffic, implementing TLS, adding middlewares, Let's Encrypt integration, and sticky sessions. +This guide builds on the concepts and setup from the [Basic Guide](basic.md). Make sure you've completed the basic guide and have a working Traefik setup with Docker Swarm before proceeding. + +In this advanced guide, you'll learn how to enhance your Traefik deployment with: + +- **Middlewares** for security headers and access control +- **Let's Encrypt** for automated certificate management +- **Sticky sessions** for stateful applications +- **Multi-layer routing** for complex authentication scenarios ## Prerequisites +- Completed the [Basic Guide](basic.md) - Docker Swarm cluster initialized -- Basic understanding of Docker Swarm concepts -- Traefik deployed using the Traefik Docker Swarm Setup guide - -## Expose Your First HTTP Service - -Let's expose a simple HTTP service using the [whoami](https://hub.docker.com/r/traefik/whoami) application. This will demonstrate basic routing to a backend service. - -First, update your existing `docker-compose.yml` file if you haven't already: - -```yaml -services: - whoami: - image: traefik/whoami - networks: - - traefik_proxy - deploy: - replicas: 3 - labels: - - "traefik.enable=true" - - "traefik.http.routers.whoami.rule=Host(`whoami.swarm.localhost`)" - - "traefik.http.routers.whoami.entrypoints=web,websecure" -``` - -Save this as `docker-compose.yml` and deploy the stack: - -```bash -docker stack deploy -c docker-compose.yml traefik -``` - -### Verify Your Service - -Your service is now available at http://whoami.swarm.localhost/. Test that it works: - -```bash -curl -H "Host: whoami.swarm.localhost" http://localhost/ -``` - -You should see output similar to: - -```bash -Hostname: whoami.1.7c8f7tr56q3p949rscxrkp80e -IP: 127.0.0.1 -IP: ::1 -IP: 10.0.1.8 -IP: fe80::215:5dff:fe00:c9e -RemoteAddr: 10.0.1.2:45098 -GET / HTTP/1.1 -Host: whoami.swarm.localhost -User-Agent: curl/7.68.0 -Accept: */* -Accept-Encoding: gzip -X-Forwarded-For: 10.0.1.1 -X-Forwarded-Host: whoami.swarm.localhost -X-Forwarded-Port: 80 -X-Forwarded-Proto: http -X-Forwarded-Server: 5789f594e7d5 -X-Real-Ip: 10.0.1.1 -``` - -This confirms that Traefik is successfully routing requests to your whoami application. - -## Add Routing Rules - -Now we'll enhance our routing by directing traffic to different services based on [URL paths](../reference/routing-configuration/http/routing/rules-and-priority.md#path-pathprefix-and-pathregexp). This is useful for API versioning, frontend/backend separation, or organizing microservices. - -Update your `docker-compose.yml` to add another service: - -```yaml -# ... - -# New service - whoami-api: - image: traefik/whoami - networks: - - traefik_proxy - environment: - - WHOAMI_NAME=API Service - deploy: - replicas: 2 - labels: - - "traefik.enable=true" - # Path-based routing - - "traefik.http.routers.whoami-api.rule=Host(`whoami.swarm.localhost`) && PathPrefix(`/api`)" - - "traefik.http.routers.whoami-api.entrypoints=web,websecure" - - "traefik.http.routers.whoami-api.service=whoami-api-svc" - - "traefik.http.services.whoami-api-svc.loadbalancer.server.port=80" - -# ... -``` - -Apply the changes: - -```bash -docker stack deploy -c docker-compose.yml traefik -``` - -### Test the Path-Based Routing - -Verify that different paths route to different services: - -```bash -# Root path should go to the main whoami service -curl -H "Host: whoami.swarm.localhost" http://localhost/ - -# /api path should go to the whoami-api service -curl -H "Host: whoami.swarm.localhost" http://localhost/api -``` - -For the `/api` requests, you should see the response showing "API Service" in the environment variables section, confirming that your path-based routing is working correctly. - -## Enable TLS - -Let's secure our service with HTTPS by adding TLS. We'll start with a self-signed certificate for local development. - -### Create a Self-Signed Certificate - -Generate a self-signed certificate and dynamic config file to tell Traefik where the cert lives: - -```bash -mkdir -p certs - -# key + cert (valid for one year) -openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout certs/local.key -out certs/local.crt \ - -subj "/CN=*.swarm.localhost" - -# dynamic config that tells Traefik where the cert lives -cat > certs/tls.yml <<'EOF' -tls: - certificates: - - certFile: /certificates/local.crt - keyFile: /certificates/local.key -EOF -``` - -Create a Docker config for the certificate files: - -```bash -docker config create swarm-cert.crt certs/local.crt -docker config create swarm-cert.key certs/local.key -docker config create swarm-tls.yml certs/tls.yml -``` - -Update your `docker-compose.yml` file with the following changes: - -```yaml -# Add to the Traefik command section: -command: - # ... existing commands ... - - "--entryPoints.websecure.address=:443" - - "--entryPoints.websecure.http.tls=true" - - "--providers.file.directory=/etc/traefik/dynamic" -``` - -```yaml -# Add to the root of your docker-compose.yml file: -configs: - swarm-cert.crt: - file: ./certs/local.crt - swarm-cert.key: - file: ./certs/local.key - swarm-tls.yml: - file: ./certs/tls.yml -``` - -Deploy the stack: - -```bash -docker stack deploy -c docker-compose.yml traefik -``` - -Your browser can access https://whoami.swarm.localhost/ for the service. You'll need to accept the security warning for the self-signed certificate. +- Working Traefik setup from the basic guide ## Add Middlewares -Middlewares allow you to modify requests or responses as they pass through Traefik. Let's add two useful middlewares: [Headers](../reference/routing-configuration/http/middlewares/headers.md) for security and [IP allowlisting](../reference/routing-configuration/http/middlewares/ipallowlist.md) for access control. +Middlewares allow you to modify requests or responses as they pass through Traefik. Let's add two useful middlewares: [Headers](../../reference/routing-configuration/http/middlewares/headers.md) for security and [IP allowlisting](../../reference/routing-configuration/http/middlewares/ipallowlist.md) for access control. Add the following labels to your whoami service deployment section in `docker-compose.yml`: @@ -189,7 +26,7 @@ deploy: # ... existing configuration ... labels: # ... existing labels ... - + # Secure Headers Middleware - "traefik.http.middlewares.secure-headers.headers.frameDeny=true" - "traefik.http.middlewares.secure-headers.headers.sslRedirect=true" @@ -198,10 +35,10 @@ deploy: - "traefik.http.middlewares.secure-headers.headers.stsIncludeSubdomains=true" - "traefik.http.middlewares.secure-headers.headers.stsPreload=true" - "traefik.http.middlewares.secure-headers.headers.stsSeconds=31536000" - + # IP Allowlist Middleware - "traefik.http.middlewares.ip-allowlist.ipallowlist.sourceRange=127.0.0.1/32,192.168.0.0/16,10.0.0.0/8" - + # Apply the middlewares - "traefik.http.routers.whoami.middlewares=secure-headers,ip-allowlist" ``` @@ -332,7 +169,7 @@ deploy: # ... existing configuration ... labels: # ... existing labels ... - + # Sticky Sessions Configuration - "traefik.http.services.whoami.loadbalancer.sticky.cookie=true" - "traefik.http.services.whoami.loadbalancer.sticky.cookie.name=sticky_cookie" @@ -374,28 +211,195 @@ You should see different `Hostname` values in these responses, as each request i !!! important "Browser Testing" When testing in browsers, you need to use the same browser session to maintain the cookie. The cookie is set with `httpOnly` and `secure` flags for security, so it will only be sent over HTTPS connections and won't be accessible via JavaScript. -For more advanced configuration options, see the [reference documentation](../reference/routing-configuration/http/load-balancing/service.md). +For more advanced configuration options, see the [reference documentation](../../reference/routing-configuration/http/load-balancing/service.md). + +## Multi-Layer Routing + +Multi-layer routing enables hierarchical relationships between routers, where parent routers can process requests through middleware before child routers make final routing decisions. This is particularly useful for authentication-based routing or staged middleware application. + +!!! info "Provider Requirement" + Multi-layer routing requires the File provider, as Docker Swarm labels do not support the `parentRefs` field. However, you can use **both Docker Swarm and File providers together** - Swarm labels for service discovery and File configuration for multi-layer routing. + +### Setup Multi-Layer Routing with Docker Swarm + +To use multi-layer routing with Docker Swarm, you need to enable the File provider alongside the Docker provider. + +Update your Traefik service in `docker-compose.yml`: + +```yaml +services: + traefik: + image: traefik:v3.4 + command: + - "--api.dashboard=true" + - "--providers.docker.swarmMode=true" + - "--providers.docker.exposedbydefault=false" + - "--providers.docker.network=traefik_proxy" + - "--providers.file.directory=/etc/traefik/dynamic" # Enable File provider + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--entrypoints.websecure.http.tls=true" + ports: + - "80:80" + - "443:443" + - "8080:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + configs: + - source: mlr-config + target: /etc/traefik/dynamic/mlr.yml + networks: + - traefik_proxy + deploy: + placement: + constraints: + - node.role == manager + +configs: + mlr-config: + file: ./dynamic/mlr.yml + +networks: + traefik_proxy: + external: true +``` + +### Authentication-Based Routing Example + +Let's create a multi-layer routing setup where a parent router authenticates requests, and child routers direct traffic based on user roles. + +First, keep your Docker Swarm services defined with labels as usual: + +```yaml +# In docker-compose.yml +services: + # ... traefik service from above ... + + # Admin backend service + admin-backend: + image: traefik/whoami + networks: + - traefik_proxy + environment: + - WHOAMI_NAME=Admin Backend + deploy: + replicas: 2 + labels: + - "traefik.enable=true" + - "traefik.http.services.admin-backend.loadbalancer.server.port=80" + + # User backend service + user-backend: + image: traefik/whoami + networks: + - traefik_proxy + environment: + - WHOAMI_NAME=User Backend + deploy: + replicas: 2 + labels: + - "traefik.enable=true" + - "traefik.http.services.user-backend.loadbalancer.server.port=80" +``` + +Now create the multi-layer routing configuration in a file. Create `dynamic/mlr.yml`: + +```yaml +http: + routers: + # Parent router with authentication middleware + api-parent: + rule: "Host(`api.swarm.localhost`) && PathPrefix(`/api`)" + middlewares: + - auth-middleware + entryPoints: + - websecure + # Note: No service and no TLS config - this is a parent router + + # Child router for admin users + api-admin: + rule: "HeadersRegexp(`X-Auth-User`, `admin`)" + service: admin-backend@swarm # Reference Swarm service + parentRefs: + - api-parent@file # Explicit reference to parent in file provider + + # Child router for regular users + api-user: + rule: "HeadersRegexp(`X-Auth-User`, `user`)" + service: user-backend@swarm # Reference Swarm service + parentRefs: + - api-parent@file # Explicit reference to parent in file provider + + middlewares: + auth-middleware: + basicAuth: + users: + - "admin:$apr1$DmXR3Add$wfdbGw6RWIhFb0ffXMM4d0" + - "user:$apr1$GJtcIY1o$mSLdsWYeXpPHVsxGDqadI." + headerField: X-Auth-User +``` + +!!! note "Generating Password Hashes" + The password hashes above are generated using `htpasswd`. To create your own user credentials: + + ```bash + # Using htpasswd (Apache utils) + htpasswd -nb admin yourpassword + ``` + +!!! important "Cross-Provider References" + Notice the `@swarm` suffix on service names and the `@file` suffix in `parentRefs`. When using the File provider to orchestrate multi-layer routing with Swarm services: + + - Use `service-name@swarm` to reference Swarm services + - Use `parent-name@file` in `parentRefs` to reference the parent router in the File provider + + The `@provider` suffix tells Traefik which provider namespace to look in for the resource. + +Deploy the stack: + +```bash +docker stack deploy -c docker-compose.yml traefik +``` + +### Test Multi-Layer Routing + +Test the routing behavior: + +```bash +# Request goes through parent router → auth middleware → admin child router +curl -k -u admin:test -H "Host: api.swarm.localhost" https://localhost/api +``` + +You should see the response from the admin-backend service when authenticating as `admin`. Try with `user:test` credentials to reach the user-backend service instead. + +### How It Works + +1. **Request arrives** at `api.swarm.localhost/api` +2. **Parent router** (`api-parent`) matches based on host and path +3. **BasicAuth middleware** authenticates the user and sets the `X-Auth-User` header with the username +4. **Child router** (`api-admin` or `api-user`) matches based on the header value +5. **Request forwarded** to the appropriate Swarm service + +For more details about multi-layer routing, see the [Multi-Layer Routing documentation](../../reference/routing-configuration/http/routing/multi-layer-routing.md). ## Conclusion -In this guide, you've learned how to: +In this advanced guide, you've learned how to: -- Expose HTTP services through Traefik in Docker Swarm -- Set up path-based routing to direct traffic to different backend services -- Secure your services with TLS using self-signed certificates - Add security with middlewares like secure headers and IP allow listing - Automate certificate management with Let's Encrypt - Implement sticky sessions for stateful applications +- Setup multi-layer routing for authentication-based routing -These fundamental capabilities provide a solid foundation for exposing any application through Traefik Proxy in Docker Swarm. Each of these can be further customized to meet your specific requirements. +These advanced capabilities allow you to build production-ready Traefik deployments with Docker Swarm. Each of these can be further customized to meet your specific requirements. ### Next Steps -Now that you understand the basics of exposing services with Traefik Proxy, you might want to explore: +Now that you've mastered both basic and advanced Traefik features with Docker Swarm, you might want to explore: -- [Advanced routing options](../reference/routing-configuration/http/routing/rules-and-priority.md) like query parameter matching, header-based routing, and more -- [Additional middlewares](../reference/routing-configuration/http/middlewares/overview.md) for authentication, rate limiting, and request modifications -- [Observability features](../reference/install-configuration/observability/metrics.md) for monitoring and debugging your Traefik deployment -- [TCP services](../reference/routing-configuration/tcp/service.md) for exposing TCP services -- [UDP services](../reference/routing-configuration/udp/service.md) for exposing UDP services -- [Docker provider documentation](../reference/install-configuration/providers/docker.md) for more details about the Docker integration +- [Advanced routing options](../../reference/routing-configuration/http/routing/rules-and-priority.md) like query parameter matching, header-based routing, and more +- [Additional middlewares](../../reference/routing-configuration/http/middlewares/overview.md) for authentication, rate limiting, and request modifications +- [Observability features](../../reference/install-configuration/observability/metrics.md) for monitoring and debugging your Traefik deployment +- [TCP services](../../reference/routing-configuration/tcp/service.md) for exposing TCP services +- [UDP services](../../reference/routing-configuration/udp/service.md) for exposing UDP services +- [Docker provider documentation](../../reference/install-configuration/providers/docker.md) for more details about the Docker integration diff --git a/docs/content/expose/swarm/basic.md b/docs/content/expose/swarm/basic.md new file mode 100644 index 000000000..19282a90e --- /dev/null +++ b/docs/content/expose/swarm/basic.md @@ -0,0 +1,191 @@ +# Exposing Services with Traefik on Docker Swarm - Basic + +This guide will help you get started with exposing your services through Traefik Proxy using Docker Swarm. You'll learn the fundamentals of routing HTTP traffic, setting up path-based routing, and securing your services with TLS. + +## Prerequisites + +- Docker Swarm cluster initialized +- Basic understanding of Docker Swarm concepts +- Traefik deployed using the [Traefik Docker Swarm Setup guide](../../setup/swarm.md) + + +## Expose Your First HTTP Service + +Let's expose a simple HTTP service using the [whoami](https://hub.docker.com/r/traefik/whoami) application. This will demonstrate basic routing to a backend service. + +First, update your existing `docker-compose.yml` file if you haven't already: + +```yaml +services: + whoami: + image: traefik/whoami + networks: + - traefik_proxy + deploy: + replicas: 3 + labels: + - "traefik.enable=true" + - "traefik.http.routers.whoami.rule=Host(`whoami.swarm.localhost`)" + - "traefik.http.routers.whoami.entrypoints=web,websecure" +``` + +Save this as `docker-compose.yml` and deploy the stack: + +```bash +docker stack deploy -c docker-compose.yml traefik +``` + +### Verify Your Service + +Your service is now available at http://whoami.swarm.localhost/. Test that it works: + +```bash +curl -H "Host: whoami.swarm.localhost" http://localhost/ +``` + +You should see output similar to: + +```bash +Hostname: whoami.1.7c8f7tr56q3p949rscxrkp80e +IP: 127.0.0.1 +IP: ::1 +IP: 10.0.1.8 +IP: fe80::215:5dff:fe00:c9e +RemoteAddr: 10.0.1.2:45098 +GET / HTTP/1.1 +Host: whoami.swarm.localhost +User-Agent: curl/7.68.0 +Accept: */* +Accept-Encoding: gzip +X-Forwarded-For: 10.0.1.1 +X-Forwarded-Host: whoami.swarm.localhost +X-Forwarded-Port: 80 +X-Forwarded-Proto: http +X-Forwarded-Server: 5789f594e7d5 +X-Real-Ip: 10.0.1.1 +``` + +This confirms that Traefik is successfully routing requests to your whoami application. + +## Add Routing Rules + +Now we'll enhance our routing by directing traffic to different services based on [URL paths](../../reference/routing-configuration/http/routing/rules-and-priority.md#path-pathprefix-and-pathregexp). This is useful for API versioning, frontend/backend separation, or organizing microservices. + +Update your `docker-compose.yml` to add another service: + +```yaml +# ... + +# New service + whoami-api: + image: traefik/whoami + networks: + - traefik_proxy + environment: + - WHOAMI_NAME=API Service + deploy: + replicas: 2 + labels: + - "traefik.enable=true" + # Path-based routing + - "traefik.http.routers.whoami-api.rule=Host(`whoami.swarm.localhost`) && PathPrefix(`/api`)" + - "traefik.http.routers.whoami-api.entrypoints=web,websecure" + - "traefik.http.routers.whoami-api.service=whoami-api-svc" + - "traefik.http.services.whoami-api-svc.loadbalancer.server.port=80" + +# ... +``` + +Apply the changes: + +```bash +docker stack deploy -c docker-compose.yml traefik +``` + +### Test the Path-Based Routing + +Verify that different paths route to different services: + +```bash +# Root path should go to the main whoami service +curl -H "Host: whoami.swarm.localhost" http://localhost/ + +# /api path should go to the whoami-api service +curl -H "Host: whoami.swarm.localhost" http://localhost/api +``` + +For the `/api` requests, you should see the response showing "API Service" in the environment variables section, confirming that your path-based routing is working correctly. + +## Enable TLS + +Let's secure our service with HTTPS by adding TLS. We'll start with a self-signed certificate for local development. + +### Create a Self-Signed Certificate + +Generate a self-signed certificate and dynamic config file to tell Traefik where the cert lives: + +```bash +mkdir -p certs + +# key + cert (valid for one year) +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout certs/local.key -out certs/local.crt \ + -subj "/CN=*.swarm.localhost" + +# dynamic config that tells Traefik where the cert lives +cat > certs/tls.yml <<'EOF' +tls: + certificates: + - certFile: /certificates/local.crt + keyFile: /certificates/local.key +EOF +``` + +Create a Docker config for the certificate files: + +```bash +docker config create swarm-cert.crt certs/local.crt +docker config create swarm-cert.key certs/local.key +docker config create swarm-tls.yml certs/tls.yml +``` + +Update your `docker-compose.yml` file with the following changes: + +```yaml +# Add to the Traefik command section: +command: + # ... existing commands ... + - "--entryPoints.websecure.address=:443" + - "--entryPoints.websecure.http.tls=true" + - "--providers.file.directory=/etc/traefik/dynamic" +``` + +```yaml +# Add to the root of your docker-compose.yml file: +configs: + swarm-cert.crt: + file: ./certs/local.crt + swarm-cert.key: + file: ./certs/local.key + swarm-tls.yml: + file: ./certs/tls.yml +``` + +Deploy the stack: + +```bash +docker stack deploy -c docker-compose.yml traefik +``` + +Your browser can access https://whoami.swarm.localhost/ for the service. You'll need to accept the security warning for the self-signed certificate. + +## Next Steps + +Now that you've mastered the basics of exposing services with Traefik on Docker Swarm, you're ready to explore more advanced features like middlewares, Let's Encrypt certificates, sticky sessions, and multi-layer routing. + +Continue to the [Advanced Guide](advanced.md) to learn about: + +- Adding middlewares for security and access control +- Generating certificates with Let's Encrypt +- Configuring sticky sessions for stateful applications +- Setting up multi-layer routing for authentication-based routing diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index aa3b8ecec..356bc8813 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -847,14 +847,14 @@ _Optional, Default=2160_ It defaults to `2160` (90 days) to follow Let's Encrypt certificates' duration. -| Certificate Duration | Renew Period | Renew Interval | -|----------------------|-------------------|-------------------------| -| >= 1 year | 4 months | 1 week | -| >= 90 days | 30 days | 1 day | -| >= 30 days | 10 days | 12 hours | -| >= 7 days | 1 day | 1 hour | -| >= 24 hours | 6 hours | 10 min | -| < 24 hours | 20 min | 1 min | +| Certificate Duration | Renew Period | Renew Interval | +|----------------------|--------------|----------------| +| >= 1 year | 4 months | 1 week | +| >= 90 days | 30 days | 1 day | +| >= 30 days | 10 days | 12 hours | +| >= 6 days | 2 days | 2 hours | +| >= 24 hours | 6 hours | 10 min | +| < 24 hours | 20 min | 1 min | !!! warning "Traefik cannot manage certificates with a duration lower than 1 hour." diff --git a/docs/content/middlewares/http/grpcweb.md b/docs/content/middlewares/http/grpcweb.md index 35b4a3ddb..f6bac73c2 100644 --- a/docs/content/middlewares/http/grpcweb.md +++ b/docs/content/middlewares/http/grpcweb.md @@ -13,7 +13,7 @@ The GrpcWeb middleware converts gRPC Web requests to HTTP/2 gRPC requests before !!! tip Please note, that Traefik needs to communicate using gRPC with the backends (h2c or HTTP/2 over TLS). - Check out the [gRPC](../../user-guides/grpc.md) user guide for more details. + Check out [Exposing gRPC Services](../../expose/overview.md#exposing-grpc-services) for more details. ## Configuration Examples diff --git a/docs/content/migrate/v2-to-v3-details.md b/docs/content/migrate/v2-to-v3-details.md index 257ee0f5f..b78ca8223 100644 --- a/docs/content/migrate/v2-to-v3-details.md +++ b/docs/content/migrate/v2-to-v3-details.md @@ -636,6 +636,10 @@ The `Headers` and `HeadersRegexp` matchers have been renamed to `Header` and `He `PathPrefix` no longer uses regular expressions to match path prefixes. +`Path` and `PathPrefix` no longer support path parameter placeholders (e.g., `{id}`, `{name}`). +Routes using placeholders like ``Path(`/route/{id}`)`` will not match in v3 syntax. +Use `PathRegexp` instead for dynamic path segments. + `QueryRegexp` has been introduced to match query values using a regular expression. `HeaderRegexp`, `HostRegexp`, `PathRegexp`, `QueryRegexp`, and `HostSNIRegexp` matchers now uses the [Go regexp syntax](https://golang.org/pkg/regexp/syntax/). @@ -716,6 +720,40 @@ http: ruleSyntax = "v2" ``` +##### Migrate Path Placeholders to PathRegexp + +In v2, `Path` and `PathPrefix` supported path parameter placeholders like `{id}` for matching dynamic path segments. +In v3, this is no longer supported and `PathRegexp` should be used instead. + +??? example "Migrating a route with path placeholders" + + v2 syntax (no longer works in v3): + + ```yaml + match: Host(`example.com`) && Path(`/products/{id}`) + ``` + + v3 syntax using `PathRegexp`: + + ```yaml + match: Host(`example.com`) && PathRegexp(`^/products/[^/]+$`) + ``` + + For more complex patterns with multiple placeholders: + + v2 syntax: + + ```yaml + match: Host(`example.com`) && Path(`/users/{userId}/orders/{orderId}`) + ``` + + v3 syntax: + + ```yaml + match: Host(`example.com`) && PathRegexp(`^/users/[^/]+/orders/[^/]+$`) ## matches any non-slash characters + match: Host(`example.com`) && PathRegexp(`^/users/[a-zA-Z0-9_-]+/orders/[a-zA-Z0-9_-]+$`) ## restricts to alphanumeric, hyphens, and underscores + ``` + ### IPWhiteList In v3, we renamed the `IPWhiteList` middleware to `IPAllowList` without changing anything to the configuration. diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md index 68b2bf269..c52d8aab0 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -568,12 +568,12 @@ Here is the list of the encoded characters that are rejected by default, along w | Encoded Character | Character | Config option to allow the encoded character | |-------------------|-------------------------|--------------------------------------------------------------------------------------| | `%2f` or `%2F` | `/` (slash) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedSlash` | -| `%5c` or `%5C` | `\` (backslash) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedBackSlash` | -| `%00` | `NULL` (null character) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedNullCharacter` | -| `%3b` or `%3B` | `;` (semicolon) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedSemicolon` | -| `%25` | `%` (percent) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedPercent` | -| `%3f` or `%3F` | `?` (question mark) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedQuestionMark` | -| `%23` | `#` (hash) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedHash` | +| `%5c` or `%5C` | `\` (backslash) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedBackSlash` | +| `%00` | `NULL` (null character) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedNullCharacter` | +| `%3b` or `%3B` | `;` (semicolon) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedSemicolon` | +| `%25` | `%` (percent) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedPercent` | +| `%3f` or `%3F` | `?` (question mark) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedQuestionMark` | +| `%23` | `#` (hash) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedHash` | Please check out the entrypoint [encodedCharacters option](../reference/install-configuration/entrypoints.md#opt-http-encodedCharacters) documentation for more details. @@ -590,12 +590,12 @@ Here is the list of the encoded characters that can be configured to `false` to | Encoded Character | Character | Config options | Default value | |-------------------|-------------------------|--------------------------------------------------------------------------------------|---------------| | `%2f` or `%2F` | `/` (slash) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedSlash` | `true` | -| `%5c` or `%5C` | `\` (backslash) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedBackSlash` | `true` | -| `%00` | `NULL` (null character) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedNullCharacter` | `true` | -| `%3b` or `%3B` | `;` (semicolon) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedSemicolon` | `true` | -| `%25` | `%` (percent) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedPercent` | `true` | -| `%3f` or `%3F` | `?` (question mark) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedQuestionMark` | `true` | -| `%23` | `#` (hash) | `entryPoints..`
`.http.encodedCharacters`
`.allowEncodedHash` | `true` | +| `%5c` or `%5C` | `\` (backslash) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedBackSlash` | `true` | +| `%00` | `NULL` (null character) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedNullCharacter` | `true` | +| `%3b` or `%3B` | `;` (semicolon) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedSemicolon` | `true` | +| `%25` | `%` (percent) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedPercent` | `true` | +| `%3f` or `%3F` | `?` (question mark) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedQuestionMark` | `true` | +| `%23` | `#` (hash) | `entryPoints.`
`.http.encodedCharacters`
`.allowEncodedHash` | `true` | Note: This check is not done against query parameters, but only against the request path as defined diff --git a/docs/content/observability/metrics/influxdb2.md b/docs/content/observability/metrics/influxdb2.md index a5ab61877..22a139746 100644 --- a/docs/content/observability/metrics/influxdb2.md +++ b/docs/content/observability/metrics/influxdb2.md @@ -42,7 +42,7 @@ metrics: _Required, Default=""_ -Token with which to connect to InfluxDB v2. +Token with which to connect to InfluxDB v2. It accepts either a token value or a file path to the token. ```yaml tab="File (YAML)" metrics: diff --git a/docs/content/providers/kubernetes-crd.md b/docs/content/providers/kubernetes-crd.md index 18f56f039..1b4276503 100644 --- a/docs/content/providers/kubernetes-crd.md +++ b/docs/content/providers/kubernetes-crd.md @@ -363,6 +363,6 @@ providers: ## Full Example -For additional information, refer to the [full example](../user-guides/crd-acme/index.md) with Let's Encrypt. +For additional information on exposing services with Kubernetes, refer to the [Kubernetes guide](../expose/kubernetes/basic.md). {% include-markdown "includes/traefik-for-business-applications.md" %} 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 d392b67a5..33e15bef5 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -222,6 +222,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of + the referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -1232,6 +1251,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references to + Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -2974,6 +3012,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -3211,6 +3268,24 @@ spec: Default value is -1, which means unlimited size. format: int64 type: integer + middlewares: + description: Middlewares defines the list of references to Middleware + resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware resource. + properties: + name: + description: Name defines the name of the referenced Middleware + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Middleware resource. + type: string + required: + - name + type: object + type: array mirrorBody: description: |- MirrorBody defines whether the body of the request should be mirrored. @@ -3298,6 +3373,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -3686,6 +3780,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. diff --git a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml index 87fbdb544..d6d433f94 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml @@ -223,6 +223,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of + the referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index 7ca937bb1..4ce03061f 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -387,6 +387,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references to + Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. diff --git a/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml b/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml index a51ddc0a4..11f302e71 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml @@ -131,6 +131,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -368,6 +387,24 @@ spec: Default value is -1, which means unlimited size. format: int64 type: integer + middlewares: + description: Middlewares defines the list of references to Middleware + resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware resource. + properties: + name: + description: Name defines the name of the referenced Middleware + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Middleware resource. + type: string + required: + - name + type: object + type: array mirrorBody: description: |- MirrorBody defines whether the body of the request should be mirrored. @@ -455,6 +492,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -843,6 +899,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. diff --git a/docs/content/reference/install-configuration/configuration-options.md b/docs/content/reference/install-configuration/configuration-options.md index 400ca6068..f79962938 100644 --- a/docs/content/reference/install-configuration/configuration-options.md +++ b/docs/content/reference/install-configuration/configuration-options.md @@ -202,7 +202,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | metrics.influxdb2.bucket | InfluxDB v2 bucket ID. | | | metrics.influxdb2.org | InfluxDB v2 org ID. | | | metrics.influxdb2.pushinterval | InfluxDB v2 push interval. | 10 | -| metrics.influxdb2.token | InfluxDB v2 access token. | | +| metrics.influxdb2.token | InfluxDB v2 access token. It accepts either a token value or a file path to the token. | | | metrics.otlp | OpenTelemetry metrics exporter type. | false | | metrics.otlp.addentrypointslabels | Enable metrics on entry points. | true | | metrics.otlp.addrouterslabels | Enable metrics on routers. | false | @@ -399,6 +399,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | providers.kubernetesingressnginx.endpoint | Kubernetes server endpoint (required for external cluster client). | | | providers.kubernetesingressnginx.ingressclass | Name of the ingress class this controller satisfies. | nginx | | providers.kubernetesingressnginx.ingressclassbyname | Define if Ingress Controller should watch for Ingress Class by Name together with Controller Class. | false | +| providers.kubernetesingressnginx.proxyconnecttimeout | Amount of time to wait until a connection to a server can be established. Timeout value is unitless and in seconds. | 60 | | providers.kubernetesingressnginx.publishservice | Service fronting the Ingress controller. Takes the form 'namespace/name'. | | | providers.kubernetesingressnginx.publishstatusaddress | Customized address (or addresses, separated by comma) to set as the load-balancer status of Ingress objects this controller satisfies. | | | providers.kubernetesingressnginx.throttleduration | Ingress refresh throttle duration. | 0 | diff --git a/docs/content/user-guides/fastproxy.md b/docs/content/reference/install-configuration/experimental/fastproxy.md similarity index 61% rename from docs/content/user-guides/fastproxy.md rename to docs/content/reference/install-configuration/experimental/fastproxy.md index 3294f9dc4..20d3d1fa9 100644 --- a/docs/content/user-guides/fastproxy.md +++ b/docs/content/reference/install-configuration/experimental/fastproxy.md @@ -1,19 +1,18 @@ --- title: "Traefik FastProxy Experimental Configuration" -description: "This section of the Traefik Proxy documentation explains how to use the new FastProxy option." +description: "This section of the Traefik Proxy documentation explains how to use the new FastProxy install configuration option." --- # Traefik FastProxy Experimental Configuration ## Overview -This guide provides instructions on how to configure and use the new experimental `fastProxy` static configuration option in Traefik. -The `fastProxy` option introduces a high-performance reverse proxy designed to enhance the performance of routing. +This guide provides instructions on how to configure and use the new experimental `fastProxy` install configuration option in Traefik. The `fastProxy` option introduces a high-performance reverse proxy designed to enhance the performance of routing. !!! info "Limitations" Please note that the new fast proxy implementation does not work with HTTP/2. - This means that when a H2C or HTTPS request with [HTTP2 enabled](../routing/services/index.md#disablehttp2) is sent to a backend, the fallback proxy is the regular one. + This means that when a H2C or HTTPS request with [HTTP2 enabled](../../routing-configuration/http/load-balancing/service.md#disablehttp2) is sent to a backend, the fallback proxy is the regular one. Additionnaly, observability features like tracing and OTEL semconv metrics are not supported for the moment. @@ -22,10 +21,10 @@ The `fastProxy` option introduces a high-performance reverse proxy designed to e The `fastProxy` option is currently experimental and subject to change in future releases. Use with caution in production environments. -### Enabling FastProxy +## Enabling FastProxy -The fastProxy option is a static configuration parameter. -To enable it, you need to configure it in your Traefik static configuration +The fastProxy option is an install configuration parameter. +To enable it, you need to configure it in your Traefik install configuration ```yaml tab="File (YAML)" experimental: diff --git a/docs/content/reference/install-configuration/experimental/plugins.md b/docs/content/reference/install-configuration/experimental/plugins.md new file mode 100644 index 000000000..ad2581516 --- /dev/null +++ b/docs/content/reference/install-configuration/experimental/plugins.md @@ -0,0 +1,43 @@ +--- +title: "Traefik Plugins Experimental Configuration" +description: "This section of the Traefik Proxy documentation explains how to use the new Plugins install configuration option." +--- + +# Traefik Plugins Experimental Configuration + +## Overview + +This guide provides instructions on how to configure and use the new experimental `plugins` install configuration option in Traefik. The `plugins` option introduces a system to extend Traefik capabilities with custom middlewares and providers. + +!!! warning "Experimental" + + The `plugins` option is currently experimental and subject to change in future releases. + Use with caution in production environments. + +## Enabling Plugins + +The plugins option is an install configuration parameter. +To enable a plugin, you need to define it in your Traefik install configuration + +```yaml tab="File (YAML)" +experimental: + plugins: + plugin-name: # The name of the plugin in the routing configuration + moduleName: "github.com/github-organization/github-repository" # The plugin module name + version: "vX.XX.X" # The version to use +``` + +```toml tab="File (TOML)" +[experimental.plugins.plugin-name] + moduleName = "github.com/github-organization/github-repository" # The plugin module name + version = "vX.XX.X" # The version to use +``` + +```bash tab="CLI" +# The plugin module name +# With plugin-name the name of the plugin in the routing configuration +--experimental.plugins.plugin-name.modulename=github.com/github-organization/github-repository +--experimental.plugins.plugin-name.version=vX.XX.X # The version to use +``` + +To learn more about how to add a new plugin to a Traefik instance, please refer to the [developer documentation](https://plugins.traefik.io/install). diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md index 973a71eb3..5c85e5f09 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-crd.md @@ -128,6 +128,6 @@ See the dedicated section in [routing](../../../../routing/providers/kubernetes- ## Full Example -For additional information, refer to the [full example](../../../../user-guides/crd-acme/index.md) with Let's Encrypt. +For additional information on exposing services with Kubernetes, refer to the [Kubernetes guide](../../../../expose/kubernetes/basic.md). {% include-markdown "includes/traefik-for-business-applications.md" %} diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress-nginx.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress-nginx.md index aa42c8acb..0524f4d2b 100644 --- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress-nginx.md +++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress-nginx.md @@ -114,23 +114,24 @@ This provider watches for incoming Ingress events and automatically translates N ## Configuration Options -| Field | Description | Default | Required | -|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------|:--------|:---------| -| `providers.providers`
`ThrottleDuration`
| Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.
If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.
**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No | -| `providers.`
`kubernetesIngressNGINX.`
`endpoint`
| Server endpoint URL.
More information [here](#endpoint). | "" | No | -| `providers.`
`kubernetesIngressNGINX.`
`token`
| Bearer token used for the Kubernetes client configuration. | "" | No | -| `providers.`
`kubernetesIngressNGINX.`
`certAuthFilePath`
| Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No | -| `providers.`
`kubernetesIngressNGINX.`
`throttleDuration`
| Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No | -| `providers.`
`kubernetesIngressNGINX.`
`watchNamespace`
| Namespace the controller watches for updates to Kubernetes objects. All namespaces are watched if this parameter is left empty. | "" | No | -| `providers.`
`kubernetesIngressNGINX.`
`watchNamespaceSelector`
| Selector selects namespaces the controller watches for updates to Kubernetes objects. | "" | No | -| `providers.`
`kubernetesIngressNGINX.`
`ingressClass`
| Name of the ingress class this controller satisfies. | "nginx" | No | -| `providers.`
`kubernetesIngressNGINX.`
`controllerClass`
| Ingress Class Controller value this controller satisfies. | "" | No | -| `providers.`
`kubernetesIngressNGINX.`
`watchIngressWithoutClass`
| Define if Ingress Controller should also watch for Ingresses without an IngressClass or the annotation specified. | false | No | -| `providers.`
`kubernetesIngressNGINX.`
`ingressClassByName`
| Define if Ingress Controller should watch for Ingress Class by Name together with Controller Class. | false | No | -| `providers.`
`kubernetesIngressNGINX.`
`publishService`
| Service fronting the Ingress controller. Takes the form `namespace/name`. | "" | No | -| `providers.`
`kubernetesIngressNGINX.`
`publishStatusAddress`
| Customized address (or addresses, separated by comma) to set as the load-balancer status of Ingress objects this controller satisfies. | "" | No | -| `providers.`
`kubernetesIngressNGINX.`
`defaultBackendService`
| Service used to serve HTTP requests not matching any known server name (catch-all). Takes the form 'namespace/name'. | "" | No | -| `providers.`
`kubernetesIngressNGINX.`
`disableSvcExternalName`
| Disable support for Services of type ExternalName. | false | No | +| Field | Description | Default | Required | +|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------| +| `providers.providers`
`ThrottleDuration`
| Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.
If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.
**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No | +| `providers.`
`kubernetesIngressNGINX.`
`endpoint`
| Server endpoint URL.
More information [here](#endpoint). | "" | No | +| `providers.`
`kubernetesIngressNGINX.`
`token`
| Bearer token used for the Kubernetes client configuration. | "" | No | +| `providers.`
`kubernetesIngressNGINX.`
`certAuthFilePath`
| Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No | +| `providers.`
`kubernetesIngressNGINX.`
`throttleDuration`
| Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No | +| `providers.`
`kubernetesIngressNGINX.`
`watchNamespace`
| Namespace the controller watches for updates to Kubernetes objects. All namespaces are watched if this parameter is left empty. | "" | No | +| `providers.`
`kubernetesIngressNGINX.`
`watchNamespaceSelector`
| Selector selects namespaces the controller watches for updates to Kubernetes objects. | "" | No | +| `providers.`
`kubernetesIngressNGINX.`
`ingressClass`
| Name of the ingress class this controller satisfies. | "nginx" | No | +| `providers.`
`kubernetesIngressNGINX.`
`controllerClass`
| Ingress Class Controller value this controller satisfies. | "" | No | +| `providers.`
`kubernetesIngressNGINX.`
`watchIngressWithoutClass`
| Define if Ingress Controller should also watch for Ingresses without an IngressClass or the annotation specified. | false | No | +| `providers.`
`kubernetesIngressNGINX.`
`ingressClassByName`
| Define if Ingress Controller should watch for Ingress Class by Name together with Controller Class. | false | No | +| `providers.`
`kubernetesIngressNGINX.`
`publishService`
| Service fronting the Ingress controller. Takes the form `namespace/name`. | "" | No | +| `providers.`
`kubernetesIngressNGINX.`
`publishStatusAddress`
| Customized address (or addresses, separated by comma) to set as the load-balancer status of Ingress objects this controller satisfies. | "" | No | +| `providers.`
`kubernetesIngressNGINX.`
`defaultBackendService`
| Service used to serve HTTP requests not matching any known server name (catch-all). Takes the form 'namespace/name'. | "" | No | +| `providers.`
`kubernetesIngressNGINX.`
`disableSvcExternalName`
| Disable support for Services of type ExternalName. | false | No | +| `providers.`
`kubernetesIngressNGINX.`
`proxyConnectTimeout`
| Amount of time to wait until a connection to a server can be established. The value is unitless and in seconds. This is used as the global connection timeout when no ingress-specific timeout is configured. An ingress-specific timeout can be configured using [`nginx.ingress.kubernetes.io/proxy-connect-timeout`](../../../../routing-configuration/kubernetes/ingress-nginx/#opt-nginx-ingress-kubernetes-ioproxy-connect-timeout) annotation. | 60 | No | diff --git a/docs/content/reference/routing-configuration/http/load-balancing/service.md b/docs/content/reference/routing-configuration/http/load-balancing/service.md index 7b009e2e3..7039ac3db 100644 --- a/docs/content/reference/routing-configuration/http/load-balancing/service.md +++ b/docs/content/reference/routing-configuration/http/load-balancing/service.md @@ -4,14 +4,23 @@ description: "A service is in charge of connecting incoming requests to the Serv --- Traefik services define how to distribute incoming traffic across your backend servers. -Each service implements one of the load balancing strategies detailed on this page to ensure optimal traffic distribution and high availability. +This page covers two main concepts: + +- **Service Load Balancer**: Routes traffic to backend servers using various load balancing strategies +- **Advanced Service Types**: Compose multiple services together for weighted distribution, mirroring, or failover ## Service Load Balancer -The load balancers are able to load balance the requests between multiple instances of your programs. - +The `loadBalancer` service type routes incoming requests to a list of backend servers. Each service has a load-balancer, even if there is only one server to forward traffic to. +The load balancer supports multiple **strategies** for distributing traffic among servers: + +- `wrr` (Weighted Round Robin) - Default strategy, distributes requests evenly across servers in rotation +- `p2c` (Power of Two Choices) - Selects two random servers and routes to the one with fewer active connections +- `hrw` (Highest Random Weight) - Uses consistent hashing based on client IP for session affinity +- `leasttime` - Routes to the server with lowest response time combined with fewest active connections + ### Configuration Example ```yaml tab="Structured (YAML)" @@ -19,6 +28,7 @@ http: services: my-service: loadBalancer: + strategy: "wrr" servers: - url: "http://private-ip-server-1/" weight: 2 @@ -42,6 +52,7 @@ http: ```toml tab="Structured (TOML)" [http.services] [http.services.my-service.loadBalancer] + strategy = "wrr" [[http.services.my-service.loadBalancer.servers]] url = "http://private-ip-server-1/" @@ -66,6 +77,7 @@ http: ```yaml tab="Labels" labels: + - "traefik.http.services.my-service.loadBalancer.strategy=wrr" - "traefik.http.services.my-service.loadBalancer.servers[0].url=http://private-ip-server-1/" - "traefik.http.services.my-service.loadBalancer.servers[0].weight=2" - "traefik.http.services.my-service.loadBalancer.servers[0].preservePath=true" @@ -83,6 +95,7 @@ labels: ```json tab="Tags" { "Tags": [ + "traefik.http.services.my-service.loadBalancer.strategy=wrr", "traefik.http.services.my-service.loadBalancer.servers[0].url=http://private-ip-server-1/", "traefik.http.services.my-service.loadBalancer.servers[0].weight=2", "traefik.http.services.my-service.loadBalancer.servers[0].preservePath=true", @@ -104,6 +117,7 @@ labels: | Field | Description | Required | |------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------| | `servers` | Represents individual backend instances for your service | Yes | +| `strategy` | Load balancing strategy for distributing traffic among servers. Valid values: `wrr` (default), `p2c`, `hrw`, `leasttime`. | No | | `sticky` | Defines a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response. | No | | `healthcheck` | Configures health check to remove unhealthy servers from the load balancing rotation. | No | | `passiveHealthcheck` | Configures the passive health check to remove unhealthy servers from the load balancing rotation. | No | @@ -124,7 +138,151 @@ Servers represent individual backend instances for your service. The [service lo | `weight` | Allows for weighted load balancing on the servers. | No | | `preservePath` | Allows to preserve the URL path. | No | -#### Health Check +### Load Balancing Strategies + +The `strategy` option on the load balancer determines how traffic is distributed among the backend servers. + +#### Weighted Round Robin (wrr) + +The default strategy. Distributes requests evenly across all servers in rotation, respecting server weights. +This strategy uses Earliest Deadline First (EDF) scheduling to provide weighted round-robin behavior. + +??? example "WRR Load Balancing -- Using the [File Provider](../../../install-configuration/providers/others/file.md)" + + ```yaml tab="Structured (YAML)" + ## Routing configuration + http: + services: + my-service: + loadBalancer: + strategy: "wrr" + servers: + - url: "http://private-ip-server-1/" + weight: 3 + - url: "http://private-ip-server-2/" + weight: 1 + ``` + + ```toml tab="Structured (TOML)" + ## Routing configuration + [http.services] + [http.services.my-service.loadBalancer] + strategy = "wrr" + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-1/" + weight = 3 + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-2/" + weight = 1 + ``` + +#### Power of Two Choices (p2c) + +Selects two servers at random and routes the request to the one with the fewest active connections. +This algorithm provides better load distribution when servers have varying response times. + +??? example "P2C Load Balancing -- Using the [File Provider](../../../install-configuration/providers/others/file.md)" + + ```yaml tab="Structured (YAML)" + ## Routing configuration + http: + services: + my-service: + loadBalancer: + strategy: "p2c" + servers: + - url: "http://private-ip-server-1/" + - url: "http://private-ip-server-2/" + - url: "http://private-ip-server-3/" + ``` + + ```toml tab="Structured (TOML)" + ## Routing configuration + [http.services] + [http.services.my-service.loadBalancer] + strategy = "p2c" + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-1/" + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-2/" + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-3/" + ``` + +#### Highest Random Weight (hrw) + +Uses consistent hashing (Rendezvous Hashing) based on the client's IP address to ensure requests from the same client are consistently routed to the same server. +This provides session affinity without requiring sticky cookies. + +The algorithm computes a score for each available backend using a hash of the client's source IP combined with the backend's identifier, and assigns the client to the backend with the highest score. + +??? example "HRW Load Balancing -- Using the [File Provider](../../../install-configuration/providers/others/file.md)" + + ```yaml tab="Structured (YAML)" + ## Routing configuration + http: + services: + my-service: + loadBalancer: + strategy: "hrw" + servers: + - url: "http://private-ip-server-1/" + - url: "http://private-ip-server-2/" + - url: "http://private-ip-server-3/" + ``` + + ```toml tab="Structured (TOML)" + ## Routing configuration + [http.services] + [http.services.my-service.loadBalancer] + strategy = "hrw" + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-1/" + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-2/" + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-3/" + ``` + +#### Least-Time + +Selects the server with the lowest average response time (Time To First Byte - TTFB), combined with the fewest active connections, weighted by server capacity. +This strategy is ideal for heterogeneous backend environments where servers have varying performance characteristics, different hardware capabilities, or varying network latency. + +The algorithm continuously measures each backend's response time and tracks active connection counts. +When routing a request, it calculates a score for each healthy server using the formula: `(avg_response_time × (1 + active_connections)) / weight`. +The server with the lowest score receives the request. +When multiple servers have identical scores, Weighted Round Robin (WRR) with Earliest Deadline First (EDF) scheduling is used as a tie-breaker. + +??? example "Least-Time Load Balancing -- Using the [File Provider](../../../install-configuration/providers/others/file.md)" + + ```yaml tab="Structured (YAML)" + ## Routing configuration + http: + services: + my-service: + loadBalancer: + strategy: "leasttime" + servers: + - url: "http://private-ip-server-1/" + - url: "http://private-ip-server-2/" + - url: "http://private-ip-server-3/" + ``` + + ```toml tab="Structured (TOML)" + ## Routing configuration + [http.services] + [http.services.my-service.loadBalancer] + strategy = "leasttime" + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-1/" + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-2/" + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-3/" + ``` + +### Health Check The `healthcheck` option configures health check to remove unhealthy servers from the load balancing rotation. Traefik will consider HTTP(s) servers healthy as long as they return a status code to the health check request (carried out every interval) between `2XX` and `3XX`, or matching the configured status. @@ -146,42 +304,41 @@ Below are the available options for the health check mechanism: | `timeout` | Defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. | 5s | No | | `headers` | Defines custom headers to be sent to the health check endpoint. | | No | | `followRedirects` | Defines whether redirects should be followed during the health check calls. | true | No | -| `hostname` | Defines the value of hostname in the Host header of the health check request. | "" | No | | `method` | Defines the HTTP method that will be used while connecting to the endpoint. | GET | No | | `status` | Defines the expected HTTP status code of the response to the health check request. | | No | -#### Sticky sessions +### Sticky Sessions When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response. On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set. -##### Stickiness on multiple levels +#### Stickiness on multiple levels When chaining or mixing load-balancers (e.g. a load-balancer of servers is one of the "children" of a load-balancer of services), for stickiness to work all the way, the option needs to be specified at all required levels. Which means the client needs to send a cookie with as many key/value pairs as there are sticky levels. -##### Stickiness & Unhealthy Servers +#### Stickiness & Unhealthy Servers If the server specified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server). -##### Cookie Name +#### Cookie Name The default cookie name is an abbreviation of a sha1 (ex: `_1d52e`). -##### MaxAge +#### MaxAge By default, the affinity cookie will never expire as the `MaxAge` option is set to zero. This option indicates the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. -##### Secure & HTTPOnly & SameSite flags +#### Secure & HTTPOnly & SameSite flags By default, the affinity cookie is created without those flags. One however can change that through configuration. `SameSite` can be `none`, `lax`, `strict` or empty. -##### Domain +#### Domain The Domain attribute of a cookie specifies the domain for which the cookie is valid. @@ -190,7 +347,7 @@ By setting the Domain attribute, the cookie can be shared across subdomains (for ??? example "Adding Stickiness -- Using the [File Provider](../../../install-configuration/providers/others/file.md)" ```yaml tab="Structured (YAML)" - ## Dynamic configuration + ## Routing configuration http: services: my-service: @@ -200,7 +357,7 @@ By setting the Domain attribute, the cookie can be shared across subdomains (for ``` ```toml tab="Structured (TOML)" - ## Dynamic configuration + ## Routing configuration [http.services] [http.services.my-service] [http.services.my-service.loadBalancer.sticky.cookie] @@ -209,7 +366,7 @@ By setting the Domain attribute, the cookie can be shared across subdomains (for ??? example "Adding Stickiness with custom Options -- Using the [File Provider](../../../install-configuration/providers/others/file.md)" ```yaml tab="Structured (YAML)" - ## Dynamic configuration + ## Routing configuration http: services: my-service: @@ -223,7 +380,7 @@ By setting the Domain attribute, the cookie can be shared across subdomains (for ``` ```toml tab="Structured (TOML)" - ## Dynamic configuration + ## Routing configuration [http.services] [http.services.my-service] [http.services.my-service.loadBalancer.sticky.cookie] @@ -237,7 +394,7 @@ By setting the Domain attribute, the cookie can be shared across subdomains (for ??? example "Setting Stickiness on all the required levels -- Using the [File Provider](../../../install-configuration/providers/others/file.md)" ```yaml tab="Structured (YAML)" - ## Dynamic configuration + ## Routing configuration http: services: wrr1: @@ -271,7 +428,7 @@ By setting the Domain attribute, the cookie can be shared across subdomains (for ``` ```toml tab="Structured (TOML)" - ## Dynamic configuration + ## Routing configuration [http.services] [http.services.wrr1] [http.services.wrr1.weighted.sticky.cookie] @@ -308,7 +465,7 @@ To keep a session open with the same server, the client would then need to speci curl -b "lvl1=whoami1; lvl2=http://127.0.0.1:8081" http://localhost:8000 ``` -#### Passive Health Check +### Passive Health Check The `passiveHealthcheck` option configures passive health check to remove unhealthy servers from the load balancing rotation. @@ -326,18 +483,27 @@ Below are the available options for the passive health check mechanism: | `failureWindow` | Defines the time window during which the failed attempts must occur for the server to be marked as unhealthy. It also defines for how long the server will be considered unhealthy. | 10s | No | | `maxFailedAttempts` | Defines the number of consecutive failed attempts allowed within the failure window before marking the server as unhealthy. | 1 | No | -## Weighted Round Robin (WRR) +## Advanced Service Types -The WRR is able to load balance the requests between multiple services based on weights. +Advanced service types allow you to compose multiple services together for weighted distribution, consistent hashing, mirroring, or failover scenarios. +These are distinct from load balancing strategies - they operate at the **service level** rather than the **server level**. -This strategy is only available to load balance between services and not between servers. +!!! info "Key Difference" + + - **Load Balancing Strategies** (wrr, p2c, hrw, leasttime): Distribute traffic among **servers** within a single `loadBalancer` service + - **Advanced Service Types** (weighted, highestRandomWeight, mirroring, failover): Distribute or manage traffic among multiple **services** + +### Weighted Round robin + +The `weighted` service type load balances requests between multiple services based on weights. +This is different from the `wrr` strategy - it operates on services, not servers. !!! info "Supported Providers" - This strategy can be defined currently with the [File](../../../install-configuration/providers/others/file.md) or [IngressRoute](../../../install-configuration/providers/kubernetes/kubernetes-crd.md) providers. To load balance between servers based on weights, the Load Balancer service should be used instead. + This service type can be defined currently with the [File](../../../install-configuration/providers/others/file.md) provider or [IngressRoute](../../../routing-configuration/kubernetes/crd/http/ingressroute.md). ```yaml tab="Structured (YAML)" -## Dynamic configuration +## Routing configuration http: services: app: @@ -360,7 +526,7 @@ http: ``` ```toml tab="Structured (TOML)" -## Dynamic configuration +## Routing configuration [http.services] [http.services.app] [[http.services.app.weighted.services]] @@ -381,7 +547,7 @@ http: url = "http://private-ip-server-2/" ``` -### Health Check +#### Health Check HealthCheck enables automatic self-healthcheck for this service, i.e. whenever one of its children is reported as down, this service becomes aware of it, and takes it into account (i.e. it ignores the down child) when running the load-balancing algorithm. In addition, if the parent of this service also has HealthCheck enabled, this service reports to its parent any status change. @@ -392,7 +558,7 @@ HealthCheck enables automatic self-healthcheck for this service, i.e. whenever o HealthCheck on Weighted services can be defined currently only with the [File provider](../../../install-configuration/providers/others/file.md). ```yaml tab="Structured (YAML)" -## Dynamic configuration +## Routing configuration http: services: app: @@ -424,7 +590,7 @@ http: ``` ```toml tab="Structured (TOML)" -## Dynamic configuration +## Routing configuration [http.services] [http.services.app] [http.services.app.weighted.healthCheck] @@ -454,83 +620,138 @@ http: url = "http://private-ip-server-2/" ``` -## P2C +### Highest Random Weight -Power of two choices algorithm is a load balancing strategy that selects two servers at random and chooses the one with the least number of active requests. +The `highestRandomWeight` service type uses consistent hashing (Rendezvous Hashing) to load balance requests between multiple services. +This ensures that requests from the same client IP are consistently routed to the same service. -??? example "P2C Load Balancing -- Using the [File Provider](../../../install-configuration/providers/others/file.md)" +This is different from the `hrw` strategy on a loadBalancer - it operates on **services**, not servers. - ```yaml tab="Structured (YAML)" - ## Dynamic configuration - http: - services: - my-service: - loadBalancer: - strategy: "p2c" - servers: - - url: "http://private-ip-server-1/" - - url: "http://private-ip-server-2/" - - url: "http://private-ip-server-3/" - ``` +!!! info "Supported Providers" - ```toml tab="Structured (TOML) " - ## Dynamic configuration - [http.services] - [http.services.my-service.loadBalancer] - strategy = "p2c" - [[http.services.my-service.loadBalancer.servers]] - url = "http://private-ip-server-1/" - [[http.services.my-service.loadBalancer.servers]] - url = "http://private-ip-server-2/" - [[http.services.my-service.loadBalancer.servers]] - url = "http://private-ip-server-3/" - ``` + This service type can be defined currently only with the [File](../../../install-configuration/providers/others/file.md) provider. -## Least-Time +```yaml tab="Structured (YAML)" +## Routing configuration +http: + services: + app: + highestRandomWeight: + services: + - name: appv1 + weight: 1 + - name: appv2 + weight: 1 -The Least-Time load balancing algorithm selects the server with the lowest average response time (Time To First Byte - TTFB), -combined with the fewest active connections, weighted by server capacity. -This strategy is ideal for heterogeneous backend environments where servers have varying performance characteristics, -different hardware capabilities, or varying network latency. + appv1: + loadBalancer: + servers: + - url: "http://private-ip-server-1/" -The algorithm continuously measures each backend's response time and tracks active connection counts. -When routing a request, -it calculates a score for each healthy server using the formula: `(avg_response_time × (1 + active_connections)) / weight`. -The server with the lowest score receives the request. -When multiple servers have identical scores, -Weighted Round Robin (WRR) with Earliest Deadline First (EDF) scheduling is used as a tie-breaker to ensure fair distribution. + appv2: + loadBalancer: + servers: + - url: "http://private-ip-server-2/" +``` -??? example "Basic Least-Time Load Balancing -- Using the [File Provider](../../../install-configuration/providers/others/file.md)" +```toml tab="Structured (TOML)" +## Routing configuration +[http.services] + [http.services.app] + [[http.services.app.highestRandomWeight.services]] + name = "appv1" + weight = 1 + [[http.services.app.highestRandomWeight.services]] + name = "appv2" + weight = 1 - ```yaml tab="Structured (YAML)" - ## Dynamic configuration - http: - services: - my-service: - loadBalancer: - strategy: "leasttime" - servers: - - url: "http://private-ip-server-1/" - - url: "http://private-ip-server-2/" - - url: "http://private-ip-server-3/" - ``` + [http.services.appv1] + [http.services.appv1.loadBalancer] + [[http.services.appv1.loadBalancer.servers]] + url = "http://private-ip-server-1/" - ```toml tab="Structured (TOML)" - ## Dynamic configuration - [http.services] - [http.services.my-service.loadBalancer] - strategy = "leasttime" - [[http.services.my-service.loadBalancer.servers]] - url = "http://private-ip-server-1/" - [[http.services.my-service.loadBalancer.servers]] - url = "http://private-ip-server-2/" - [[http.services.my-service.loadBalancer.servers]] - url = "http://private-ip-server-3/" - ``` + [http.services.appv2] + [http.services.appv2.loadBalancer] + [[http.services.appv2.loadBalancer.servers]] + url = "http://private-ip-server-2/" +``` -## Mirroring +#### Health Check -The mirroring is able to mirror requests sent to a service to other services. Please note that by default the whole request is buffered in memory while it is being mirrored. See the `maxBodySize` option in the example below for how to modify this behaviour. You can also omit the request body by setting the `mirrorBody` option to false. +HealthCheck enables automatic self-healthcheck for this service, similar to the Weighted Round Robin service type. + +!!! note "Behavior" + + If HealthCheck is enabled for a given service and any of its descendants does not have it enabled, the creation of the service will fail. + + HealthCheck on Highest Random Weight services can be defined currently only with the [File provider](../../../install-configuration/providers/others/file.md). + +```yaml tab="Structured (YAML)" +## Routing configuration +http: + services: + app: + highestRandomWeight: + healthCheck: {} + services: + - name: appv1 + weight: 1 + - name: appv2 + weight: 1 + + appv1: + loadBalancer: + healthCheck: + path: /status + interval: 10s + timeout: 3s + servers: + - url: "http://private-ip-server-1/" + + appv2: + loadBalancer: + healthCheck: + path: /status + interval: 10s + timeout: 3s + servers: + - url: "http://private-ip-server-2/" +``` + +```toml tab="Structured (TOML)" +## Routing configuration +[http.services] + [http.services.app] + [http.services.app.highestRandomWeight.healthCheck] + [[http.services.app.highestRandomWeight.services]] + name = "appv1" + weight = 1 + [[http.services.app.highestRandomWeight.services]] + name = "appv2" + weight = 1 + + [http.services.appv1] + [http.services.appv1.loadBalancer] + [http.services.appv1.loadBalancer.healthCheck] + path = "/health" + interval = "10s" + timeout = "3s" + [[http.services.appv1.loadBalancer.servers]] + url = "http://private-ip-server-1/" + + [http.services.appv2] + [http.services.appv2.loadBalancer] + [http.services.appv2.loadBalancer.healthCheck] + path = "/health" + interval = "10s" + timeout = "3s" + [[http.services.appv2.loadBalancer.servers]] + url = "http://private-ip-server-2/" +``` + +### Mirroring + +The `mirroring` service type mirrors requests sent to a service to other services. Please note that by default the whole request is buffered in memory while it is being mirrored. See the `maxBodySize` option in the example below for how to modify this behaviour. You can also omit the request body by setting the `mirrorBody` option to false. !!! warning "Default behavior of `percent`" @@ -538,10 +759,10 @@ The mirroring is able to mirror requests sent to a service to other services. Pl !!! info "Supported Providers" - This strategy can be defined currently with the [File](../../../install-configuration/providers/others/file.md) or [IngressRoute](../../../install-configuration/providers/kubernetes/kubernetes-crd.md) providers. + This service type can be defined currently with the [File](../../../install-configuration/providers/others/file.md) provider or [IngressRoute](../../../routing-configuration/kubernetes/crd/http/ingressroute.md). ```yaml tab="Structured (YAML)" -## Dynamic configuration +## Routing configuration http: services: mirrored-api: @@ -572,7 +793,7 @@ http: ``` ```toml tab="Structured (TOML)" -## Dynamic configuration +## Routing configuration [http.services] [http.services.mirrored-api] [http.services.mirrored-api.mirroring] @@ -599,7 +820,7 @@ http: url = "http://private-ip-server-2/" ``` -### Health Check +#### Health Check HealthCheck enables automatic self-healthcheck for this service, i.e. if the main handler of the service becomes unreachable, the information is propagated upwards to its parent. @@ -610,7 +831,7 @@ HealthCheck enables automatic self-healthcheck for this service, i.e. if the mai HealthCheck on Mirroring services can be defined currently only with the [File provider](../../../install-configuration/providers/others/file.md). ```yaml tab="Structured (YAML)" -## Dynamic configuration +## Routing configuration http: services: mirrored-api: @@ -637,7 +858,7 @@ http: ``` ```toml tab="Structured (TOML)" -## Dynamic configuration +## Routing configuration [http.services] [http.services.mirrored-api] [http.services.mirrored-api.mirroring] @@ -658,7 +879,7 @@ http: [http.services.appv2] [http.services.appv2.loadBalancer] - [http.services.appv1.loadBalancer.healthCheck] + [http.services.appv2.loadBalancer.healthCheck] path = "/health" interval = "10s" timeout = "3s" @@ -666,17 +887,17 @@ http: url = "http://private-ip-server-2/" ``` -## Failover +### Failover -A failover service job is to forward all requests to a fallback service when the main service becomes unreachable. +The `failover` service type forwards all requests to a fallback service when the main service becomes unreachable. !!! info "Relation to HealthCheck" The failover service relies on the HealthCheck system to get notified when its main service becomes unreachable, which means HealthCheck needs to be enabled and functional on the main service. However, HealthCheck does not need to be enabled on the failover service itself for it to be functional. It is only required in order to propagate upwards the information when the failover itself becomes down (i.e. both its main and its fallback are down too). !!! info "Supported Provider" - This strategy can currently only be defined with the [File](../../../install-configuration/providers/others/file.md) provider. + This service type can currently only be defined with the [File](../../../install-configuration/providers/others/file.md) provider. -### HealthCheck +#### HealthCheck HealthCheck enables automatic self-healthcheck for this service, i.e. if the main and the fallback services become unreachable, the information is propagated upwards to its parent. @@ -687,7 +908,7 @@ HealthCheck enables automatic self-healthcheck for this service, i.e. if the mai HealthCheck on a Failover service can be defined currently only with the [File provider](../../../install-configuration/providers/others/file.md). ```yaml tab="Structured (YAML)" -## Dynamic configuration +## Routing configuration http: services: app: @@ -716,7 +937,7 @@ http: ``` ```toml tab="Structured (TOML)" -## Dynamic configuration +## Routing configuration [http.services] [http.services.app] [http.services.app.failover.healthCheck] diff --git a/docs/content/reference/routing-configuration/http/middlewares/grpcweb.md b/docs/content/reference/routing-configuration/http/middlewares/grpcweb.md index d478c0b60..4b02ae73e 100644 --- a/docs/content/reference/routing-configuration/http/middlewares/grpcweb.md +++ b/docs/content/reference/routing-configuration/http/middlewares/grpcweb.md @@ -8,7 +8,7 @@ The `grpcWeb` middleware converts gRPC Web requests to HTTP/2 gRPC requests befo !!! tip Please note, that Traefik needs to communicate using gRPC with the backends (h2c or HTTP/2 over TLS). - Check out the [gRPC](../../../../user-guides/grpc.md) user guide for more details. + Check out [Exposing gRPC Services](../../../../expose/overview.md#exposing-grpc-services) for more details. ## Configuration Examples diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md index e1593c1d5..f06d3c772 100644 --- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md +++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md @@ -254,27 +254,30 @@ The following annotations are organized by category for easier navigation. ### Authentication -| Annotation | Limitations / Notes | -|-------------------------------------------------------|--------------------------------------------------------------------------------------------| -| `nginx.ingress.kubernetes.io/auth-type` | | -| `nginx.ingress.kubernetes.io/auth-secret` | | -| `nginx.ingress.kubernetes.io/auth-secret-type` | | -| `nginx.ingress.kubernetes.io/auth-realm` | | +| Annotation | Limitations / Notes | +|-------------------------------------------------------|-------------------------------------------------------------------------------------------| +| `nginx.ingress.kubernetes.io/auth-type` | | +| `nginx.ingress.kubernetes.io/auth-secret` | | +| `nginx.ingress.kubernetes.io/auth-secret-type` | | +| `nginx.ingress.kubernetes.io/auth-realm` | | | `nginx.ingress.kubernetes.io/auth-url` | Only URL and response headers copy supported. Forward auth behaves differently than NGINX. | -| `nginx.ingress.kubernetes.io/auth-method` | | -| `nginx.ingress.kubernetes.io/auth-response-headers` | | +| `nginx.ingress.kubernetes.io/auth-signin` | Redirects to signin URL on 401 response. | +| `nginx.ingress.kubernetes.io/auth-method` | | +| `nginx.ingress.kubernetes.io/auth-response-headers` | | ### SSL/TLS -| Annotation | Limitations / Notes | -|-------------------------------------------------------|--------------------------------------------------------------------------------------------| -| `nginx.ingress.kubernetes.io/ssl-redirect` | Cannot opt-out per route if enabled globally. | -| `nginx.ingress.kubernetes.io/force-ssl-redirect` | Cannot opt-out per route if enabled globally. | -| `nginx.ingress.kubernetes.io/ssl-passthrough` | Some differences in SNI/default backend handling. | -| `nginx.ingress.kubernetes.io/proxy-ssl-server-name` | | -| `nginx.ingress.kubernetes.io/proxy-ssl-name` | | -| `nginx.ingress.kubernetes.io/proxy-ssl-verify` | | -| `nginx.ingress.kubernetes.io/proxy-ssl-secret` | | +| Annotation | Limitations / Notes | +|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------| +| `nginx.ingress.kubernetes.io/ssl-redirect` | Cannot opt-out per route if enabled globally. | +| `nginx.ingress.kubernetes.io/force-ssl-redirect` | Cannot opt-out per route if enabled globally. | +| `nginx.ingress.kubernetes.io/ssl-passthrough` | Some differences in SNI/default backend handling. | +| `nginx.ingress.kubernetes.io/proxy-ssl-server-name` | | +| `nginx.ingress.kubernetes.io/proxy-ssl-name` | | +| `nginx.ingress.kubernetes.io/proxy-ssl-verify` | | +| `nginx.ingress.kubernetes.io/proxy-ssl-secret` | | +| `nginx.ingress.kubernetes.io/auth-tls-secret` | When validation fails, the rejection happens during the TLS handshake rather than returning a 400 Bad Request. | +| `nginx.ingress.kubernetes.io/auth-tls-verify-client` | When validation fails, the rejection happens during the TLS handshake rather than returning a 400 Bad Request. | ### Session Affinity @@ -316,6 +319,8 @@ The following annotations are organized by category for easier navigation. | Annotation | Limitations / Notes | |-------------------------------------------------------|--------------------------------------------------------------------------------------------| +| `nginx.ingress.kubernetes.io/app-root` | | +| `nginx.ingress.kubernetes.io/from-to-www-redirect` | Doesn't support wildcard hosts. | | `nginx.ingress.kubernetes.io/use-regex` | | | `nginx.ingress.kubernetes.io/rewrite-target` | | | `nginx.ingress.kubernetes.io/permanent-redirect` | Defaults to a 301 Moved Permanently status code. | @@ -329,6 +334,11 @@ The following annotations are organized by category for easier navigation. |-------------------------------------------------------|--------------------------------------------------------------------------------------------| | `nginx.ingress.kubernetes.io/whitelist-source-range` | | +### Timeout + +| Annotation | Limitations / Notes | +|-----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `nginx.ingress.kubernetes.io/proxy-connect-timeout` | Timeout can be defined globally at the provider level using the [`proxyConnectTimeout` option](../../../install-configuration/providers/kubernetes/kubernetes-ingress-nginx/#opt-providers-kubernetesIngressNGINX-proxyConnectTimeout). | ## Limitations @@ -356,12 +366,8 @@ The following annotations are organized by category for easier navigation. | Annotation | Notes | |-----------------------------------------------------------------------------|------------------------------------------------------| -| `nginx.ingress.kubernetes.io/app-root` | | -| `nginx.ingress.kubernetes.io/affinity-canary-behavior` | | -| `nginx.ingress.kubernetes.io/auth-signin` | | -| `nginx.ingress.kubernetes.io/auth-tls-secret` | | -| `nginx.ingress.kubernetes.io/auth-tls-verify-depth` | | -| `nginx.ingress.kubernetes.io/auth-tls-verify-client` | | +| `nginx.ingress.kubernetes.io/affinity-canary-behavior` | | | +| `nginx.ingress.kubernetes.io/auth-tls-verify-depth` | | | `nginx.ingress.kubernetes.io/auth-tls-error-page` | | | `nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream` | | | `nginx.ingress.kubernetes.io/auth-tls-match-cn` | | @@ -399,8 +405,7 @@ The following annotations are organized by category for easier navigation. | `nginx.ingress.kubernetes.io/global-rate-limit-ignored-cidrs` | | | `nginx.ingress.kubernetes.io/preserve-trailing-slash` | Traefik preserves trailing slash by default. | | `nginx.ingress.kubernetes.io/proxy-cookie-domain` | | -| `nginx.ingress.kubernetes.io/proxy-cookie-path` | | -| `nginx.ingress.kubernetes.io/proxy-connect-timeout` | | +| `nginx.ingress.kubernetes.io/proxy-cookie-path` | | | | `nginx.ingress.kubernetes.io/proxy-send-timeout` | | | `nginx.ingress.kubernetes.io/proxy-read-timeout` | | | `nginx.ingress.kubernetes.io/proxy-next-upstream` | | diff --git a/docs/content/reference/routing-configuration/other-providers/consul-catalog.md b/docs/content/reference/routing-configuration/other-providers/consul-catalog.md index 7958a4c01..9a9a2dd8e 100644 --- a/docs/content/reference/routing-configuration/other-providers/consul-catalog.md +++ b/docs/content/reference/routing-configuration/other-providers/consul-catalog.md @@ -13,7 +13,90 @@ With Consul Catalog, Traefik can leverage tags attached to a service to generate We recommend to *not* use tags to store sensitive data (certificates, credentials, etc). Instead, we recommend to store sensitive data in a safer storage (secrets, file, etc). -## Routing Configuration +## Configuration Examples + +??? example "Configuring Consul Catalog & Deploying / Exposing one Service" + + Enabling the consul catalog provider + + ```yaml tab="Structured (YAML)" + providers: + consulCatalog: {} + ``` + + ```toml tab="Structured (TOML)" + [providers.consulCatalog] + ``` + + ```bash tab="CLI" + --providers.consulcatalog=true + ``` + + Attaching tags to services (when registering a service in Consul) + + ```bash + consul services register -name=my-service -tag="traefik.http.routers.my-service.rule=Host(`example.com`)" + ``` + + Or using a service definition file: + + ```json + { + "service": { + "name": "my-service", + "tags": [ + "traefik.http.routers.my-service.rule=Host(`example.com`)" + ] + } + } + ``` + +??? example "Specify a Custom Port for the Container" + + Forward requests for `http://example.com` to `http://:12345`: + + ```json + { + "service": { + "name": "my-service", + "tags": [ + "traefik.http.routers.my-service.rule=Host(`example.com`)", + "traefik.http.routers.my-service.service=my-service", + "traefik.http.services.my-service.loadbalancer.server.port=12345" + ] + } + } + ``` + + !!! important "Traefik Connecting to the Wrong Port: `HTTP/502 Gateway Error`" + By default, Traefik uses the first exposed port of a container. + + Setting the tag `traefik.http.services.xxx.loadbalancer.server.port` + overrides that behavior. + +??? example "Specifying more than one router and service per container" + + Forwarding requests to more than one port on a container requires referencing the service loadbalancer port definition using the service parameter on the router. + + In this example, requests are forwarded for `http://example-a.com` to `http://:8000` in addition to `http://example-b.com` forwarding to `http://:9000`: + + ```json + { + "service": { + "name": "my-service", + "tags": [ + "traefik.http.routers.www-router.rule=Host(`example-a.com`)", + "traefik.http.routers.www-router.service=www-service", + "traefik.http.services.www-service.loadbalancer.server.port=8000", + "traefik.http.routers.admin-router.rule=Host(`example-b.com`)", + "traefik.http.routers.admin-router.service=admin-service", + "traefik.http.services.admin-service.loadbalancer.server.port=9000" + ] + } + } + ``` + +## Configuration Options !!! info "tags" @@ -35,120 +118,24 @@ To update the configuration of the Router automatically attached to the service, For example, to change the rule, you could add the tag ```traefik.http.routers.my-service.rule=Host(`example.com`)```. -??? info "`traefik.http.routers..rule`" - - See [rule](../http/routing/rules-and-priority.md) for more information. - - ```yaml - traefik.http.routers.myrouter.rule=Host(`example.com`) - ``` +#### Configuration Options -??? info "`traefik.http.routers..ruleSyntax`" - - !!! warning - - RuleSyntax option is deprecated and will be removed in the next major version. - Please do not use this field and rewrite the router rules to use the v3 syntax. - - See [ruleSyntax](../http/routing/rules-and-priority.md#rulesyntax) for more information. - - ```yaml - traefik.http.routers.myrouter.ruleSyntax=v3 - ``` - -??? info "`traefik.http.routers..priority`" - - See [priority](../http/routing/rules-and-priority.md#priority-calculation) for more information. - - ```yaml - - "traefik.tcp.routers.mytcprouter.priority=42" - ``` - -??? info "`traefik.http.routers..entrypoints`" - - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - traefik.http.routers.myrouter.entrypoints=web,websecure - ``` - -??? info "`traefik.http.routers..middlewares`" - - See [middlewares overview](../http/middlewares/overview.md) for more information. - - ```yaml - traefik.http.routers.myrouter.middlewares=auth,prefix,cb - ``` - -??? info "`traefik.http.routers..service`" - - See [service](../http/load-balancing/service.md) for more information. - - ```yaml - traefik.http.routers.myrouter.service=myservice - ``` - -??? info "`traefik.http.routers..tls`" - - See [tls](../http/tls/overview.md) for more information. - - ```yaml - traefik.http.routers.myrouter.tls=true - ``` - -??? info "`traefik.http.routers..tls.certresolver`" - - See [certResolver](../../install-configuration/tls/certificate-resolvers/overview.md) for more information. - - ```yaml - traefik.http.routers.myrouter.tls.certresolver=myresolver - ``` - -??? info "`traefik.http.routers..tls.domains[n].main`" - - See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. - - ```yaml - traefik.http.routers.myrouter.tls.domains[0].main=example.org - ``` - -??? info "`traefik.http.routers..tls.domains[n].sans`" - - See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. - - ```yaml - traefik.http.routers.myrouter.tls.domains[0].sans=test.example.org,dev.example.org - ``` - -??? info "`traefik.http.routers..tls.options`" - - ```yaml - traefik.http.routers.myrouter.tls.options=foobar - ``` - -??? info "`traefik.http.routers..observability.accesslogs`" - - The accessLogs option controls whether the router will produce access-logs. - - ```yaml - "traefik.http.routers.myrouter.observability.accesslogs=true" - ``` - -??? info "`traefik.http.routers..observability.metrics`" - - The metrics option controls whether the router will produce metrics. - - ```yaml - "traefik.http.routers.myrouter.observability.metrics=true" - ``` - -??? info "`traefik.http.routers..observability.tracing`" - - The tracing option controls whether the router will produce traces. - - ```yaml - "traefik.http.routers.myrouter.observability.tracing=true" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.http.routers..rule` | See [rule](../http/routing/rules-and-priority.md#rules) for more information. | ```Host(`example.com`)``` | +| `traefik.http.routers..ruleSyntax` | See [ruleSyntax](../http/routing/rules-and-priority.md#rulesyntax) for more information.
RuleSyntax option is deprecated and will be removed in the next major version.
Please do not use this field and rewrite the router rules to use the v3 syntax. | `v3` | +| `traefik.http.routers..priority` | See [priority](../http/routing/rules-and-priority.md#priority-calculation) for more information. | `42` | +| `traefik.http.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `web,websecure` | +| `traefik.http.routers..middlewares` | See [middlewares overview](../http/middlewares/overview.md) for more information. | `auth,prefix,cb` | +| `traefik.http.routers..service` | See [service](../http/load-balancing/service.md) for more information. | `myservice` | +| `traefik.http.routers..tls` | See [tls](../http/tls/overview.md) for more information. | `true` | +| `traefik.http.routers..tls.certresolver` | See [certResolver](../../install-configuration/tls/certificate-resolvers/overview.md) for more information. | `myresolver` | +| `traefik.http.routers..tls.domains[n].main` | See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. | `example.org` | +| `traefik.http.routers..tls.domains[n].sans` | See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. | `test.example.org,dev.example.org` | +| `traefik.http.routers..tls.options` | | `foobar` | +| `traefik.http.routers..observability.accesslogs` | The accessLogs option controls whether the router will produce access-logs. | `true` | +| `traefik.http.routers..observability.metrics` | The metrics option controls whether the router will produce metrics. | `true` | +| `traefik.http.routers..observability.tracing` | The tracing option controls whether the router will produce traces. | `true` | ### Services @@ -158,181 +145,34 @@ add tags starting with `traefik.http.services.{name-of-your-choice}.`, followed For example, to change the `passHostHeader` behavior, you'd add the tag `traefik.http.services.{name-of-your-choice}.loadbalancer.passhostheader=false`. -??? info "`traefik.http.services..loadbalancer.server.port`" - - Registers a port. - Useful when the service exposes multiples ports. - - ```yaml - traefik.http.services.myservice.loadbalancer.server.port=8080 - ``` +#### Configuration Options -??? info "`traefik.http.services..loadbalancer.server.scheme`" - - Overrides the default scheme. - - ```yaml - traefik.http.services.myservice.loadbalancer.server.scheme=http - ``` - -??? info "`traefik.http.services..loadbalancer.server.weight`" - - Overrides the default weight. - - ```yaml - traefik.http.services.myservice.loadbalancer.server.weight=42 - ``` - -??? info "`traefik.http.services..loadbalancer.serverstransport`" - - Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one. - See [serverstransport](../http/load-balancing/serverstransport.md) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.serverstransport=foobar@file - ``` - -??? info "`traefik.http.services..loadbalancer.passhostheader`" - - ```yaml - traefik.http.services.myservice.loadbalancer.passhostheader=true - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.headers.`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.headers.X-Foo=foobar - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.hostname`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.hostname=example.org - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.interval`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.interval=10 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.unhealthyinterval`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.unhealthyinterval=10 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.path`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.path=/foo - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.method`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.status`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.status=42 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.port`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.port=42 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.scheme`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.scheme=http - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.timeout`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.timeout=10 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.followredirects`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.followredirects=true - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie=true - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.httponly`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.httponly=true - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.name`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.name=foobar - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.path`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.path=/foobar - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.secure`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.secure=true - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.samesite`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.samesite=none - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.maxage`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.maxage=42 - ``` - -??? info "`traefik.http.services..loadbalancer.responseforwarding.flushinterval`" - - ```yaml - traefik.http.services.myservice.loadbalancer.responseforwarding.flushinterval=10 - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.http.services..loadbalancer.server.port` | Registers a port.
Useful when the service exposes multiples ports. | `8080` | +| `traefik.http.services..loadbalancer.server.scheme` | Overrides the default scheme. | `http` | +| `traefik.http.services..loadbalancer.server.weight` | Overrides the default weight. | `42` | +| `traefik.http.services..loadbalancer.serverstransport` | Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one.
See [serverstransport](../http/load-balancing/serverstransport.md) for more information. | `foobar@file` | +| `traefik.http.services..loadbalancer.passhostheader` | | `true` | +| `traefik.http.services..loadbalancer.healthcheck.headers.` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `foobar` | +| `traefik.http.services..loadbalancer.healthcheck.hostname` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `example.org` | +| `traefik.http.services..loadbalancer.healthcheck.interval` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10` | +| `traefik.http.services..loadbalancer.healthcheck.unhealthyinterval` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10` | +| `traefik.http.services..loadbalancer.healthcheck.path` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `/foo` | +| `traefik.http.services..loadbalancer.healthcheck.method` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `foobar` | +| `traefik.http.services..loadbalancer.healthcheck.status` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `42` | +| `traefik.http.services..loadbalancer.healthcheck.port` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `42` | +| `traefik.http.services..loadbalancer.healthcheck.scheme` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `http` | +| `traefik.http.services..loadbalancer.healthcheck.timeout` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10` | +| `traefik.http.services..loadbalancer.healthcheck.followredirects` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.httponly` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.name` | | `foobar` | +| `traefik.http.services..loadbalancer.sticky.cookie.path` | | `/foobar` | +| `traefik.http.services..loadbalancer.sticky.cookie.secure` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.samesite` | | `none` | +| `traefik.http.services..loadbalancer.sticky.cookie.maxage` | | `42` | +| `traefik.http.services..loadbalancer.responseforwarding.flushinterval` | | `10` | ### Middleware @@ -375,124 +215,32 @@ You can declare TCP Routers, Middlewares and/or Services using tags. #### TCP Routers -??? info "`traefik.tcp.routers..entrypoints`" - - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.entrypoints=ep1,ep2 - ``` +##### Configuration Options -??? info "`traefik.tcp.routers..rule`" - - See [rule](../tcp/routing/rules-and-priority.md#rules) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.rule=HostSNI(`example.com`) - ``` - -??? info "`traefik.tcp.routers..ruleSyntax`" - - !!! warning - - RuleSyntax option is deprecated and will be removed in the next major version. - Please do not use this field and rewrite the router rules to use the v3 syntax. - - configure the rule syntax to be used for parsing the rule on a per-router basis. - - ```yaml - traefik.tcp.routers.mytcprouter.ruleSyntax=v3 - ``` - -??? info "`traefik.tcp.routers..priority`" - See [priority](../tcp/routing/rules-and-priority.md#priority-calculation) for more information. - ```yaml - - "traefik.tcp.routers.mytcprouter.priority=42" - ``` - -??? info "`traefik.tcp.routers..service`" - - See [service](../tcp/service.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.service=myservice - ``` - -??? info "`traefik.tcp.routers..tls`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls=true - ``` - -??? info "`traefik.tcp.routers..tls.certresolver`" - - See [certResolver](../tcp/tls.md#configuration-options) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.certresolver=myresolver - ``` - -??? info "`traefik.tcp.routers..tls.domains[n].main`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.domains[0].main=example.org - ``` - -??? info "`traefik.tcp.routers..tls.domains[n].sans`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.domains[0].sans=test.example.org,dev.example.org - ``` - -??? info "`traefik.tcp.routers..tls.options`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.options=mysoptions - ``` - -??? info "`traefik.tcp.routers..tls.passthrough`" - - See [Passthrough](../tcp/tls.md#opt-passthrough) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.passthrough=true - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.tcp.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `ep1,ep2` | +| `traefik.tcp.routers..rule` | See [rule](../tcp/routing/rules-and-priority.md#rules) for more information. | ```HostSNI(`example.com`)``` | +| `traefik.tcp.routers..ruleSyntax` | configure the rule syntax to be used for parsing the rule on a per-router basis.
RuleSyntax option is deprecated and will be removed in the next major version.
Please do not use this field and rewrite the router rules to use the v3 syntax. | `v3` | +| `traefik.tcp.routers..priority` | See [priority](../tcp/routing/rules-and-priority.md#priority-calculation) for more information. | `42` | +| `traefik.tcp.routers..service` | See [service](../tcp/service.md) for more information. | `myservice` | +| `traefik.tcp.routers..tls` | See [TLS](../tcp/tls.md) for more information. | `true` | +| `traefik.tcp.routers..tls.certresolver` | See [certResolver](../tcp/tls.md#configuration-options) for more information. | `myresolver` | +| `traefik.tcp.routers..tls.domains[n].main` | See [TLS](../tcp/tls.md) for more information. | `example.org` | +| `traefik.tcp.routers..tls.domains[n].sans` | See [TLS](../tcp/tls.md) for more information. | `test.example.org,dev.example.org` | +| `traefik.tcp.routers..tls.options` | See [TLS](../tcp/tls.md) for more information. | `mysoptions` | +| `traefik.tcp.routers..tls.passthrough` | See [Passthrough](../tcp/tls.md#opt-passthrough) for more information. | `true` | #### TCP Services -??? info "`traefik.tcp.services..loadbalancer.server.port`" - - Registers a port of the application. - - ```yaml - traefik.tcp.services.mytcpservice.loadbalancer.server.port=423 - ``` +##### Configuration Options -??? info "`traefik.tcp.services..loadbalancer.server.tls`" - - Determines whether to use TLS when dialing with the backend. - - ```yaml - traefik.tcp.services.mytcpservice.loadbalancer.server.tls=true - ``` - -??? info "`traefik.tcp.services..loadbalancer.serverstransport`" - - Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one. - See [serverstransport](../tcp/serverstransport.md) for more information. - - ```yaml - traefik.tcp.services.mytcpservice.loadbalancer.serverstransport=foobar@file - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.tcp.services..loadbalancer.server.port` | Registers a port of the application. | `423` | +| `traefik.tcp.services..loadbalancer.server.tls` | Determines whether to use TLS when dialing with the backend. | `true` | +| `traefik.tcp.services..loadbalancer.serverstransport` | Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one.
See [serverstransport](../tcp/serverstransport.md) for more information. | `foobar@file` | #### TCP Middleware @@ -534,67 +282,28 @@ You can declare UDP Routers and/or Services using tags. #### UDP Routers -??? info "`traefik.udp.routers..entrypoints`" - - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - traefik.udp.routers.myudprouter.entrypoints=ep1,ep2 - ``` +##### Configuration Options -??? info "`traefik.udp.routers..service`" - - See [service](../udp/service.md) for more information. - - ```yaml - traefik.udp.routers.myudprouter.service=myservice - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.udp.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `ep1,ep2` | +| `traefik.udp.routers..service` | See [service](../udp/service.md) for more information. | `myservice` | #### UDP Services -??? info "`traefik.udp.services..loadbalancer.server.port`" - - Registers a port of the application. - - ```yaml - traefik.udp.services.myudpservice.loadbalancer.server.port=423 - ``` +##### Configuration Options + +| Label | Description | Value | +|------|-------------|-------| +| `traefik.udp.services..loadbalancer.server.port` | Registers a port of the application. | `423` | ### Specific Provider Options -#### `traefik.enable` - -```yaml -traefik.enable=true -``` - -You can tell Traefik to consider (or not) the service by setting `traefik.enable` to true or false. - -This option overrides the value of `exposedByDefault`. - -#### `traefik.consulcatalog.connect` - -```yaml -traefik.consulcatalog.connect=true -``` - -You can tell Traefik to consider (or not) the service as a Connect capable one by setting `traefik.consulcatalog.connect` to true or false. - -This option overrides the value of `connectByDefault`. - -#### `traefik.consulcatalog.canary` - -```yaml -traefik.consulcatalog.canary=true -``` - -When ConsulCatalog, in the context of a Nomad orchestrator, -is a provider (of service registration) for Traefik, -one might have the need to distinguish within Traefik between a [Canary](https://learn.hashicorp.com/tutorials/nomad/job-blue-green-and-canary-deployments#deploy-with-canaries) instance of a service, or a production one. -For example if one does not want them to be part of the same load-balancer. - -Therefore, this option, which is meant to be provided as one of the values of the `canary_tags` field in the Nomad [service stanza](https://www.nomadproject.io/docs/job-specification/service#canary_tags), -allows Traefik to identify that the associated instance is a canary one. +| Label | Description | Value | +|------|-------------|-------| +| `traefik.enable` | You can tell Traefik to consider (or not) the service by setting `traefik.enable` to true or false.
This option overrides the value of `exposedByDefault`. | `true` | +| `traefik.consulcatalog.connect` | You can tell Traefik to consider (or not) the service as a Connect capable one by setting `traefik.consulcatalog.connect` to true or false.
This option overrides the value of `connectByDefault`. | `true` | +| `traefik.consulcatalog.canary` | When ConsulCatalog, in the context of a Nomad orchestrator, is a provider (of service registration) for Traefik, one might have the need to distinguish within Traefik between a [Canary](https://learn.hashicorp.com/tutorials/nomad/job-blue-green-and-canary-deployments#deploy-with-canaries) instance of a service, or a production one.
For example if one does not want them to be part of the same load-balancer.

Therefore, this option, which is meant to be provided as one of the values of the `canary_tags` field in the Nomad [service stanza](https://www.nomadproject.io/docs/job-specification/service#canary_tags), allows Traefik to identify that the associated instance is a canary one. | `true` | #### Port Lookup diff --git a/docs/content/reference/routing-configuration/other-providers/docker.md b/docs/content/reference/routing-configuration/other-providers/docker.md index 4cc633bd1..6b9953b00 100644 --- a/docs/content/reference/routing-configuration/other-providers/docker.md +++ b/docs/content/reference/routing-configuration/other-providers/docker.md @@ -19,12 +19,12 @@ With Docker, Traefik can leverage labels attached to a container to generate rou Enabling the docker provider - ```yaml tab="File (YAML)" + ```yaml tab="Structured (YAML)" providers: docker: {} ``` - ```toml tab="File (TOML)" + ```toml tab="Structured (TOML)" [providers.docker] ``` @@ -81,7 +81,7 @@ With Docker, Traefik can leverage labels attached to a container to generate rou - traefik.http.services.admin-service.loadbalancer.server.port=9000 ``` -## Routing Configuration +## Configuration Options !!! info "Labels" @@ -145,118 +145,24 @@ For example, to change the rule, you could add the label ```traefik.http.routers !!! warning "The character `@` is not authorized in the router name ``." -??? info "`traefik.http.routers..rule`" +#### Configuration Options - See [rule](../http/routing/rules-and-priority.md) for more information. - - ```yaml - "traefik.http.routers.myrouter.rule=Host(`example.com`)" - ``` - -??? info "`traefik.http.routers..ruleSyntax`" - - !!! warning - - RuleSyntax option is deprecated and will be removed in the next major version. - Please do not use this field and rewrite the router rules to use the v3 syntax. - - See [ruleSyntax](../http/routing/rules-and-priority.md#rulesyntax) for more information. - - ```yaml - traefik.http.routers.myrouter.ruleSyntax=v3 - ``` - -??? info "`traefik.http.routers..entrypoints`" - - ```yaml - "traefik.http.routers.myrouter.entrypoints=ep1,ep2" - ``` - -??? info "`traefik.http.routers..middlewares`" - - See [middlewares overview](../http/middlewares/overview.md) for more information. - - ```yaml - "traefik.http.routers.myrouter.middlewares=auth,prefix,cb" - ``` - -??? info "`traefik.http.routers..service`" - - See [service](../http/load-balancing/service.md) for more information. - - ```yaml - "traefik.http.routers.myrouter.service=myservice" - ``` - -??? info "`traefik.http.routers..tls`" - - See [tls](../http/tls/overview.md) for more information. - - ```yaml - "traefik.http.routers.myrouter.tls=true" - ``` - -??? info "`traefik.http.routers..tls.certresolver`" - - See [certResolver](../../install-configuration/tls/certificate-resolvers/overview.md) for more information. - - ```yaml - "traefik.http.routers.myrouter.tls.certresolver=myresolver" - ``` - -??? info "`traefik.http.routers..tls.domains[n].main`" - - See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. - - ```yaml - "traefik.http.routers.myrouter.tls.domains[0].main=example.org" - ``` - -??? info "`traefik.http.routers..tls.domains[n].sans`" - - See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. - - ```yaml - "traefik.http.routers.myrouter.tls.domains[0].sans=test.example.org,dev.example.org" - ``` - -??? info "`traefik.http.routers..tls.options`" - - ```yaml - "traefik.http.routers.myrouter.tls.options=foobar" - ``` - -??? info "`traefik.http.routers..observability.accesslogs`" - - The accessLogs option controls whether the router will produce access-logs. - - ```yaml - "traefik.http.routers.myrouter.observability.accesslogs=true" - ``` - -??? info "`traefik.http.routers..observability.metrics`" - - The metrics option controls whether the router will produce metrics. - - ```yaml - "traefik.http.routers.myrouter.observability.metrics=true" - ``` - -??? info "`traefik.http.routers..observability.tracing`" - - The tracing option controls whether the router will produce traces. - - ```yaml - "traefik.http.routers.myrouter.observability.tracing=true" - ``` - -??? info "`traefik.http.routers..priority`" - - See [priority](../http/routing/rules-and-priority.md#priority-calculation) for more information. - - ```yaml - "traefik.http.routers.myrouter.priority=42" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.http.routers..rule` | See [rule](../http/routing/rules-and-priority.md#rules) for more information. | ```Host(`example.com`)``` | +| `traefik.http.routers..ruleSyntax` | See [ruleSyntax](../http/routing/rules-and-priority.md#rulesyntax) for more information.
RuleSyntax option is deprecated and will be removed in the next major version.
Please do not use this field and rewrite the router rules to use the v3 syntax. | `v3` | +| `traefik.http.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `ep1,ep2` | +| `traefik.http.routers..middlewares` | See [middlewares overview](../http/middlewares/overview.md) for more information. | `auth,prefix,cb` | +| `traefik.http.routers..service` | See [service](../http/load-balancing/service.md) for more information. | `myservice` | +| `traefik.http.routers..tls` | See [tls](../http/tls/overview.md) for more information. | `true` | +| `traefik.http.routers..tls.certresolver` | See [certResolver](../../install-configuration/tls/certificate-resolvers/overview.md) for more information. | `myresolver` | +| `traefik.http.routers..tls.domains[n].main` | See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. | `example.org` | +| `traefik.http.routers..tls.domains[n].sans` | See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. | `test.example.org,dev.example.org` | +| `traefik.http.routers..tls.options` | | `foobar` | +| `traefik.http.routers..observability.accesslogs` | The accessLogs option controls whether the router will produce access-logs. | `true` | +| `traefik.http.routers..observability.metrics` | The metrics option controls whether the router will produce metrics. | `true` | +| `traefik.http.routers..observability.tracing` | The tracing option controls whether the router will produce traces. | `true` | +| `traefik.http.routers..priority` | See [priority](../http/routing/rules-and-priority.md#priority-calculation) for more information. | `42` | ### Services @@ -268,182 +174,34 @@ you'd add the label `traefik.http.services..loadbalancer.pa !!! warning "The character `@` is not authorized in the service name ``." -??? info "`traefik.http.services..loadbalancer.server.port`" +#### Configuration Options - Registers a port. - Useful when the container exposes multiples ports. - - ```yaml - "traefik.http.services.myservice.loadbalancer.server.port=8080" - ``` - -??? info "`traefik.http.services..loadbalancer.server.scheme`" - - Overrides the default scheme. - - ```yaml - "traefik.http.services.myservice.loadbalancer.server.scheme=http" - ``` - -??? info "`traefik.http.services..loadbalancer.server.url`" - - Defines the service URL. - This option cannot be used in combination with `port` or `scheme` definition. - - ```yaml - traefik.http.services..loadbalancer.server.url=http://foobar:8080 - ``` - -??? info "`traefik.http.services..loadbalancer.serverstransport`" - - Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one. - See [serverstransport](../http/load-balancing/serverstransport.md) for more information. - - ```yaml - "traefik.http.services.myservice.loadbalancer.serverstransport=foobar@file" - ``` - -??? info "`traefik.http.services..loadbalancer.passhostheader`" - - ```yaml - "traefik.http.services.myservice.loadbalancer.passhostheader=true" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.headers.`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - "traefik.http.services.myservice.loadbalancer.healthcheck.headers.X-Foo=foobar" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.hostname`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - "traefik.http.services.myservice.loadbalancer.healthcheck.hostname=example.org" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.interval`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - "traefik.http.services.myservice.loadbalancer.healthcheck.interval=10s" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.unhealthyinterval`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - "traefik.http.services.myservice.loadbalancer.healthcheck.unhealthyinterval=10s" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.path`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - "traefik.http.services.myservice.loadbalancer.healthcheck.path=/foo" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.method`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - "traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.status`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - "traefik.http.services.myservice.loadbalancer.healthcheck.status=42" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.port`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - "traefik.http.services.myservice.loadbalancer.healthcheck.port=42" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.scheme`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - "traefik.http.services.myservice.loadbalancer.healthcheck.scheme=http" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.timeout`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - "traefik.http.services.myservice.loadbalancer.healthcheck.timeout=10s" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.followredirects`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - "traefik.http.services.myservice.loadbalancer.healthcheck.followredirects=true" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie`" - - ```yaml - "traefik.http.services.myservice.loadbalancer.sticky.cookie=true" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.httponly`" - - ```yaml - "traefik.http.services.myservice.loadbalancer.sticky.cookie.httponly=true" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.name`" - - ```yaml - "traefik.http.services.myservice.loadbalancer.sticky.cookie.name=foobar" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.path`" - - ```yaml - "traefik.http.services.myservice.loadbalancer.sticky.cookie.path=/foobar" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.secure`" - - ```yaml - "traefik.http.services.myservice.loadbalancer.sticky.cookie.secure=true" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.samesite`" - - ```yaml - "traefik.http.services.myservice.loadbalancer.sticky.cookie.samesite=none" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.maxage`" - - ```yaml - "traefik.http.services.myservice.loadbalancer.sticky.cookie.maxage=42" - ``` - -??? info "`traefik.http.services..loadbalancer.responseforwarding.flushinterval`" - - ```yaml - "traefik.http.services.myservice.loadbalancer.responseforwarding.flushinterval=10" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.http.services..loadbalancer.server.port` | Registers a port.
Useful when the container exposes multiples ports. | `8080` | +| `traefik.http.services..loadbalancer.server.scheme` | Overrides the default scheme. | `http` | +| `traefik.http.services..loadbalancer.server.url` | Defines the service URL.
This option cannot be used in combination with `port` or `scheme` definition. | `http://foobar:8080` | +| `traefik.http.services..loadbalancer.serverstransport` | Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one.
See [serverstransport](../http/load-balancing/serverstransport.md) for more information. | `foobar@file` | +| `traefik.http.services..loadbalancer.passhostheader` | | `true` | +| `traefik.http.services..loadbalancer.healthcheck.headers.` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `foobar` | +| `traefik.http.services..loadbalancer.healthcheck.hostname` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `example.org` | +| `traefik.http.services..loadbalancer.healthcheck.interval` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10s` | +| `traefik.http.services..loadbalancer.healthcheck.unhealthyinterval` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10s` | +| `traefik.http.services..loadbalancer.healthcheck.path` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `/foo` | +| `traefik.http.services..loadbalancer.healthcheck.method` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `foobar` | +| `traefik.http.services..loadbalancer.healthcheck.status` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `42` | +| `traefik.http.services..loadbalancer.healthcheck.port` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `42` | +| `traefik.http.services..loadbalancer.healthcheck.scheme` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `http` | +| `traefik.http.services..loadbalancer.healthcheck.timeout` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10s` | +| `traefik.http.services..loadbalancer.healthcheck.followredirects` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.httponly` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.name` | | `foobar` | +| `traefik.http.services..loadbalancer.sticky.cookie.path` | | `/foobar` | +| `traefik.http.services..loadbalancer.sticky.cookie.secure` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.samesite` | | `none` | +| `traefik.http.services..loadbalancer.sticky.cookie.maxage` | | `42` | +| `traefik.http.services..loadbalancer.responseforwarding.flushinterval` | | `10` | ### Middleware @@ -497,123 +255,31 @@ You can declare TCP Routers and/or Services using labels. #### TCP Routers -??? info "`traefik.tcp.routers..entrypoints`" +##### Configuration Options - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - "traefik.tcp.routers.mytcprouter.entrypoints=ep1,ep2" - ``` - -??? info "`traefik.tcp.routers..rule`" - - See [rule](../tcp/routing/rules-and-priority.md#rules) for more information. - - ```yaml - "traefik.tcp.routers.mytcprouter.rule=HostSNI(`example.com`)" - ``` - -??? info "`traefik.tcp.routers..ruleSyntax`" - - !!! warning - - RuleSyntax option is deprecated and will be removed in the next major version. - Please do not use this field and rewrite the router rules to use the v3 syntax. - - configure the rule syntax to be used for parsing the rule on a per-router basis. - - ```yaml - traefik.tcp.routers.mytcprouter.ruleSyntax=v3 - ``` - -??? info "`traefik.tcp.routers..service`" - - See [service](../tcp/service.md) for more information. - - ```yaml - "traefik.tcp.routers.mytcprouter.service=myservice" - ``` - -??? info "`traefik.tcp.routers..tls`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - "traefik.tcp.routers.mytcprouter.tls=true" - ``` - -??? info "`traefik.tcp.routers..tls.certresolver`" - - See [certResolver](../tcp/tls.md#configuration-options) for more information. - - ```yaml - "traefik.tcp.routers.mytcprouter.tls.certresolver=myresolver" - ``` - -??? info "`traefik.tcp.routers..tls.domains[n].main`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - "traefik.tcp.routers.mytcprouter.tls.domains[0].main=example.org" - ``` - -??? info "`traefik.tcp.routers..tls.domains[n].sans`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - "traefik.tcp.routers.mytcprouter.tls.domains[0].sans=test.example.org,dev.example.org" - ``` - -??? info "`traefik.tcp.routers..tls.options`" - - ```yaml - "traefik.tcp.routers.mytcprouter.tls.options=mysoptions" - ``` - -??? info "`traefik.tcp.routers..tls.passthrough`" - - See [TLS](../tcp/tls.md#opt-passthrough) for more information. - - ```yaml - "traefik.tcp.routers.mytcprouter.tls.passthrough=true" - ``` - -??? info "`traefik.tcp.routers..priority`" - - See [priority](../tcp/routing/rules-and-priority.md) for more information. - - ```yaml - "traefik.tcp.routers.mytcprouter.priority=42" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.tcp.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `ep1,ep2` | +| `traefik.tcp.routers..rule` | See [rule](../tcp/routing/rules-and-priority.md#rules) for more information. | ```HostSNI(`example.com`)``` | +| `traefik.tcp.routers..ruleSyntax` | configure the rule syntax to be used for parsing the rule on a per-router basis.
RuleSyntax option is deprecated and will be removed in the next major version.
Please do not use this field and rewrite the router rules to use the v3 syntax. | `v3` | +| `traefik.tcp.routers..service` | See [service](../tcp/service.md) for more information. | `myservice` | +| `traefik.tcp.routers..tls` | See [TLS](../tcp/tls.md) for more information. | `true` | +| `traefik.tcp.routers..tls.certresolver` | See [certResolver](../tcp/tls.md#configuration-options) for more information. | `myresolver` | +| `traefik.tcp.routers..tls.domains[n].main` | See [TLS](../tcp/tls.md) for more information. | `example.org` | +| `traefik.tcp.routers..tls.domains[n].sans` | See [TLS](../tcp/tls.md) for more information. | `test.example.org,dev.example.org` | +| `traefik.tcp.routers..tls.options` | | `mysoptions` | +| `traefik.tcp.routers..tls.passthrough` | See [Passthrough](../tcp/tls.md#opt-passthrough) for more information. | `true` | +| `traefik.tcp.routers..priority` | See [priority](../tcp/routing/rules-and-priority.md#priority-calculation) for more information. | `42` | #### TCP Services -??? info "`traefik.tcp.services..loadbalancer.server.port`" +##### Configuration Options - Registers a port of the application. - - ```yaml - "traefik.tcp.services.mytcpservice.loadbalancer.server.port=423" - ``` - -??? info "`traefik.tcp.services..loadbalancer.server.tls`" - - Determines whether to use TLS when dialing with the backend. - - ```yaml - "traefik.tcp.services.mytcpservice.loadbalancer.server.tls=true" - ``` - -??? info "`traefik.tcp.services..loadbalancer.serverstransport`" - - Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one. - See [serverstransport](../tcp/serverstransport.md) for more information. - - ```yaml - "traefik.tcp.services.mytcpservice.loadbalancer.serverstransport=foobar@file" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.tcp.services..loadbalancer.server.port` | Registers a port of the application. | `423` | +| `traefik.tcp.services..loadbalancer.server.tls` | Determines whether to use TLS when dialing with the backend. | `true` | +| `traefik.tcp.services..loadbalancer.serverstransport` | Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one.
See [serverstransport](../tcp/serverstransport.md) for more information. | `foobar@file` | #### TCP Middleware @@ -659,76 +325,25 @@ You can declare UDP Routers and/or Services using labels. #### UDP Routers -??? info "`traefik.udp.routers..entrypoints`" +##### Configuration Options - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - "traefik.udp.routers.myudprouter.entrypoints=ep1,ep2" - ``` - -??? info "`traefik.udp.routers..service`" - - See [service](../udp/service.md) for more information. - - ```yaml - "traefik.udp.routers.myudprouter.service=myservice" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.udp.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `ep1,ep2` | +| `traefik.udp.routers..service` | See [service](../udp/service.md) for more information. | `myservice` | #### UDP Services -??? info "`traefik.udp.services..loadbalancer.server.port`" +##### Configuration Options - Registers a port of the application. - - ```yaml - "traefik.udp.services.myudpservice.loadbalancer.server.port=423" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.udp.services..loadbalancer.server.port` | Registers a port of the application. | `423` | ### Specific Provider Options -#### `traefik.enable` - -```yaml -- "traefik.enable=true" -``` - -You can tell Traefik to consider (or not) the container by setting `traefik.enable` to true or false. - -This option overrides the value of `exposedByDefault`. - -#### `traefik.docker.allownonrunning` - -```yaml -- "traefik.docker.allownonrunning=true" -``` - -By default, Traefik only considers containers in "running" state. -This option controls whether containers that are not in "running" state (e.g., stopped, paused, exited) should still be visible to Traefik for service discovery. - -When this label is set to true, Traefik will: - -- Keep the router and service configuration even when the container is not running -- Create services with empty backend server lists -- Return 503 Service Unavailable for requests to stopped containers (instead of 404 Not Found) -- Execute the full middleware chain, allowing middlewares to intercept requests - -!!! warning "Configuration Collision" - - As the `traefik.docker.allownonrunning` enables the discovery of all containers exposing this option disregarding their state, - if multiple stopped containers expose the same router but their configurations diverge, then the routers will be dropped. - -#### `traefik.docker.network` - -```yaml -- "traefik.docker.network=mynetwork" -``` - -Overrides the default docker network to use for connections to the container. - -If a container is linked to several networks, be sure to set the proper network name (you can check this with `docker inspect `), -otherwise it will randomly pick one (depending on how docker is returning them). - -!!! warning - - When deploying a stack from a compose file `stack`, the networks defined are prefixed with `stack`. +| Label | Description | Value | +|------|-------------|-------| +| `traefik.enable` | You can tell Traefik to consider (or not) the container by setting `traefik.enable` to true or false.
This option overrides the value of `exposedByDefault`. | `true` | +| `traefik.docker.allownonrunning` | By default, Traefik only considers containers in "running" state.
This option controls whether containers that are not in "running" state (e.g., stopped, paused, exited) should still be visible to Traefik for service discovery.

When this label is set to true, Traefik will:
- Keep the router and service configuration even when the container is not running
- Create services with empty backend server lists
- Return 503 Service Unavailable for requests to stopped containers (instead of 404 Not Found)
- Execute the full middleware chain, allowing middlewares to intercept requests

As the `traefik.docker.allownonrunning` enables the discovery of all containers exposing this option disregarding their state, if multiple stopped containers expose the same router but their configurations diverge, then the routers will be dropped. | `true` | +| `traefik.docker.network` | Overrides the default docker network to use for connections to the container.
If a container is linked to several networks, be sure to set the proper network name (you can check this with `docker inspect `), otherwise it will randomly pick one (depending on how docker is returning them).

When deploying a stack from a compose file `stack`, the networks defined are prefixed with `stack`. | `mynetwork` | diff --git a/docs/content/reference/routing-configuration/other-providers/ecs.md b/docs/content/reference/routing-configuration/other-providers/ecs.md index 03b4f4d45..a69e43b6b 100644 --- a/docs/content/reference/routing-configuration/other-providers/ecs.md +++ b/docs/content/reference/routing-configuration/other-providers/ecs.md @@ -13,7 +13,96 @@ With ECS, Traefik can leverage labels attached to a container to generate routin We recommend to *not* use labels to store sensitive data (certificates, credentials, etc). Instead, we recommend to store sensitive data in a safer storage (secrets, file, etc). -## Routing Configurationred +## Configuration Examples + +??? example "Configuring ECS & Deploying / Exposing one Service" + + Enabling the ECS provider + + ```yaml tab="Structured (YAML)" + providers: + ecs: {} + ``` + + ```toml tab="Structured (TOML)" + [providers.ecs] + ``` + + ```bash tab="CLI" + --providers.ecs=true + ``` + + Attaching labels to containers (in your ECS task definition) + + ```json + { + "family": "my-service", + "containerDefinitions": [ + { + "name": "my-container", + "image": "my-image:latest", + "labels": { + "traefik.http.routers.my-container.rule": "Host(`example.com`)" + } + } + ] + } + ``` + +??? example "Specify a Custom Port for the Container" + + Forward requests for `http://example.com` to `http://:12345`: + + ```json + { + "family": "my-service", + "containerDefinitions": [ + { + "name": "my-container", + "image": "my-image:latest", + "labels": { + "traefik.http.routers.my-container.rule": "Host(`example.com`)", + "traefik.http.routers.my-container.service": "my-service", + "traefik.http.services.my-service.loadbalancer.server.port": "12345" + } + } + ] + } + ``` + + !!! important "Traefik Connecting to the Wrong Port: `HTTP/502 Gateway Error`" + By default, Traefik uses the first exposed port of a container. + + Setting the label `traefik.http.services.xxx.loadbalancer.server.port` + overrides that behavior. + +??? example "Specifying more than one router and service per container" + + Forwarding requests to more than one port on a container requires referencing the service loadbalancer port definition using the service parameter on the router. + + In this example, requests are forwarded for `http://example-a.com` to `http://:8000` in addition to `http://example-b.com` forwarding to `http://:9000`: + + ```json + { + "family": "my-service", + "containerDefinitions": [ + { + "name": "my-container", + "image": "my-image:latest", + "labels": { + "traefik.http.routers.www-router.rule": "Host(`example-a.com`)", + "traefik.http.routers.www-router.service": "www-service", + "traefik.http.services.www-service.loadbalancer.server.port": "8000", + "traefik.http.routers.admin-router.rule": "Host(`example-b.com`)", + "traefik.http.routers.admin-router.service": "admin-service", + "traefik.http.services.admin-service.loadbalancer.server.port": "9000" + } + } + ] + } + ``` + +## Configuration Options !!! info "labels" @@ -37,120 +126,24 @@ For example, to change the rule, you could add the label ```traefik.http.routers !!! warning "The character `@` is not authorized in the router name ``." -??? info "`traefik.http.routers..rule`" - - See [rule](../http/routing/rules-and-priority.md#rules) for more information. - - ```yaml - traefik.http.routers.myrouter.rule=Host(`example.com`) - ``` +#### Configuration Options -??? info "`traefik.http.routers..ruleSyntax`" - - !!! warning - - RuleSyntax option is deprecated and will be removed in the next major version. - Please do not use this field and rewrite the router rules to use the v3 syntax. - - See [ruleSyntax](../http/routing/rules-and-priority.md#rulesyntax) for more information. - - ```yaml - traefik.http.routers.myrouter.ruleSyntax=v3 - ``` - -??? info "`traefik.http.routers..entrypoints`" - - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - traefik.http.routers.myrouter.entrypoints=web,websecure - ``` - -??? info "`traefik.http.routers..middlewares`" - - See [middlewares overview](../http/middlewares/overview.md) for more information. - - ```yaml - traefik.http.routers.myrouter.middlewares=auth,prefix,cb - ``` - -??? info "`traefik.http.routers..service`" - - See [service](../http/load-balancing/service.md) for more information. - - ```yaml - traefik.http.routers.myrouter.service=myservice - ``` - -??? info "`traefik.http.routers..tls`" - - See [tls](../http/tls/overview.md) for more information. - - ```yaml - traefik.http.routers.myrouter.tls=true - ``` - -??? info "`traefik.http.routers..tls.certresolver`" - - See [certResolver](../../install-configuration/tls/certificate-resolvers/overview.md) for more information. - - ```yaml - traefik.http.routers.myrouter.tls.certresolver=myresolver - ``` - -??? info "`traefik.http.routers..tls.domains[n].main`" - - See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. - - ```yaml - traefik.http.routers.myrouter.tls.domains[0].main=example.org - ``` - -??? info "`traefik.http.routers..tls.domains[n].sans`" - - See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. - - ```yaml - traefik.http.routers.myrouter.tls.domains[0].sans=test.example.org,dev.example.org - ``` - -??? info "`traefik.http.routers..tls.options`" - - ```yaml - traefik.http.routers.myrouter.tls.options=foobar - ``` - -??? info "`traefik.http.routers..observability.accesslogs`" - - The accessLogs option controls whether the router will produce access-logs. - - ```yaml - "traefik.http.routers.myrouter.observability.accesslogs=true" - ``` - -??? info "`traefik.http.routers..observability.metrics`" - - The metrics option controls whether the router will produce metrics. - - ```yaml - "traefik.http.routers.myrouter.observability.metrics=true" - ``` - -??? info "`traefik.http.routers..observability.tracing`" - - The tracing option controls whether the router will produce traces. - - ```yaml - "traefik.http.routers.myrouter.observability.tracing=true" - ``` - -??? info "`traefik.http.routers..priority`" - - See [priority](../http/routing/rules-and-priority.md#priority-calculation) for more information. - - ```yaml - traefik.http.routers.myrouter.priority=42 - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.http.routers..rule` | See [rule](../http/routing/rules-and-priority.md#rules) for more information. | ```Host(`example.com`)``` | +| `traefik.http.routers..ruleSyntax` | See [ruleSyntax](../http/routing/rules-and-priority.md#rulesyntax) for more information.
RuleSyntax option is deprecated and will be removed in the next major version.
Please do not use this field and rewrite the router rules to use the v3 syntax. | `v3` | +| `traefik.http.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `web,websecure` | +| `traefik.http.routers..middlewares` | See [middlewares overview](../http/middlewares/overview.md) for more information. | `auth,prefix,cb` | +| `traefik.http.routers..service` | See [service](../http/load-balancing/service.md) for more information. | `myservice` | +| `traefik.http.routers..tls` | See [tls](../http/tls/overview.md) for more information. | `true` | +| `traefik.http.routers..tls.certresolver` | See [certResolver](../../install-configuration/tls/certificate-resolvers/overview.md) for more information. | `myresolver` | +| `traefik.http.routers..tls.domains[n].main` | See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. | `example.org` | +| `traefik.http.routers..tls.domains[n].sans` | See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. | `test.example.org,dev.example.org` | +| `traefik.http.routers..tls.options` | | `foobar` | +| `traefik.http.routers..observability.accesslogs` | The accessLogs option controls whether the router will produce access-logs. | `true` | +| `traefik.http.routers..observability.metrics` | The metrics option controls whether the router will produce metrics. | `true` | +| `traefik.http.routers..observability.tracing` | The tracing option controls whether the router will produce traces. | `true` | +| `traefik.http.routers..priority` | See [priority](../http/routing/rules-and-priority.md#priority-calculation) for more information. | `42` | ### Services @@ -162,175 +155,33 @@ you'd add the label `traefik.http.services.{name-of-your-choice}.loadbalancer.pa !!! warning "The character `@` is not authorized in the service name ``." -??? info "`traefik.http.services..loadbalancer.server.port`" - - Registers a port. - Useful when the service exposes multiples ports. - - ```yaml - traefik.http.services.myservice.loadbalancer.server.port=8080 - ``` +#### Configuration Options -??? info "`traefik.http.services..loadbalancer.server.scheme`" - - Overrides the default scheme. - - ```yaml - traefik.http.services.myservice.loadbalancer.server.scheme=http - ``` - -??? info "`traefik.http.services..loadbalancer.serverstransport`" - - Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one. - See [serverstransport](../http/load-balancing/serverstransport.md) for more information. - - ```yaml - traefik.http.services..loadbalancer.serverstransport=foobar@file - ``` - -??? info "`traefik.http.services..loadbalancer.passhostheader`" - - ```yaml - traefik.http.services.myservice.loadbalancer.passhostheader=true - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.headers.`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.headers.X-Foo=foobar - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.hostname`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.hostname=example.org - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.interval`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.interval=10 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.unhealthyinterval`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.unhealthyinterval=10 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.path`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.path=/foo - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.method`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.status`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.status=42 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.port`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.port=42 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.scheme`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.scheme=http - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.timeout`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.timeout=10 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.followredirects`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.followredirects=true - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie=true - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.httponly`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.httponly=true - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.name`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.name=foobar - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.path`" - - ```yaml - "traefik.http.services.myservice.loadbalancer.sticky.cookie.path=/foobar" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.secure`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.secure=true - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.samesite`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.samesite=none - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.maxage`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.maxage=42 - ``` - -??? info "`traefik.http.services..loadbalancer.responseforwarding.flushinterval`" - - `FlushInterval` specifies the flush interval to flush to the client while copying the response body. - - ```yaml - traefik.http.services.myservice.loadbalancer.responseforwarding.flushinterval=10 - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.http.services..loadbalancer.server.port` | Registers a port.
Useful when the service exposes multiples ports. | `8080` | +| `traefik.http.services..loadbalancer.server.scheme` | Overrides the default scheme. | `http` | +| `traefik.http.services..loadbalancer.serverstransport` | Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one.
See [serverstransport](../http/load-balancing/serverstransport.md) for more information. | `foobar@file` | +| `traefik.http.services..loadbalancer.passhostheader` | | `true` | +| `traefik.http.services..loadbalancer.healthcheck.headers.` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `foobar` | +| `traefik.http.services..loadbalancer.healthcheck.hostname` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `example.org` | +| `traefik.http.services..loadbalancer.healthcheck.interval` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10` | +| `traefik.http.services..loadbalancer.healthcheck.unhealthyinterval` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10` | +| `traefik.http.services..loadbalancer.healthcheck.path` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `/foo` | +| `traefik.http.services..loadbalancer.healthcheck.method` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `foobar` | +| `traefik.http.services..loadbalancer.healthcheck.status` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `42` | +| `traefik.http.services..loadbalancer.healthcheck.port` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `42` | +| `traefik.http.services..loadbalancer.healthcheck.scheme` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `http` | +| `traefik.http.services..loadbalancer.healthcheck.timeout` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10` | +| `traefik.http.services..loadbalancer.healthcheck.followredirects` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.httponly` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.name` | | `foobar` | +| `traefik.http.services..loadbalancer.sticky.cookie.path` | | `/foobar` | +| `traefik.http.services..loadbalancer.sticky.cookie.secure` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.samesite` | | `none` | +| `traefik.http.services..loadbalancer.sticky.cookie.maxage` | | `42` | +| `traefik.http.services..loadbalancer.responseforwarding.flushinterval` | `FlushInterval` specifies the flush interval to flush to the client while copying the response body. | `10` | ### Middleware @@ -375,133 +226,32 @@ You can declare TCP Routers and/or Services using labels. #### TCP Routers -??? info "`traefik.tcp.routers..entrypoints`" - - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.entrypoints=ep1,ep2 - ``` +##### Configuration Options -??? info "`traefik.tcp.routers..rule`" - - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.rule=HostSNI(`example.com`) - ``` - -??? info "`traefik.tcp.routers..ruleSyntax`" - - !!! warning - - RuleSyntax option is deprecated and will be removed in the next major version. - Please do not use this field and rewrite the router rules to use the v3 syntax. - - configure the rule syntax to be used for parsing the rule on a per-router basis. - - ```yaml - traefik.tcp.routers.mytcprouter.ruleSyntax=v3 - ``` - -??? info "`traefik.tcp.routers..service`" - - See [service](../tcp/service.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.service=myservice - ``` - -??? info "`traefik.tcp.routers..tls`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls=true - ``` - -??? info "`traefik.tcp.routers..tls.certresolver`" - - See [certResolver](../tcp/tls.md#configuration-options) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.certresolver=myresolver - ``` - -??? info "`traefik.tcp.routers..tls.domains[n].main`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.domains[0].main=example.org - ``` - -??? info "`traefik.tcp.routers..tls.domains[n].sans`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.domains[0].sans=test.example.org,dev.example.org - ``` - -??? info "`traefik.tcp.routers..tls.options`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.options=mysoptions - ``` - -??? info "`traefik.tcp.routers..tls.passthrough`" - - See [Passthrough](../tcp/tls.md#opt-passthrough) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.passthrough=true - ``` - -??? info "`traefik.tcp.routers..priority`" - - See [priority](../tcp/routing/rules-and-priority.md#priority-calculation) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.priority=42 - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.tcp.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `ep1,ep2` | +| `traefik.tcp.routers..rule` | See [rule](../tcp/routing/rules-and-priority.md#rules) for more information. | ```HostSNI(`example.com`)``` | +| `traefik.tcp.routers..ruleSyntax` | configure the rule syntax to be used for parsing the rule on a per-router basis.
RuleSyntax option is deprecated and will be removed in the next major version.
Please do not use this field and rewrite the router rules to use the v3 syntax. | `v3` | +| `traefik.tcp.routers..service` | See [service](../tcp/service.md) for more information. | `myservice` | +| `traefik.tcp.routers..tls` | See [TLS](../tcp/tls.md) for more information. | `true` | +| `traefik.tcp.routers..tls.certresolver` | See [certResolver](../tcp/tls.md#configuration-options) for more information. | `myresolver` | +| `traefik.tcp.routers..tls.domains[n].main` | See [TLS](../tcp/tls.md) for more information. | `example.org` | +| `traefik.tcp.routers..tls.domains[n].sans` | See [TLS](../tcp/tls.md) for more information. | `test.example.org,dev.example.org` | +| `traefik.tcp.routers..tls.options` | See [TLS](../tcp/tls.md) for more information. | `mysoptions` | +| `traefik.tcp.routers..tls.passthrough` | See [Passthrough](../tcp/tls.md#opt-passthrough) for more information. | `true` | +| `traefik.tcp.routers..priority` | See [priority](../tcp/routing/rules-and-priority.md#priority-calculation) for more information. | `42` | #### TCP Services -??? info "`traefik.tcp.services..loadbalancer.server.port`" - - Registers a port of the application. - - ```yaml - traefik.tcp.services.mytcpservice.loadbalancer.server.port=423 - ``` +##### Configuration Options -??? info "`traefik.tcp.services..loadbalancer.server.tls`" - - Determines whether to use TLS when dialing with the backend. - - ```yaml - traefik.tcp.services.mytcpservice.loadbalancer.server.tls=true - ``` - -??? info "`traefik.http.services..loadbalancer.server.weight`" - - Overrides the default weight. - - ```yaml - traefik.http.services.myservice.loadbalancer.server.weight=42 - ``` - -??? info "`traefik.tcp.services..loadbalancer.serverstransport`" - - Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one. - See [serverstransport](../tcp/serverstransport.md) for more information. - - ```yaml - traefik.tcp.services..loadbalancer.serverstransport=foobar@file - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.tcp.services..loadbalancer.server.port` | Registers a port of the application. | `423` | +| `traefik.tcp.services..loadbalancer.server.tls` | Determines whether to use TLS when dialing with the backend. | `true` | +| `traefik.tcp.services..loadbalancer.server.weight` | Overrides the default weight. | `42` | +| `traefik.tcp.services..loadbalancer.serverstransport` | Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one.
See [serverstransport](../tcp/serverstransport.md) for more information. | `foobar@file` | ### UDP @@ -543,40 +293,25 @@ More information about available middlewares in the dedicated [middlewares secti #### UDP Routers -??? info "`traefik.udp.routers..entrypoints`" - - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - traefik.udp.routers.myudprouter.entrypoints=ep1,ep2 - ``` +##### Configuration Options -??? info "`traefik.udp.routers..service`" - - See [service](../udp/service.md) for more information. - - ```yaml - traefik.udp.routers.myudprouter.service=myservice - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.udp.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `ep1,ep2` | +| `traefik.udp.routers..service` | See [service](../udp/service.md) for more information. | `myservice` | #### UDP Services -??? info "`traefik.udp.services..loadbalancer.server.port`" - - Registers a port of the application. - - ```yaml - traefik.udp.services.myudpservice.loadbalancer.server.port=423 - ``` +##### Configuration Options + +| Label | Description | Value | +|------|-------------|-------| +| `traefik.udp.services..loadbalancer.server.port` | Registers a port of the application. | `423` | ### Specific Provider Options -#### `traefik.enable` +#### Configuration Options -```yaml -traefik.enable=true -``` - -You can tell Traefik to consider (or not) the ECS service by setting `traefik.enable` to true or false. - -This option overrides the value of `exposedByDefault`. +| Label | Description | Value | +|------|-------------|-------| +| `traefik.enable` | You can tell Traefik to consider (or not) the ECS service by setting `traefik.enable` to true or false.
This option overrides the value of `exposedByDefault`. | `true` | diff --git a/docs/content/reference/routing-configuration/other-providers/file.toml b/docs/content/reference/routing-configuration/other-providers/file.toml index 9a6c18e59..2f94a07dd 100644 --- a/docs/content/reference/routing-configuration/other-providers/file.toml +++ b/docs/content/reference/routing-configuration/other-providers/file.toml @@ -112,31 +112,33 @@ [http.services.Service03.loadBalancer.responseForwarding] flushInterval = "42s" [http.services.Service04] - [http.services.Service04.mirroring] + middlewares = ["foobar", "foobar"] + [http.services.Service05] + [http.services.Service05.mirroring] service = "foobar" mirrorBody = true maxBodySize = 42 - [[http.services.Service04.mirroring.mirrors]] + [[http.services.Service05.mirroring.mirrors]] name = "foobar" percent = 42 - [[http.services.Service04.mirroring.mirrors]] + [[http.services.Service05.mirroring.mirrors]] name = "foobar" percent = 42 - [http.services.Service04.mirroring.healthCheck] - [http.services.Service05] - [http.services.Service05.weighted] + [http.services.Service05.mirroring.healthCheck] + [http.services.Service06] + [http.services.Service06.weighted] - [[http.services.Service05.weighted.services]] + [[http.services.Service06.weighted.services]] name = "foobar" weight = 42 - [[http.services.Service05.weighted.services]] + [[http.services.Service06.weighted.services]] name = "foobar" weight = 42 - [http.services.Service05.weighted.sticky] - [http.services.Service05.weighted.sticky.cookie] + [http.services.Service06.weighted.sticky] + [http.services.Service06.weighted.sticky.cookie] name = "foobar" secure = true httpOnly = true @@ -144,7 +146,7 @@ maxAge = 42 path = "foobar" domain = "foobar" - [http.services.Service05.weighted.healthCheck] + [http.services.Service06.weighted.healthCheck] [http.middlewares] [http.middlewares.Middleware01] [http.middlewares.Middleware01.addPrefix] diff --git a/docs/content/reference/routing-configuration/other-providers/file.yaml b/docs/content/reference/routing-configuration/other-providers/file.yaml index c15d2e423..50200a4fb 100644 --- a/docs/content/reference/routing-configuration/other-providers/file.yaml +++ b/docs/content/reference/routing-configuration/other-providers/file.yaml @@ -120,6 +120,10 @@ http: flushInterval: 42s serversTransport: foobar Service04: + middlewares: + - foobar + - foobar + Service05: mirroring: service: foobar mirrorBody: true @@ -130,7 +134,7 @@ http: - name: foobar percent: 42 healthCheck: {} - Service05: + Service06: weighted: services: - name: foobar diff --git a/docs/content/reference/routing-configuration/other-providers/kv.md b/docs/content/reference/routing-configuration/other-providers/kv.md index 8f11d50ba..6707ac2ba 100644 --- a/docs/content/reference/routing-configuration/other-providers/kv.md +++ b/docs/content/reference/routing-configuration/other-providers/kv.md @@ -5,6 +5,62 @@ description: "Read the technical documentation to learn the Traefik Routing Conf # Traefik & KV Stores +## Configuration Examples + +??? example "Configuring KV Store & Deploying / Exposing one Service" + + Enabling a KV store provider (example: Consul) + + ```yaml tab="Structured (YAML)" + providers: + consul: + endpoints: + - "127.0.0.1:8500" + ``` + + ```toml tab="Structured (TOML)" + [providers.consul] + endpoints = ["127.0.0.1:8500"] + ``` + + ```bash tab="CLI" + --providers.consul.endpoints=127.0.0.1:8500 + ``` + + Setting keys in the KV store (example: Consul) + + ```bash + consul kv put traefik/http/routers/my-router/rule "Host(`example.com`)" + consul kv put traefik/http/routers/my-router/service "my-service" + consul kv put traefik/http/services/my-service/loadbalancer/servers/0/url "http://127.0.0.1:8080" + ``` + +??? example "Specify a Custom Port for the Service" + + Forward requests for `http://example.com` to `http://127.0.0.1:12345`: + + ```bash + consul kv put traefik/http/routers/my-router/rule "Host(`example.com`)" + consul kv put traefik/http/routers/my-router/service "my-service" + consul kv put traefik/http/services/my-service/loadbalancer/servers/0/url "http://127.0.0.1:12345" + ``` + +??? example "Specifying more than one router and service" + + Forwarding requests to more than one service requires defining multiple routers and services. + + In this example, requests are forwarded for `http://example-a.com` to `http://127.0.0.1:8000` in addition to `http://example-b.com` forwarding to `http://127.0.0.1:9000`: + + ```bash + consul kv put traefik/http/routers/www-router/rule "Host(`example-a.com`)" + consul kv put traefik/http/routers/www-router/service "www-service" + consul kv put traefik/http/services/www-service/loadbalancer/servers/0/url "http://127.0.0.1:8000" + + consul kv put traefik/http/routers/admin-router/rule "Host(`example-b.com`)" + consul kv put traefik/http/routers/admin-router/service "admin-service" + consul kv put traefik/http/services/admin-service/loadbalancer/servers/0/url "http://127.0.0.1:9000" + ``` + ## Configuration Options !!! info "Keys" @@ -93,15 +149,6 @@ description: "Read the technical documentation to learn the Traefik Routing Conf If you declare multiple middleware with the same name but with different parameters, the middleware fails to be declared. -##### Configuration Example - -```bash -# Declaring a middleware -traefik/http/middlewares/myAddPrefix/addPrefix/prefix=/foobar -# Referencing a middleware -traefik/http/routers//middlewares/0=myAddPrefix -``` - #### ServerTransport ##### Configuration Options @@ -110,17 +157,6 @@ traefik/http/routers//middlewares/0=myAddPrefix |-----------------------------------------------------------------|-----------------------------------------------------------------|-----------------------------------------| | `traefik/http/serversTransports//st_option` | With `st_option` the ServerTransport option to set (ex `maxIdleConnsPerHost`).
More information about available options in the dedicated [ServerTransport section](../http/load-balancing/serverstransport.md). | ServerTransport Options | -##### Configuration Example - -```bash -# Declaring a ServerTransport -traefik/http/serversTransports/myServerTransport/maxIdleConnsPerHost=-1 -traefik/http/serversTransports/myServerTransport/certificates/0/certFile=mypath/cert.pem -traefik/http/serversTransports/myServerTransport/certificates/0/keyFile=mypath/key.pem -# Referencing a middleware -traefik/http/services/myService/serversTransports/0=myServerTransport -``` - ### TCP You can declare TCP Routers and/or Services using KV. @@ -170,15 +206,6 @@ More information about available middlewares in the dedicated [middlewares secti If you declare multiple middleware with the same name but with different parameters, the middleware fails to be declared. -##### Configuration Example - -```bash -# Declaring a middleware -traefik/tcp/middlewares/test-inflightconn/amount=10 -# Referencing a middleware -traefik/tcp/routers//middlewares/0=test-inflightconn -``` - #### ServerTransport ##### Configuration Options @@ -187,15 +214,6 @@ traefik/tcp/routers//middlewares/0=test-inflightconn |-----------------------------------------------------------------|-----------------------------------------------------------------|-----------------------------------------| | `traefik/tcp/serversTransports//st_option` | With `st_option` the ServerTransport option to set (ex `maxIdleConnsPerHost`).
More information about available options in the dedicated [ServerTransport section](../tcp/serverstransport.md). | ServerTransport Options | -##### Configuration Example - -```bash -# Declaring a ServerTransport -traefik/tcp/serversTransports/myServerTransport/maxIdleConnsPerHost=-1 -# Referencing a middleware -traefik/tcp/services/myService/serversTransports/0=myServerTransport -``` - ### UDP You can declare UDP Routers and/or Services using KV. diff --git a/docs/content/reference/routing-configuration/other-providers/nomad.md b/docs/content/reference/routing-configuration/other-providers/nomad.md index 18c962ed7..d4bf63b9e 100644 --- a/docs/content/reference/routing-configuration/other-providers/nomad.md +++ b/docs/content/reference/routing-configuration/other-providers/nomad.md @@ -13,7 +13,108 @@ With Nomad, Traefik can leverage tags attached to a service to generate routing We recommend to *not* use tags to store sensitive data (certificates, credentials, etc). Instead, we recommend to store sensitive data in a safer storage (secrets, file, etc). -## Routing Configuration +## Configuration Examples + +??? example "Configuring Nomad & Deploying / Exposing one Service" + + Enabling the nomad provider + + ```yaml tab="Structured (YAML)" + providers: + nomad: {} + ``` + + ```toml tab="Structured (TOML)" + [providers.nomad] + ``` + + ```bash tab="CLI" + --providers.nomad=true + ``` + + Attaching tags to services (in your Nomad job file) + + ```hcl + job "my-service" { + datacenters = ["dc1"] + + group "web" { + task "app" { + driver = "docker" + + service { + name = "my-service" + tags = [ + "traefik.http.routers.my-service.rule=Host(`example.com`)", + ] + } + } + } + } + ``` + +??? example "Specify a Custom Port for the Container" + + Forward requests for `http://example.com` to `http://:12345`: + + ```hcl + job "my-service" { + datacenters = ["dc1"] + + group "web" { + task "app" { + driver = "docker" + + service { + name = "my-service" + tags = [ + "traefik.http.routers.my-service.rule=Host(`example.com`)", + "traefik.http.routers.my-service.service=my-service", + "traefik.http.services.my-service.loadbalancer.server.port=12345", + ] + } + } + } + } + ``` + + !!! important "Traefik Connecting to the Wrong Port: `HTTP/502 Gateway Error`" + By default, Traefik uses the first exposed port of a container. + + Setting the tag `traefik.http.services.xxx.loadbalancer.server.port` + overrides that behavior. + +??? example "Specifying more than one router and service per container" + + Forwarding requests to more than one port on a container requires referencing the service loadbalancer port definition using the service parameter on the router. + + In this example, requests are forwarded for `http://example-a.com` to `http://:8000` in addition to `http://example-b.com` forwarding to `http://:9000`: + + ```hcl + job "my-service" { + datacenters = ["dc1"] + + group "web" { + task "app" { + driver = "docker" + + service { + name = "my-service" + tags = [ + "traefik.http.routers.www-router.rule=Host(`example-a.com`)", + "traefik.http.routers.www-router.service=www-service", + "traefik.http.services.www-service.loadbalancer.server.port=8000", + "traefik.http.routers.admin-router.rule=Host(`example-b.com`)", + "traefik.http.routers.admin-router.service=admin-service", + "traefik.http.services.admin-service.loadbalancer.server.port=9000", + ] + } + } + } + } + ``` + +## Configuration Options !!! info "Tags" @@ -35,120 +136,24 @@ To update the configuration of the Router automatically attached to the service, For example, to change the rule, you could add the tag ```traefik.http.routers.my-service.rule=Host(`example.com`)```. -??? info "`traefik.http.routers..rule`" +#### Configuration Options - See [rule](../http/routing/rules-and-priority.md) for more information. - - ```yaml - traefik.http.routers.myrouter.rule=Host(`example.com`) - ``` - -??? info "`traefik.http.routers..ruleSyntax`" - - !!! warning - - RuleSyntax option is deprecated and will be removed in the next major version. - Please do not use this field and rewrite the router rules to use the v3 syntax. - - See [ruleSyntax](../http/routing/rules-and-priority.md#rulesyntax) for more information. - - ```yaml - traefik.http.routers.myrouter.ruleSyntax=v3 - ``` - -??? info "`traefik.http.routers..entrypoints`" - - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - traefik.http.routers.myrouter.entrypoints=web,websecure - ``` - -??? info "`traefik.http.routers..middlewares`" - - See [middlewares overview](../http/middlewares/overview.md) for more information. - - ```yaml - traefik.http.routers.myrouter.middlewares=auth,prefix,cb - ``` - -??? info "`traefik.http.routers..service`" - - See [service](../http/load-balancing/service.md) for more information. - - ```yaml - traefik.http.routers.myrouter.service=myservice - ``` - -??? info "`traefik.http.routers..tls`" - - See [tls](../http/tls/overview.md) for more information. - - ```yaml - traefik.http.routers.myrouter.tls=true - ``` - -??? info "`traefik.http.routers..tls.certresolver`" - - See [certResolver](../../install-configuration/tls/certificate-resolvers/overview.md) for more information. - - ```yaml - traefik.http.routers.myrouter.tls.certresolver=myresolver - ``` - -??? info "`traefik.http.routers..tls.domains[n].main`" - - See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. - - ```yaml - traefik.http.routers.myrouter.tls.domains[0].main=example.org - ``` - -??? info "`traefik.http.routers..tls.domains[n].sans`" - - See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. - - ```yaml - traefik.http.routers.myrouter.tls.domains[0].sans=test.example.org,dev.example.org - ``` - -??? info "`traefik.http.routers..tls.options`" - - ```yaml - traefik.http.routers.myrouter.tls.options=foobar - ``` - -??? info "`traefik.http.routers..priority`" - - See [priority](../http/routing/rules-and-priority.md#priority-calculation) for more information. - - ```yaml - traefik.http.routers.myrouter.priority=42 - ``` - -??? info "`traefik.http.routers..observability.accesslogs`" - - The accessLogs option controls whether the router will produce access-logs. - - ```yaml - "traefik.http.routers.myrouter.observability.accesslogs=true" - ``` - -??? info "`traefik.http.routers..observability.metrics`" - - The metrics option controls whether the router will produce metrics. - - ```yaml - "traefik.http.routers.myrouter.observability.metrics=true" - ``` - -??? info "`traefik.http.routers..observability.tracing`" - - The tracing option controls whether the router will produce traces. - - ```yaml - "traefik.http.routers.myrouter.observability.tracing=true" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.http.routers..rule` | See [rule](../http/routing/rules-and-priority.md#rules) for more information. | ```Host(`example.com`)``` | +| `traefik.http.routers..ruleSyntax` | See [ruleSyntax](../http/routing/rules-and-priority.md#rulesyntax) for more information.
RuleSyntax option is deprecated and will be removed in the next major version.
Please do not use this field and rewrite the router rules to use the v3 syntax. | `v3` | +| `traefik.http.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `web,websecure` | +| `traefik.http.routers..middlewares` | See [middlewares overview](../http/middlewares/overview.md) for more information. | `auth,prefix,cb` | +| `traefik.http.routers..service` | See [service](../http/load-balancing/service.md) for more information. | `myservice` | +| `traefik.http.routers..tls` | See [tls](../http/tls/overview.md) for more information. | `true` | +| `traefik.http.routers..tls.certresolver` | See [certResolver](../../install-configuration/tls/certificate-resolvers/overview.md) for more information. | `myresolver` | +| `traefik.http.routers..tls.domains[n].main` | See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. | `example.org` | +| `traefik.http.routers..tls.domains[n].sans` | See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. | `test.example.org,dev.example.org` | +| `traefik.http.routers..tls.options` | | `foobar` | +| `traefik.http.routers..priority` | See [priority](../http/routing/rules-and-priority.md#priority-calculation) for more information. | `42` | +| `traefik.http.routers..observability.accesslogs` | The accessLogs option controls whether the router will produce access-logs. | `true` | +| `traefik.http.routers..observability.metrics` | The metrics option controls whether the router will produce metrics. | `true` | +| `traefik.http.routers..observability.tracing` | The tracing option controls whether the router will produce traces. | `true` | ### Services @@ -158,173 +163,33 @@ add tags starting with `traefik.http.services.{name-of-your-choice}.`, followed For example, to change the `passHostHeader` behavior, you'd add the tag `traefik.http.services.{name-of-your-choice}.loadbalancer.passhostheader=false`. -??? info "`traefik.http.services..loadbalancer.server.port`" +#### Configuration Options - Registers a port. - Useful when the service exposes multiples ports. - - ```yaml - traefik.http.services.myservice.loadbalancer.server.port=8080 - ``` - -??? info "`traefik.http.services..loadbalancer.server.scheme`" - - Overrides the default scheme. - - ```yaml - traefik.http.services.myservice.loadbalancer.server.scheme=http - ``` - -??? info "`traefik.http.services..loadbalancer.server.weight`" - - Overrides the default weight. - - ```yaml - traefik.http.services.myservice.loadbalancer.server.weight=42 - ``` - -??? info "`traefik.http.services..loadbalancer.serverstransport`" - - Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one. - See [serverstransport](../http/load-balancing/serverstransport.md) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.serverstransport=foobar@file - ``` - -??? info "`traefik.http.services..loadbalancer.passhostheader`" - - ```yaml - traefik.http.services.myservice.loadbalancer.passhostheader=true - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.headers.`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.headers.X-Foo=foobar - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.hostname`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.hostname=example.org - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.interval`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.interval=10 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.unhealthyinterval`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.unhealthyinterval=10 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.path`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.path=/foo - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.status`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.status=42 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.port`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.port=42 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.scheme`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.scheme=http - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.timeout`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.timeout=10 - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.followredirects`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - traefik.http.services.myservice.loadbalancer.healthcheck.followredirects=true - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie=true - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.httponly`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.httponly=true - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.name`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.name=foobar - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.path`" - - ```yaml - - "traefik.http.services.myservice.loadbalancer.sticky.cookie.path=/foobar" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.secure`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.secure=true - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.samesite`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.samesite=none - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.maxage`" - - ```yaml - traefik.http.services.myservice.loadbalancer.sticky.cookie.maxage=42 - ``` - -??? info "`traefik.http.services..loadbalancer.responseforwarding.flushinterval`" - - ```yaml - traefik.http.services.myservice.loadbalancer.responseforwarding.flushinterval=10 - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.http.services..loadbalancer.server.port` | Registers a port.
Useful when the service exposes multiples ports. | `8080` | +| `traefik.http.services..loadbalancer.server.scheme` | Overrides the default scheme. | `http` | +| `traefik.http.services..loadbalancer.server.weight` | Overrides the default weight. | `42` | +| `traefik.http.services..loadbalancer.serverstransport` | Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one.
See [serverstransport](../http/load-balancing/serverstransport.md) for more information. | `foobar@file` | +| `traefik.http.services..loadbalancer.passhostheader` | | `true` | +| `traefik.http.services..loadbalancer.healthcheck.headers.` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `foobar` | +| `traefik.http.services..loadbalancer.healthcheck.hostname` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `example.org` | +| `traefik.http.services..loadbalancer.healthcheck.interval` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10` | +| `traefik.http.services..loadbalancer.healthcheck.unhealthyinterval` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10` | +| `traefik.http.services..loadbalancer.healthcheck.path` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `/foo` | +| `traefik.http.services..loadbalancer.healthcheck.status` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `42` | +| `traefik.http.services..loadbalancer.healthcheck.port` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `42` | +| `traefik.http.services..loadbalancer.healthcheck.scheme` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `http` | +| `traefik.http.services..loadbalancer.healthcheck.timeout` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10` | +| `traefik.http.services..loadbalancer.healthcheck.followredirects` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.httponly` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.name` | | `foobar` | +| `traefik.http.services..loadbalancer.sticky.cookie.path` | | `/foobar` | +| `traefik.http.services..loadbalancer.sticky.cookie.secure` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.samesite` | | `none` | +| `traefik.http.services..loadbalancer.sticky.cookie.maxage` | | `42` | +| `traefik.http.services..loadbalancer.responseforwarding.flushinterval` | | `10` | ### Middleware @@ -367,125 +232,31 @@ You can declare TCP Routers and/or Services using tags. #### TCP Routers -??? info "`traefik.tcp.routers..entrypoints`" +##### Configuration Options - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.entrypoints=ep1,ep2 - ``` - -??? info "`traefik.tcp.routers..rule`" - - See [rule](../tcp/routing/rules-and-priority.md#rules) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.rule=HostSNI(`example.com`) - ``` - -??? info "`traefik.tcp.routers..ruleSyntax`" - - !!! warning - - RuleSyntax option is deprecated and will be removed in the next major version. - Please do not use this field and rewrite the router rules to use the v3 syntax. - - configure the rule syntax to be used for parsing the rule on a per-router basis. - - ```yaml - traefik.tcp.routers.mytcprouter.ruleSyntax=v3 - ``` - -??? info "`traefik.tcp.routers..priority`" - - See [priority](../tcp/routing/rules-and-priority.md#priority-calculation) for more information. - - ```yaml - traefik.tcp.routers.myrouter.priority=42 - ``` - -??? info "`traefik.tcp.routers..service`" - - See [service](../tcp/service.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.service=myservice - ``` - -??? info "`traefik.tcp.routers..tls`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls=true - ``` - -??? info "`traefik.tcp.routers..tls.certresolver`" - - See [certResolver](../tcp/tls.md#configuration-options) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.certresolver=myresolver - ``` - -??? info "`traefik.tcp.routers..tls.domains[n].main`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.domains[0].main=example.org - ``` - -??? info "`traefik.tcp.routers..tls.domains[n].sans`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.domains[0].sans=test.example.org,dev.example.org - ``` - -??? info "`traefik.tcp.routers..tls.options`" - - See [TLS](../tcp/tls.md#configuration-options) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.options=myoptions - ``` - -??? info "`traefik.tcp.routers..tls.passthrough`" - - See [Passthrough](../tcp/tls.md#opt-passthrough) for more information. - - ```yaml - traefik.tcp.routers.mytcprouter.tls.passthrough=true - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.tcp.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `ep1,ep2` | +| `traefik.tcp.routers..rule` | See [rule](../tcp/routing/rules-and-priority.md#rules) for more information. | ```HostSNI(`example.com`)``` | +| `traefik.tcp.routers..ruleSyntax` | configure the rule syntax to be used for parsing the rule on a per-router basis.
RuleSyntax option is deprecated and will be removed in the next major version.
Please do not use this field and rewrite the router rules to use the v3 syntax. | `v3` | +| `traefik.tcp.routers..priority` | See [priority](../tcp/routing/rules-and-priority.md#priority-calculation) for more information. | `42` | +| `traefik.tcp.routers..service` | See [service](../tcp/service.md) for more information. | `myservice` | +| `traefik.tcp.routers..tls` | See [TLS](../tcp/tls.md) for more information. | `true` | +| `traefik.tcp.routers..tls.certresolver` | See [certResolver](../tcp/tls.md#configuration-options) for more information. | `myresolver` | +| `traefik.tcp.routers..tls.domains[n].main` | See [TLS](../tcp/tls.md) for more information. | `example.org` | +| `traefik.tcp.routers..tls.domains[n].sans` | See [TLS](../tcp/tls.md) for more information. | `test.example.org,dev.example.org` | +| `traefik.tcp.routers..tls.options` | See [TLS](../tcp/tls.md#configuration-options) for more information. | `myoptions` | +| `traefik.tcp.routers..tls.passthrough` | See [Passthrough](../tcp/tls.md#opt-passthrough) for more information. | `true` | #### TCP Services -??? info "`traefik.tcp.services..loadbalancer.server.port`" +##### Configuration Options - Registers a port of the application. - - ```yaml - traefik.tcp.services.mytcpservice.loadbalancer.server.port=423 - ``` - -??? info "`traefik.tcp.services..loadbalancer.server.tls`" - - Determines whether to use TLS when dialing with the backend. - - ```yaml - traefik.tcp.services.mytcpservice.loadbalancer.server.tls=true - ``` - -??? info "`traefik.tcp.services..loadbalancer.serverstransport`" - - Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one. - See [serverstransport](../tcp/serverstransport.md) for more information. - - ```yaml - traefik.tcp.services.myservice.loadbalancer.serverstransport=foobar@file - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.tcp.services..loadbalancer.server.port` | Registers a port of the application. | `423` | +| `traefik.tcp.services..loadbalancer.server.tls` | Determines whether to use TLS when dialing with the backend. | `true` | +| `traefik.tcp.services..loadbalancer.serverstransport` | Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one.
See [serverstransport](../tcp/serverstransport.md) for more information. | `foobar@file` | #### TCP Middleware @@ -527,56 +298,27 @@ You can declare UDP Routers and/or Services using tags. #### UDP Routers -??? info "`traefik.udp.routers..entrypoints`" +##### Configuration Options - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - traefik.udp.routers.myudprouter.entrypoints=ep1,ep2 - ``` - -??? info "`traefik.udp.routers..service`" - - See [service](../udp/service.md) for more information. - - ```yaml - traefik.udp.routers.myudprouter.service=myservice - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.udp.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `ep1,ep2` | +| `traefik.udp.routers..service` | See [service](../udp/service.md) for more information. | `myservice` | #### UDP Services -??? info "`traefik.udp.services..loadbalancer.server.port`" +##### Configuration Options - Registers a port of the application. - - ```yaml - traefik.udp.services.myudpservice.loadbalancer.server.port=423 - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.udp.services..loadbalancer.server.port` | Registers a port of the application. | `423` | ### Specific Provider Options -#### `traefik.enable` - -```yaml -traefik.enable=true -``` - -You can tell Traefik to consider (or not) the service by setting `traefik.enable` to true or false. - -This option overrides the value of `exposedByDefault`. - -#### `traefik.nomad.canary` - -```yaml -traefik.nomad.canary=true -``` - -When Nomad orchestrator is a provider (of service registration) for Traefik, -one might have the need to distinguish within Traefik between a [Canary](https://learn.hashicorp.com/tutorials/nomad/job-blue-green-and-canary-deployments#deploy-with-canaries) instance of a service, or a production one. -For example if one does not want them to be part of the same load-balancer. - -Therefore, this option, which is meant to be provided as one of the values of the `canary_tags` field in the Nomad [service stanza](https://www.nomadproject.io/docs/job-specification/service#canary_tags), -allows Traefik to identify that the associated instance is a canary one. +| Label | Description | Value | +|------|-------------|-------| +| `traefik.enable` | You can tell Traefik to consider (or not) the service by setting `traefik.enable` to true or false.
This option overrides the value of `exposedByDefault`. | `true` | +| `traefik.nomad.canary` | When Nomad orchestrator is a provider (of service registration) for Traefik, one might have the need to distinguish within Traefik between a [Canary](https://learn.hashicorp.com/tutorials/nomad/job-blue-green-and-canary-deployments#deploy-with-canaries) instance of a service, or a production one.
For example if one does not want them to be part of the same load-balancer.

Therefore, this option, which is meant to be provided as one of the values of the `canary_tags` field in the Nomad [service stanza](https://www.nomadproject.io/docs/job-specification/service#canary_tags), allows Traefik to identify that the associated instance is a canary one. | `true` | #### Port Lookup diff --git a/docs/content/reference/routing-configuration/other-providers/swarm.md b/docs/content/reference/routing-configuration/other-providers/swarm.md index 2885b1e48..a49e6c861 100644 --- a/docs/content/reference/routing-configuration/other-providers/swarm.md +++ b/docs/content/reference/routing-configuration/other-providers/swarm.md @@ -19,7 +19,7 @@ With Docker Swarm, Traefik can leverage labels attached to a service to generate Enabling the docker provider (Swarm Mode) - ```yaml tab="File (YAML)" + ```yaml tab="Structured (YAML)" providers: swarm: # swarm classic (1.12-) @@ -28,7 +28,7 @@ With Docker Swarm, Traefik can leverage labels attached to a service to generate endpoint: "tcp://127.0.0.1:2377" ``` - ```toml tab="File (TOML)" + ```toml tab="Structured (TOML)" [providers.swarm] # swarm classic (1.12-) # endpoint = "tcp://127.0.0.1:2375" @@ -104,7 +104,7 @@ With Docker Swarm, Traefik can leverage labels attached to a service to generate - traefik.http.services.admin-service.loadbalancer.server.port=9000 ``` -## Routing Configuration +## Configuration Options !!! info "Labels" @@ -156,120 +156,24 @@ For example, to change the rule, you could add the label ```traefik.http.routers !!! warning "The character `@` is not authorized in the router name ``." -??? info "`traefik.http.routers..rule`" +#### Configuration Options - See [rule](../http/routing/rules-and-priority.md) for more information. - - ```yaml - - "traefik.http.routers.myrouter.rule=Host(`example.com`)" - ``` - -??? info "`traefik.http.routers..ruleSyntax`" - - !!! warning - - RuleSyntax option is deprecated and will be removed in the next major version. - Please do not use this field and rewrite the router rules to use the v3 syntax. - - See [ruleSyntax](../http/routing/rules-and-priority.md#rulesyntax) for more information. - - ```yaml - traefik.http.routers.myrouter.ruleSyntax=v3 - ``` - -??? info "`traefik.http.routers..entrypoints`" - - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - - "traefik.http.routers.myrouter.entrypoints=ep1,ep2" - ``` - -??? info "`traefik.http.routers..middlewares`" - - See [middlewares overview](../http/middlewares/overview.md) for more information. - - ```yaml - - "traefik.http.routers.myrouter.middlewares=auth,prefix,cb" - ``` - -??? info "`traefik.http.routers..service`" - - See [service](../http/load-balancing/service.md) for more information. - - ```yaml - - "traefik.http.routers.myrouter.service=myservice" - ``` - -??? info "`traefik.http.routers..tls`" - - See [tls](../http/tls/overview.md) for more information. - - ```yaml - - "traefik.http.routers.myrouter.tls=true" - ``` - -??? info "`traefik.http.routers..tls.certresolver`" - - See [certResolver](../../install-configuration/tls/certificate-resolvers/overview.md) for more information. - - ```yaml - - "traefik.http.routers.myrouter.tls.certresolver=myresolver" - ``` - -??? info "`traefik.http.routers..tls.domains[n].main`" - - See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. - - ```yaml - - "traefik.http.routers.myrouter.tls.domains[0].main=example.org" - ``` - -??? info "`traefik.http.routers..tls.domains[n].sans`" - - See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. - - ```yaml - - "traefik.http.routers.myrouter.tls.domains[0].sans=test.example.org,dev.example.org" - ``` - -??? info "`traefik.http.routers..tls.options`" - - ```yaml - - "traefik.http.routers.myrouter.tls.options=foobar" - ``` - -??? info "`traefik.http.routers..observability.accesslogs`" - - The accessLogs option controls whether the router will produce access-logs. - - ```yaml - "traefik.http.routers.myrouter.observability.accesslogs=true" - ``` - -??? info "`traefik.http.routers..observability.metrics`" - - The metrics option controls whether the router will produce metrics. - - ```yaml - "traefik.http.routers.myrouter.observability.metrics=true" - ``` - -??? info "`traefik.http.routers..observability.tracing`" - - The tracing option controls whether the router will produce traces. - - ```yaml - "traefik.http.routers.myrouter.observability.tracing=true" - ``` - -??? info "`traefik.http.routers..priority`" - - See [priority](../http/routing/rules-and-priority.md#priority-calculation) for more information. - - ```yaml - - "traefik.http.routers.myrouter.priority=42" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.http.routers..rule` | See [rule](../http/routing/rules-and-priority.md#rules) for more information. | ```Host(`example.com`)``` | +| `traefik.http.routers..ruleSyntax` | See [ruleSyntax](../http/routing/rules-and-priority.md#rulesyntax) for more information.
RuleSyntax option is deprecated and will be removed in the next major version.
Please do not use this field and rewrite the router rules to use the v3 syntax. | `v3` | +| `traefik.http.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `ep1,ep2` | +| `traefik.http.routers..middlewares` | See [middlewares overview](../http/middlewares/overview.md) for more information. | `auth,prefix,cb` | +| `traefik.http.routers..service` | See [service](../http/load-balancing/service.md) for more information. | `myservice` | +| `traefik.http.routers..tls` | See [tls](../http/tls/overview.md) for more information. | `true` | +| `traefik.http.routers..tls.certresolver` | See [certResolver](../../install-configuration/tls/certificate-resolvers/overview.md) for more information. | `myresolver` | +| `traefik.http.routers..tls.domains[n].main` | See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. | `example.org` | +| `traefik.http.routers..tls.domains[n].sans` | See [domains](../../install-configuration/tls/certificate-resolvers/acme.md#domain-definition) for more information. | `test.example.org,dev.example.org` | +| `traefik.http.routers..tls.options` | | `foobar` | +| `traefik.http.routers..observability.accesslogs` | The accessLogs option controls whether the router will produce access-logs. | `true` | +| `traefik.http.routers..observability.metrics` | The metrics option controls whether the router will produce metrics. | `true` | +| `traefik.http.routers..observability.tracing` | The tracing option controls whether the router will produce traces. | `true` | +| `traefik.http.routers..priority` | See [priority](../http/routing/rules-and-priority.md#priority-calculation) for more information. | `42` | ### Services @@ -281,189 +185,34 @@ you'd add the label `traefik.http.services..loadbalancer.pa !!! warning "The character `@` is not authorized in the service name ``." -??? info "`traefik.http.services..loadbalancer.server.port`" +#### Configuration Options - Registers a port. - Useful when the container exposes multiples ports. - - Mandatory for Docker Swarm (see the section ["Port Detection with Docker Swarm"](../../install-configuration/providers/swarm.md#port-detection)). - {: #port } - - ```yaml - - "traefik.http.services.myservice.loadbalancer.server.port=8080" - ``` - -??? info "`traefik.http.services..loadbalancer.server.scheme`" - - Overrides the default scheme. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.server.scheme=http" - ``` - -??? info "`traefik.http.services..loadbalancer.server.url`" - - Defines the service URL. - This option cannot be used in combination with `port` or `scheme` definition. - - ```yaml - traefik.http.services..loadbalancer.server.url=http://foobar:8080 - ``` - -??? info "`traefik.http.services..loadbalancer.server.weight`" - - Overrides the default weight. - - ```yaml - traefik.http.services.myservice.loadbalancer.server.weight=42 - ``` - -??? info "`traefik.http.services..loadbalancer.serverstransport`" - - Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one. - See [serverstransport](../http/load-balancing/serverstransport.md) for more information. - - ```yaml - - "traefik.http.services..loadbalancer.serverstransport=foobar@file" - ``` - -??? info "`traefik.http.services..loadbalancer.passhostheader`" - - ```yaml - - "traefik.http.services.myservice.loadbalancer.passhostheader=true" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.headers.`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.healthcheck.headers.X-Foo=foobar" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.hostname`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.healthcheck.hostname=example.org" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.interval`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.healthcheck.interval=10s" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.unhealthyinterval`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.healthcheck.unhealthyinterval=10s" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.path`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.healthcheck.path=/foo" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.method`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.healthcheck.method=foobar" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.status`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.healthcheck.status=42" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.port`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.healthcheck.port=42" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.scheme`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.healthcheck.scheme=http" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.timeout`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.healthcheck.timeout=10s" - ``` - -??? info "`traefik.http.services..loadbalancer.healthcheck.followredirects`" - - See [health check](../http/load-balancing/service.md#health-check) for more information. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.healthcheck.followredirects=true" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie`" - - ```yaml - - "traefik.http.services.myservice.loadbalancer.sticky.cookie=true" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.httponly`" - - ```yaml - - "traefik.http.services.myservice.loadbalancer.sticky.cookie.httponly=true" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.name`" - - ```yaml - - "traefik.http.services.myservice.loadbalancer.sticky.cookie.name=foobar" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.path`" - - ```yaml - - "traefik.http.services.myservice.loadbalancer.sticky.cookie.path=/foobar" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.secure`" - - ```yaml - - "traefik.http.services.myservice.loadbalancer.sticky.cookie.secure=true" - ``` - -??? info "`traefik.http.services..loadbalancer.sticky.cookie.samesite`" - - ```yaml - - "traefik.http.services.myservice.loadbalancer.sticky.cookie.samesite=none" - ``` - -??? info "`traefik.http.services..loadbalancer.responseforwarding.flushinterval`" - - See [response forwarding](../http/load-balancing/service.md#configuration-options) for more information. - - ```yaml - - "traefik.http.services.myservice.loadbalancer.responseforwarding.flushinterval=10" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.http.services..loadbalancer.server.port` | Registers a port.
Useful when the container exposes multiples ports.
Mandatory for Docker Swarm (see the section ["Port Detection with Docker Swarm"](../../install-configuration/providers/swarm.md#port-detection)). | `8080` | +| `traefik.http.services..loadbalancer.server.scheme` | Overrides the default scheme. | `http` | +| `traefik.http.services..loadbalancer.server.url` | Defines the service URL.
This option cannot be used in combination with `port` or `scheme` definition. | `http://foobar:8080` | +| `traefik.http.services..loadbalancer.server.weight` | Overrides the default weight. | `42` | +| `traefik.http.services..loadbalancer.serverstransport` | Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one.
See [serverstransport](../http/load-balancing/serverstransport.md) for more information. | `foobar@file` | +| `traefik.http.services..loadbalancer.passhostheader` | | `true` | +| `traefik.http.services..loadbalancer.healthcheck.headers.` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `foobar` | +| `traefik.http.services..loadbalancer.healthcheck.hostname` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `example.org` | +| `traefik.http.services..loadbalancer.healthcheck.interval` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10s` | +| `traefik.http.services..loadbalancer.healthcheck.unhealthyinterval` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10s` | +| `traefik.http.services..loadbalancer.healthcheck.path` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `/foo` | +| `traefik.http.services..loadbalancer.healthcheck.method` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `foobar` | +| `traefik.http.services..loadbalancer.healthcheck.status` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `42` | +| `traefik.http.services..loadbalancer.healthcheck.port` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `42` | +| `traefik.http.services..loadbalancer.healthcheck.scheme` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `http` | +| `traefik.http.services..loadbalancer.healthcheck.timeout` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `10s` | +| `traefik.http.services..loadbalancer.healthcheck.followredirects` | See [health check](../http/load-balancing/service.md#health-check) for more information. | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.httponly` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.name` | | `foobar` | +| `traefik.http.services..loadbalancer.sticky.cookie.path` | | `/foobar` | +| `traefik.http.services..loadbalancer.sticky.cookie.secure` | | `true` | +| `traefik.http.services..loadbalancer.sticky.cookie.samesite` | | `none` | +| `traefik.http.services..loadbalancer.responseforwarding.flushinterval` | See [response forwarding](../http/load-balancing/service.md#configuration-options) for more information. | `10` | ### Middleware @@ -519,125 +268,31 @@ You can declare TCP Routers and/or Services using labels. #### TCP Routers -??? info "`traefik.tcp.routers..entrypoints`" +##### Configuration Options - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - - "traefik.tcp.routers.mytcprouter.entrypoints=ep1,ep2" - ``` - -??? info "`traefik.tcp.routers..rule`" - - See [rule](../tcp/routing/rules-and-priority.md#rules) for more information. - - ```yaml - - "traefik.tcp.routers.mytcprouter.rule=HostSNI(`example.com`)" - ``` - -??? info "`traefik.tcp.routers..ruleSyntax`" - - !!! warning - - RuleSyntax option is deprecated and will be removed in the next major version. - Please do not use this field and rewrite the router rules to use the v3 syntax. - - configure the rule syntax to be used for parsing the rule on a per-router basis. - - ```yaml - traefik.tcp.routers.mytcprouter.ruleSyntax=v3 - ``` - -??? info "`traefik.tcp.routers..service`" - - See [service](../tcp/service.md) for more information. - - ```yaml - - "traefik.tcp.routers.mytcprouter.service=myservice" - ``` - -??? info "`traefik.tcp.routers..tls`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - - "traefik.tcp.routers.mytcprouter.tls=true" - ``` - -??? info "`traefik.tcp.routers..tls.certresolver`" - - See [certResolver](../tcp/tls.md#configuration-options) for more information. - - ```yaml - - "traefik.tcp.routers.mytcprouter.tls.certresolver=myresolver" - ``` - -??? info "`traefik.tcp.routers..tls.domains[n].main`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - - "traefik.tcp.routers.mytcprouter.tls.domains[0].main=example.org" - ``` - -??? info "`traefik.tcp.routers..tls.domains[n].sans`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - - "traefik.tcp.routers.mytcprouter.tls.domains[0].sans=test.example.org,dev.example.org" - ``` - -??? info "`traefik.tcp.routers..tls.options`" - - See [TLS](../tcp/tls.md) for more information. - - ```yaml - - "traefik.tcp.routers.mytcprouter.tls.options=mysoptions" - ``` - -??? info "`traefik.tcp.routers..tls.passthrough`" - - See [Passthrough](../tcp/tls.md#opt-passthrough) for more information. - - ```yaml - - "traefik.tcp.routers.mytcprouter.tls.passthrough=true" - ``` - -??? info "`traefik.tcp.routers..priority`" - - See [priority](../tcp/routing/rules-and-priority.md) for more information. - - ```yaml - - "traefik.tcp.routers.myrouter.priority=42" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.tcp.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `ep1,ep2` | +| `traefik.tcp.routers..rule` | See [rule](../tcp/routing/rules-and-priority.md#rules) for more information. | ```HostSNI(`example.com`)``` | +| `traefik.tcp.routers..ruleSyntax` | configure the rule syntax to be used for parsing the rule on a per-router basis.
RuleSyntax option is deprecated and will be removed in the next major version.
Please do not use this field and rewrite the router rules to use the v3 syntax. | `v3` | +| `traefik.tcp.routers..service` | See [service](../tcp/service.md) for more information. | `myservice` | +| `traefik.tcp.routers..tls` | See [TLS](../tcp/tls.md) for more information. | `true` | +| `traefik.tcp.routers..tls.certresolver` | See [certResolver](../tcp/tls.md#configuration-options) for more information. | `myresolver` | +| `traefik.tcp.routers..tls.domains[n].main` | See [TLS](../tcp/tls.md) for more information. | `example.org` | +| `traefik.tcp.routers..tls.domains[n].sans` | See [TLS](../tcp/tls.md) for more information. | `test.example.org,dev.example.org` | +| `traefik.tcp.routers..tls.options` | See [TLS](../tcp/tls.md) for more information. | `mysoptions` | +| `traefik.tcp.routers..tls.passthrough` | See [Passthrough](../tcp/tls.md#opt-passthrough) for more information. | `true` | +| `traefik.tcp.routers..priority` | See [priority](../tcp/routing/rules-and-priority.md#priority-calculation) for more information. | `42` | #### TCP Services -??? info "`traefik.tcp.services..loadbalancer.server.port`" +##### Configuration Options - Registers a port of the application. - - ```yaml - - "traefik.tcp.services.mytcpservice.loadbalancer.server.port=423" - ``` - -??? info "`traefik.tcp.services..loadbalancer.server.tls`" - - Determines whether to use TLS when dialing with the backend. - - ```yaml - - "traefik.tcp.services.mytcpservice.loadbalancer.server.tls=true" - ``` - -??? info "`traefik.tcp.services..loadbalancer.serverstransport`" - - Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one. - See [serverstransport](../tcp/serverstransport.md) for more information. - - ```yaml - - "traefik.tcp.services..loadbalancer.serverstransport=foobar@file" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.tcp.services..loadbalancer.server.port` | Registers a port of the application. | `423` | +| `traefik.tcp.services..loadbalancer.server.tls` | Determines whether to use TLS when dialing with the backend. | `true` | +| `traefik.tcp.services..loadbalancer.serverstransport` | Allows to reference a ServersTransport resource that is defined either with the File provider or the Kubernetes CRD one.
See [serverstransport](../tcp/serverstransport.md) for more information. | `foobar@file` | #### TCP Middleware @@ -684,65 +339,25 @@ You can declare UDP Routers and/or Services using labels. #### UDP Routers -??? info "`traefik.udp.routers..entrypoints`" +##### Configuration Options - See [entry points](../../install-configuration/entrypoints.md) for more information. - - ```yaml - - "traefik.udp.routers.myudprouter.entrypoints=ep1,ep2" - ``` - -??? info "`traefik.udp.routers..service`" - - See [service](../udp/service.md) for more information. - - ```yaml - - "traefik.udp.routers.myudprouter.service=myservice" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.udp.routers..entrypoints` | See [entry points](../../install-configuration/entrypoints.md) for more information. | `ep1,ep2` | +| `traefik.udp.routers..service` | See [service](../udp/service.md) for more information. | `myservice` | #### UDP Services -??? info "`traefik.udp.services..loadbalancer.server.port`" +##### Configuration Options - Registers a port of the application. - - ```yaml - - "traefik.udp.services.myudpservice.loadbalancer.server.port=423" - ``` +| Label | Description | Value | +|------|-------------|-------| +| `traefik.udp.services..loadbalancer.server.port` | Registers a port of the application. | `423` | ### Specific Provider Options -#### `traefik.enable` - -```yaml -- "traefik.enable=true" -``` - -You can tell Traefik to consider (or not) the container by setting `traefik.enable` to true or false. - -This option overrides the value of `exposedByDefault`. - -#### `traefik.swarm.network` - -```yaml -- "traefik.swarm.network=mynetwork" -``` - -Overrides the default docker network to use for connections to the container. - -If a container is linked to several networks, be sure to set the proper network name (you can check this with `docker inspect `), -otherwise it will randomly pick one (depending on how docker is returning them). - -!!! warning - When deploying a stack from a compose file `stack`, the networks defined are prefixed with `stack`. - -#### `traefik.swarm.lbswarm` - -```yaml -- "traefik.swarm.lbswarm=true" -``` - -Enables Swarm's inbuilt load balancer (only relevant in Swarm Mode). - -If you enable this option, Traefik will use the virtual IP provided by docker swarm instead of the containers IPs. -Which means that Traefik will not perform any kind of load balancing and will delegate this task to swarm. +| Label | Description | Value | +|------|-------------|-------| +| `traefik.enable` | You can tell Traefik to consider (or not) the container by setting `traefik.enable` to true or false.
This option overrides the value of `exposedByDefault`. | `true` | +| `traefik.swarm.network` | Overrides the default docker network to use for connections to the container.
If a container is linked to several networks, be sure to set the proper network name (you can check this with `docker inspect `), otherwise it will randomly pick one (depending on how docker is returning them).

When deploying a stack from a compose file `stack`, the networks defined are prefixed with `stack`. | `mynetwork` | +| `traefik.swarm.lbswarm` | Enables Swarm's inbuilt load balancer (only relevant in Swarm Mode).
If you enable this option, Traefik will use the virtual IP provided by docker swarm instead of the containers IPs.
Which means that Traefik will not perform any kind of load balancing and will delegate this task to swarm. | `true` | diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index b109c0527..ea5296a10 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -2045,6 +2045,6 @@ If the ServersTransportTCP CRD is defined in another provider the cross-provider ## Further -Also see the [full example](../../user-guides/crd-acme/index.md) with Let's Encrypt. +For additional information on exposing services with Kubernetes, see the [Kubernetes guide](../../expose/kubernetes/basic.md). {% include-markdown "includes/traefik-for-business-applications.md" %} diff --git a/docs/content/setup/kubernetes.md b/docs/content/setup/kubernetes.md index 4cdd6def0..6454e5dc4 100644 --- a/docs/content/setup/kubernetes.md +++ b/docs/content/setup/kubernetes.md @@ -101,11 +101,12 @@ ports: port: 80 nodePort: 30000 # Instructs this entry point to redirect all traffic to the 'websecure' entry point - redirections: - entryPoint: - to: websecure - scheme: https - permanent: true + http: + redirections: + entryPoint: + to: websecure + scheme: https + permanent: true # Defines the HTTPS entry point named 'websecure' websecure: diff --git a/docs/content/user-guides/cert-manager.md b/docs/content/user-guides/cert-manager.md deleted file mode 100644 index c6edd855f..000000000 --- a/docs/content/user-guides/cert-manager.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -title: "Integration with cert-manager" -description: "Learn how to use cert-manager certificates with Traefik Proxy for your routers. Read the technical documentation." ---- - -# cert-manager - -Provision TLS Certificate for Traefik Proxy with cert-manager on Kubernetes -{: .subtitle } - -## Pre-requisites - -To obtain certificates from cert-manager that can be used in Traefik Proxy, you will need to: - -1. Have cert-manager properly configured -2. Have Traefik Proxy configured - -The certificates can then be used in an Ingress / IngressRoute / HTTPRoute. - -## Example with ACME and HTTP challenge - -!!! example "ACME issuer for HTTP challenge" - - ```yaml tab="Issuer" - apiVersion: cert-manager.io/v1 - kind: Issuer - metadata: - name: acme - - spec: - acme: - # Production server is on https://acme-v02.api.letsencrypt.org/directory - # Use staging by default. - server: https://acme-staging-v02.api.letsencrypt.org/directory - privateKeySecretRef: - name: acme - solvers: - - http01: - ingress: - ingressClassName: traefik - ``` - - ```yaml tab="Certificate" - apiVersion: cert-manager.io/v1 - kind: Certificate - metadata: - name: whoami - namespace: traefik - spec: - secretName: domain-tls # <=== Name of secret where the generated certificate will be stored. - dnsNames: - - "domain.example.com" - issuerRef: - name: acme - kind: Issuer - ``` - -Let's see now how to use it with the various Kubernetes providers of Traefik Proxy. -The enabled providers can be seen on the [dashboard](../reference/install-configuration/api-dashboard.md) of Traefik Proxy and also in the INFO logs when Traefik Proxy starts. - -### With an Ingress - -To use this certificate with an Ingress, the [Kubernetes Ingress](../providers/kubernetes-ingress.md) provider has to be enabled. - -!!! info Traefik Helm Chart - - This provider is enabled by default in the Traefik Helm Chart. - -!!! example "Route with this Certificate" - - ```yaml tab="Ingress" - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - name: domain - annotations: - traefik.ingress.kubernetes.io/router.entrypoints: websecure - - spec: - rules: - - host: domain.example.com - http: - paths: - - path: / - pathType: Exact - backend: - service: - name: domain-service - port: - number: 80 - tls: - - secretName: domain-tls # <=== Use the name defined in Certificate resource. - ``` - -### With an IngressRoute - -To use this certificate with an IngressRoute, the [Kubernetes CRD](../providers/kubernetes-crd.md) provider has to be enabled. - -!!! info Traefik Helm Chart - - This provider is enabled by default in the Traefik Helm Chart. - -!!! example "Route with this Certificate" - - ```yaml tab="IngressRoute" - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: domain - - spec: - entryPoints: - - websecure - - routes: - - match: Host(`domain.example.com`) - kind: Rule - services: - - name: domain-service - port: 80 - tls: - secretName: domain-tls # <=== Use the name defined in Certificate resource. - ``` - -### With an HTTPRoute - -To use this certificate with an HTTPRoute, the [Kubernetes Gateway](../routing/providers/kubernetes-gateway.md) provider has to be enabled. - -!!! info Traefik Helm Chart - - This provider is disabled by default in the Traefik Helm Chart. - -!!! example "Route with this Certificate" - - ```yaml tab="HTTPRoute" - --- - apiVersion: gateway.networking.k8s.io/v1 - kind: Gateway - metadata: - name: domain-gateway - spec: - gatewayClassName: traefik - listeners: - - name: websecure - port: 8443 - protocol: HTTPS - hostname: domain.example.com - tls: - certificateRefs: - - name: domain-tls # <==== Use the name defined in Certificate resource. - --- - apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - name: domain - spec: - parentRefs: - - name: domain-gateway - hostnames: - - domain.example.com - rules: - - matches: - - path: - type: Exact - value: / - - backendRefs: - - name: domain-service - port: 80 - weight: 1 - ``` - -## Troubleshooting - -There are multiple event sources available to investigate when using cert-manager: - -1. Kubernetes events in `Certificate` and `CertificateRequest` resources -2. cert-manager logs -3. Dashboard and/or (debug) logs from Traefik Proxy - -cert-manager documentation provides a [detailed guide](https://cert-manager.io/docs/troubleshooting/) on how to troubleshoot a certificate request. - -{% include-markdown "includes/traefik-for-business-applications.md" %} diff --git a/docs/content/user-guides/crd-acme/02-services.yml b/docs/content/user-guides/crd-acme/02-services.yml deleted file mode 100644 index 753d9e65b..000000000 --- a/docs/content/user-guides/crd-acme/02-services.yml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: traefik - -spec: - ports: - - protocol: TCP - name: web - port: 8000 - - protocol: TCP - name: admin - port: 8080 - - protocol: TCP - name: websecure - port: 4443 - selector: - app: traefik - ---- -apiVersion: v1 -kind: Service -metadata: - name: whoami - -spec: - ports: - - protocol: TCP - name: web - port: 80 - selector: - app: whoami diff --git a/docs/content/user-guides/crd-acme/03-deployments.yml b/docs/content/user-guides/crd-acme/03-deployments.yml deleted file mode 100644 index 7318b0daf..000000000 --- a/docs/content/user-guides/crd-acme/03-deployments.yml +++ /dev/null @@ -1,74 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - namespace: default - name: traefik-ingress-controller - ---- -kind: Deployment -apiVersion: apps/v1 -metadata: - namespace: default - name: traefik - labels: - app: traefik - -spec: - replicas: 1 - selector: - matchLabels: - app: traefik - template: - metadata: - labels: - app: traefik - spec: - serviceAccountName: traefik-ingress-controller - containers: - - name: traefik - image: traefik:v3.6 - args: - - --api.insecure - - --accesslog - - --entryPoints.web.Address=:8000 - - --entryPoints.websecure.Address=:4443 - - --providers.kubernetescrd - - --certificatesresolvers.myresolver.acme.tlschallenge - - --certificatesresolvers.myresolver.acme.email=foo@you.com - - --certificatesresolvers.myresolver.acme.storage=acme.json - # Please note that this is the staging Let's Encrypt server. - # Once you get things working, you should remove that whole line altogether. - - --certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory - ports: - - name: web - containerPort: 8000 - - name: websecure - containerPort: 4443 - - name: admin - containerPort: 8080 - ---- -kind: Deployment -apiVersion: apps/v1 -metadata: - namespace: default - name: whoami - labels: - app: whoami - -spec: - replicas: 2 - selector: - matchLabels: - app: whoami - template: - metadata: - labels: - app: whoami - spec: - containers: - - name: whoami - image: traefik/whoami - ports: - - name: web - containerPort: 80 diff --git a/docs/content/user-guides/crd-acme/04-ingressroutes.yml b/docs/content/user-guides/crd-acme/04-ingressroutes.yml deleted file mode 100644 index 6f57d99f5..000000000 --- a/docs/content/user-guides/crd-acme/04-ingressroutes.yml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: simpleingressroute - namespace: default -spec: - entryPoints: - - web - routes: - - match: Host(`your.example.com`) && PathPrefix(`/notls`) - kind: Rule - services: - - name: whoami - port: 80 - ---- -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: ingressroutetls - namespace: default -spec: - entryPoints: - - websecure - routes: - - match: Host(`your.example.com`) && PathPrefix(`/tls`) - kind: Rule - services: - - name: whoami - port: 80 - tls: - certResolver: myresolver diff --git a/docs/content/user-guides/crd-acme/05-tlsoption.yml b/docs/content/user-guides/crd-acme/05-tlsoption.yml deleted file mode 100644 index a94a610dc..000000000 --- a/docs/content/user-guides/crd-acme/05-tlsoption.yml +++ /dev/null @@ -1,17 +0,0 @@ ---- -apiVersion: traefik.io/v1alpha1 -kind: TLSOption -metadata: - name: default - namespace: default -spec: - minVersion: VersionTLS12 - cipherSuites: - - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 # TLS 1.2 - - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 # TLS 1.2 - - TLS_AES_256_GCM_SHA384 # TLS 1.3 - - TLS_CHACHA20_POLY1305_SHA256 # TLS 1.3 - curvePreferences: - - CurveP521 - - CurveP384 - sniStrict: true diff --git a/docs/content/user-guides/crd-acme/index.md b/docs/content/user-guides/crd-acme/index.md deleted file mode 100644 index f408b03c5..000000000 --- a/docs/content/user-guides/crd-acme/index.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: "Traefik CRD TLS Documentation" -description: "Learn how to use Traefik Proxy w/ an IngressRoute Custom Resource Definition (CRD) for Kubernetes, and TLS with Let's Encrypt. Read the technical documentation." ---- - -# Traefik & CRD & Let's Encrypt - -Traefik with an IngressRoute Custom Resource Definition for Kubernetes, and TLS Through Let's Encrypt. -{: .subtitle } - -This document is intended to be a fully working example demonstrating how to set up Traefik in [Kubernetes](https://kubernetes.io), -with the dynamic configuration coming from the [IngressRoute Custom Resource](../../providers/kubernetes-crd.md), -and TLS setup with [Let's Encrypt](https://letsencrypt.org). -However, for the sake of simplicity, we're using [k3s](https://github.com/rancher/k3s) docker image for the Kubernetes cluster setup. - -Please note that for this setup, given that we're going to use ACME's TLS-ALPN-01 challenge, the host you'll be running it on must be able to receive connections from the outside on port 443. -And of course its internet facing IP address must match the domain name you intend to use. - -In the following, the Kubernetes resources defined in YAML configuration files can be applied to the setup in two different ways: - -- the first, and usual way, is simply with the `kubectl apply` command. -- the second, which can be used for this tutorial, is to directly place the files in the directory used by the k3s docker image for such inputs (`/var/lib/rancher/k3s/server/manifests`). - -!!! important "Kubectl Version" - - With the `rancher/k3s` version used in this guide (`0.8.0`), the kubectl version needs to be >= `1.11`. - -## k3s Docker-compose Configuration - -Our starting point is the docker-compose configuration file, to start the k3s cluster. -You can start it with: - -```bash -docker compose -f k3s.yml up -``` - -```yaml ---8<-- "content/user-guides/crd-acme/k3s.yml" -``` - -## Cluster Resources - -Let's now have a look (in the order they should be applied, if using `kubectl apply`) at all the required resources for the full setup. - -### IngressRoute Definition - -First, you will need to install Traefik CRDs containing the definition of the `IngressRoute` and the `Middleware` kinds, -and the RBAC authorization resources which will be referenced through the `serviceAccountName` of the deployment. - -```bash -# Install Traefik Resource Definitions: -kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.6/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml - -# Install RBAC for Traefik: -kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.6/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml -``` - -### Services - -Then, the services. One for Traefik itself, and one for the app it routes for, i.e. in this case our demo HTTP server: [whoami](https://github.com/traefik/whoami). - -```bash -kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.6/docs/content/user-guides/crd-acme/02-services.yml -``` - -```yaml ---8<-- "content/user-guides/crd-acme/02-services.yml" -``` - -### Deployments - -Next, the deployments, i.e. the actual pods behind the services. -Again, one pod for Traefik, and one for the whoami app. - -```bash -kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.6/docs/content/user-guides/crd-acme/03-deployments.yml -``` - -```yaml ---8<-- "content/user-guides/crd-acme/03-deployments.yml" -``` - -### Port Forwarding - -Now, as an exception to what we said above, please note that you should not let the ingressRoute resources below be applied automatically to your cluster. -The reason is, as soon as the ACME provider of Traefik detects we have TLS routers, it will try to generate the certificates for the corresponding domains. -And this will not work, because as it is, our Traefik pod is not reachable from the outside, which will make the ACME TLS challenge fail. -Therefore, for the whole thing to work, we must delay applying the ingressRoute resources until we have port-forwarding set up properly, which is the next step. - -```bash -kubectl port-forward --address 0.0.0.0 service/traefik 8000:8000 8080:8080 443:4443 -n default -``` - -Also, and this is out of the scope of this guide, please note that because of the privileged ports limitation on Linux, the above command might fail to listen on port 443. -In which case you can use tricks such as elevating caps of `kubectl` with `setcaps`, or using `authbind`, or setting up a NAT between your host and the WAN. -Look it up. - -### Traefik Routers - -We can now finally apply the actual ingressRoutes, with: - -```bash -kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.6/docs/content/user-guides/crd-acme/04-ingressroutes.yml -``` - -```yaml ---8<-- "content/user-guides/crd-acme/04-ingressroutes.yml" -``` - -Give it a few seconds for the ACME TLS challenge to complete, and you should then be able to access your whoami pod (routed through Traefik), from the outside. -Both with or (just for fun, do not do that in production) without TLS: - -```bash -curl [-k] https://your.example.com/tls -``` - -```bash -curl http://your.example.com:8000/notls -``` - -Note that you'll have to use `-k` as long as you're using the staging server of Let's Encrypt, since it is not an authorized certificate authority on systems where it hasn't been manually added. - -### Force TLS v1.2+ - -Nowadays, TLS v1.0 and v1.1 are deprecated. -In order to force TLS v1.2 or later on all your IngressRoute, you can define the `default` TLSOption: - -```bash -kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.6/docs/content/user-guides/crd-acme/05-tlsoption.yml -``` - -```yaml ---8<-- "content/user-guides/crd-acme/05-tlsoption.yml" -``` diff --git a/docs/content/user-guides/crd-acme/k3s.yml b/docs/content/user-guides/crd-acme/k3s.yml deleted file mode 100644 index 8701d7305..000000000 --- a/docs/content/user-guides/crd-acme/k3s.yml +++ /dev/null @@ -1,30 +0,0 @@ -server: - image: rancher/k3s:v1.34.2-k3s1 - command: server --disable-agent --no-deploy traefik - environment: - - K3S_CLUSTER_SECRET=somethingtotallyrandom - - K3S_KUBECONFIG_OUTPUT=/output/kubeconfig.yaml - - K3S_KUBECONFIG_MODE=666 - volumes: - # k3s will generate a kubeconfig.yaml in this directory. This volume is mounted - # on your host, so you can then 'export KUBECONFIG=/somewhere/on/your/host/out/kubeconfig.yaml', - # in order for your kubectl commands to work. - - /somewhere/on/your/host/out:/output - # This directory is where you put all the (yaml) configuration files of - # the Kubernetes resources. - - /somewhere/on/your/host/in:/var/lib/rancher/k3s/server/manifests - ports: - - 6443:6443 - -node: - image: rancher/k3s:v1.34.2-k3s1 - privileged: true - links: - - server - environment: - - K3S_URL=https://server:6443 - - K3S_CLUSTER_SECRET=somethingtotallyrandom - volumes: - # this is where you would place a alternative traefik image (saved as a .tar file with - # 'docker save'), if you want to use it, instead of the traefik:v3.6 image. - - /somewhere/on/your/host/custom-image:/var/lib/rancher/k3s/agent/images diff --git a/docs/content/user-guides/grpc.md b/docs/content/user-guides/grpc.md deleted file mode 100644 index 488008381..000000000 --- a/docs/content/user-guides/grpc.md +++ /dev/null @@ -1,283 +0,0 @@ ---- -title: "Traefik Proxy gRPC Examples" -description: "This section of the Traefik Proxy documentation explains how to use Traefik as reverse proxy for gRPC applications." ---- - -# gRPC Examples - -## With HTTP (h2c) - -This section explains how to use Traefik as reverse proxy for gRPC application. - -### Traefik Configuration - -Static configuration: - -```yaml tab="File (YAML)" -entryPoints: - web: - address: :80 - -providers: - file: - directory: /path/to/dynamic/config - -api: {} -``` - -```toml tab="File (TOML)" -[entryPoints] - [entryPoints.web] - address = ":80" - -[api] - -[providers.file] - directory = "/path/to/dynamic/config" -``` - -```yaml tab="CLI" ---entryPoints.web.address=:80 ---providers.file.directory=/path/to/dynamic/config ---api.insecure=true -``` - -`/path/to/dynamic/config/dynamic_conf.{yml,toml}`: - -```yaml tab="YAML" -## dynamic configuration ## - -http: - routers: - routerTest: - service: srv-grpc - rule: Host(`frontend.local`) - - services: - srv-grpc: - loadBalancer: - servers: - - url: h2c://backend.local:8080 -``` - -```toml tab="TOML" -## dynamic configuration ## - -[http] - - [http.routers] - [http.routers.routerTest] - service = "srv-grpc" - rule = "Host(`frontend.local`)" - - [http.services] - [http.services.srv-grpc] - [http.services.srv-grpc.loadBalancer] - [[http.services.srv-grpc.loadBalancer.servers]] - url = "h2c://backend.local:8080" -``` - -!!! warning - For providers with labels, you will have to specify the `traefik.http.services..loadbalancer.server.scheme=h2c` - -### Conclusion - -We don't need specific configuration to use gRPC in Traefik, we just need to use `h2c` protocol, or use HTTPS communications to have HTTP2 with the backend. - -## With HTTPS - -This section explains how to use Traefik as reverse proxy for gRPC application with self-signed certificates. - -![gRPC architecture](../assets/img/user-guides/grpc.svg) - -### gRPC Server Certificate - -In order to secure the gRPC server, we generate a self-signed certificate for service url: - -```bash -openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./backend.key -out ./backend.cert -``` - -That will prompt for information, the important answer is: - -```txt -Common Name (e.g. server FQDN or YOUR name) []: backend.local -``` - -### gRPC Client Certificate - -Generate your self-signed certificate for router url: - -```bash -openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./frontend.key -out ./frontend.cert -``` - -with - -```txt -Common Name (e.g. server FQDN or YOUR name) []: frontend.local -``` - -### Traefik Configuration - -At last, we configure our Traefik instance to use both self-signed certificates. - -Static configuration: - -```yaml tab="File (YAML)" -entryPoints: - websecure: - address: :4443 - -serversTransport: - # For secure connection on backend.local - rootCAs: - - ./backend.cert - -providers: - file: - directory: /path/to/dynamic/config - -api: {} -``` - -```toml tab="File (TOML)" -[entryPoints] - [entryPoints.websecure] - address = ":4443" - - -[serversTransport] - # For secure connection on backend.local - rootCAs = [ "./backend.cert" ] - -[api] - -[provider.file] - directory = "/path/to/dynamic/config" -``` - -```yaml tab="CLI" ---entryPoints.websecure.address=:4443 -# For secure connection on backend.local ---serversTransport.rootCAs=./backend.cert ---providers.file.directory=/path/to/dynamic/config ---api.insecure=true -``` - -`/path/to/dynamic/config/dynamic_conf.{yml,toml}`: - -```yaml tab="YAML" -## dynamic configuration ## - -http: - routers: - routerTest: - service: srv-grpc - rule: Host(`frontend.local`) - services: - srv-grpc: - loadBalancer: - servers: - # Access on backend with HTTPS - - url: https://backend.local:8080 -tls: - # For secure connection on frontend.local - certificates: - - certfile: ./frontend.cert - keyfile: ./frontend.key -``` - -```toml tab="TOML" -## dynamic configuration ## - -[http] - - [http.routers] - [http.routers.routerTest] - service = "srv-grpc" - rule = "Host(`frontend.local`)" - - [http.services] - [http.services.srv-grpc] - [http.services.srv-grpc.loadBalancer] - [[http.services.srv-grpc.loadBalancer.servers]] - # Access on backend with HTTPS - url = "https://backend.local:8080" - -[tls] - - # For secure connection on frontend.local - [[tls.certificates]] - certFile = "./frontend.cert" - keyFile = "./frontend.key" -``` - -!!! warning - With some services, the server URLs use the IP, so you may need to configure `insecureSkipVerify` instead of the `rootCAs` to activate HTTPS without hostname verification. - -### A gRPC example in go (modify for https) - -We use the gRPC greeter example in [grpc-go](https://github.com/grpc/grpc-go/tree/master/examples/helloworld) - -!!! warning - In order to use this gRPC example, we need to modify it to use HTTPS - -So we modify the "gRPC server example" to use our own self-signed certificate: - -```go -// ... - -// Read cert and key file -backendCert, _ := os.ReadFile("./backend.cert") -backendKey, _ := os.ReadFile("./backend.key") - -// Generate Certificate struct -cert, err := tls.X509KeyPair(backendCert, backendKey) -if err != nil { - log.Fatalf("failed to parse certificate: %v", err) -} - -// Create credentials -creds := credentials.NewServerTLSFromCert(&cert) - -// Use Credentials in gRPC server options -serverOption := grpc.Creds(creds) -var s *grpc.Server = grpc.NewServer(serverOption) -defer s.Stop() - -pb.RegisterGreeterServer(s, &server{}) -err := s.Serve(lis) - -// ... -``` - -Next we will modify gRPC Client to use our Traefik self-signed certificate: - -```go -// ... - -// Read cert file -frontendCert, _ := os.ReadFile("./frontend.cert") - -// Create CertPool -roots := x509.NewCertPool() -roots.AppendCertsFromPEM(frontendCert) - -// Create credentials -credsClient := credentials.NewClientTLSFromCert(roots, "") - -// Dial with specific Transport (with credentials) -conn, err := grpc.Dial("frontend.local:4443", grpc.WithTransportCredentials(credsClient)) -if err != nil { - log.Fatalf("did not connect: %v", err) -} - -defer conn.Close() -client := pb.NewGreeterClient(conn) - -name := "World" -r, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: name}) - -// ... -``` diff --git a/docs/content/user-guides/websocket.md b/docs/content/user-guides/websocket.md deleted file mode 100644 index cb122d90f..000000000 --- a/docs/content/user-guides/websocket.md +++ /dev/null @@ -1,355 +0,0 @@ ---- -title: "Traefik WebSocket Documentation" -description: "How to configure WebSocket and WebSocket Secure (WSS) connections with Traefik Proxy." ---- - -# WebSocket - -Configuring Traefik to handle WebSocket and WebSocket Secure (WSS) connections. -{: .subtitle } - -## Overview - -WebSocket is a communication protocol that provides full-duplex communication channels over a single TCP connection. -WebSocket Secure (WSS) is the encrypted version of WebSocket, using TLS/SSL encryption. - -Traefik supports WebSocket and WebSocket Secure (WSS) out of the box. This guide will walk through examples of how to configure Traefik for different WebSocket scenarios. - -## Basic WebSocket Configuration - -A basic WebSocket configuration only requires defining a router and a service that points to your WebSocket server. - -```yaml tab="Docker & Swarm" -labels: - - "traefik.http.routers.my-websocket.rule=Host(`ws.example.com`)" - - "traefik.http.routers.my-websocket.service=my-websocket-service" - - "traefik.http.services.my-websocket-service.loadbalancer.server.port=8000" -``` - -```yaml tab="Kubernetes" -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: my-websocket-route -spec: - entryPoints: - - web - routes: - - match: Host(`ws.example.com`) - kind: Rule - services: - - name: my-websocket-service - port: 8000 -``` - -```yaml tab="File (YAML)" -http: - routers: - my-websocket: - rule: "Host(`ws.example.com`)" - service: my-websocket-service - - services: - my-websocket-service: - loadBalancer: - servers: - - url: "http://my-websocket-server:8000" -``` - -```toml tab="File (TOML)" -[http.routers] - [http.routers.my-websocket] - rule = "Host(`ws.example.com`)" - service = "my-websocket-service" - -[http.services] - [http.services.my-websocket-service] - [http.services.my-websocket-service.loadBalancer] - [[http.services.my-websocket-service.loadBalancer.servers]] - url = "http://my-websocket-server:8000" -``` - -## WebSocket Secure (WSS) Configuration - -WebSocket Secure (WSS) requires TLS configuration. -The client connects using the `wss://` protocol instead of `ws://`. - -```yaml tab="Docker & Swarm" -labels: - - "traefik.http.routers.my-websocket-secure.rule=Host(`wss.example.com`)" - - "traefik.http.routers.my-websocket-secure.service=my-websocket-service" - - "traefik.http.routers.my-websocket-secure.tls=true" - - "traefik.http.services.my-websocket-service.loadbalancer.server.port=8000" -``` - -```yaml tab="Kubernetes" -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: my-websocket-secure-route -spec: - entryPoints: - - websecure - routes: - - match: Host(`wss.example.com`) - kind: Rule - services: - - name: my-websocket-service - port: 8000 - tls: {} -``` - -```yaml tab="File (YAML)" -http: - routers: - my-websocket-secure: - rule: "Host(`wss.example.com`)" - service: my-websocket-service - tls: {} - - services: - my-websocket-service: - loadBalancer: - servers: - - url: "http://my-websocket-server:8000" -``` - -```toml tab="File (TOML)" -[http.routers] - [http.routers.my-websocket-secure] - rule = "Host(`wss.example.com`)" - service = "my-websocket-service" - [http.routers.my-websocket-secure.tls] - -[http.services] - [http.services.my-websocket-service] - [http.services.my-websocket-service.loadBalancer] - [[http.services.my-websocket-service.loadBalancer.servers]] - url = "http://my-websocket-server:8000" -``` - -## SSL Termination for WebSockets - -In this scenario, clients connect to Traefik using WSS (encrypted), but Traefik connects to your backend server using WS (unencrypted). -This is called SSL termination. - -```yaml tab="Docker & Swarm" -labels: - - "traefik.http.routers.my-wss-termination.rule=Host(`wss.example.com`)" - - "traefik.http.routers.my-wss-termination.service=my-ws-service" - - "traefik.http.routers.my-wss-termination.tls=true" - - "traefik.http.services.my-ws-service.loadbalancer.server.port=8000" -``` - -```yaml tab="Kubernetes" -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: my-wss-termination-route -spec: - entryPoints: - - websecure - routes: - - match: Host(`wss.example.com`) - kind: Rule - services: - - name: my-ws-service - port: 8000 - tls: {} -``` - -```yaml tab="File (YAML)" -http: - routers: - my-wss-termination: - rule: "Host(`wss.example.com`)" - service: my-ws-service - tls: {} - - services: - my-ws-service: - loadBalancer: - servers: - - url: "http://my-ws-server:8000" -``` - -```toml tab="File (TOML)" -[http.routers] - [http.routers.my-wss-termination] - rule = "Host(`wss.example.com`)" - service = "my-ws-service" - [http.routers.my-wss-termination.tls] - -[http.services] - [http.services.my-ws-service] - [http.services.my-ws-service.loadBalancer] - [[http.services.my-ws-service.loadBalancer.servers]] - url = "http://my-ws-server:8000" -``` - -## End-to-End WebSocket Secure (WSS) - -For end-to-end encryption, Traefik can be configured to connect to your backend using HTTPS. - -```yaml tab="Docker & Swarm" -labels: - - "traefik.http.routers.my-wss-e2e.rule=Host(`wss.example.com`)" - - "traefik.http.routers.my-wss-e2e.service=my-wss-service" - - "traefik.http.routers.my-wss-e2e.tls=true" - - "traefik.http.services.my-wss-service.loadbalancer.server.port=8443" - # If the backend uses a self-signed certificate - - "traefik.http.serversTransports.insecureTransport.insecureSkipVerify=true" - - "traefik.http.services.my-wss-service.loadBalancer.serversTransport=insecureTransport" -``` - -```yaml tab="Kubernetes" -apiVersion: traefik.io/v1alpha1 -kind: ServersTransport -metadata: - name: insecure-transport -spec: - insecureSkipVerify: true - ---- -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: my-wss-e2e-route -spec: - entryPoints: - - websecure - routes: - - match: Host(`wss.example.com`) - kind: Rule - services: - - name: my-wss-service - port: 8443 - serversTransport: insecure-transport - tls: {} -``` - -```yaml tab="File (YAML)" -http: - serversTransports: - insecureTransport: - insecureSkipVerify: true - - routers: - my-wss-e2e: - rule: "Host(`wss.example.com`)" - service: my-wss-service - tls: {} - - services: - my-wss-service: - loadBalancer: - serversTransport: insecureTransport - servers: - - url: "https://my-wss-server:8443" -``` - -```toml tab="File (TOML)" -[http.serversTransports] - [http.serversTransports.insecureTransport] - insecureSkipVerify = true - -[http.routers] - [http.routers.my-wss-e2e] - rule = "Host(`wss.example.com`)" - service = "my-wss-service" - [http.routers.my-wss-e2e.tls] - -[http.services] - [http.services.my-wss-service] - [http.services.my-wss-service.loadBalancer] - serversTransport = "insecureTransport" - [[http.services.my-wss-service.loadBalancer.servers]] - url = "https://my-wss-server:8443" -``` - -## EntryPoints Configuration for WebSockets - -In your Traefik static configuration, you'll need to define entryPoints for both WS and WSS: - -```yaml tab="File (YAML)" -entryPoints: - web: - address: ":80" - websecure: - address: ":443" -``` - -```toml tab="File (TOML)" -[entryPoints] - [entryPoints.web] - address = ":80" - [entryPoints.websecure] - address = ":443" -``` - -## Testing WebSocket Connections - -You can test your WebSocket configuration using various tools: - -1. Browser Developer Tools: Most modern browsers include WebSocket debugging in their developer tools. -2. WebSocket client tools like [wscat](https://github.com/websockets/wscat) or online tools like [Piesocket's WebSocket Tester](https://www.piesocket.com/websocket-tester). - -Example wscat commands: - -```bash -# Test standard WebSocket -wscat -c ws://ws.example.com - -# Test WebSocket Secure -wscat -c wss://wss.example.com -``` - -## Common Issues and Solutions - -### Headers and Origin Checks - -Some WebSocket servers implement origin checking. Traefik passes the original headers to your backend, including the `Origin` header. - -If you need to manipulate headers for WebSocket connections, you can use Traefik's Headers middleware: - -```yaml tab="Docker & Swarm" -labels: - - "traefik.http.middlewares.my-headers.headers.customrequestheaders.Origin=https://allowed-origin.com" - - "traefik.http.routers.my-websocket.middlewares=my-headers" -``` - -```yaml tab="Kubernetes" -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: my-headers -spec: - headers: - customRequestHeaders: - Origin: "https://allowed-origin.com" - ---- -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: my-websocket-route -spec: - routes: - - match: Host(`ws.example.com`) - kind: Rule - middlewares: - - name: my-headers - services: - - name: my-websocket-service - port: 8000 -``` - -### Certificate Issues with WSS - -If you're experiencing certificate issues with WSS: - -1. Ensure your certificates are valid and not expired -2. For testing with self-signed certificates, configure your clients to accept them -3. When using Let's Encrypt, ensure your domain is properly configured - -For backends with self-signed certificates, use the `insecureSkipVerify` option in the ServersTransport configuration as shown in the examples above. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 73185498f..27a630d28 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -136,12 +136,15 @@ plugins: 'middlewares/tcp/ipwhitelist.md': 'reference/routing-configuration/tcp/middlewares/ipallowlist.md' 'middlewares/tcp/ipallowlist.md': 'reference/routing-configuration/tcp/middlewares/ipallowlist.md' ## User Guides - 'user-guides/crd-acme/index.md': 'expose/kubernetes.md' - 'user-guides/cert-manager.md': 'expose/kubernetes.md' - 'user-guides/docker-compose/basic-example/index.md': 'expose/docker.md' - 'user-guides/docker-compose/acme-tls/index.md': 'expose/docker.md' - 'user-guides/docker-compose/acme-http/index.md': 'expose/docker.md' - 'user-guides/docker-compose/acme-dns/index.md': 'expose/docker.md' + 'user-guides/crd-acme/index.md': 'expose/kubernetes/basic.md' + 'user-guides/cert-manager.md': 'expose/kubernetes/advanced.md' + 'user-guides/docker-compose/basic-example/index.md': 'expose/docker/basic.md' + 'user-guides/docker-compose/acme-tls/index.md': 'expose/docker/basic.md' + 'user-guides/docker-compose/acme-http/index.md': 'expose/docker/basic.md' + 'user-guides/docker-compose/acme-dns/index.md': 'expose/docker/basic.md' + 'user-guides/fastproxy.md': 'reference/install-configuration/experimental/fastproxy.md' + 'user-guides/grpc.md': 'expose/overview.md#exposing-grpc-services' + 'user-guides/websocket.md': 'expose/overview.md#exposing-websocket-services' # References # Static Configuration 'reference/static-configuration/overview.md': 'reference/install-configuration/configuration-options.md' @@ -201,9 +204,15 @@ nav: - 'Swarm': 'setup/swarm.md' - 'Expose': - 'Overview': 'expose/overview.md' - - 'Kubernetes': 'expose/kubernetes.md' - - 'Docker': 'expose/docker.md' - - 'Swarm': 'expose/swarm.md' + - 'Kubernetes': + - 'Basic': 'expose/kubernetes/basic.md' + - 'Advanced': 'expose/kubernetes/advanced.md' + - 'Docker': + - 'Basic': 'expose/docker/basic.md' + - 'Advanced': 'expose/docker/advanced.md' + - 'Swarm': + - 'Basic': 'expose/swarm/basic.md' + - 'Advanced': 'expose/swarm/advanced.md' - 'Secure': - 'Secure Access with JWT Traefik Hub API Gateway': 'secure/secure-api-access-with-jwt.md' - 'Secure Access with OIDC Traefik Hub API Gateway': 'secure/secure-api-access-with-oidc.md' @@ -263,6 +272,9 @@ nav: - 'Tracing': 'reference/install-configuration/observability/tracing.md' - 'Logs & AccessLogs': 'reference/install-configuration/observability/logs-and-accesslogs.md' - 'Health Check (CLI & Ping)': 'reference/install-configuration/observability/healthcheck.md' + - 'Experimental': + - 'FastProxy': 'reference/install-configuration/experimental/fastproxy.md' + - 'Plugins': 'reference/install-configuration/experimental/plugins.md' - 'Options List': 'reference/install-configuration/configuration-options.md' - 'Routing Configuration': - 'Common Configuration' : @@ -369,12 +381,6 @@ nav: - 'Deprecation Notices': - 'Releases': 'deprecation/releases.md' - 'Features': 'deprecation/features.md' - - 'User Guides': - - 'FastProxy': 'user-guides/fastproxy.md' - - 'Kubernetes and Let''s Encrypt': 'user-guides/crd-acme/index.md' - - 'Kubernetes and cert-manager': 'user-guides/cert-manager.md' - - 'gRPC Examples': 'user-guides/grpc.md' - - 'WebSocket Examples': 'user-guides/websocket.md' - 'Contributing': - 'Thank You!': 'contributing/thank-you.md' - 'Submitting Issues': 'contributing/submitting-issues.md' diff --git a/docs/requirements.txt b/docs/requirements.txt index f0d99127c..7801cf737 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -8,7 +8,7 @@ click==8.1.7 colorama==0.4.6 ghp-import==2.1.0 importlib_metadata==7.1.0 -Jinja2==3.1.3 +Jinja2==3.1.6 Markdown==3.3.6 MarkupSafe==2.1.5 mergedeep==1.3.4 @@ -21,4 +21,4 @@ PyYAML==6.0.1 pyyaml_env_tag==0.1 six==1.16.0 watchdog==4.0.0 -zipp==3.18.1 +zipp==3.19.1 diff --git a/go.mod b/go.mod index 0582836fd..5c0935e89 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/hashicorp/go-version v1.8.0 github.com/hashicorp/nomad/api v0.0.0-20231213195942-64e3dca9274b // No tag on the repo. github.com/http-wasm/http-wasm-host-go v0.7.0 + github.com/huandu/xstrings v1.5.0 github.com/influxdata/influxdb-client-go/v2 v2.7.0 github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab // No tag on the repo. github.com/klauspost/compress v1.18.0 @@ -71,6 +72,7 @@ require ( github.com/tidwall/gjson v1.17.0 github.com/traefik/grpc-web v0.16.0 github.com/traefik/paerser v0.2.2 + github.com/traefik/traefik/dynamic/ext v0.0.0-00010101000000-000000000000 github.com/traefik/yaegi v0.16.1 github.com/unrolled/render v1.0.2 github.com/unrolled/secure v1.0.9 @@ -180,7 +182,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect - github.com/containerd/containerd v1.7.23 // indirect + github.com/containerd/containerd v1.7.29 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -253,7 +255,6 @@ require ( github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/hcl v1.0.1-vault-5 // indirect github.com/hashicorp/serf v0.10.1 // indirect - github.com/huandu/xstrings v1.5.0 // indirect github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.182 // indirect github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect github.com/imdario/mergo v0.3.16 // indirect @@ -411,6 +412,9 @@ require ( sigs.k8s.io/randfill v1.0.0 // indirect ) +// Dynamic config extension. +replace github.com/traefik/traefik/dynamic/ext => ./pkg/config/dynamic/ext + // Containous forks replace ( github.com/abbot/go-http-auth => github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e diff --git a/go.sum b/go.sum index 3bad3f623..00b764cce 100644 --- a/go.sum +++ b/go.sum @@ -297,8 +297,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/containerd/containerd v1.7.23 h1:H2CClyUkmpKAGlhQp95g2WXHfLYc7whAuvZGBNYOOwQ= -github.com/containerd/containerd v1.7.23/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw= +github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= +github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index be4c1f2f4..ab1c56ea5 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -223,6 +223,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of + the referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -1233,6 +1252,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references to + Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -2975,6 +3013,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -3212,6 +3269,24 @@ spec: Default value is -1, which means unlimited size. format: int64 type: integer + middlewares: + description: Middlewares defines the list of references to Middleware + resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware resource. + properties: + name: + description: Name defines the name of the referenced Middleware + resource. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Middleware resource. + type: string + required: + - name + type: object + type: array mirrorBody: description: |- MirrorBody defines whether the body of the request should be mirrored. @@ -3299,6 +3374,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. @@ -3687,6 +3781,25 @@ spec: - Service - TraefikService type: string + middlewares: + description: Middlewares defines the list of references + to Middleware resources to apply to the service. + items: + description: MiddlewareRef is a reference to a Middleware + resource. + properties: + name: + description: Name defines the name of the referenced + Middleware resource. + type: string + namespace: + description: Namespace defines the namespace of the + referenced Middleware resource. + type: string + required: + - name + type: object + type: array name: description: |- Name defines the name of the referenced Kubernetes Service or TraefikService. diff --git a/integration/fixtures/service_middleware.toml b/integration/fixtures/service_middleware.toml new file mode 100644 index 000000000..066985b85 --- /dev/null +++ b/integration/fixtures/service_middleware.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.router1] + service = "service1" + rule = "Path(`/whoami`)" + +[http.middlewares] + [http.middlewares.add-header.headers.customRequestHeaders] + X-Custom-Header = "service-middleware-test" + +[http.services] + [http.services.service1] + middlewares = ["add-header"] + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "{{ .Server }}" \ No newline at end of file diff --git a/integration/gateway-api-conformance-reports/v1.4.0/experimental-v3.6-default-report.yaml b/integration/gateway-api-conformance-reports/v1.4.0/experimental-v3.7-default-report.yaml similarity index 98% rename from integration/gateway-api-conformance-reports/v1.4.0/experimental-v3.6-default-report.yaml rename to integration/gateway-api-conformance-reports/v1.4.0/experimental-v3.7-default-report.yaml index 429592e4a..e6961a03d 100644 --- a/integration/gateway-api-conformance-reports/v1.4.0/experimental-v3.6-default-report.yaml +++ b/integration/gateway-api-conformance-reports/v1.4.0/experimental-v3.7-default-report.yaml @@ -8,7 +8,7 @@ implementation: organization: traefik project: traefik url: https://traefik.io/ - version: v3.6 + version: v3.7 kind: ConformanceReport mode: default profiles: @@ -30,12 +30,13 @@ profiles: result: success statistics: Failed: 0 - Passed: 13 + Passed: 15 Skipped: 0 supportedFeatures: - GatewayPort8080 - HTTPRouteBackendProtocolH2C - HTTPRouteBackendProtocolWebSocket + - HTTPRouteBackendRequestHeaderModification - HTTPRouteDestinationPortMatching - HTTPRouteHostRewrite - HTTPRouteMethodMatching @@ -50,7 +51,6 @@ profiles: - GatewayHTTPListenerIsolation - GatewayInfrastructurePropagation - GatewayStaticAddresses - - HTTPRouteBackendRequestHeaderModification - HTTPRouteBackendTimeout - HTTPRouteCORS - HTTPRouteNamedRouteRule diff --git a/integration/simple_test.go b/integration/simple_test.go index c1eb0eb17..1d1e1a818 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -2364,3 +2364,37 @@ func (s *SimpleSuite) TestEncodedCharactersDifferentEntryPoints() { require.NoError(s.T(), err) } } + +func (s *SimpleSuite) TestServiceMiddleware() { + s.createComposeProject("base") + + s.composeUp() + defer s.composeDown() + + whoamiIP := s.getComposeServiceIP("whoami1") + + file := s.adaptFile("fixtures/service_middleware.toml", struct { + Server string + }{Server: "http://" + whoamiIP}) + + s.traefikCmd(withConfigFile(file)) + + // Wait for Traefik to be ready + err := try.GetRequest("http://127.0.0.1:8080/api/http/services", 2*time.Second, try.BodyContains("service1")) + require.NoError(s.T(), err) + + // Make a request and verify the middleware added the custom header + 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) + + // Read the response body to check if the whoami service received the custom header + body, err := io.ReadAll(response.Body) + require.NoError(s.T(), err) + + // The whoami service should have received the X-Custom-Header that was added by the service middleware + assert.Contains(s.T(), string(body), "X-Custom-Header: service-middleware-test") +} diff --git a/pkg/config/dynamic/ext/ext.go b/pkg/config/dynamic/ext/ext.go new file mode 100644 index 000000000..6fc62b373 --- /dev/null +++ b/pkg/config/dynamic/ext/ext.go @@ -0,0 +1,7 @@ +package ext + +// HTTP is a dynamic.HTTP extension. +type HTTP struct{} + +// Router is a dynamic.Router extension. +type Router struct{} diff --git a/pkg/config/dynamic/ext/go.mod b/pkg/config/dynamic/ext/go.mod new file mode 100644 index 000000000..8dc321263 --- /dev/null +++ b/pkg/config/dynamic/ext/go.mod @@ -0,0 +1,3 @@ +module github.com/traefik/traefik/dynamic/ext + +go 1.24.0 diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index a209f6b10..a58143daa 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -5,6 +5,7 @@ import ( "time" ptypes "github.com/traefik/paerser/types" + "github.com/traefik/traefik/dynamic/ext" otypes "github.com/traefik/traefik/v3/pkg/observability/types" traefiktls "github.com/traefik/traefik/v3/pkg/tls" "github.com/traefik/traefik/v3/pkg/types" @@ -33,6 +34,8 @@ const ( // HTTPConfiguration contains all the HTTP configuration parameters. type HTTPConfiguration struct { + ext.HTTP `yaml:",inline"` + Routers map[string]*Router `json:"routers,omitempty" toml:"routers,omitempty" yaml:"routers,omitempty" export:"true"` Services map[string]*Service `json:"services,omitempty" toml:"services,omitempty" yaml:"services,omitempty" export:"true"` Middlewares map[string]*Middleware `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` @@ -55,6 +58,7 @@ type Model struct { // Service holds a service configuration (can only be of one type at the same time). type Service struct { + Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"` LoadBalancer *ServersLoadBalancer `json:"loadBalancer,omitempty" toml:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty" export:"true"` HighestRandomWeight *HighestRandomWeight `json:"highestRandomWeight,omitempty" toml:"highestRandomWeight,omitempty" yaml:"highestRandomWeight,omitempty" label:"-" export:"true"` Weighted *WeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-" export:"true"` @@ -62,10 +66,22 @@ type Service struct { Failover *Failover `json:"failover,omitempty" toml:"failover,omitempty" yaml:"failover,omitempty" label:"-" export:"true"` } +// Merge merges another Service into this one. +// Returns true if the merge succeeds, false if configurations conflict. +func (s *Service) Merge(other *Service) bool { + if s.LoadBalancer == nil || other.LoadBalancer == nil { + return reflect.DeepEqual(s, other) + } + + return s.LoadBalancer.Merge(other.LoadBalancer) +} + // +k8s:deepcopy-gen=true // Router holds the router configuration. type Router struct { + ext.Router `yaml:",inline"` + EntryPoints []string `json:"entryPoints,omitempty" toml:"entryPoints,omitempty" yaml:"entryPoints,omitempty" export:"true"` 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"` @@ -342,8 +358,39 @@ type ServersLoadBalancer struct { ServersTransport string `json:"serversTransport,omitempty" toml:"serversTransport,omitempty" yaml:"serversTransport,omitempty" export:"true"` } -// Mergeable tells if the given service is mergeable. -func (l *ServersLoadBalancer) Mergeable(loadBalancer *ServersLoadBalancer) bool { +// Merge merges the other load balancer into this one. +// Returns true if merge succeeded, false if configurations conflict. +func (l *ServersLoadBalancer) Merge(other *ServersLoadBalancer) bool { + if !l.mergeable(other) { + return false + } + + // Deduplicate and append servers. + uniq := make(map[string]struct{}, len(l.Servers)) + for _, server := range l.Servers { + uniq[server.URL] = struct{}{} + } + for _, server := range other.Servers { + if _, ok := uniq[server.URL]; !ok { + l.Servers = append(l.Servers, server) + } + } + + return true +} + +// SetDefaults Default values for a ServersLoadBalancer. +func (l *ServersLoadBalancer) SetDefaults() { + defaultPassHostHeader := DefaultPassHostHeader + l.PassHostHeader = &defaultPassHostHeader + + l.Strategy = BalancerStrategyWRR + l.ResponseForwarding = &ResponseForwarding{} + l.ResponseForwarding.SetDefaults() +} + +// mergeable tells if the given service is mergeable. +func (l *ServersLoadBalancer) mergeable(loadBalancer *ServersLoadBalancer) bool { savedServers := l.Servers defer func() { l.Servers = savedServers @@ -359,16 +406,6 @@ func (l *ServersLoadBalancer) Mergeable(loadBalancer *ServersLoadBalancer) bool return reflect.DeepEqual(l, loadBalancer) } -// SetDefaults Default values for a ServersLoadBalancer. -func (l *ServersLoadBalancer) SetDefaults() { - defaultPassHostHeader := DefaultPassHostHeader - l.PassHostHeader = &defaultPassHostHeader - - l.Strategy = BalancerStrategyWRR - l.ResponseForwarding = &ResponseForwarding{} - l.ResponseForwarding.SetDefaults() -} - // +k8s:deepcopy-gen=true // ResponseForwarding holds the response forwarding configuration. diff --git a/pkg/config/dynamic/tcp_config.go b/pkg/config/dynamic/tcp_config.go index 51a96373a..2cb3f0aa7 100644 --- a/pkg/config/dynamic/tcp_config.go +++ b/pkg/config/dynamic/tcp_config.go @@ -35,6 +35,16 @@ type TCPService struct { Weighted *TCPWeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-" export:"true"` } +// Merge merges another TCPService into this one. +// Returns true if the merge succeeds, false if configurations conflict. +func (s *TCPService) Merge(other *TCPService) bool { + if s.LoadBalancer == nil || other.LoadBalancer == nil { + return reflect.DeepEqual(s, other) + } + + return s.LoadBalancer.Merge(other.LoadBalancer) +} + // +k8s:deepcopy-gen=true // TCPWeightedRoundRobin is a weighted round robin tcp load-balancer of services. @@ -102,8 +112,29 @@ type TCPServersLoadBalancer struct { HealthCheck *TCPServerHealthCheck `json:"healthCheck,omitempty" toml:"healthCheck,omitempty" yaml:"healthCheck,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` } -// Mergeable tells if the given service is mergeable. -func (l *TCPServersLoadBalancer) Mergeable(loadBalancer *TCPServersLoadBalancer) bool { +// Merge merges the other load balancer into this one. +// Returns true if the merge succeeds, false if configurations conflict. +func (l *TCPServersLoadBalancer) Merge(other *TCPServersLoadBalancer) bool { + if !l.mergeable(other) { + return false + } + + // Deduplicate and append servers. + uniq := make(map[string]struct{}, len(l.Servers)) + for _, server := range l.Servers { + uniq[server.Address] = struct{}{} + } + for _, server := range other.Servers { + if _, ok := uniq[server.Address]; !ok { + l.Servers = append(l.Servers, server) + } + } + + return true +} + +// mergeable tells if the given service is mergeable. +func (l *TCPServersLoadBalancer) mergeable(loadBalancer *TCPServersLoadBalancer) bool { savedServers := l.Servers defer func() { l.Servers = savedServers diff --git a/pkg/config/dynamic/udp_config.go b/pkg/config/dynamic/udp_config.go index 9e601a2df..96d22163a 100644 --- a/pkg/config/dynamic/udp_config.go +++ b/pkg/config/dynamic/udp_config.go @@ -20,6 +20,16 @@ type UDPService struct { Weighted *UDPWeightedRoundRobin `json:"weighted,omitempty" toml:"weighted,omitempty" yaml:"weighted,omitempty" label:"-" export:"true"` } +// Merge merges another UDPService into this one. +// Returns true if the merge succeeds, false if configurations conflict. +func (s *UDPService) Merge(other *UDPService) bool { + if s.LoadBalancer == nil || other.LoadBalancer == nil { + return reflect.DeepEqual(s, other) + } + + return s.LoadBalancer.Merge(other.LoadBalancer) +} + // +k8s:deepcopy-gen=true // UDPWeightedRoundRobin is a weighted round robin UDP load-balancer of services. @@ -56,8 +66,29 @@ type UDPServersLoadBalancer struct { Servers []UDPServer `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server" export:"true"` } -// Mergeable reports whether the given load-balancer can be merged with the receiver. -func (l *UDPServersLoadBalancer) Mergeable(loadBalancer *UDPServersLoadBalancer) bool { +// Merge merges the other load balancer into this one. +// Returns true if merge succeeded, false if configurations conflict. +func (l *UDPServersLoadBalancer) Merge(other *UDPServersLoadBalancer) bool { + if !l.mergeable(other) { + return false + } + + // Deduplicate and append servers. + uniq := make(map[string]struct{}, len(l.Servers)) + for _, server := range l.Servers { + uniq[server.Address] = struct{}{} + } + for _, server := range other.Servers { + if _, ok := uniq[server.Address]; !ok { + l.Servers = append(l.Servers, server) + } + } + + return true +} + +// mergeable reports whether the given load-balancer can be merged with the receiver. +func (l *UDPServersLoadBalancer) mergeable(loadBalancer *UDPServersLoadBalancer) bool { savedServers := l.Servers defer func() { l.Servers = savedServers diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index f19b3448c..965175453 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -489,6 +489,7 @@ func (in *HRWService) DeepCopy() *HRWService { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPConfiguration) DeepCopyInto(out *HTTPConfiguration) { *out = *in + out.HTTP = in.HTTP if in.Routers != nil { in, out := &in.Routers, &out.Routers *out = make(map[string]*Router, len(*in)) @@ -1390,6 +1391,7 @@ func (in *Retry) DeepCopy() *Retry { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Router) DeepCopyInto(out *Router) { *out = *in + out.Router = in.Router if in.EntryPoints != nil { in, out := &in.EntryPoints, &out.EntryPoints *out = make([]string, len(*in)) @@ -1672,6 +1674,11 @@ func (in *ServersTransport) DeepCopy() *ServersTransport { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Service) DeepCopyInto(out *Service) { *out = *in + if in.Middlewares != nil { + in, out := &in.Middlewares, &out.Middlewares + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.LoadBalancer != nil { in, out := &in.LoadBalancer, &out.LoadBalancer *out = new(ServersLoadBalancer) diff --git a/pkg/healthcheck/healthcheck_tcp_test.go b/pkg/healthcheck/healthcheck_tcp_test.go index 70f91e8a8..4f80fd017 100644 --- a/pkg/healthcheck/healthcheck_tcp_test.go +++ b/pkg/healthcheck/healthcheck_tcp_test.go @@ -466,42 +466,52 @@ func TestServiceTCPHealthChecker_Launch(t *testing.T) { }, } - lb := &testLoadBalancer{} + // Create load balancer with event channel for synchronization. + lb := &testLoadBalancer{ + RWMutex: &sync.RWMutex{}, + eventCh: make(chan struct{}, len(test.server.StatusSequence)+5), + } serviceInfo := &truntime.TCPServiceInfo{} service := NewServiceTCPHealthChecker(ctx, test.config, lb, serviceInfo, targets, "serviceName") go service.Launch(ctx) - // How much time to wait for the health check to actually complete. - deadline := time.Now().Add(200 * time.Millisecond) - // TLS handshake can take much longer. + // Timeout for each event - TLS handshake can take longer. + eventTimeout := 500 * time.Millisecond if test.server.TLS { - deadline = time.Now().Add(1000 * time.Millisecond) + eventTimeout = 2 * time.Second } - // Wait for all health checks to complete deterministically + // Wait for health check events using channel synchronization. + // Iterate over StatusSequence to release each connection via Next(). for i := range test.server.StatusSequence { test.server.Next() - initialUpserted := lb.numUpsertedServers - initialRemoved := lb.numRemovedServers - - for time.Now().Before(deadline) { - time.Sleep(5 * time.Millisecond) - if lb.numUpsertedServers > initialUpserted || lb.numRemovedServers > initialRemoved { - // Stop the health checker immediately after the last expected sequence completes - // to prevent extra health checks from firing and modifying the counters. - if i == len(test.server.StatusSequence)-1 { - cancel() - } - break + select { + case <-lb.eventCh: + // Event received + // On the last iteration, stop the health checker immediately + // to prevent extra checks from modifying the counters. + if i == len(test.server.StatusSequence)-1 { + test.server.Close() + cancel() } + case <-time.After(eventTimeout): + t.Fatalf("timeout waiting for health check event %d/%d", i+1, len(test.server.StatusSequence)) } } - assert.Equal(t, test.expNumRemovedServers, lb.numRemovedServers, "removed servers") - assert.Equal(t, test.expNumUpsertedServers, lb.numUpsertedServers, "upserted servers") + // Small delay to let goroutines clean up. + time.Sleep(10 * time.Millisecond) + + lb.RLock() + removedServers := lb.numRemovedServers + upsertedServers := lb.numUpsertedServers + lb.RUnlock() + + assert.Equal(t, test.expNumRemovedServers, removedServers, "removed servers") + assert.Equal(t, test.expNumUpsertedServers, upsertedServers, "upserted servers") assert.Equal(t, map[string]string{test.server.Addr.String(): test.targetStatus}, serviceInfo.GetAllStatus()) }) } @@ -597,6 +607,8 @@ type sequencedTCPServer struct { StatusSequence []tcpMockSequence TLS bool release chan struct{} + mu sync.Mutex + listener net.Listener } func newTCPServer(t *testing.T, tlsEnabled bool, statusSequence ...tcpMockSequence) *sequencedTCPServer { @@ -624,17 +636,28 @@ func (s *sequencedTCPServer) Next() { s.release <- struct{}{} } +func (s *sequencedTCPServer) Close() { + s.mu.Lock() + defer s.mu.Unlock() + if s.listener != nil { + s.listener.Close() + s.listener = nil + } +} + func (s *sequencedTCPServer) Start(t *testing.T) { t.Helper() go func() { - var listener net.Listener - for _, seq := range s.StatusSequence { <-s.release - if listener != nil { - listener.Close() + + s.mu.Lock() + if s.listener != nil { + s.listener.Close() + s.listener = nil } + s.mu.Unlock() if !seq.accept { continue @@ -643,7 +666,7 @@ func (s *sequencedTCPServer) Start(t *testing.T) { lis, err := net.ListenTCP("tcp", s.Addr) require.NoError(t, err) - listener = lis + var listener net.Listener = lis if s.TLS { cert, err := tls.X509KeyPair(localhostCert, localhostKey) @@ -670,8 +693,18 @@ func (s *sequencedTCPServer) Start(t *testing.T) { ) } + s.mu.Lock() + s.listener = listener + s.mu.Unlock() + conn, err := listener.Accept() - require.NoError(t, err) + if err != nil { + // Listener was closed during shutdown - this is expected behavior. + if strings.Contains(err.Error(), "use of closed network connection") { + return + } + require.NoError(t, err) + } t.Cleanup(func() { _ = conn.Close() }) diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go index cf2bdf2a4..077a01349 100644 --- a/pkg/healthcheck/healthcheck_test.go +++ b/pkg/healthcheck/healthcheck_test.go @@ -418,7 +418,12 @@ func TestServiceHealthChecker_Launch(t *testing.T) { targetURL, timeout := test.server.Start(t, cancel) - lb := &testLoadBalancer{RWMutex: &sync.RWMutex{}} + // Create load balancer with event channel for synchronization. + expectedEvents := test.expNumRemovedServers + test.expNumUpsertedServers + lb := &testLoadBalancer{ + RWMutex: &sync.RWMutex{}, + eventCh: make(chan struct{}, expectedEvents+5), + } config := &dynamic.ServerHealthCheck{ Mode: test.mode, @@ -441,18 +446,30 @@ func TestServiceHealthChecker_Launch(t *testing.T) { wg.Done() }() - select { - case <-time.After(timeout): - t.Fatal("test did not complete in time") - case <-ctx.Done(): - wg.Wait() + // Wait for expected health check events using channel synchronization. + for i := range expectedEvents { + select { + case <-lb.eventCh: + // Event received. + // On the last event, cancel to prevent extra health checks. + if i == expectedEvents-1 { + cancel() + } + case <-time.After(timeout): + t.Fatalf("timeout waiting for health check event %d/%d", i+1, expectedEvents) + } } - lb.Lock() - defer lb.Unlock() + // Wait for the health checker goroutine to exit before making assertions. + wg.Wait() - assert.Equal(t, test.expNumRemovedServers, lb.numRemovedServers, "removed servers") - assert.Equal(t, test.expNumUpsertedServers, lb.numUpsertedServers, "upserted servers") + lb.RLock() + removedServers := lb.numRemovedServers + upsertedServers := lb.numUpsertedServers + lb.RUnlock() + + assert.Equal(t, test.expNumRemovedServers, removedServers, "removed servers") + assert.Equal(t, test.expNumUpsertedServers, upsertedServers, "upserted servers") assert.InDelta(t, test.expGaugeValue, gauge.GaugeValue, delta, "ServerUp Gauge") assert.Equal(t, []string{"service", "foobar", "url", targetURL.String()}, gauge.LastLabelValues) assert.Equal(t, map[string]string{targetURL.String(): test.targetStatus}, serviceInfo.GetAllStatus()) diff --git a/pkg/healthcheck/mock_test.go b/pkg/healthcheck/mock_test.go index 0adcf1b25..cea0f670c 100644 --- a/pkg/healthcheck/mock_test.go +++ b/pkg/healthcheck/mock_test.go @@ -168,14 +168,29 @@ type testLoadBalancer struct { numRemovedServers int numUpsertedServers int + + // eventCh is used to signal when a status change occurs, allowing tests + // to synchronize with health check events deterministically. + eventCh chan struct{} } func (lb *testLoadBalancer) SetStatus(ctx context.Context, childName string, up bool) { + lb.Lock() if up { lb.numUpsertedServers++ } else { lb.numRemovedServers++ } + lb.Unlock() + + // Signal the event if a listener is registered. + if lb.eventCh != nil { + select { + case lb.eventCh <- struct{}{}: + default: + // Don't block if channel is full or no listener. + } + } } type MetricsMock struct { diff --git a/pkg/middlewares/chain/chain.go b/pkg/middlewares/chain/chain.go index d87596b42..e7b88381e 100644 --- a/pkg/middlewares/chain/chain.go +++ b/pkg/middlewares/chain/chain.go @@ -13,14 +13,14 @@ const ( typeName = "Chain" ) -type chainBuilder interface { - BuildChain(ctx context.Context, middlewares []string) *alice.Chain +type middlewareChainBuilder interface { + BuildMiddlewareChain(ctx context.Context, middlewares []string) *alice.Chain } // New creates a chain middleware. -func New(ctx context.Context, next http.Handler, config dynamic.Chain, builder chainBuilder, name string) (http.Handler, error) { +func New(ctx context.Context, next http.Handler, config dynamic.Chain, builder middlewareChainBuilder, name string) (http.Handler, error) { middlewares.GetLogger(ctx, name, typeName).Debug().Msg("Creating middleware") - middlewareChain := builder.BuildChain(ctx, config.Middlewares) + middlewareChain := builder.BuildMiddlewareChain(ctx, config.Middlewares) return middlewareChain.Then(next) } diff --git a/pkg/observability/metrics/influxdb2.go b/pkg/observability/metrics/influxdb2.go index aaa159391..1428c586c 100644 --- a/pkg/observability/metrics/influxdb2.go +++ b/pkg/observability/metrics/influxdb2.go @@ -3,6 +3,7 @@ package metrics import ( "context" "errors" + "strings" "time" "github.com/go-kit/kit/metrics/influx" @@ -138,11 +139,16 @@ func newInfluxDB2Client(config *otypes.InfluxDB2) (influxdb2.Client, error) { return nil, errors.New("token, org or bucket property is missing") } + token, err := config.Token.Read() + if err != nil { + return nil, err + } + // Disable InfluxDB2 logs. // See https://github.com/influxdata/influxdb-client-go/blob/v2.7.0/options.go#L128 influxdb2log.Log = nil - return influxdb2.NewClient(config.Address, config.Token), nil + return influxdb2.NewClient(config.Address, strings.TrimSpace(string(token))), nil } type influxDB2Writer struct { diff --git a/pkg/observability/metrics/prometheus_test.go b/pkg/observability/metrics/prometheus_test.go index f3b8c9f49..b7e052de1 100644 --- a/pkg/observability/metrics/prometheus_test.go +++ b/pkg/observability/metrics/prometheus_test.go @@ -413,13 +413,13 @@ func TestPrometheusMetricRemoval(t *testing.T) { th.WithRouter("foo@providerName", th.WithServiceName("bar")), th.WithRouter("router2", th.WithServiceName("bar@providerName")), ), - th.WithLoadBalancerServices( - th.WithService("bar@providerName", th.WithServers( + th.WithServices( + th.WithService("bar@providerName", th.WithServiceServersLoadBalancer(th.WithServers( th.WithServer("http://localhost:9000"), th.WithServer("http://localhost:9999"), th.WithServer("http://localhost:9998"), - )), - th.WithService("service1", th.WithServers(th.WithServer("http://localhost:9000"))), + ))), + th.WithService("service1", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer("http://localhost:9000")))), ), ), } @@ -429,8 +429,8 @@ func TestPrometheusMetricRemoval(t *testing.T) { th.WithRouters( th.WithRouter("foo@providerName", th.WithServiceName("bar")), ), - th.WithLoadBalancerServices( - th.WithService("bar@providerName", th.WithServers(th.WithServer("http://localhost:9000"))), + th.WithServices( + th.WithService("bar@providerName", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer("http://localhost:9000")))), ), ), } @@ -500,8 +500,8 @@ func TestPrometheusMetricRemoveEndpointForRecoveredService(t *testing.T) { conf1 := dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithLoadBalancerServices( - th.WithService("service1", th.WithServers(th.WithServer("http://localhost:9000"))), + th.WithServices( + th.WithService("service1", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer("http://localhost:9000")))), ), ), } @@ -512,8 +512,8 @@ func TestPrometheusMetricRemoveEndpointForRecoveredService(t *testing.T) { conf3 := dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithLoadBalancerServices( - th.WithService("service1", th.WithServers(th.WithServer("http://localhost:9001"))), + th.WithServices( + th.WithService("service1", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer("http://localhost:9001")))), ), ), } @@ -539,8 +539,8 @@ func TestPrometheusRemovedMetricsReset(t *testing.T) { conf1 := dynamic.Configuration{ HTTP: th.BuildConfiguration( - th.WithLoadBalancerServices(th.WithService("service", - th.WithServers(th.WithServer("http://localhost:9000"))), + th.WithServices( + th.WithService("service", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer("http://localhost:9000")))), ), ), } diff --git a/pkg/observability/types/metrics.go b/pkg/observability/types/metrics.go index 331544513..fa788ccf9 100644 --- a/pkg/observability/types/metrics.go +++ b/pkg/observability/types/metrics.go @@ -6,6 +6,7 @@ import ( "time" "github.com/traefik/paerser/types" + tTypes "github.com/traefik/traefik/v3/pkg/types" ) // Metrics provides options to expose and send Traefik metrics to different third party monitoring systems. @@ -87,15 +88,15 @@ func (s *Statsd) SetDefaults() { // InfluxDB2 contains address, token and metrics pushing interval configuration. type InfluxDB2 struct { - Address string `description:"InfluxDB v2 address." json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"` - Token string `description:"InfluxDB v2 access token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"` - PushInterval types.Duration `description:"InfluxDB v2 push interval." json:"pushInterval,omitempty" toml:"pushInterval,omitempty" yaml:"pushInterval,omitempty" export:"true"` - Org string `description:"InfluxDB v2 org ID." json:"org,omitempty" toml:"org,omitempty" yaml:"org,omitempty" export:"true"` - Bucket string `description:"InfluxDB v2 bucket ID." json:"bucket,omitempty" toml:"bucket,omitempty" yaml:"bucket,omitempty" export:"true"` - AddEntryPointsLabels bool `description:"Enable metrics on entry points." json:"addEntryPointsLabels,omitempty" toml:"addEntryPointsLabels,omitempty" yaml:"addEntryPointsLabels,omitempty" export:"true"` - AddRoutersLabels bool `description:"Enable metrics on routers." json:"addRoutersLabels,omitempty" toml:"addRoutersLabels,omitempty" yaml:"addRoutersLabels,omitempty" export:"true"` - AddServicesLabels bool `description:"Enable metrics on services." json:"addServicesLabels,omitempty" toml:"addServicesLabels,omitempty" yaml:"addServicesLabels,omitempty" export:"true"` - AdditionalLabels map[string]string `description:"Additional labels (influxdb tags) on all metrics" json:"additionalLabels,omitempty" toml:"additionalLabels,omitempty" yaml:"additionalLabels,omitempty" export:"true"` + Address string `description:"InfluxDB v2 address." json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"` + Token tTypes.FileOrContent `description:"InfluxDB v2 access token. It accepts either a token value or a file path to the token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"` + PushInterval types.Duration `description:"InfluxDB v2 push interval." json:"pushInterval,omitempty" toml:"pushInterval,omitempty" yaml:"pushInterval,omitempty" export:"true"` + Org string `description:"InfluxDB v2 org ID." json:"org,omitempty" toml:"org,omitempty" yaml:"org,omitempty" export:"true"` + Bucket string `description:"InfluxDB v2 bucket ID." json:"bucket,omitempty" toml:"bucket,omitempty" yaml:"bucket,omitempty" export:"true"` + AddEntryPointsLabels bool `description:"Enable metrics on entry points." json:"addEntryPointsLabels,omitempty" toml:"addEntryPointsLabels,omitempty" yaml:"addEntryPointsLabels,omitempty" export:"true"` + AddRoutersLabels bool `description:"Enable metrics on routers." json:"addRoutersLabels,omitempty" toml:"addRoutersLabels,omitempty" yaml:"addRoutersLabels,omitempty" export:"true"` + AddServicesLabels bool `description:"Enable metrics on services." json:"addServicesLabels,omitempty" toml:"addServicesLabels,omitempty" yaml:"addServicesLabels,omitempty" export:"true"` + AdditionalLabels map[string]string `description:"Additional labels (influxdb tags) on all metrics" json:"additionalLabels,omitempty" toml:"additionalLabels,omitempty" yaml:"additionalLabels,omitempty" export:"true"` } // SetDefaults sets the default values. diff --git a/pkg/provider/acme/account.go b/pkg/provider/acme/account.go index 434f58e3a..2c57fd9bd 100644 --- a/pkg/provider/acme/account.go +++ b/pkg/provider/acme/account.go @@ -58,7 +58,7 @@ func (a *Account) GetPrivateKey() crypto.PrivateKey { privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey) if err != nil { log.Error().Str(logs.ProviderName, "acme"). - Err(err).Msgf("Cannot unmarshal private key %+v", a.PrivateKey) + Err(err).Msg("Cannot unmarshal private key") return nil } diff --git a/pkg/provider/acme/provider.go b/pkg/provider/acme/provider.go index ab72250de..aa3d045cd 100644 --- a/pkg/provider/acme/provider.go +++ b/pkg/provider/acme/provider.go @@ -812,8 +812,8 @@ func getCertificateRenewDurations(certificatesDuration int) (time.Duration, time return 30 * 24 * time.Hour, 24 * time.Hour // 30 days, 1 day case certificatesDuration >= 30*24: // >= 30 days return 10 * 24 * time.Hour, 12 * time.Hour // 10 days, 12 hours - case certificatesDuration >= 7*24: // >= 7 days - return 24 * time.Hour, time.Hour // 1 days, 1 hour + case certificatesDuration >= 6*24: // >= 6 days + return 2 * 24 * time.Hour, 2 * time.Hour // 2 days, 2 hours case certificatesDuration >= 24: // >= 1 days return 6 * time.Hour, 10 * time.Minute // 6 hours, 10 minutes default: diff --git a/pkg/provider/acme/provider_test.go b/pkg/provider/acme/provider_test.go index 477164e3a..e7a16952f 100644 --- a/pkg/provider/acme/provider_test.go +++ b/pkg/provider/acme/provider_test.go @@ -612,6 +612,12 @@ func Test_getCertificateRenewDurations(t *testing.T) { expectRenewPeriod: time.Hour * 24 * 30, expectRenewInterval: time.Hour * 24, }, + { + desc: "45 Days certificates (Let's Encrypt 2028 standard): 10 days renew period, 12 hour renew interval", + certificatesDurations: 24 * 45, + expectRenewPeriod: time.Hour * 24 * 10, + expectRenewInterval: time.Hour * 12, + }, { desc: "30 Days certificates: 10 days renew period, 12 hour renew interval", certificatesDurations: 24 * 30, @@ -619,10 +625,16 @@ func Test_getCertificateRenewDurations(t *testing.T) { expectRenewInterval: time.Hour * 12, }, { - desc: "7 Days certificates: 1 days renew period, 1 hour renew interval", + desc: "7 Days certificates: 2 days renew period, 2 hour renew interval", certificatesDurations: 24 * 7, - expectRenewPeriod: time.Hour * 24, - expectRenewInterval: time.Hour, + expectRenewPeriod: time.Hour * 24 * 2, + expectRenewInterval: time.Hour * 2, + }, + { + desc: "160 hour certificate (Let's Encrypt 'shortlived' profile): 2 days renew period, 2 hour renew interval", + certificatesDurations: 160, + expectRenewPeriod: time.Hour * 24 * 2, + expectRenewInterval: time.Hour * 2, }, { desc: "24 Hours certificates: 6 hours renew period, 10 minutes renew interval", diff --git a/pkg/provider/configuration.go b/pkg/provider/configuration.go index d310881f8..f69ad1f1d 100644 --- a/pkg/provider/configuration.go +++ b/pkg/provider/configuration.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "maps" - "reflect" "slices" "strings" "text/template" @@ -14,388 +13,8 @@ import ( "github.com/rs/zerolog/log" "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/observability/logs" - "github.com/traefik/traefik/v3/pkg/tls" ) -// Merge merges multiple configurations. -func Merge(ctx context.Context, configurations map[string]*dynamic.Configuration) *dynamic.Configuration { - logger := log.Ctx(ctx) - - configuration := &dynamic.Configuration{ - HTTP: &dynamic.HTTPConfiguration{ - Routers: make(map[string]*dynamic.Router), - Middlewares: make(map[string]*dynamic.Middleware), - Services: make(map[string]*dynamic.Service), - ServersTransports: make(map[string]*dynamic.ServersTransport), - }, - TCP: &dynamic.TCPConfiguration{ - Routers: make(map[string]*dynamic.TCPRouter), - Services: make(map[string]*dynamic.TCPService), - Middlewares: make(map[string]*dynamic.TCPMiddleware), - ServersTransports: make(map[string]*dynamic.TCPServersTransport), - }, - UDP: &dynamic.UDPConfiguration{ - Routers: make(map[string]*dynamic.UDPRouter), - Services: make(map[string]*dynamic.UDPService), - }, - TLS: &dynamic.TLSConfiguration{ - Stores: make(map[string]tls.Store), - }, - } - - servicesToDelete := map[string]struct{}{} - services := map[string][]string{} - - routersToDelete := map[string]struct{}{} - routers := map[string][]string{} - - servicesTCPToDelete := map[string]struct{}{} - servicesTCP := map[string][]string{} - - routersTCPToDelete := map[string]struct{}{} - routersTCP := map[string][]string{} - - servicesUDPToDelete := map[string]struct{}{} - servicesUDP := map[string][]string{} - - routersUDPToDelete := map[string]struct{}{} - routersUDP := map[string][]string{} - - middlewaresToDelete := map[string]struct{}{} - middlewares := map[string][]string{} - - middlewaresTCPToDelete := map[string]struct{}{} - middlewaresTCP := map[string][]string{} - - transportsToDelete := map[string]struct{}{} - transports := map[string][]string{} - - transportsTCPToDelete := map[string]struct{}{} - transportsTCP := map[string][]string{} - - storesToDelete := map[string]struct{}{} - stores := map[string][]string{} - - var sortedKeys []string - for key := range configurations { - sortedKeys = append(sortedKeys, key) - } - slices.Sort(sortedKeys) - - for _, root := range sortedKeys { - conf := configurations[root] - for serviceName, service := range conf.HTTP.Services { - services[serviceName] = append(services[serviceName], root) - if !AddService(configuration.HTTP, serviceName, service) { - servicesToDelete[serviceName] = struct{}{} - } - } - - for routerName, router := range conf.HTTP.Routers { - routers[routerName] = append(routers[routerName], root) - if !AddRouter(configuration.HTTP, routerName, router) { - routersToDelete[routerName] = struct{}{} - } - } - - for transportName, transport := range conf.HTTP.ServersTransports { - transports[transportName] = append(transports[transportName], root) - if !AddTransport(configuration.HTTP, transportName, transport) { - transportsToDelete[transportName] = struct{}{} - } - } - - for serviceName, service := range conf.TCP.Services { - servicesTCP[serviceName] = append(servicesTCP[serviceName], root) - if !AddServiceTCP(configuration.TCP, serviceName, service) { - servicesTCPToDelete[serviceName] = struct{}{} - } - } - - for routerName, router := range conf.TCP.Routers { - routersTCP[routerName] = append(routersTCP[routerName], root) - if !AddRouterTCP(configuration.TCP, routerName, router) { - routersTCPToDelete[routerName] = struct{}{} - } - } - - for transportName, transport := range conf.TCP.ServersTransports { - transportsTCP[transportName] = append(transportsTCP[transportName], root) - if !AddTransportTCP(configuration.TCP, transportName, transport) { - transportsTCPToDelete[transportName] = struct{}{} - } - } - - for serviceName, service := range conf.UDP.Services { - servicesUDP[serviceName] = append(servicesUDP[serviceName], root) - if !AddServiceUDP(configuration.UDP, serviceName, service) { - servicesUDPToDelete[serviceName] = struct{}{} - } - } - - for routerName, router := range conf.UDP.Routers { - routersUDP[routerName] = append(routersUDP[routerName], root) - if !AddRouterUDP(configuration.UDP, routerName, router) { - routersUDPToDelete[routerName] = struct{}{} - } - } - - for middlewareName, middleware := range conf.HTTP.Middlewares { - middlewares[middlewareName] = append(middlewares[middlewareName], root) - if !AddMiddleware(configuration.HTTP, middlewareName, middleware) { - middlewaresToDelete[middlewareName] = struct{}{} - } - } - - for middlewareName, middleware := range conf.TCP.Middlewares { - middlewaresTCP[middlewareName] = append(middlewaresTCP[middlewareName], root) - if !AddMiddlewareTCP(configuration.TCP, middlewareName, middleware) { - middlewaresTCPToDelete[middlewareName] = struct{}{} - } - } - - for storeName, store := range conf.TLS.Stores { - stores[storeName] = append(stores[storeName], root) - if !AddStore(configuration.TLS, storeName, store) { - storesToDelete[storeName] = struct{}{} - } - } - } - - for serviceName := range servicesToDelete { - logger.Error().Str(logs.ServiceName, serviceName). - Interface("configuration", services[serviceName]). - Msg("Service defined multiple times with different configurations") - delete(configuration.HTTP.Services, serviceName) - } - - for routerName := range routersToDelete { - logger.Error().Str(logs.RouterName, routerName). - Interface("configuration", routers[routerName]). - Msg("Router defined multiple times with different configurations") - delete(configuration.HTTP.Routers, routerName) - } - - for transportName := range transportsToDelete { - logger.Error().Str(logs.ServersTransportName, transportName). - Interface("configuration", transports[transportName]). - Msg("ServersTransport defined multiple times with different configurations") - delete(configuration.HTTP.ServersTransports, transportName) - } - - for serviceName := range servicesTCPToDelete { - logger.Error().Str(logs.ServiceName, serviceName). - Interface("configuration", servicesTCP[serviceName]). - Msg("Service TCP defined multiple times with different configurations") - delete(configuration.TCP.Services, serviceName) - } - - for routerName := range routersTCPToDelete { - logger.Error().Str(logs.RouterName, routerName). - Interface("configuration", routersTCP[routerName]). - Msg("Router TCP defined multiple times with different configurations") - delete(configuration.TCP.Routers, routerName) - } - - for transportName := range transportsTCPToDelete { - logger.Error().Str(logs.ServersTransportName, transportName). - Interface("configuration", transportsTCP[transportName]). - Msg("ServersTransport TCP defined multiple times with different configurations") - delete(configuration.TCP.ServersTransports, transportName) - } - - for serviceName := range servicesUDPToDelete { - logger.Error().Str(logs.ServiceName, serviceName). - Interface("configuration", servicesUDP[serviceName]). - Msg("UDP service defined multiple times with different configurations") - delete(configuration.UDP.Services, serviceName) - } - - for routerName := range routersUDPToDelete { - logger.Error().Str(logs.RouterName, routerName). - Interface("configuration", routersUDP[routerName]). - Msg("UDP router defined multiple times with different configurations") - delete(configuration.UDP.Routers, routerName) - } - - for middlewareName := range middlewaresToDelete { - logger.Error().Str(logs.MiddlewareName, middlewareName). - Interface("configuration", middlewares[middlewareName]). - Msg("Middleware defined multiple times with different configurations") - delete(configuration.HTTP.Middlewares, middlewareName) - } - - for middlewareName := range middlewaresTCPToDelete { - logger.Error().Str(logs.MiddlewareName, middlewareName). - Interface("configuration", middlewaresTCP[middlewareName]). - Msg("TCP Middleware defined multiple times with different configurations") - delete(configuration.TCP.Middlewares, middlewareName) - } - - for storeName := range storesToDelete { - logger.Error().Str("storeName", storeName). - Msgf("TLS store defined multiple times with different configurations in %v", stores[storeName]) - delete(configuration.TLS.Stores, storeName) - } - - return configuration -} - -// AddServiceTCP adds a service to a configuration. -func AddServiceTCP(configuration *dynamic.TCPConfiguration, serviceName string, service *dynamic.TCPService) bool { - if _, ok := configuration.Services[serviceName]; !ok { - configuration.Services[serviceName] = service - return true - } - - if !configuration.Services[serviceName].LoadBalancer.Mergeable(service.LoadBalancer) { - return false - } - - uniq := map[string]struct{}{} - for _, server := range configuration.Services[serviceName].LoadBalancer.Servers { - uniq[server.Address] = struct{}{} - } - - for _, server := range service.LoadBalancer.Servers { - if _, ok := uniq[server.Address]; !ok { - configuration.Services[serviceName].LoadBalancer.Servers = append(configuration.Services[serviceName].LoadBalancer.Servers, server) - } - } - - return true -} - -// AddRouterTCP adds a router to a configuration. -func AddRouterTCP(configuration *dynamic.TCPConfiguration, routerName string, router *dynamic.TCPRouter) bool { - if _, ok := configuration.Routers[routerName]; !ok { - configuration.Routers[routerName] = router - return true - } - - return reflect.DeepEqual(configuration.Routers[routerName], router) -} - -// AddMiddlewareTCP adds a middleware to a configuration. -func AddMiddlewareTCP(configuration *dynamic.TCPConfiguration, middlewareName string, middleware *dynamic.TCPMiddleware) bool { - if _, ok := configuration.Middlewares[middlewareName]; !ok { - configuration.Middlewares[middlewareName] = middleware - return true - } - - return reflect.DeepEqual(configuration.Middlewares[middlewareName], middleware) -} - -// AddTransportTCP adds a servers transport to a configuration. -func AddTransportTCP(configuration *dynamic.TCPConfiguration, transportName string, transport *dynamic.TCPServersTransport) bool { - if _, ok := configuration.ServersTransports[transportName]; !ok { - configuration.ServersTransports[transportName] = transport - return true - } - - return reflect.DeepEqual(configuration.ServersTransports[transportName], transport) -} - -// AddServiceUDP adds a service to a configuration. -func AddServiceUDP(configuration *dynamic.UDPConfiguration, serviceName string, service *dynamic.UDPService) bool { - if _, ok := configuration.Services[serviceName]; !ok { - configuration.Services[serviceName] = service - return true - } - - if !configuration.Services[serviceName].LoadBalancer.Mergeable(service.LoadBalancer) { - return false - } - - uniq := map[string]struct{}{} - for _, server := range configuration.Services[serviceName].LoadBalancer.Servers { - uniq[server.Address] = struct{}{} - } - - for _, server := range service.LoadBalancer.Servers { - if _, ok := uniq[server.Address]; !ok { - configuration.Services[serviceName].LoadBalancer.Servers = append(configuration.Services[serviceName].LoadBalancer.Servers, server) - } - } - - return true -} - -// AddRouterUDP adds a router to a configuration. -func AddRouterUDP(configuration *dynamic.UDPConfiguration, routerName string, router *dynamic.UDPRouter) bool { - if _, ok := configuration.Routers[routerName]; !ok { - configuration.Routers[routerName] = router - return true - } - - return reflect.DeepEqual(configuration.Routers[routerName], router) -} - -// AddService adds a service to a configuration. -func AddService(configuration *dynamic.HTTPConfiguration, serviceName string, service *dynamic.Service) bool { - if _, ok := configuration.Services[serviceName]; !ok { - configuration.Services[serviceName] = service - return true - } - - if !configuration.Services[serviceName].LoadBalancer.Mergeable(service.LoadBalancer) { - return false - } - - uniq := map[string]struct{}{} - for _, server := range configuration.Services[serviceName].LoadBalancer.Servers { - uniq[server.URL] = struct{}{} - } - - for _, server := range service.LoadBalancer.Servers { - if _, ok := uniq[server.URL]; !ok { - configuration.Services[serviceName].LoadBalancer.Servers = append(configuration.Services[serviceName].LoadBalancer.Servers, server) - } - } - - return true -} - -// AddRouter adds a router to a configuration. -func AddRouter(configuration *dynamic.HTTPConfiguration, routerName string, router *dynamic.Router) bool { - if _, ok := configuration.Routers[routerName]; !ok { - configuration.Routers[routerName] = router - return true - } - - return reflect.DeepEqual(configuration.Routers[routerName], router) -} - -// AddTransport adds a servers transport to a configuration. -func AddTransport(configuration *dynamic.HTTPConfiguration, transportName string, transport *dynamic.ServersTransport) bool { - if _, ok := configuration.ServersTransports[transportName]; !ok { - configuration.ServersTransports[transportName] = transport - return true - } - - return reflect.DeepEqual(configuration.ServersTransports[transportName], transport) -} - -// AddMiddleware adds a middleware to a configuration. -func AddMiddleware(configuration *dynamic.HTTPConfiguration, middlewareName string, middleware *dynamic.Middleware) bool { - if _, ok := configuration.Middlewares[middlewareName]; !ok { - configuration.Middlewares[middlewareName] = middleware - return true - } - - return reflect.DeepEqual(configuration.Middlewares[middlewareName], middleware) -} - -// AddStore adds a middleware to a configurations. -func AddStore(configuration *dynamic.TLSConfiguration, storeName string, store tls.Store) bool { - if _, ok := configuration.Stores[storeName]; !ok { - configuration.Stores[storeName] = store - return true - } - - return reflect.DeepEqual(configuration.Stores[storeName], store) -} - // MakeDefaultRuleTemplate creates the default rule template. func MakeDefaultRuleTemplate(defaultRule string, funcMap template.FuncMap) (*template.Template, error) { defaultFuncMap := sprig.TxtFuncMap() diff --git a/pkg/provider/consulcatalog/config.go b/pkg/provider/consulcatalog/config.go index 34c9a90dd..06d7c94c0 100644 --- a/pkg/provider/consulcatalog/config.go +++ b/pkg/provider/consulcatalog/config.go @@ -106,7 +106,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, items []itemData, cer configurations[svcName] = confFromLabel } - return provider.Merge(ctx, configurations) + return provider.Merge(ctx, provider.NameSortedConfigurations(configurations), provider.ResourceStrategyMerge) } func (p *Provider) keepContainer(ctx context.Context, item itemData) bool { diff --git a/pkg/provider/consulcatalog/connect_tls.go b/pkg/provider/consulcatalog/connect_tls.go index b73acb1a1..16456e51a 100644 --- a/pkg/provider/consulcatalog/connect_tls.go +++ b/pkg/provider/consulcatalog/connect_tls.go @@ -10,8 +10,9 @@ import ( // connectCert holds our certificates as a client of the Consul Connect protocol. type connectCert struct { - root []string - leaf keyPair + trustDomain string + root []string + leaf keyPair } func (c *connectCert) getRoot() []types.FileOrContent { @@ -52,7 +53,8 @@ func (c *connectCert) equals(other *connectCert) bool { } func (c *connectCert) serversTransport(item itemData) *dynamic.ServersTransport { - spiffeID := fmt.Sprintf("spiffe:///ns/%s/dc/%s/svc/%s", + spiffeID := fmt.Sprintf("spiffe://%s/ns/%s/dc/%s/svc/%s", + c.trustDomain, item.Namespace, item.Datacenter, item.Name, @@ -72,7 +74,8 @@ func (c *connectCert) serversTransport(item itemData) *dynamic.ServersTransport } func (c *connectCert) tcpServersTransport(item itemData) *dynamic.TCPServersTransport { - spiffeID := fmt.Sprintf("spiffe:///ns/%s/dc/%s/svc/%s", + spiffeID := fmt.Sprintf("spiffe://%s/ns/%s/dc/%s/svc/%s", + c.trustDomain, item.Namespace, item.Datacenter, item.Name, diff --git a/pkg/provider/consulcatalog/consul_catalog.go b/pkg/provider/consulcatalog/consul_catalog.go index 9da53ce26..0519db395 100644 --- a/pkg/provider/consulcatalog/consul_catalog.go +++ b/pkg/provider/consulcatalog/consul_catalog.go @@ -465,7 +465,7 @@ func (p *Provider) watchConnectTLS(ctx context.Context) error { } leafWatcher.HybridHandler = leafWatcherHandler(ctx, leafChan) - rootsChan := make(chan []string) + rootsChan := make(chan caRootList) rootsWatcher, err := watch.Parse(map[string]any{ "type": "connect_roots", }) @@ -497,9 +497,9 @@ func (p *Provider) watchConnectTLS(ctx context.Context) error { }() var ( - certInfo *connectCert - leafCerts keyPair - rootCerts []string + certInfo *connectCert + leafCert keyPair + caRoots caRootList ) for { @@ -510,13 +510,14 @@ func (p *Provider) watchConnectTLS(ctx context.Context) error { case err := <-errChan: return fmt.Errorf("leaf or roots watcher terminated: %w", err) - case rootCerts = <-rootsChan: - case leafCerts = <-leafChan: + case caRoots = <-rootsChan: + case leafCert = <-leafChan: } newCertInfo := &connectCert{ - root: rootCerts, - leaf: leafCerts, + trustDomain: caRoots.trustDomain, + root: caRoots.roots, + leaf: leafCert, } if newCertInfo.isReady() && !newCertInfo.equals(certInfo) { log.Ctx(ctx).Debug().Msgf("Updating connect certs for service %s", p.ServiceName) @@ -546,7 +547,12 @@ func (p *Provider) includesHealthStatus(status string) bool { return false } -func rootsWatchHandler(ctx context.Context, dest chan<- []string) func(watch.BlockingParamVal, any) { +type caRootList struct { + trustDomain string + roots []string +} + +func rootsWatchHandler(ctx context.Context, dest chan<- caRootList) func(watch.BlockingParamVal, any) { return func(_ watch.BlockingParamVal, raw any) { if raw == nil { log.Ctx(ctx).Error().Msg("Root certificate watcher called with nil") @@ -566,7 +572,7 @@ func rootsWatchHandler(ctx context.Context, dest chan<- []string) func(watch.Blo select { case <-ctx.Done(): - case dest <- roots: + case dest <- caRootList{trustDomain: v.TrustDomain, roots: roots}: } } } diff --git a/pkg/provider/docker/config.go b/pkg/provider/docker/config.go index 96133db4f..d88f538a9 100644 --- a/pkg/provider/docker/config.go +++ b/pkg/provider/docker/config.go @@ -101,7 +101,7 @@ func (p *DynConfBuilder) build(ctx context.Context, containersInspected []docker configurations[containerName] = confFromLabel } - return provider.Merge(ctx, configurations) + return provider.Merge(ctx, provider.NameSortedConfigurations(configurations), provider.ResourceStrategyMerge) } func (p *DynConfBuilder) buildTCPServiceConfiguration(ctx context.Context, container dockerData, configuration *dynamic.TCPConfiguration) error { diff --git a/pkg/provider/ecs/config.go b/pkg/provider/ecs/config.go index 7e87f5675..ad39ae176 100644 --- a/pkg/provider/ecs/config.go +++ b/pkg/provider/ecs/config.go @@ -86,7 +86,7 @@ func (p *Provider) buildConfiguration(ctx context.Context, instances []ecsInstan configurations[instanceName] = confFromLabel } - return provider.Merge(ctx, configurations) + return provider.Merge(ctx, provider.NameSortedConfigurations(configurations), provider.ResourceStrategyMerge) } func (p *Provider) buildTCPServiceConfiguration(instance ecsInstance, configuration *dynamic.TCPConfiguration) error { diff --git a/pkg/provider/file/file.go b/pkg/provider/file/file.go index 04395878e..a93d4b551 100644 --- a/pkg/provider/file/file.go +++ b/pkg/provider/file/file.go @@ -221,7 +221,12 @@ func (p *Provider) buildConfiguration() (*dynamic.Configuration, error) { ctx := log.With().Str(logs.ProviderName, providerName).Logger().WithContext(context.Background()) if len(p.Directory) > 0 { - return p.loadFileConfigFromDirectory(ctx, p.Directory, nil) + configurations, err := p.collectFileConfigs(ctx, p.Directory, "") + if err != nil { + return nil, fmt.Errorf("collecting file configs: %w", err) + } + + return provider.Merge(ctx, configurations, provider.ResourceStrategySkipDuplicates), nil } if len(p.Filename) > 0 { @@ -376,47 +381,28 @@ func (p *Provider) loadFileConfig(ctx context.Context, filename string, parseTem return configuration, nil } -func (p *Provider) loadFileConfigFromDirectory(ctx context.Context, directory string, configuration *dynamic.Configuration) (*dynamic.Configuration, error) { +// collectFileConfigs recursively collects configurations from files in the given directory. +func (p *Provider) collectFileConfigs(ctx context.Context, directory, prefix string) ([]provider.NamedConfiguration, error) { + var configurations []provider.NamedConfiguration + fileList, err := os.ReadDir(directory) if err != nil { - return configuration, fmt.Errorf("unable to read directory %s: %w", directory, err) + return nil, fmt.Errorf("reading directory %s: %w", directory, err) } - if configuration == nil { - configuration = &dynamic.Configuration{ - HTTP: &dynamic.HTTPConfiguration{ - Routers: make(map[string]*dynamic.Router), - Middlewares: make(map[string]*dynamic.Middleware), - Services: make(map[string]*dynamic.Service), - ServersTransports: make(map[string]*dynamic.ServersTransport), - }, - TCP: &dynamic.TCPConfiguration{ - Routers: make(map[string]*dynamic.TCPRouter), - Services: make(map[string]*dynamic.TCPService), - Middlewares: make(map[string]*dynamic.TCPMiddleware), - ServersTransports: make(map[string]*dynamic.TCPServersTransport), - }, - TLS: &dynamic.TLSConfiguration{ - Stores: make(map[string]tls.Store), - Options: make(map[string]tls.Options), - }, - UDP: &dynamic.UDPConfiguration{ - Routers: make(map[string]*dynamic.UDPRouter), - Services: make(map[string]*dynamic.UDPService), - }, - } - } - - configTLSMaps := make(map[*tls.CertAndStores]struct{}) - for _, item := range fileList { - logger := log.Ctx(ctx).With().Str("filename", item.Name()).Logger() + itemPath := filepath.Join(directory, item.Name()) + filename := item.Name() + if prefix != "" { + filename = filepath.Join(prefix, item.Name()) + } if item.IsDir() { - configuration, err = p.loadFileConfigFromDirectory(logger.WithContext(ctx), filepath.Join(directory, item.Name()), configuration) + sub, err := p.collectFileConfigs(ctx, itemPath, filename) if err != nil { - return configuration, fmt.Errorf("unable to load content configuration from subdirectory %s: %w", item, err) + return nil, fmt.Errorf("loading content configuration from subdirectory %s: %w", item, err) } + configurations = append(configurations, sub...) continue } @@ -427,132 +413,18 @@ func (p *Provider) loadFileConfigFromDirectory(ctx context.Context, directory st continue } - var c *dynamic.Configuration - c, err = p.loadFileConfig(logger.WithContext(ctx), filepath.Join(directory, item.Name()), true) + c, err := p.loadFileConfig(ctx, itemPath, true) if err != nil { - return configuration, fmt.Errorf("%s: %w", filepath.Join(directory, item.Name()), err) + return nil, fmt.Errorf("%s: %w", itemPath, err) } - for name, conf := range c.HTTP.Routers { - if _, exists := configuration.HTTP.Routers[name]; exists { - logger.Warn().Str(logs.RouterName, name).Msg("HTTP router already configured, skipping") - } else { - configuration.HTTP.Routers[name] = conf - } - } - - for name, conf := range c.HTTP.Middlewares { - if _, exists := configuration.HTTP.Middlewares[name]; exists { - logger.Warn().Str(logs.MiddlewareName, name).Msg("HTTP middleware already configured, skipping") - } else { - configuration.HTTP.Middlewares[name] = conf - } - } - - for name, conf := range c.HTTP.Services { - if _, exists := configuration.HTTP.Services[name]; exists { - logger.Warn().Str(logs.ServiceName, name).Msg("HTTP service already configured, skipping") - } else { - configuration.HTTP.Services[name] = conf - } - } - - for name, conf := range c.HTTP.ServersTransports { - if _, exists := configuration.HTTP.ServersTransports[name]; exists { - logger.Warn().Str(logs.ServersTransportName, name).Msg("HTTP servers transport already configured, skipping") - } else { - configuration.HTTP.ServersTransports[name] = conf - } - } - - for name, conf := range c.TCP.Routers { - if _, exists := configuration.TCP.Routers[name]; exists { - logger.Warn().Str(logs.RouterName, name).Msg("TCP router already configured, skipping") - } else { - configuration.TCP.Routers[name] = conf - } - } - - for name, conf := range c.TCP.Middlewares { - if _, exists := configuration.TCP.Middlewares[name]; exists { - logger.Warn().Str(logs.MiddlewareName, name).Msg("TCP middleware already configured, skipping") - } else { - configuration.TCP.Middlewares[name] = conf - } - } - - for name, conf := range c.TCP.Services { - if _, exists := configuration.TCP.Services[name]; exists { - logger.Warn().Str(logs.ServiceName, name).Msg("TCP service already configured, skipping") - } else { - configuration.TCP.Services[name] = conf - } - } - - for name, conf := range c.TCP.ServersTransports { - if _, exists := configuration.TCP.ServersTransports[name]; exists { - logger.Warn().Str(logs.ServersTransportName, name).Msg("TCP servers transport already configured, skipping") - } else { - configuration.TCP.ServersTransports[name] = conf - } - } - - for name, conf := range c.UDP.Routers { - if _, exists := configuration.UDP.Routers[name]; exists { - logger.Warn().Str(logs.RouterName, name).Msg("UDP router already configured, skipping") - } else { - configuration.UDP.Routers[name] = conf - } - } - - for name, conf := range c.UDP.Services { - if _, exists := configuration.UDP.Services[name]; exists { - logger.Warn().Str(logs.ServiceName, name).Msg("UDP service already configured, skipping") - } else { - configuration.UDP.Services[name] = conf - } - } - - for _, conf := range c.TLS.Certificates { - if _, exists := configTLSMaps[conf]; exists { - logger.Warn().Msgf("TLS configuration %v already configured, skipping", conf) - } else { - configTLSMaps[conf] = struct{}{} - } - } - - for name, conf := range c.TLS.Options { - if _, exists := configuration.TLS.Options[name]; exists { - logger.Warn().Msgf("TLS options %v already configured, skipping", name) - } else { - if configuration.TLS.Options == nil { - configuration.TLS.Options = map[string]tls.Options{} - } - configuration.TLS.Options[name] = conf - } - } - - for name, conf := range c.TLS.Stores { - if _, exists := configuration.TLS.Stores[name]; exists { - logger.Warn().Msgf("TLS store %v already configured, skipping", name) - } else { - if configuration.TLS.Stores == nil { - configuration.TLS.Stores = map[string]tls.Store{} - } - configuration.TLS.Stores[name] = conf - } - } + configurations = append(configurations, provider.NamedConfiguration{ + Name: filename, + Configuration: c, + }) } - if len(configTLSMaps) > 0 && configuration.TLS == nil { - configuration.TLS = &dynamic.TLSConfiguration{} - } - - for conf := range configTLSMaps { - configuration.TLS.Certificates = append(configuration.TLS.Certificates, conf) - } - - return configuration, nil + return configurations, nil } func (p *Provider) decodeConfiguration(filePath, content string) (*dynamic.Configuration, error) { diff --git a/pkg/provider/kubernetes/crd/fixtures/with_traefik_service_middleware.yml b/pkg/provider/kubernetes/crd/fixtures/with_traefik_service_middleware.yml new file mode 100644 index 000000000..6989e5dc1 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_traefik_service_middleware.yml @@ -0,0 +1,45 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: stripprefix + namespace: default + +spec: + stripPrefix: + prefixes: + - /tobestripped + +--- +apiVersion: traefik.io/v1alpha1 +kind: TraefikService +metadata: + name: test-weighted + namespace: default + +spec: + weighted: + services: + - name: whoami + port: 80 + weight: 1 + middlewares: + - name: stripprefix + +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`foo.com`) && PathPrefix(`/bar`) + kind: Rule + priority: 12 + services: + - name: test-weighted + kind: TraefikService \ No newline at end of file diff --git a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/loadbalancerspec.go b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/loadbalancerspec.go index 726f425a2..d4fffd444 100644 --- a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/loadbalancerspec.go +++ b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/loadbalancerspec.go @@ -37,6 +37,7 @@ type LoadBalancerSpecApplyConfiguration struct { Name *string `json:"name,omitempty"` Kind *string `json:"kind,omitempty"` Namespace *string `json:"namespace,omitempty"` + Middlewares []MiddlewareRefApplyConfiguration `json:"middlewares,omitempty"` Sticky *dynamic.Sticky `json:"sticky,omitempty"` Port *intstr.IntOrString `json:"port,omitempty"` Scheme *string `json:"scheme,omitempty"` @@ -81,6 +82,19 @@ func (b *LoadBalancerSpecApplyConfiguration) WithNamespace(value string) *LoadBa return b } +// WithMiddlewares adds the given value to the Middlewares field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Middlewares field. +func (b *LoadBalancerSpecApplyConfiguration) WithMiddlewares(values ...*MiddlewareRefApplyConfiguration) *LoadBalancerSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMiddlewares") + } + b.Middlewares = append(b.Middlewares, *values[i]) + } + return b +} + // WithSticky sets the Sticky field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Sticky field is set to the value of the last call. diff --git a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirroring.go b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirroring.go index 7140f1c37..1c7981fbf 100644 --- a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirroring.go +++ b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirroring.go @@ -70,6 +70,19 @@ func (b *MirroringApplyConfiguration) WithNamespace(value string) *MirroringAppl return b } +// WithMiddlewares adds the given value to the Middlewares field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Middlewares field. +func (b *MirroringApplyConfiguration) WithMiddlewares(values ...*MiddlewareRefApplyConfiguration) *MirroringApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMiddlewares") + } + b.LoadBalancerSpecApplyConfiguration.Middlewares = append(b.LoadBalancerSpecApplyConfiguration.Middlewares, *values[i]) + } + return b +} + // WithSticky sets the Sticky field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Sticky field is set to the value of the last call. diff --git a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirrorservice.go b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirrorservice.go index f7fde8d3d..79dd60441 100644 --- a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirrorservice.go +++ b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/mirrorservice.go @@ -68,6 +68,19 @@ func (b *MirrorServiceApplyConfiguration) WithNamespace(value string) *MirrorSer return b } +// WithMiddlewares adds the given value to the Middlewares field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Middlewares field. +func (b *MirrorServiceApplyConfiguration) WithMiddlewares(values ...*MiddlewareRefApplyConfiguration) *MirrorServiceApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMiddlewares") + } + b.LoadBalancerSpecApplyConfiguration.Middlewares = append(b.LoadBalancerSpecApplyConfiguration.Middlewares, *values[i]) + } + return b +} + // WithSticky sets the Sticky field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Sticky field is set to the value of the last call. diff --git a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/service.go b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/service.go index d0b8342c1..3b3dcfd41 100644 --- a/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/service.go +++ b/pkg/provider/kubernetes/crd/generated/applyconfiguration/traefikio/v1alpha1/service.go @@ -67,6 +67,19 @@ func (b *ServiceApplyConfiguration) WithNamespace(value string) *ServiceApplyCon return b } +// WithMiddlewares adds the given value to the Middlewares field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Middlewares field. +func (b *ServiceApplyConfiguration) WithMiddlewares(values ...*MiddlewareRefApplyConfiguration) *ServiceApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMiddlewares") + } + b.LoadBalancerSpecApplyConfiguration.Middlewares = append(b.LoadBalancerSpecApplyConfiguration.Middlewares, *values[i]) + } + return b +} + // WithSticky sets the Sticky field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Sticky field is set to the value of the last call. diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index f4d671d2a..4e803b354 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -266,7 +266,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) continue } - errorPage, errorPageService, err := p.createErrorPageMiddleware(client, middleware.Namespace, middleware.Spec.Errors) + errorPage, errorPageService, err := p.createErrorPageMiddleware(ctxMid, client, middleware.Namespace, middleware.Spec.Errors) if err != nil { logger.Error().Err(err).Msg("Error while reading error page middleware") continue @@ -645,7 +645,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) return conf } -func (p *Provider) createErrorPageMiddleware(client Client, namespace string, errorPage *traefikv1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) { +func (p *Provider) createErrorPageMiddleware(ctx context.Context, client Client, namespace string, errorPage *traefikv1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) { if errorPage == nil { return nil, nil, nil } @@ -663,7 +663,7 @@ func (p *Provider) createErrorPageMiddleware(client Client, namespace string, er allowEmptyServices: p.AllowEmptyServices, } - balancerServerHTTP, err := cb.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec) + balancerServerHTTP, err := cb.buildServersLB(ctx, namespace, errorPage.Service.LoadBalancerSpec) if err != nil { return nil, nil, err } diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index cb77b4c4c..d9b3845b4 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -82,7 +82,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli serviceKey := makeServiceKey(route.Match, ingressName) - mds, err := p.makeMiddlewareKeys(ctx, ingressRoute.Namespace, route.Middlewares) + mds, err := makeMiddlewareKeys(ctx, ingressRoute.Namespace, route.Middlewares, p.AllowCrossNamespace) if err != nil { logger.Error().Err(err).Msg("Failed to create middleware keys") continue @@ -172,13 +172,13 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli return conf } -func (p *Provider) makeMiddlewareKeys(ctx context.Context, ingRouteNamespace string, middlewares []traefikv1alpha1.MiddlewareRef) ([]string, error) { +func makeMiddlewareKeys(ctx context.Context, namespace string, middlewares []traefikv1alpha1.MiddlewareRef, allowCrossNamespace bool) ([]string, error) { var mds []string for _, mi := range middlewares { name := mi.Name - if !p.AllowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+providerName) { + if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+providerName) { // Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd), // if the provider namespace kubernetescrd is used, // we don't allow this format to avoid cross namespace references. @@ -196,10 +196,10 @@ func (p *Provider) makeMiddlewareKeys(ctx context.Context, ingRouteNamespace str continue } - ns := ingRouteNamespace + ns := namespace if len(mi.Namespace) > 0 { - if !isNamespaceAllowed(p.AllowCrossNamespace, ingRouteNamespace, mi.Namespace) { - return nil, fmt.Errorf("middleware %s/%s is not in the IngressRoute namespace %s", mi.Namespace, mi.Name, ingRouteNamespace) + if !isNamespaceAllowed(allowCrossNamespace, namespace, mi.Namespace) { + return nil, fmt.Errorf("middleware %s/%s is not in the parent namespace %s", mi.Namespace, mi.Name, namespace) } ns = mi.Namespace @@ -333,6 +333,7 @@ func (c configBuilder) buildServicesLB(ctx context.Context, namespace string, tS Sticky: sticky, }, } + return nil } @@ -378,7 +379,7 @@ func (c configBuilder) buildMirroring(ctx context.Context, tService *traefikv1al } // buildServersLB creates the configuration for the load-balancer of servers defined by svc. -func (c configBuilder) buildServersLB(namespace string, svc traefikv1alpha1.LoadBalancerSpec) (*dynamic.Service, error) { +func (c configBuilder) buildServersLB(ctx context.Context, namespace string, svc traefikv1alpha1.LoadBalancerSpec) (*dynamic.Service, error) { lb := &dynamic.ServersLoadBalancer{} lb.SetDefaults() @@ -501,7 +502,16 @@ func (c configBuilder) buildServersLB(namespace string, svc traefikv1alpha1.Load return nil, err } - return &dynamic.Service{LoadBalancer: lb}, nil + service := &dynamic.Service{LoadBalancer: lb} + if len(svc.Middlewares) > 0 { + mds, err := makeMiddlewareKeys(ctx, namespace, svc.Middlewares, c.allowCrossNamespace) + if err != nil { + return nil, fmt.Errorf("could not create middleware keys: %w", err) + } + service.Middlewares = mds + } + + return service, nil } func (c configBuilder) makeServersTransportKey(parentNamespace string, serversTransportName string) (string, error) { @@ -687,7 +697,7 @@ func (c configBuilder) nameAndService(ctx context.Context, parentNamespace strin switch service.Kind { case "", "Service": - serversLB, err := c.buildServersLB(namespace, service) + serversLB, err := c.buildServersLB(ctx, namespace, service) if err != nil { return "", nil, err } diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 39b9fafb3..64b922509 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -3279,6 +3279,71 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, }, + { + desc: "TraefikService with service middleware", + paths: []string{"services.yml", "with_traefik_service_middleware.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TLS: &dynamic.TLSConfiguration{}, + 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{ + "default-test-route-6b204d94623b3df4370c": { + EntryPoints: []string{"web"}, + Service: "default-test-weighted", + Rule: "Host(`foo.com`) && PathPrefix(`/bar`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-stripprefix": { + StripPrefix: &dynamic.StripPrefix{ + Prefixes: []string{"/tobestripped"}, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-test-weighted": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-80", + Weight: pointer(1), + }, + }, + }, + }, + "default-whoami-80": { + Middlewares: []string{"default-stripprefix"}, + LoadBalancer: &dynamic.ServersLoadBalancer{ + Strategy: dynamic.BalancerStrategyWRR, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: pointer(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, { desc: "one kube services in a highest random weight", paths: []string{"with_highest_random_weight.yml"}, diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go index 2ac5876f4..0c9151188 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go @@ -110,6 +110,8 @@ type LoadBalancerSpec struct { Kind string `json:"kind,omitempty"` // Namespace defines the namespace of the referenced Kubernetes Service or TraefikService. Namespace string `json:"namespace,omitempty"` + // Middlewares defines the list of references to Middleware resources to apply to the service. + Middlewares []MiddlewareRef `json:"middlewares,omitempty"` // Sticky defines the sticky sessions configuration. // More info: https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/http/load-balancing/service/#sticky-sessions Sticky *dynamic.Sticky `json:"sticky,omitempty"` 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 e0cddac6b..c50c12230 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go @@ -685,6 +685,11 @@ func (in *IngressRouteUDPSpec) DeepCopy() *IngressRouteUDPSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LoadBalancerSpec) DeepCopyInto(out *LoadBalancerSpec) { *out = *in + if in.Middlewares != nil { + in, out := &in.Middlewares, &out.Middlewares + *out = make([]MiddlewareRef, len(*in)) + copy(*out, *in) + } if in.Sticky != nil { in, out := &in.Sticky, &out.Sticky *out = new(dynamic.Sticky) diff --git a/pkg/provider/kubernetes/gateway/features.go b/pkg/provider/kubernetes/gateway/features.go index 88dedd573..5c63c4078 100644 --- a/pkg/provider/kubernetes/gateway/features.go +++ b/pkg/provider/kubernetes/gateway/features.go @@ -44,5 +44,6 @@ func extendedHTTPRouteFeatures() sets.Set[features.Feature] { features.HTTPRouteBackendProtocolH2CFeature, features.HTTPRouteBackendProtocolWebSocketFeature, features.HTTPRouteDestinationPortMatchingFeature, + features.HTTPRouteBackendRequestHeaderModificationFeature, ) } diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/backend_filter_request_header_modifier.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/backend_filter_request_header_modifier.yml new file mode 100644 index 000000000..1d0e7be4c --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/backend_filter_request_header_modifier.yml @@ -0,0 +1,62 @@ +--- +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: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + kinds: + - kind: HTTPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +kind: HTTPRoute +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: http-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + hostnames: + - "foo.com" + rules: + - matches: + - path: + type: PathPrefix + value: /bar + backendRefs: + - name: whoami + port: 80 + weight: 1 + kind: Service + group: "" + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + set: + - name: X-Foo + value: Bar + add: + - name: X-Bar + value: Foo + remove: + - X-Baz \ No newline at end of file diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go index 158014af8..9028b024c 100644 --- a/pkg/provider/kubernetes/gateway/httproute.go +++ b/pkg/provider/kubernetes/gateway/httproute.go @@ -140,6 +140,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, var err error routerName := makeRouterName(rule, routeKey) + // TODO loadMiddlewares errors could change the condition. router.Middlewares, err = p.loadMiddlewares(conf, route.Namespace, routerName, routeRule.Filters, match.Path) switch { case err != nil: @@ -164,7 +165,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, default: var serviceCondition *metav1.Condition - router.Service, serviceCondition = p.loadWRRService(ctx, listener, conf, routerName, routeRule, route) + router.Service, serviceCondition = p.loadWRRService(ctx, listener, conf, routerName, routeRule, route, match.Path) if serviceCondition != nil { condition = *serviceCondition } @@ -179,7 +180,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, return conf, condition } -func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute) (string, *metav1.Condition) { +func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute, pathMatch *gatev1.HTTPPathMatch) (string, *metav1.Condition) { name := routeKey + "-wrr" if _, ok := conf.HTTP.Services[name]; ok { return name, nil @@ -188,7 +189,9 @@ func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, var wrr dynamic.WeightedRoundRobin var condition *metav1.Condition for _, backendRef := range routeRule.BackendRefs { - svcName, errCondition := p.loadService(ctx, listener, conf, route, backendRef) + // TODO in loadService we need to always return a non-nil serviceName even when there is an error which is not the + // usual defacto. + svcName, errCondition := p.loadService(ctx, listener, conf, route, backendRef, pathMatch) weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1))) if errCondition != nil { log.Ctx(ctx).Error(). @@ -215,7 +218,7 @@ func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, // loadService returns a dynamic.Service config corresponding to the given gatev1.HTTPBackendRef. // Note that the returned dynamic.Service config can be nil (for cross-provider, internal services, and backendFunc). -func (p *Provider) loadService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef) (string, *metav1.Condition) { +func (p *Provider) loadService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, pathMatch *gatev1.HTTPPathMatch) (string, *metav1.Condition) { kind := ptr.Deref(backendRef.Kind, kindService) group := groupCore @@ -241,6 +244,19 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co } } + middlewares, err := p.loadMiddlewares(conf, namespace, serviceName, backendRef.Filters, pathMatch) + if err != nil { + return serviceName, &metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteReasonInvalidKind), + Message: fmt.Sprintf("Cannot load filters on HTTPBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err), + } + } + + // TODO may be we could incorporate this "ignored" case into the loadHTTPBackendRef. if group != groupCore || kind != kindService { name, service, err := p.loadHTTPBackendRef(namespace, backendRef) if err != nil { @@ -255,6 +271,7 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co } if service != nil { + service.Middlewares = middlewares conf.HTTP.Services[name] = service } @@ -286,7 +303,7 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co conf.HTTP.ServersTransports[serviceName] = st } - conf.HTTP.Services[serviceName] = &dynamic.Service{LoadBalancer: lb} + conf.HTTP.Services[serviceName] = &dynamic.Service{LoadBalancer: lb, Middlewares: middlewares} return serviceName, nil } diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 1f5c1df8f..b6b19d80e 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -1917,6 +1917,77 @@ func TestLoadHTTPRoutes(t *testing.T) { TLS: &dynamic.TLSConfiguration{}, }, }, + { + desc: "Simple HTTPRoute, backend filter request header modifier", + paths: []string{"services.yml", "httproute/backend_filter_request_header_modifier.yml"}, + entryPoints: map[string]Entrypoint{"web": { + Address: ":80", + }}, + 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{ + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-99f4d29346f69ccb6fc3": { + EntryPoints: []string{"web"}, + Service: "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-99f4d29346f69ccb6fc3-wrr", + Rule: "Host(`foo.com`) && (Path(`/bar`) || PathPrefix(`/bar/`))", + Priority: 10408, + RuleSyntax: "default", + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-whoami-http-requestheadermodifier-0": { + RequestHeaderModifier: &dynamic.HeaderModifier{ + Set: map[string]string{"X-Foo": "Bar"}, + Add: map[string]string{"X-Bar": "Foo"}, + Remove: []string{"X-Baz"}, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-99f4d29346f69ccb6fc3-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-http-80", + Weight: ptr.To(1), + }, + }, + }, + }, + "default-whoami-http-80": { + Middlewares: []string{"default-whoami-http-requestheadermodifier-0"}, + LoadBalancer: &dynamic.ServersLoadBalancer{ + Strategy: dynamic.BalancerStrategyWRR, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: ptr.To(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, { desc: "Simple HTTPRoute, redirect HTTP to HTTPS", paths: []string{"services.yml", "httproute/filter_http_to_https.yml"}, @@ -1954,6 +2025,7 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, }, + Services: map[string]*dynamic.Service{ "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-364ce6ec04c3d49b19c4-wrr": { Weighted: &dynamic.WeightedRoundRobin{}, diff --git a/pkg/provider/kubernetes/ingress-nginx/annotations.go b/pkg/provider/kubernetes/ingress-nginx/annotations.go index c44c632dc..4abb00e22 100644 --- a/pkg/provider/kubernetes/ingress-nginx/annotations.go +++ b/pkg/provider/kubernetes/ingress-nginx/annotations.go @@ -16,8 +16,12 @@ type ingressConfig struct { AuthSecretType *string `annotation:"nginx.ingress.kubernetes.io/auth-secret-type"` AuthURL *string `annotation:"nginx.ingress.kubernetes.io/auth-url"` + AuthSignin *string `annotation:"nginx.ingress.kubernetes.io/auth-signin"` AuthResponseHeaders *string `annotation:"nginx.ingress.kubernetes.io/auth-response-headers"` + AuthTLSSecret *string `annotation:"nginx.ingress.kubernetes.io/auth-tls-secret"` + AuthTLSVerifyClient *string `annotation:"nginx.ingress.kubernetes.io/auth-tls-verify-client"` + ForceSSLRedirect *bool `annotation:"nginx.ingress.kubernetes.io/force-ssl-redirect"` SSLRedirect *bool `annotation:"nginx.ingress.kubernetes.io/ssl-redirect"` @@ -25,12 +29,15 @@ type ingressConfig struct { UseRegex *bool `annotation:"nginx.ingress.kubernetes.io/use-regex"` RewriteTarget *string `annotation:"nginx.ingress.kubernetes.io/rewrite-target"` + AppRoot *string `annotation:"nginx.ingress.kubernetes.io/app-root"` PermanentRedirect *string `annotation:"nginx.ingress.kubernetes.io/permanent-redirect"` PermanentRedirectCode *int `annotation:"nginx.ingress.kubernetes.io/permanent-redirect-code"` TemporalRedirect *string `annotation:"nginx.ingress.kubernetes.io/temporal-redirect"` TemporalRedirectCode *int `annotation:"nginx.ingress.kubernetes.io/temporal-redirect-code"` + FromToWwwRedirect *bool `annotation:"nginx.ingress.kubernetes.io/from-to-www-redirect"` + Affinity *string `annotation:"nginx.ingress.kubernetes.io/affinity"` SessionCookieName *string `annotation:"nginx.ingress.kubernetes.io/session-cookie-name"` SessionCookieSecure *bool `annotation:"nginx.ingress.kubernetes.io/session-cookie-secure"` @@ -44,10 +51,11 @@ type ingressConfig struct { BackendProtocol *string `annotation:"nginx.ingress.kubernetes.io/backend-protocol"` - ProxySSLSecret *string `annotation:"nginx.ingress.kubernetes.io/proxy-ssl-secret"` - ProxySSLVerify *string `annotation:"nginx.ingress.kubernetes.io/proxy-ssl-verify"` - ProxySSLName *string `annotation:"nginx.ingress.kubernetes.io/proxy-ssl-name"` - ProxySSLServerName *string `annotation:"nginx.ingress.kubernetes.io/proxy-ssl-server-name"` + ProxySSLSecret *string `annotation:"nginx.ingress.kubernetes.io/proxy-ssl-secret"` + ProxySSLVerify *string `annotation:"nginx.ingress.kubernetes.io/proxy-ssl-verify"` + ProxySSLName *string `annotation:"nginx.ingress.kubernetes.io/proxy-ssl-name"` + ProxySSLServerName *string `annotation:"nginx.ingress.kubernetes.io/proxy-ssl-server-name"` + ProxyConnectTimeout *int `annotation:"nginx.ingress.kubernetes.io/proxy-connect-timeout"` EnableCORS *bool `annotation:"nginx.ingress.kubernetes.io/enable-cors"` EnableCORSAllowCredentials *bool `annotation:"nginx.ingress.kubernetes.io/cors-allow-credentials"` diff --git a/pkg/provider/kubernetes/ingress-nginx/annotations_test.go b/pkg/provider/kubernetes/ingress-nginx/annotations_test.go index 36ce89434..64ccb03a8 100644 --- a/pkg/provider/kubernetes/ingress-nginx/annotations_test.go +++ b/pkg/provider/kubernetes/ingress-nginx/annotations_test.go @@ -28,6 +28,9 @@ func Test_parseIngressConfig(t *testing.T) { "nginx.ingress.kubernetes.io/session-cookie-max-age": "3600", "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS", "nginx.ingress.kubernetes.io/cors-expose-headers": "foo, bar", + "nginx.ingress.kubernetes.io/auth-url": "http://auth.example.com/verify", + "nginx.ingress.kubernetes.io/auth-signin": "https://auth.example.com/oauth2/start?rd=foo", + "nginx.ingress.kubernetes.io/proxy-connect-timeout": "30", }, expected: ingressConfig{ SSLPassthrough: ptr.To(true), @@ -40,6 +43,9 @@ func Test_parseIngressConfig(t *testing.T) { SessionCookieMaxAge: ptr.To(3600), BackendProtocol: ptr.To("HTTPS"), CORSExposeHeaders: ptr.To([]string{"foo", "bar"}), + AuthURL: ptr.To("http://auth.example.com/verify"), + AuthSignin: ptr.To("https://auth.example.com/oauth2/start?rd=foo"), + ProxyConnectTimeout: ptr.To(30), }, }, { @@ -56,6 +62,7 @@ func Test_parseIngressConfig(t *testing.T) { annotations: map[string]string{ "nginx.ingress.kubernetes.io/ssl-passthrough": "notabool", "nginx.ingress.kubernetes.io/session-cookie-max-age (in seconds)": "notanint", + "nginx.ingress.kubernetes.io/proxy-connect-timeout": "notanint", }, }, } diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-app-root-wrong.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-app-root-wrong.yml new file mode 100644 index 000000000..72a6f927f --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-app-root-wrong.yml @@ -0,0 +1,22 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-app-root + namespace: default + annotations: + nginx.ingress.kubernetes.io/app-root: foo + +spec: + ingressClassName: nginx + rules: + - host: app-root.localhost + http: + paths: + - path: /bar + pathType: Prefix + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-app-root.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-app-root.yml new file mode 100644 index 000000000..f72758013 --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-app-root.yml @@ -0,0 +1,22 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-app-root + namespace: default + annotations: + nginx.ingress.kubernetes.io/app-root: /foo + +spec: + ingressClassName: nginx + rules: + - host: app-root.localhost + http: + paths: + - path: /bar + pathType: Prefix + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-auth-tls-secret.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-auth-tls-secret.yml new file mode 100644 index 000000000..2d9e5c7e2 --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-auth-tls-secret.yml @@ -0,0 +1,26 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-auth-tls-secret + namespace: default + annotations: + nginx.ingress.kubernetes.io/auth-tls-secret: "default/ca-secret" + +spec: + ingressClassName: nginx + tls: + - hosts: + - auth-tls-secret.localhost + - secretName: whoami-tls + rules: + - host: auth-tls-secret.localhost + http: + paths: + - path: / + pathType: Exact + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-auth-tls-verify-client.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-auth-tls-verify-client.yml new file mode 100644 index 000000000..a327d59d4 --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-auth-tls-verify-client.yml @@ -0,0 +1,27 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-auth-tls-verify-client + namespace: default + annotations: + nginx.ingress.kubernetes.io/auth-tls-secret: "default/ca-secret" + nginx.ingress.kubernetes.io/auth-tls-verify-client: "optional" + +spec: + ingressClassName: nginx + tls: + - hosts: + - auth-tls-verify-client.localhost + - secretName: whoami-tls + rules: + - host: auth-tls-verify-client.localhost + http: + paths: + - path: / + pathType: Exact + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/01-ingress-with-basicauth.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-basicauth.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/01-ingress-with-basicauth.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-basicauth.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/08-ingress-with-cors.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-cors.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/08-ingress-with-cors.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-cors.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-custom-headers.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-custom-headers.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-custom-headers.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-custom-headers.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/05-ingress-with-default-backend.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-default-backend.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/05-ingress-with-default-backend.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-default-backend.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/05-ingress-with-default-backend2.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-default-backend2.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/05-ingress-with-default-backend2.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-default-backend2.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/02-ingress-with-forwardauth.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-forwardauth.yml similarity index 86% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/02-ingress-with-forwardauth.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-forwardauth.yml index 220499792..8187383c4 100644 --- a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/02-ingress-with-forwardauth.yml +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-forwardauth.yml @@ -8,6 +8,7 @@ metadata: nginx.ingress.kubernetes.io/auth-url: "http://whoami.default.svc/" nginx.ingress.kubernetes.io/auth-method: "GET" nginx.ingress.kubernetes.io/auth-response-headers: "X-Foo" + nginx.ingress.kubernetes.io/auth-signin: "https://auth.example.com/oauth2/start?rd=foo" spec: ingressClassName: nginx diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-host.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-host.yml new file mode 100644 index 000000000..7f7803946 --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-host.yml @@ -0,0 +1,22 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-host + namespace: default + annotations: + nginx.ingress.kubernetes.io/from-to-www-redirect: "true" + +spec: + ingressClassName: nginx + rules: + - host: host.localhost + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/00-ingress-with-no-annotation.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-no-annotation.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/00-ingress-with-no-annotation.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-no-annotation.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/14-ingress-with-permanent-redirect-code-correct-code.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-permanent-redirect-code-correct-code.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/14-ingress-with-permanent-redirect-code-correct-code.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-permanent-redirect-code-correct-code.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/14-ingress-with-permanent-redirect-code-wrong-code.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-permanent-redirect-code-wrong-code.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/14-ingress-with-permanent-redirect-code-wrong-code.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-permanent-redirect-code-wrong-code.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/14-ingress-with-permanent-redirect.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-permanent-redirect.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/14-ingress-with-permanent-redirect.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-permanent-redirect.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/07-ingress-with-proxy-ssl.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-proxy-ssl.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/07-ingress-with-proxy-ssl.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-proxy-ssl.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-proxy-timeout.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-proxy-timeout.yml new file mode 100644 index 000000000..69cc4612e --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-proxy-timeout.yml @@ -0,0 +1,22 @@ +--- +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: ingress-with-proxy-timeout + namespace: default + annotations: + nginx.ingress.kubernetes.io/proxy-connect-timeout: "30" + +spec: + ingressClassName: nginx + rules: + - host: whoami.localhost + http: + paths: + - path: / + pathType: Exact + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-rewrite-target.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-rewrite-target.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-rewrite-target.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-rewrite-target.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/09-ingress-with-service-upstream.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-service-upstream.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/09-ingress-with-service-upstream.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-service-upstream.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/04-ingress-with-ssl-passthrough.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-ssl-passthrough.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/04-ingress-with-ssl-passthrough.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-ssl-passthrough.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/03-ingress-with-ssl-redirect.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-ssl-redirect.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/03-ingress-with-ssl-redirect.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-ssl-redirect.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/06-ingress-with-sticky.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-sticky.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/06-ingress-with-sticky.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-sticky.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/16-ingress-with-temporal-and-permanent-redirect.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-temporal-and-permanent-redirect.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/16-ingress-with-temporal-and-permanent-redirect.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-temporal-and-permanent-redirect.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/17-ingress-with-temporal-redirect-code-correct-code.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-temporal-redirect-code-correct-code.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/17-ingress-with-temporal-redirect-code-correct-code.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-temporal-redirect-code-correct-code.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/17-ingress-with-temporal-redirect-code-wrong-code.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-temporal-redirect-code-wrong-code.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/17-ingress-with-temporal-redirect-code-wrong-code.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-temporal-redirect-code-wrong-code.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/15-ingress-with-temporal-redirect.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-temporal-redirect.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/15-ingress-with-temporal-redirect.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-temporal-redirect.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/10-ingress-with-upstream-vhost.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-upstream-vhost.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/10-ingress-with-upstream-vhost.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-upstream-vhost.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/10-ingress-with-use-regex.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-use-regex.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/10-ingress-with-use-regex.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-use-regex.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/13-ingress-with-whitelist-empty.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-whitelist-empty.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/13-ingress-with-whitelist-empty.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-whitelist-empty.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/12-ingress-with-whitelist-multiple-ip-and-cidr.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-whitelist-multiple-ip-and-cidr.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/12-ingress-with-whitelist-multiple-ip-and-cidr.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-whitelist-multiple-ip-and-cidr.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-whitelist-single-cidr.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-whitelist-single-cidr.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-whitelist-single-cidr.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-whitelist-single-cidr.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/10-ingress-with-whitelist-single-ip.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-whitelist-single-ip.yml similarity index 100% rename from pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/10-ingress-with-whitelist-single-ip.yml rename to pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-whitelist-single-ip.yml diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-www-host.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-www-host.yml new file mode 100644 index 000000000..c02df3650 --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-www-host.yml @@ -0,0 +1,22 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-www-host + namespace: default + annotations: + nginx.ingress.kubernetes.io/from-to-www-redirect: "true" + +spec: + ingressClassName: nginx + rules: + - host: www.host.localhost + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingresses-with-www-redirect.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingresses-with-www-redirect.yml new file mode 100644 index 000000000..03fcb9758 --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingresses-with-www-redirect.yml @@ -0,0 +1,42 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-www-host + namespace: default + annotations: + nginx.ingress.kubernetes.io/from-to-www-redirect: "true" + +spec: + ingressClassName: nginx + rules: + - host: www.host.localhost + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: whoami + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-host + namespace: default + +spec: + ingressClassName: nginx + rules: + - host: host.localhost + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/secrets.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/secrets.yml index 7c53f7fb0..32a475059 100644 --- a/pkg/provider/kubernetes/ingress-nginx/fixtures/secrets.yml +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/secrets.yml @@ -7,3 +7,13 @@ metadata: data: tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t tls.key: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t + +--- +kind: Secret +apiVersion: v1 +metadata: + namespace: default + name: ca-secret + +data: + ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t \ No newline at end of file diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go index 3c8834069..a59707a19 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go @@ -41,6 +41,8 @@ const ( defaultBackendName = "default-backend" defaultBackendTLSName = "default-backend-tls" + + defaultProxyConnectTimeout = 60 ) type backendAddress struct { @@ -80,6 +82,8 @@ type Provider struct { DefaultBackendService string `description:"Service used to serve HTTP requests not matching any known server name (catch-all). Takes the form 'namespace/name'." json:"defaultBackendService,omitempty" toml:"defaultBackendService,omitempty" yaml:"defaultBackendService,omitempty" export:"true"` DisableSvcExternalName bool `description:"Disable support for Services of type ExternalName." json:"disableSvcExternalName,omitempty" toml:"disableSvcExternalName,omitempty" yaml:"disableSvcExternalName,omitempty" export:"true"` + ProxyConnectTimeout int `description:"Amount of time to wait until a connection to a server can be established. Timeout value is unitless and in seconds." json:"proxyConnectTimeout,omitempty" toml:"proxyConnectTimeout,omitempty" yaml:"proxyConnectTimeout,omitempty" export:"true"` + // NonTLSEntryPoints contains the names of entrypoints that are configured without TLS. NonTLSEntryPoints []string `json:"-" toml:"-" yaml:"-" label:"-" file:"-"` @@ -93,6 +97,7 @@ type Provider struct { func (p *Provider) SetDefaults() { p.IngressClass = defaultAnnotationValue p.ControllerClass = defaultControllerName + p.ProxyConnectTimeout = defaultProxyConnectTimeout } // Init the provider. @@ -261,7 +266,21 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration ingresses := p.k8sClient.ListIngresses() + hosts := make(map[string]bool) + for _, ing := range ingresses { + if !p.shouldProcessIngress(ing, ingressClasses) { + continue + } + + for _, rule := range ing.Spec.Rules { + if !hosts[rule.Host] { + hosts[rule.Host] = true + } + } + } + uniqCerts := make(map[string]*tls.CertAndStores) + tlsOptions := make(map[string]tls.Options) for _, ingress := range ingresses { logger := log.Ctx(ctx).With().Str("ingress", ingress.Name).Str("namespace", ingress.Namespace).Logger() ctxIngress := logger.WithContext(ctx) @@ -289,6 +308,23 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration } } + var clientAuthTLSOptionName string + if ingressConfig.AuthTLSSecret != nil { + tlsOptName := provider.Normalize(ingress.Namespace + "-" + ingress.Name + "-" + *ingressConfig.AuthTLSSecret) + + if _, exists := tlsOptions[tlsOptName]; !exists { + tlsOpt, err := p.buildClientAuthTLSOption(ingress.Namespace, ingressConfig) + if err != nil { + logger.Error().Err(err).Msg("Error configuring client auth TLS") + continue + } + + tlsOptions[tlsOptName] = tlsOpt + } + + clientAuthTLSOptionName = tlsOptName + } + namedServersTransport, err := p.buildServersTransport(ingress.Namespace, ingress.Name, ingressConfig) if err != nil { logger.Error().Err(err).Msg("Ignoring Ingress cannot create proxy SSL configuration") @@ -317,7 +353,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration Service: defaultBackendName, } - if err := p.applyMiddlewares(ingress.Namespace, defaultBackendName, "", ingressConfig, hasTLS, rt, conf); err != nil { + if err := p.applyMiddlewares(ingress.Namespace, defaultBackendName, "", "", hosts, ingressConfig, hasTLS, rt, conf); err != nil { logger.Error().Err(err).Msg("Error applying middlewares") } @@ -331,8 +367,11 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration Service: defaultBackendName, TLS: &dynamic.RouterTLSConfig{}, } + if clientAuthTLSOptionName != "" { + rtTLS.TLS.Options = clientAuthTLSOptionName + } - if err := p.applyMiddlewares(ingress.Namespace, defaultBackendTLSName, "", ingressConfig, false, rtTLS, conf); err != nil { + if err := p.applyMiddlewares(ingress.Namespace, defaultBackendTLSName, "", "", hosts, ingressConfig, false, rtTLS, conf); err != nil { logger.Error().Err(err).Msg("Error applying middlewares") } @@ -409,7 +448,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration Service: key, } - if err := p.applyMiddlewares(ingress.Namespace, key, "", ingressConfig, hasTLS, rt, conf); err != nil { + if err := p.applyMiddlewares(ingress.Namespace, key, "", "", hosts, ingressConfig, hasTLS, rt, conf); err != nil { logger.Error().Err(err).Msg("Error applying middlewares") } @@ -422,8 +461,11 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration Service: key, TLS: &dynamic.RouterTLSConfig{}, } + if clientAuthTLSOptionName != "" { + rtTLS.TLS.Options = clientAuthTLSOptionName + } - if err := p.applyMiddlewares(ingress.Namespace, key+"-tls", "", ingressConfig, false, rtTLS, conf); err != nil { + if err := p.applyMiddlewares(ingress.Namespace, key+"-tls", "", "", hosts, ingressConfig, false, rtTLS, conf); err != nil { logger.Error().Err(err).Msg("Error applying middlewares") } @@ -476,6 +518,10 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration } if hasTLS { rt.TLS = &dynamic.RouterTLSConfig{} + + if clientAuthTLSOptionName != "" { + rt.TLS.Options = clientAuthTLSOptionName + } } routerKey := provider.Normalize(fmt.Sprintf("%s-%s-rule-%d-path-%d", ingress.Namespace, ingress.Name, ri, pi)) @@ -488,7 +534,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration conf.HTTP.ServersTransports[namedServersTransport.Name] = namedServersTransport.ServersTransport } - if err := p.applyMiddlewares(ingress.Namespace, routerKey, pa.Path, ingressConfig, hasTLS, rt, conf); err != nil { + if err := p.applyMiddlewares(ingress.Namespace, routerKey, pa.Path, rule.Host, hosts, ingressConfig, hasTLS, rt, conf); err != nil { logger.Error().Err(err).Msg("Error applying middlewares") } } @@ -497,25 +543,30 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration conf.TLS = &dynamic.TLSConfiguration{ Certificates: slices.Collect(maps.Values(uniqCerts)), + Options: tlsOptions, } return conf } func (p *Provider) buildServersTransport(namespace, name string, cfg ingressConfig) (*namedServersTransport, error) { - scheme := parseBackendProtocol(ptr.Deref(cfg.BackendProtocol, "HTTP")) - if scheme != "https" { - return nil, nil - } - + proxyConnectTimeout := ptr.Deref(cfg.ProxyConnectTimeout, p.ProxyConnectTimeout) nst := &namedServersTransport{ Name: provider.Normalize(namespace + "-" + name), ServersTransport: &dynamic.ServersTransport{ - ServerName: ptr.Deref(cfg.ProxySSLName, ptr.Deref(cfg.ProxySSLServerName, "")), - InsecureSkipVerify: strings.ToLower(ptr.Deref(cfg.ProxySSLVerify, "off")) == "off", + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(time.Duration(proxyConnectTimeout) * time.Second), + }, }, } + if scheme := parseBackendProtocol(ptr.Deref(cfg.BackendProtocol, "HTTP")); scheme != "https" { + return nst, nil + } + + nst.ServersTransport.ServerName = ptr.Deref(cfg.ProxySSLName, ptr.Deref(cfg.ProxySSLServerName, "")) + nst.ServersTransport.InsecureSkipVerify = strings.ToLower(ptr.Deref(cfg.ProxySSLVerify, "off")) == "off" + if sslSecret := ptr.Deref(cfg.ProxySSLSecret, ""); sslSecret != "" { parts := strings.Split(sslSecret, "/") if len(parts) != 2 { @@ -792,7 +843,15 @@ func (p *Provider) loadCertificates(ctx context.Context, ingress *netv1.Ingress, return nil } -func (p *Provider) applyMiddlewares(namespace, routerKey, rulePath string, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) error { +func (p *Provider) applyMiddlewares(namespace, routerKey, rulePath, ruleHost string, hosts map[string]bool, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) error { + applyAppRootConfiguration(routerKey, ingressConfig, rt, conf) + applyFromToWwwRedirect(hosts, ruleHost, routerKey, ingressConfig, rt, conf) + applyRedirect(routerKey, ingressConfig, rt, conf) + + // Apply SSL redirect is mandatory to be applied after all other middlewares. + // TODO: check how to remove this, and create the HTTP router elsewhere. + p.applySSLRedirectConfiguration(routerKey, ingressConfig, hasTLS, rt, conf) + if err := p.applyBasicAuthConfiguration(namespace, routerKey, ingressConfig, rt, conf); err != nil { return fmt.Errorf("applying basic auth configuration: %w", err) } @@ -807,12 +866,6 @@ func (p *Provider) applyMiddlewares(namespace, routerKey, rulePath string, ingre applyRewriteTargetConfiguration(rulePath, routerKey, ingressConfig, rt, conf) - // Apply SSL redirect is mandatory to be applied after all other middlewares. - // TODO: check how to remove this, and create the HTTP router elsewhere. - p.applySSLRedirectConfiguration(routerKey, ingressConfig, hasTLS, rt, conf) - - applyRedirect(routerKey, ingressConfig, rt, conf) - applyUpstreamVhost(routerKey, ingressConfig, rt, conf) if err := p.applyCustomHeaders(routerKey, ingressConfig, rt, conf); err != nil { @@ -910,6 +963,65 @@ func applyRewriteTargetConfiguration(rulePath, routerName string, ingressConfig rt.Middlewares = append(rt.Middlewares, rewriteTargetMiddlewareName) } +func applyAppRootConfiguration(routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) { + if ingressConfig.AppRoot == nil || !strings.HasPrefix(*ingressConfig.AppRoot, "/") { + return + } + + appRootMiddlewareName := routerName + "-app-root" + conf.HTTP.Middlewares[appRootMiddlewareName] = &dynamic.Middleware{ + RedirectRegex: &dynamic.RedirectRegex{ + Regex: `^(https?://[^/]+)/$`, + Replacement: "$1" + *ingressConfig.AppRoot, + }, + } + + rt.Middlewares = append(rt.Middlewares, appRootMiddlewareName) +} + +func applyFromToWwwRedirect(hosts map[string]bool, ruleHost, routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) { + if ingressConfig.FromToWwwRedirect == nil || !*ingressConfig.FromToWwwRedirect { + return + } + + wwwType := strings.HasPrefix(ruleHost, "www.") + wildcardType := strings.HasPrefix(ruleHost, "*.") + bypass := wwwType && hosts[strings.TrimPrefix(ruleHost, "www.")] || !wwwType && hosts["www."+ruleHost] || wildcardType + + if bypass { + // Wildcard host not compatible with this annotation. (limitation) + // hosts already configured for www. and normal hosts. + return + } + + newRule := fmt.Sprintf("Host(`www.%s`)", ruleHost) + if wwwType { + // if current ingress host is www.example.com, redirect from example.com => www.example.com + host := strings.TrimPrefix(ruleHost, "www.") + newRule = fmt.Sprintf("Host(`%s`)", host) + } + + fromToWwwRedirectMiddlewareName := routerName + "-from-to-www-redirect" + conf.HTTP.Middlewares[fromToWwwRedirectMiddlewareName] = &dynamic.Middleware{ + RedirectRegex: &dynamic.RedirectRegex{ + Regex: `(https?)://[^/]+:([0-9]+)/(.*)`, + Replacement: fmt.Sprintf("$1://%s:$2/$3", ruleHost), + Permanent: true, + }, + } + + wwwRedirectRouter := &dynamic.Router{ + Rule: newRule, + EntryPoints: rt.EntryPoints, + Priority: rt.Priority, + // "default" stands for the default rule syntax in Traefik v3, i.e. the v3 syntax. + RuleSyntax: "default", + Middlewares: []string{fromToWwwRedirectMiddlewareName}, + Service: rt.Service, + } + conf.HTTP.Routers[routerName+"-from-to-www-redirect"] = wwwRedirectRouter +} + func (p *Provider) applyBasicAuthConfiguration(namespace, routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) error { if ingressConfig.AuthType == nil { return nil @@ -1125,7 +1237,7 @@ func (p *Provider) applySSLRedirectConfiguration(routerName string, ingressConfi ForcePermanentRedirect: true, }, } - rt.Middlewares = append([]string{redirectMiddlewareName}, rt.Middlewares...) + rt.Middlewares = append(rt.Middlewares, redirectMiddlewareName) } // An Ingress that is not forcing sslRedirect and has no TLS configuration does not redirect, @@ -1148,6 +1260,7 @@ func applyForwardAuthConfiguration(routerName string, ingressConfig ingressConfi ForwardAuth: &dynamic.ForwardAuth{ Address: *ingressConfig.AuthURL, AuthResponseHeaders: authResponseHeaders, + AuthSigninURL: ptr.Deref(ingressConfig.AuthSignin, ""), }, } rt.Middlewares = append(rt.Middlewares, forwardMiddlewareName) @@ -1270,3 +1383,61 @@ func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *s return eventsChanBuffered } + +func (p *Provider) buildClientAuthTLSOption(ingressNamespace string, config ingressConfig) (tls.Options, error) { + secretParts := strings.SplitN(*config.AuthTLSSecret, "/", 2) + if len(secretParts) != 2 { + return tls.Options{}, errors.New("auth-tls-secret is not in a correct namespace/name format") + } + + // Expected format: namespace/name. + secretNamespace := secretParts[0] + secretName := secretParts[1] + + if secretNamespace == "" { + return tls.Options{}, errors.New("auth-tls-secret has empty namespace") + } + if secretName == "" { + return tls.Options{}, errors.New("auth-tls-secret has empty name") + } + // Cross-namespace secrets are not supported. + if secretNamespace != ingressNamespace { + return tls.Options{}, fmt.Errorf("cross-namespace auth-tls-secret is not supported: secret namespace %q does not match ingress namespace %q", secretNamespace, ingressNamespace) + } + + blocks, err := p.certificateBlocks(secretNamespace, secretName) + if err != nil { + return tls.Options{}, fmt.Errorf("reading client certificate: %w", err) + } + + if blocks.CA == nil { + return tls.Options{}, errors.New("secret does not contain a CA certificate") + } + + // Default verifyClient value is "on" on ingress-nginx. + // on means that client certificate is required and must be signed by a trusted CA certificate. + clientAuthType := tls.RequireAndVerifyClientCert + if config.AuthTLSVerifyClient != nil { + switch *config.AuthTLSVerifyClient { + // off means that client certificate is not requested and no verification will be passed. + case "off": + clientAuthType = tls.NoClientCert + // optional means that the client certificate is requested, but not required. + // If the certificate is present, it needs to be verified. + case "optional": + clientAuthType = tls.VerifyClientCertIfGiven + // optional_no_ca means that the client certificate is requested, but does not require it to be signed by a trusted CA certificate. + case "optional_no_ca": + clientAuthType = tls.RequestClientCert + } + } + + tlsOpt := tls.Options{} + tlsOpt.SetDefaults() + tlsOpt.ClientAuth = tls.ClientAuth{ + CAFiles: []types.FileOrContent{*blocks.CA}, + ClientAuthType: clientAuthType, + } + + return tlsOpt, nil +} diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go index b419b8db6..24c799a7a 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go @@ -6,9 +6,12 @@ import ( "os" "path/filepath" "testing" + "time" + "github.com/go-acme/lego/v4/challenge/tlsalpn01" "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/provider/kubernetes/k8s" "github.com/traefik/traefik/v3/pkg/tls" @@ -31,7 +34,7 @@ func TestLoadIngresses(t *testing.T) { desc: "Empty, no IngressClass", paths: []string{ "services.yml", - "ingresses/01-ingress-with-basicauth.yml", + "ingresses/ingress-with-basicauth.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -44,7 +47,9 @@ func TestLoadIngresses(t *testing.T) { Services: map[string]*dynamic.Service{}, ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -52,7 +57,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/11-ingress-with-custom-headers.yml", + "ingresses/ingress-with-custom-headers.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -86,23 +91,32 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-custom-headers", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-custom-headers": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, }, - TLS: &dynamic.TLSConfiguration{}, }, }, { desc: "No annotation", paths: []string{ - "ingresses/00-ingress-with-no-annotation.yml", + "ingresses/ingress-with-no-annotation.yml", "ingressclasses.yml", "services.yml", "secrets.yml", @@ -147,15 +161,22 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-no-annotation", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-no-annotation": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, }, TLS: &dynamic.TLSConfiguration{ Certificates: []*tls.CertAndStores{ @@ -166,6 +187,7 @@ func TestLoadIngresses(t *testing.T) { }, }, }, + Options: map[string]tls.Options{}, }, }, }, @@ -174,7 +196,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/01-ingress-with-basicauth.yml", + "ingresses/ingress-with-basicauth.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -211,17 +233,26 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-basicauth", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-basicauth": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, }, - TLS: &dynamic.TLSConfiguration{}, }, }, { @@ -229,7 +260,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/02-ingress-with-forwardauth.yml", + "ingresses/ingress-with-forwardauth.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -250,6 +281,7 @@ func TestLoadIngresses(t *testing.T) { ForwardAuth: &dynamic.ForwardAuth{ Address: "http://whoami.default.svc/", AuthResponseHeaders: []string{"X-Foo"}, + AuthSigninURL: "https://auth.example.com/oauth2/start?rd=foo", }, }, }, @@ -264,17 +296,26 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-forwardauth", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-forwardauth": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, }, - TLS: &dynamic.TLSConfiguration{}, }, }, { @@ -283,7 +324,7 @@ func TestLoadIngresses(t *testing.T) { "services.yml", "secrets.yml", "ingressclasses.yml", - "ingresses/03-ingress-with-ssl-redirect.yml", + "ingresses/ingress-with-ssl-redirect.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -349,8 +390,9 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-ssl-redirect", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, @@ -366,8 +408,9 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-without-ssl-redirect", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, @@ -383,15 +426,32 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-force-ssl-redirect", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-ssl-redirect": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + "default-ingress-without-ssl-redirect": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + "default-ingress-with-force-ssl-redirect": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, }, TLS: &dynamic.TLSConfiguration{ Certificates: []*tls.CertAndStores{ @@ -402,6 +462,7 @@ func TestLoadIngresses(t *testing.T) { }, }, }, + Options: map[string]tls.Options{}, }, }, }, @@ -411,7 +472,7 @@ func TestLoadIngresses(t *testing.T) { "services.yml", "secrets.yml", "ingressclasses.yml", - "ingresses/04-ingress-with-ssl-passthrough.yml", + "ingresses/ingress-with-ssl-passthrough.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -446,7 +507,9 @@ func TestLoadIngresses(t *testing.T) { Services: map[string]*dynamic.Service{}, ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -455,7 +518,7 @@ func TestLoadIngresses(t *testing.T) { "services.yml", "secrets.yml", "ingressclasses.yml", - "ingresses/06-ingress-with-sticky.yml", + "ingresses/ingress-with-sticky.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -482,8 +545,9 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-sticky", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, @@ -502,9 +566,17 @@ func TestLoadIngresses(t *testing.T) { }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-sticky": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, }, - TLS: &dynamic.TLSConfiguration{}, }, }, { @@ -513,7 +585,7 @@ func TestLoadIngresses(t *testing.T) { "services.yml", "secrets.yml", "ingressclasses.yml", - "ingresses/07-ingress-with-proxy-ssl.yml", + "ingresses/ingress-with-proxy-ssl.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -554,10 +626,15 @@ func TestLoadIngresses(t *testing.T) { ServerName: "whoami.localhost", InsecureSkipVerify: false, RootCAs: []types.FileOrContent{"-----BEGIN CERTIFICATE-----"}, + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, }, }, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -565,7 +642,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/08-ingress-with-cors.yml", + "ingresses/ingress-with-cors.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -604,17 +681,26 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-cors", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-cors": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, }, - TLS: &dynamic.TLSConfiguration{}, }, }, { @@ -622,7 +708,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/09-ingress-with-service-upstream.yml", + "ingresses/ingress-with-service-upstream.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -646,17 +732,26 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.10.1:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-service-upstream", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-service-upstream": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, }, - TLS: &dynamic.TLSConfiguration{}, }, }, { @@ -664,7 +759,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/10-ingress-with-upstream-vhost.yml", + "ingresses/ingress-with-upstream-vhost.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -703,12 +798,21 @@ func TestLoadIngresses(t *testing.T) { ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, + ServersTransport: "default-ingress-with-upstream-vhost", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-upstream-vhost": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -716,7 +820,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/10-ingress-with-use-regex.yml", + "ingresses/ingress-with-use-regex.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -748,12 +852,21 @@ func TestLoadIngresses(t *testing.T) { ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, + ServersTransport: "default-ingress-with-use-regex", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-use-regex": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -761,7 +874,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/11-ingress-with-rewrite-target.yml", + "ingresses/ingress-with-rewrite-target.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -801,12 +914,355 @@ func TestLoadIngresses(t *testing.T) { ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, + ServersTransport: "default-ingress-with-rewrite-target", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-rewrite-target": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, + }, + }, + { + desc: "App Root", + paths: []string{ + "services.yml", + "ingressclasses.yml", + "ingresses/ingress-with-app-root.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-app-root-rule-0-path-0": { + Rule: "Host(`app-root.localhost`) && (Path(`/bar`) || PathPrefix(`/bar/`))", + RuleSyntax: "default", + Service: "default-ingress-with-app-root-whoami-80", + Middlewares: []string{"default-ingress-with-app-root-rule-0-path-0-app-root"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-ingress-with-app-root-rule-0-path-0-app-root": { + RedirectRegex: &dynamic.RedirectRegex{ + Regex: `^(https?://[^/]+)/$`, + Replacement: "$1/foo", + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-ingress-with-app-root-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-app-root", + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-app-root": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, + }, + }, + { + desc: "App Root - no prefix slash", + paths: []string{ + "services.yml", + "ingressclasses.yml", + "ingresses/ingress-with-app-root-wrong.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-app-root-rule-0-path-0": { + Rule: "Host(`app-root.localhost`) && (Path(`/bar`) || PathPrefix(`/bar/`))", + RuleSyntax: "default", + Service: "default-ingress-with-app-root-whoami-80", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-ingress-with-app-root-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-app-root", + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-app-root": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, + }, + }, + { + desc: "From To WWW Redirect - www host", + paths: []string{ + "services.yml", + "ingressclasses.yml", + "ingresses/ingress-with-www-host.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-www-host-rule-0-path-0": { + Rule: "Host(`www.host.localhost`) && PathPrefix(`/`)", + RuleSyntax: "default", + Service: "default-ingress-with-www-host-whoami-80", + }, + "default-ingress-with-www-host-rule-0-path-0-from-to-www-redirect": { + Rule: "Host(`host.localhost`)", + RuleSyntax: "default", + Service: "default-ingress-with-www-host-whoami-80", + Middlewares: []string{"default-ingress-with-www-host-rule-0-path-0-from-to-www-redirect"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-ingress-with-www-host-rule-0-path-0-from-to-www-redirect": { + RedirectRegex: &dynamic.RedirectRegex{ + Regex: `(https?)://[^/]+:([0-9]+)/(.*)`, + Replacement: "$1://www.host.localhost:$2/$3", + Permanent: true, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-ingress-with-www-host-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-www-host", + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-www-host": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, + }, + }, + { + desc: "From To WWW Redirect - host", + paths: []string{ + "services.yml", + "ingressclasses.yml", + "ingresses/ingress-with-host.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-host-rule-0-path-0": { + Rule: "Host(`host.localhost`) && PathPrefix(`/`)", + RuleSyntax: "default", + Service: "default-ingress-with-host-whoami-80", + }, + "default-ingress-with-host-rule-0-path-0-from-to-www-redirect": { + Rule: "Host(`www.host.localhost`)", + RuleSyntax: "default", + Service: "default-ingress-with-host-whoami-80", + Middlewares: []string{"default-ingress-with-host-rule-0-path-0-from-to-www-redirect"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-ingress-with-host-rule-0-path-0-from-to-www-redirect": { + RedirectRegex: &dynamic.RedirectRegex{ + Regex: `(https?)://[^/]+:([0-9]+)/(.*)`, + Replacement: "$1://host.localhost:$2/$3", + Permanent: true, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-ingress-with-host-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-host", + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-host": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, + }, + }, + { + desc: "From To WWW Redirect - multiple ingresses", + paths: []string{ + "services.yml", + "ingressclasses.yml", + "ingresses/ingresses-with-www-redirect.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-host-rule-0-path-0": { + Rule: "Host(`host.localhost`) && PathPrefix(`/`)", + RuleSyntax: "default", + Service: "default-ingress-with-host-whoami-80", + }, + "default-ingress-with-www-host-rule-0-path-0": { + Rule: "Host(`www.host.localhost`) && PathPrefix(`/`)", + RuleSyntax: "default", + Service: "default-ingress-with-www-host-whoami-80", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-ingress-with-host-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-host", + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + }, + }, + "default-ingress-with-www-host-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-www-host", + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-www-host": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + "default-ingress-with-host": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -859,7 +1315,9 @@ func TestLoadIngresses(t *testing.T) { }, ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -867,7 +1325,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/10-ingress-with-whitelist-single-ip.yml", + "ingresses/ingress-with-whitelist-single-ip.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -906,12 +1364,21 @@ func TestLoadIngresses(t *testing.T) { ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, + ServersTransport: "default-ingress-with-whitelist-single-ip", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-whitelist-single-ip": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -919,7 +1386,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/11-ingress-with-whitelist-single-cidr.yml", + "ingresses/ingress-with-whitelist-single-cidr.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -958,12 +1425,21 @@ func TestLoadIngresses(t *testing.T) { ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, + ServersTransport: "default-ingress-with-whitelist-single-cidr", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-whitelist-single-cidr": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -971,7 +1447,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/12-ingress-with-whitelist-multiple-ip-and-cidr.yml", + "ingresses/ingress-with-whitelist-multiple-ip-and-cidr.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -1010,12 +1486,21 @@ func TestLoadIngresses(t *testing.T) { ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, + ServersTransport: "default-ingress-with-whitelist-multiple-ip-and-cidr", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-whitelist-multiple-ip-and-cidr": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1023,7 +1508,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/13-ingress-with-whitelist-empty.yml", + "ingresses/ingress-with-whitelist-empty.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -1056,12 +1541,21 @@ func TestLoadIngresses(t *testing.T) { ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, + ServersTransport: "default-ingress-with-whitelist-empty", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-whitelist-empty": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, }, - TLS: &dynamic.TLSConfiguration{}, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, }, }, { @@ -1069,7 +1563,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/14-ingress-with-permanent-redirect.yml", + "ingresses/ingress-with-permanent-redirect.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -1105,17 +1599,26 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-permanent-redirect", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-permanent-redirect": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, }, - TLS: &dynamic.TLSConfiguration{}, }, }, { @@ -1123,7 +1626,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/14-ingress-with-permanent-redirect-code-wrong-code.yml", + "ingresses/ingress-with-permanent-redirect-code-wrong-code.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -1159,17 +1662,26 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-permanent-redirect", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-permanent-redirect": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, }, - TLS: &dynamic.TLSConfiguration{}, }, }, { @@ -1177,7 +1689,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/14-ingress-with-permanent-redirect-code-correct-code.yml", + "ingresses/ingress-with-permanent-redirect-code-correct-code.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -1213,17 +1725,26 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-permanent-redirect", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-permanent-redirect": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, }, - TLS: &dynamic.TLSConfiguration{}, }, }, { @@ -1231,7 +1752,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/16-ingress-with-temporal-and-permanent-redirect.yml", + "ingresses/ingress-with-temporal-and-permanent-redirect.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -1267,17 +1788,26 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-redirect", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-redirect": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, }, - TLS: &dynamic.TLSConfiguration{}, }, }, { @@ -1285,7 +1815,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/15-ingress-with-temporal-redirect.yml", + "ingresses/ingress-with-temporal-redirect.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -1321,17 +1851,26 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-temporal-redirect", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-temporal-redirect": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, }, - TLS: &dynamic.TLSConfiguration{}, }, }, { @@ -1339,7 +1878,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/17-ingress-with-temporal-redirect-code-wrong-code.yml", + "ingresses/ingress-with-temporal-redirect-code-wrong-code.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -1375,17 +1914,26 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-temporal-redirect", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-temporal-redirect": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, }, - TLS: &dynamic.TLSConfiguration{}, }, }, { @@ -1393,7 +1941,7 @@ func TestLoadIngresses(t *testing.T) { paths: []string{ "services.yml", "ingressclasses.yml", - "ingresses/17-ingress-with-temporal-redirect-code-correct-code.yml", + "ingresses/ingress-with-temporal-redirect-code-correct-code.yml", }, expected: &dynamic.Configuration{ TCP: &dynamic.TCPConfiguration{ @@ -1429,17 +1977,283 @@ func TestLoadIngresses(t *testing.T) { URL: "http://10.10.0.2:80", }, }, - Strategy: "wrr", - PassHostHeader: ptr.To(true), + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ServersTransport: "default-ingress-with-temporal-redirect", ResponseForwarding: &dynamic.ResponseForwarding{ FlushInterval: dynamic.DefaultFlushInterval, }, }, }, }, - ServersTransports: map[string]*dynamic.ServersTransport{}, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-temporal-redirect": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, + }, + }, + { + desc: "Proxy connect timeout", + paths: []string{ + "services.yml", + "ingressclasses.yml", + "ingresses/ingress-with-proxy-timeout.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-proxy-timeout-rule-0-path-0": { + Rule: "Host(`whoami.localhost`) && Path(`/`)", + RuleSyntax: "default", + Service: "default-ingress-with-proxy-timeout-whoami-80", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-ingress-with-proxy-timeout-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + {URL: "http://10.10.0.1:80"}, + {URL: "http://10.10.0.2:80"}, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + ServersTransport: "default-ingress-with-proxy-timeout", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-proxy-timeout": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(30 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Options: map[string]tls.Options{}, + }, + }, + }, + { + desc: "Auth TLS secret", + paths: []string{ + "services.yml", + "secrets.yml", + "ingressclasses.yml", + "ingresses/ingress-with-auth-tls-secret.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-auth-tls-secret-rule-0-path-0": { + Rule: "Host(`auth-tls-secret.localhost`) && Path(`/`)", + RuleSyntax: "default", + Service: "default-ingress-with-auth-tls-secret-whoami-80", + TLS: &dynamic.RouterTLSConfig{ + Options: "default-ingress-with-auth-tls-secret-default-ca-secret", + }, + }, + "default-ingress-with-auth-tls-secret-rule-0-path-0-http": { + EntryPoints: []string{"web"}, + Rule: "Host(`auth-tls-secret.localhost`) && Path(`/`)", + RuleSyntax: "default", + Middlewares: []string{"default-ingress-with-auth-tls-secret-rule-0-path-0-redirect-scheme"}, + Service: "noop@internal", + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-ingress-with-auth-tls-secret-rule-0-path-0-redirect-scheme": { + RedirectScheme: &dynamic.RedirectScheme{ + Scheme: "https", + ForcePermanentRedirect: true, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-ingress-with-auth-tls-secret-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + ServersTransport: "default-ingress-with-auth-tls-secret", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-auth-tls-secret": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{ + CertFile: "-----BEGIN CERTIFICATE-----", + KeyFile: "-----BEGIN CERTIFICATE-----", + }, + }, + }, + Options: map[string]tls.Options{ + "default-ingress-with-auth-tls-secret-default-ca-secret": { + ClientAuth: tls.ClientAuth{ + CAFiles: []types.FileOrContent{"-----BEGIN CERTIFICATE-----"}, + ClientAuthType: "RequireAndVerifyClientCert", + }, + CipherSuites: []string{ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + }, + ALPNProtocols: []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol}, + }, + }, + }, + }, + }, + + { + desc: "Auth TLS verify client", + paths: []string{ + "services.yml", + "secrets.yml", + "ingressclasses.yml", + "ingresses/ingress-with-auth-tls-verify-client.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-auth-tls-verify-client-rule-0-path-0": { + Rule: "Host(`auth-tls-verify-client.localhost`) && Path(`/`)", + RuleSyntax: "default", + Service: "default-ingress-with-auth-tls-verify-client-whoami-80", + TLS: &dynamic.RouterTLSConfig{ + Options: "default-ingress-with-auth-tls-verify-client-default-ca-secret", + }, + }, + "default-ingress-with-auth-tls-verify-client-rule-0-path-0-http": { + EntryPoints: []string{"web"}, + Rule: "Host(`auth-tls-verify-client.localhost`) && Path(`/`)", + RuleSyntax: "default", + Middlewares: []string{"default-ingress-with-auth-tls-verify-client-rule-0-path-0-redirect-scheme"}, + Service: "noop@internal", + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-ingress-with-auth-tls-verify-client-rule-0-path-0-redirect-scheme": { + RedirectScheme: &dynamic.RedirectScheme{ + Scheme: "https", + ForcePermanentRedirect: true, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-ingress-with-auth-tls-verify-client-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + ServersTransport: "default-ingress-with-auth-tls-verify-client", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-auth-tls-verify-client": { + ForwardingTimeouts: &dynamic.ForwardingTimeouts{ + DialTimeout: ptypes.Duration(60 * time.Second), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{ + CertFile: "-----BEGIN CERTIFICATE-----", + KeyFile: "-----BEGIN CERTIFICATE-----", + }, + }, + }, + Options: map[string]tls.Options{ + "default-ingress-with-auth-tls-verify-client-default-ca-secret": { + ClientAuth: tls.ClientAuth{ + CAFiles: []types.FileOrContent{"-----BEGIN CERTIFICATE-----"}, + ClientAuthType: "VerifyClientCertIfGiven", + }, + CipherSuites: []string{ + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + }, + ALPNProtocols: []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol}, + }, + }, }, - TLS: &dynamic.TLSConfiguration{}, }, }, } diff --git a/pkg/provider/kubernetes/ingress/annotations.go b/pkg/provider/kubernetes/ingress/annotations.go index fe7f52d52..bbdc4b8d6 100644 --- a/pkg/provider/kubernetes/ingress/annotations.go +++ b/pkg/provider/kubernetes/ingress/annotations.go @@ -46,6 +46,7 @@ type ServiceIng struct { ServersScheme string `json:"serversScheme,omitempty"` ServersTransport string `json:"serversTransport,omitempty"` PassHostHeader *bool `json:"passHostHeader"` + Middlewares []string `json:"middlewares,omitempty"` Sticky *dynamic.Sticky `json:"sticky,omitempty" label:"allowEmpty"` NativeLB *bool `json:"nativeLB,omitempty"` NodePortLB bool `json:"nodePortLB,omitempty"` diff --git a/pkg/provider/kubernetes/ingress/annotations_test.go b/pkg/provider/kubernetes/ingress/annotations_test.go index 1f86f51c7..cf6df8ad8 100644 --- a/pkg/provider/kubernetes/ingress/annotations_test.go +++ b/pkg/provider/kubernetes/ingress/annotations_test.go @@ -161,6 +161,18 @@ func Test_parseServiceConfig(t *testing.T) { }, }, }, + { + desc: "service middlewares annotation", + annotations: map[string]string{ + "traefik.ingress.kubernetes.io/service.middlewares": "middleware1,middleware2", + }, + expected: &ServiceConfig{ + Service: &ServiceIng{ + Middlewares: []string{"middleware1", "middleware2"}, + PassHostHeader: pointer(true), + }, + }, + }, { desc: "empty map", annotations: map[string]string{}, diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 714aebdfb..de4073214 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -563,6 +563,7 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In if svcConfig != nil && svcConfig.Service != nil { svc.LoadBalancer.Sticky = svcConfig.Service.Sticky + svc.Middlewares = svcConfig.Service.Middlewares if svcConfig.Service.PassHostHeader != nil { svc.LoadBalancer.PassHostHeader = svcConfig.Service.PassHostHeader diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index c67e84e57..b7aac6014 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -2357,7 +2357,7 @@ func TestIngressEndpointPublishedService(t *testing.T) { ingress, err := kubeClient.NetworkingV1().Ingresses(metav1.NamespaceDefault).Get(t.Context(), "foo", metav1.GetOptions{}) require.NoError(t, err) - assert.Equal(t, test.expected, ingress.Status.LoadBalancer.Ingress) + assert.ElementsMatch(t, test.expected, ingress.Status.LoadBalancer.Ingress) }) } } diff --git a/pkg/provider/merge.go b/pkg/provider/merge.go new file mode 100644 index 000000000..760aa4bd6 --- /dev/null +++ b/pkg/provider/merge.go @@ -0,0 +1,330 @@ +package provider + +import ( + "context" + "maps" + "reflect" + "slices" + "strings" + + "github.com/huandu/xstrings" + "github.com/rs/zerolog/log" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/observability/logs" + "github.com/traefik/traefik/v3/pkg/tls" +) + +type resourceMeta struct { + logField string + displayName string +} + +var resourceLogFields = map[reflect.Type]resourceMeta{ + reflect.TypeFor[dynamic.Router](): {logs.RouterName, "HTTP router"}, + reflect.TypeFor[dynamic.Service](): {logs.ServiceName, "HTTP service"}, + reflect.TypeFor[dynamic.Middleware](): {logs.MiddlewareName, "HTTP middleware"}, + reflect.TypeFor[dynamic.ServersTransport](): {logs.ServersTransportName, "HTTP servers transport"}, + reflect.TypeFor[dynamic.TCPRouter](): {logs.RouterName, "TCP router"}, + reflect.TypeFor[dynamic.TCPService](): {logs.ServiceName, "TCP service"}, + reflect.TypeFor[dynamic.TCPMiddleware](): {logs.MiddlewareName, "TCP middleware"}, + reflect.TypeFor[dynamic.TCPServersTransport](): {logs.ServersTransportName, "TCP servers transport"}, + reflect.TypeFor[dynamic.UDPRouter](): {logs.RouterName, "UDP router"}, + reflect.TypeFor[dynamic.UDPService](): {logs.ServiceName, "UDP service"}, +} + +// ResourceStrategy defines how the merge should handle resources. +type ResourceStrategy int + +const ( + // ResourceStrategyMerge tries to call the Merge method on the resource. + ResourceStrategyMerge ResourceStrategy = iota + // ResourceStrategySkipDuplicates skips duplicate resources. + ResourceStrategySkipDuplicates +) + +// NamedConfiguration is a configuration with its name. +type NamedConfiguration struct { + Name string + Configuration *dynamic.Configuration +} + +// NameSortedConfigurations returns the configurations sorted by name. +func NameSortedConfigurations(configurations map[string]*dynamic.Configuration) []NamedConfiguration { + origins := slices.Sorted(maps.Keys(configurations)) + + sorted := make([]NamedConfiguration, 0, len(origins)) + for _, origin := range origins { + sorted = append(sorted, NamedConfiguration{Name: origin, Configuration: configurations[origin]}) + } + + return sorted +} + +// Merge merges multiple configurations. +func Merge(ctx context.Context, configurations []NamedConfiguration, strategy ResourceStrategy) *dynamic.Configuration { + merged := &dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: make(map[string]*dynamic.Router), + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + ServersTransports: make(map[string]*dynamic.ServersTransport), + }, + TCP: &dynamic.TCPConfiguration{ + Routers: make(map[string]*dynamic.TCPRouter), + Services: make(map[string]*dynamic.TCPService), + Middlewares: make(map[string]*dynamic.TCPMiddleware), + ServersTransports: make(map[string]*dynamic.TCPServersTransport), + }, + UDP: &dynamic.UDPConfiguration{ + Routers: make(map[string]*dynamic.UDPRouter), + Services: make(map[string]*dynamic.UDPService), + }, + TLS: &dynamic.TLSConfiguration{ + Stores: make(map[string]tls.Store), + }, + } + + tracker := newMergeTracker() + + for _, c := range configurations { + if c.Configuration.HTTP != nil { + mergeResourceMaps(ctx, reflect.ValueOf(merged.HTTP).Elem(), reflect.ValueOf(c.Configuration.HTTP).Elem(), c.Name, tracker, strategy) + } + if c.Configuration.TCP != nil { + mergeResourceMaps(ctx, reflect.ValueOf(merged.TCP).Elem(), reflect.ValueOf(c.Configuration.TCP).Elem(), c.Name, tracker, strategy) + } + if c.Configuration.UDP != nil { + mergeResourceMaps(ctx, reflect.ValueOf(merged.UDP).Elem(), reflect.ValueOf(c.Configuration.UDP).Elem(), c.Name, tracker, strategy) + } + if c.Configuration.TLS != nil { + mergeResourceMaps(ctx, reflect.ValueOf(merged.TLS).Elem(), reflect.ValueOf(c.Configuration.TLS).Elem(), c.Name, tracker, strategy) + + merged.TLS.Certificates = mergeCertificates(ctx, merged.TLS.Certificates, c.Configuration.TLS.Certificates, c.Name, strategy) + } + } + + deleteConflicts(ctx, tracker) + + return merged +} + +// mergeResourceMaps merges all the resource maps defined in the provided struct. +// Conflicts are recorded in the given merge tracker. +func mergeResourceMaps(ctx context.Context, dst, src reflect.Value, origin string, tracker *mergeTracker, strategy ResourceStrategy) { + dstType := dst.Type() + + for i := range dstType.NumField() { + field := dstType.Field(i) + if !field.IsExported() { + continue + } + + dstField := dst.Field(i) + srcField := src.Field(i) + + // Merge the resource maps of embedded structs. + if field.Anonymous { + mergeResourceMaps(ctx, dstField, srcField, origin, tracker, strategy) + continue + } + + if dstField.Kind() == reflect.Map { + mergeResourceMap(ctx, dstField, srcField, origin, tracker, strategy) + } + } +} + +// mergeResourceMap merges a resource map src into dst. +// New keys from src are added to dst. +// Duplicate keys are merged if the resource type implements a Merge method, otherwise +// the values must be identical. Conflicts are recorded in the given merge tracker. +func mergeResourceMap(ctx context.Context, dst, src reflect.Value, origin string, tracker *mergeTracker, strategy ResourceStrategy) { + if src.IsNil() { + return + } + + if dst.IsNil() { + dst.Set(reflect.MakeMap(dst.Type())) + } + + for _, resourceKey := range src.MapKeys() { + resourceKeyStr := resourceKey.String() + tracker.recordOrigin(dst, resourceKeyStr, origin) + + srcValue := src.MapIndex(resourceKey) + dstValue := dst.MapIndex(resourceKey) + + // Key doesn't exist in dst, add it. + if !dstValue.IsValid() { + dst.SetMapIndex(resourceKey, srcValue) + continue + } + + // Key exists, need to merge or detect conflict. + switch strategy { + case ResourceStrategyMerge: + if !tryMerge(dstValue, srcValue) { + tracker.markForDeletion(dst, resourceKeyStr, dst.Type().Elem()) + } + case ResourceStrategySkipDuplicates: + logSkippedDuplicate(ctx, dst.Type().Elem(), resourceKeyStr, origin) + } + } +} + +// tryMerge attempts to merge two resources. +// Returns true if the merge succeeds, false if values conflict. +func tryMerge(dst, src reflect.Value) bool { + if dst.Kind() != reflect.Ptr { + return reflect.DeepEqual(dst.Interface(), src.Interface()) + } + + if dst.IsNil() || src.IsNil() { + return reflect.DeepEqual(dst.Interface(), src.Interface()) + } + + // Check if the struct has the method `func (* T) Merge(other T) bool`. + // We use reflection to detect this method because Go's type system doesn't allow type assertions + // on generic interfaces (Mergeable[T]) practically. + mergeMethod := dst.MethodByName("Merge") + if mergeMethod.IsValid() { + methodType := mergeMethod.Type() + if methodType.NumIn() == 1 && methodType.NumOut() == 1 && methodType.Out(0).Kind() == reflect.Bool { + // Make sure the parameter type matches the type holding the method. + if methodType.In(0).AssignableTo(src.Type()) { + results := mergeMethod.Call([]reflect.Value{src}) + return results[0].Bool() + } + } + } + + // When Merge is not implemented, merge is not allowed; the values must be the same. + return reflect.DeepEqual(dst.Elem().Interface(), src.Elem().Interface()) +} + +// deleteConflicts removes conflicting items and logs errors. +func deleteConflicts(ctx context.Context, tracker *mergeTracker) { + logger := log.Ctx(ctx) + + for ck, info := range tracker.toDelete { + resourceNameField, resourceTypeWords := resourceLogMeta(info.resourceType) + logger.Error(). + Str(resourceNameField, ck.resourceKey). + Interface("configuration", tracker.origins[ck]). + Msgf("%s defined multiple times with different configurations", resourceTypeWords) + + info.resourceMap.SetMapIndex(reflect.ValueOf(ck.resourceKey), reflect.Value{}) + } +} + +// mergeCertificates merges multiple certificates. +func mergeCertificates(ctx context.Context, certificates []*tls.CertAndStores, newCertificates []*tls.CertAndStores, origin string, strategy ResourceStrategy) []*tls.CertAndStores { + for _, certificate := range newCertificates { + var found bool + for _, existingCertificate := range certificates { + if existingCertificate.Certificate == certificate.Certificate { + found = true + + switch strategy { + case ResourceStrategyMerge: + existingCertificate.Stores = mergeStores(existingCertificate.Stores, certificate.Stores) + case ResourceStrategySkipDuplicates: + log.Ctx(ctx).Warn(). + Str("origin", origin). + Msgf("TLS certificate %v already configured, skipping", certificate.Certificate) + } + + break + } + } + + if !found { + certificates = append(certificates, certificate) + } + } + + return certificates +} + +// mergeStores merges two store slices, deduplicating entries while. Order is preserved. +func mergeStores(existing, other []string) []string { + seen := make(map[string]struct{}, len(existing)) + for _, s := range existing { + seen[s] = struct{}{} + } + + for _, s := range other { + if _, ok := seen[s]; !ok { + existing = append(existing, s) + seen[s] = struct{}{} + } + } + + return existing +} + +// logSkippedDuplicate logs a warning when a duplicate resource is skipped. +func logSkippedDuplicate(ctx context.Context, resourceType reflect.Type, resourceKey, origin string) { + resourceNameField, resourceTypeWords := resourceLogMeta(resourceType) + + log.Ctx(ctx).Warn(). + Str("origin", origin). + Str(resourceNameField, resourceKey). + Msgf("%s already configured, skipping", resourceTypeWords) +} + +// resourceLogMeta returns the log field name and human-readable type description for the given resource element type. +func resourceLogMeta(resourceType reflect.Type) (resourceNameField, resourceTypeWords string) { + if resourceType.Kind() == reflect.Ptr { + resourceType = resourceType.Elem() + } + + meta, ok := resourceLogFields[resourceType] + if ok { + return meta.logField, meta.displayName + } + + resourceTypeName := resourceType.Name() + resourceNameField = xstrings.ToCamelCase(resourceTypeName) + "Name" + resourceTypeWords = strings.ReplaceAll(xstrings.ToKebabCase(resourceTypeName), "-", " ") + + return resourceNameField, resourceTypeWords +} + +// mergeTracker tracks item origins and items marked for deletion during merge. +type mergeTracker struct { + toDelete map[conflictKey]conflictInfo + origins map[conflictKey][]string +} + +// conflictKey uniquely identifies an entry in a map. +type conflictKey struct { + mapPtr uintptr + resourceKey string +} + +// conflictInfo stores information about a merge conflict. +type conflictInfo struct { + resourceMap reflect.Value // The map to delete from. + resourceType reflect.Type +} + +func newMergeTracker() *mergeTracker { + return &mergeTracker{ + toDelete: make(map[conflictKey]conflictInfo), + origins: make(map[conflictKey][]string), + } +} + +func (t *mergeTracker) recordOrigin(resourceMap reflect.Value, resourceKey, origin string) { + ck := conflictKey{mapPtr: resourceMap.Pointer(), resourceKey: resourceKey} + t.origins[ck] = append(t.origins[ck], origin) +} + +func (t *mergeTracker) markForDeletion(resourceMap reflect.Value, resourceKey string, resourceType reflect.Type) { + ck := conflictKey{mapPtr: resourceMap.Pointer(), resourceKey: resourceKey} + t.toDelete[ck] = conflictInfo{ + resourceMap: resourceMap, + resourceType: resourceType, + } +} diff --git a/pkg/provider/merge_test.go b/pkg/provider/merge_test.go new file mode 100644 index 000000000..2dd890dd6 --- /dev/null +++ b/pkg/provider/merge_test.go @@ -0,0 +1,557 @@ +package provider + +import ( + "context" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/tls" +) + +// testResource is a simple type without a Merge method. +type testResource struct { + Value string +} + +// testMergeableResource implements the Merge method. +type testMergeableResource struct { + Config string + Servers []string +} + +// Merge merges another testMergeableResource into this one. +// Returns true if the merge succeeds, false if configurations conflict. +func (r *testMergeableResource) Merge(other *testMergeableResource) bool { + if r.Config != other.Config { + return false + } + r.Servers = append(r.Servers, other.Servers...) + + return true +} + +// testCollectionSet is a container with a map field. +type testCollectionSet struct { + Resources map[string]*testResource +} + +// testMergeableCollectionSet is a container with a mergeable map field. +type testMergeableCollectionSet struct { + Resources map[string]*testMergeableResource +} + +// TestEmbedded is an embedded struct for testing anonymous field handling. +// Must be exported for reflection to process it. +type TestEmbedded struct { + EmbeddedItems map[string]*testResource +} + +// testCollectionSetWithEmbedded has both embedded and direct map fields. +type testCollectionSetWithEmbedded struct { + TestEmbedded + + Items map[string]*testResource +} + +func TestMergeCollectionSet_BasicMapMerge(t *testing.T) { + dst := &testCollectionSet{ + Resources: map[string]*testResource{ + "existing": {Value: "dst"}, + }, + } + src := &testCollectionSet{ + Resources: map[string]*testResource{ + "new": {Value: "src"}, + }, + } + + tracker := newMergeTracker() + mergeResourceMaps(context.Background(), reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "provider", tracker, ResourceStrategyMerge) + + assert.Empty(t, tracker.toDelete) + assert.Equal(t, &testCollectionSet{ + Resources: map[string]*testResource{ + "existing": {Value: "dst"}, + "new": {Value: "src"}, + }, + }, dst) +} + +func TestMergeCollectionSet_EmbeddedStruct(t *testing.T) { + dst := &testCollectionSetWithEmbedded{ + TestEmbedded: TestEmbedded{ + EmbeddedItems: map[string]*testResource{ + "embedded1": {Value: "dst-embedded"}, + }, + }, + Items: map[string]*testResource{ + "item1": {Value: "dst-item"}, + }, + } + src := &testCollectionSetWithEmbedded{ + TestEmbedded: TestEmbedded{ + EmbeddedItems: map[string]*testResource{ + "embedded2": {Value: "src-embedded"}, + }, + }, + Items: map[string]*testResource{ + "item2": {Value: "src-item"}, + }, + } + + tracker := newMergeTracker() + mergeResourceMaps(context.Background(), reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "provider", tracker, ResourceStrategyMerge) + + assert.Empty(t, tracker.toDelete) + assert.Equal(t, &testCollectionSetWithEmbedded{ + TestEmbedded: TestEmbedded{ + EmbeddedItems: map[string]*testResource{ + "embedded1": {Value: "dst-embedded"}, + "embedded2": {Value: "src-embedded"}, + }, + }, + Items: map[string]*testResource{ + "item1": {Value: "dst-item"}, + "item2": {Value: "src-item"}, + }, + }, dst) +} + +func TestMergeCollectionSet_MergeableInterface(t *testing.T) { + dst := &testMergeableCollectionSet{ + Resources: map[string]*testMergeableResource{ + "svc1": {Config: "same", Servers: []string{"server1"}}, + }, + } + src := &testMergeableCollectionSet{ + Resources: map[string]*testMergeableResource{ + "svc1": {Config: "same", Servers: []string{"server2"}}, + }, + } + + tracker := newMergeTracker() + mergeResourceMaps(context.Background(), reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "provider", tracker, ResourceStrategyMerge) + + assert.Empty(t, tracker.toDelete) + assert.Equal(t, &testMergeableCollectionSet{ + Resources: map[string]*testMergeableResource{ + "svc1": {Config: "same", Servers: []string{"server1", "server2"}}, + }, + }, dst) +} + +func TestMergeCollectionSet_MergeableConflict(t *testing.T) { + dst := &testMergeableCollectionSet{ + Resources: map[string]*testMergeableResource{ + "svc1": {Config: "config-A", Servers: []string{"server1"}}, + }, + } + src := &testMergeableCollectionSet{ + Resources: map[string]*testMergeableResource{ + "svc1": {Config: "config-B", Servers: []string{"server2"}}, + }, + } + + tracker := newMergeTracker() + mergeResourceMaps(context.Background(), reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "provider1", tracker, ResourceStrategyMerge) + + // Merge() returns false due to config mismatch -> marked for deletion. + assert.Len(t, tracker.toDelete, 1) + assertMarkedForDeletion(t, tracker.toDelete, "svc1") +} + +func TestMergeCollectionSet_DeepEqualFallback(t *testing.T) { + dst := &testCollectionSet{ + Resources: map[string]*testResource{ + "res1": {Value: "same"}, + }, + } + src := &testCollectionSet{ + Resources: map[string]*testResource{ + "res1": {Value: "same"}, + }, + } + + tracker := newMergeTracker() + mergeResourceMaps(context.Background(), reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "provider1", tracker, ResourceStrategyMerge) + + // Same values -> no conflict. + assert.Empty(t, tracker.toDelete) + assert.Equal(t, &testCollectionSet{ + Resources: map[string]*testResource{ + "res1": {Value: "same"}, + }, + }, dst) +} + +func TestMergeCollectionSet_DeepEqualConflict(t *testing.T) { + dst := &testCollectionSet{ + Resources: map[string]*testResource{ + "res1": {Value: "value-A"}, + }, + } + src := &testCollectionSet{ + Resources: map[string]*testResource{ + "res1": {Value: "value-B"}, + }, + } + + tracker := newMergeTracker() + mergeResourceMaps(context.Background(), reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem(), "provider1", tracker, ResourceStrategyMerge) + + // Different values, no Merge method -> conflict. + assert.Len(t, tracker.toDelete, 1) + assertMarkedForDeletion(t, tracker.toDelete, "res1") +} + +func TestMerge(t *testing.T) { + testCases := []struct { + desc string + configurations map[string]*dynamic.Configuration + strategy ResourceStrategy + expected *dynamic.Configuration + }{ + { + desc: "HTTP routers: multiple providers different routers", + configurations: map[string]*dynamic.Configuration{ + "provider1": { + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "router1": {Rule: "Host(`example1.com`)"}, + }, + }, + }, + "provider2": { + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "router2": {Rule: "Host(`example2.com`)"}, + }, + }, + }, + }, + strategy: ResourceStrategyMerge, + expected: buildExpectedConfiguration(func(c *dynamic.Configuration) { + c.HTTP.Routers["router1"] = &dynamic.Router{Rule: "Host(`example1.com`)"} + c.HTTP.Routers["router2"] = &dynamic.Router{Rule: "Host(`example2.com`)"} + }), + }, + { + desc: "HTTP routers: conflict multiple providers same router different config", + configurations: map[string]*dynamic.Configuration{ + "provider1": { + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "router1": {Rule: "Host(`example1.com`)"}, + }, + }, + }, + "provider2": { + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "router1": {Rule: "Host(`example2.com`)"}, + }, + }, + }, + }, + strategy: ResourceStrategyMerge, + expected: buildExpectedConfiguration(nil), + }, + { + desc: "HTTP services: multiple providers same service servers merged", + configurations: map[string]*dynamic.Configuration{ + "provider1": { + HTTP: &dynamic.HTTPConfiguration{ + Services: map[string]*dynamic.Service{ + "service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + {URL: "http://server1:80"}, + }, + }, + }, + }, + }, + }, + "provider2": { + HTTP: &dynamic.HTTPConfiguration{ + Services: map[string]*dynamic.Service{ + "service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + {URL: "http://server2:80"}, + }, + }, + }, + }, + }, + }, + }, + strategy: ResourceStrategyMerge, + expected: buildExpectedConfiguration(func(c *dynamic.Configuration) { + c.HTTP.Services["service1"] = &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + {URL: "http://server1:80"}, + {URL: "http://server2:80"}, + }, + }, + } + }), + }, + { + desc: "HTTP services: multiple providers same service duplicate servers deduplicated", + configurations: map[string]*dynamic.Configuration{ + "provider1": { + HTTP: &dynamic.HTTPConfiguration{ + Services: map[string]*dynamic.Service{ + "service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + {URL: "http://server1:80"}, + }, + }, + }, + }, + }, + }, + "provider2": { + HTTP: &dynamic.HTTPConfiguration{ + Services: map[string]*dynamic.Service{ + "service1": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + {URL: "http://server1:80"}, + }, + }, + }, + }, + }, + }, + }, + strategy: ResourceStrategyMerge, + expected: buildExpectedConfiguration(func(c *dynamic.Configuration) { + c.HTTP.Services["service1"] = &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + {URL: "http://server1:80"}, + }, + }, + } + }), + }, + { + desc: "TLS certificates: different certificates both kept", + configurations: map[string]*dynamic.Configuration{ + "provider1": { + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{CertFile: "cert1.pem", KeyFile: "key1.pem"}, + Stores: []string{"store1"}, + }, + }, + }, + }, + "provider2": { + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{CertFile: "cert2.pem", KeyFile: "key2.pem"}, + Stores: []string{"store2"}, + }, + }, + }, + }, + }, + strategy: ResourceStrategyMerge, + expected: buildExpectedConfiguration(func(c *dynamic.Configuration) { + c.TLS.Certificates = []*tls.CertAndStores{ + { + Certificate: tls.Certificate{CertFile: "cert1.pem", KeyFile: "key1.pem"}, + Stores: []string{"store1"}, + }, + { + Certificate: tls.Certificate{CertFile: "cert2.pem", KeyFile: "key2.pem"}, + Stores: []string{"store2"}, + }, + } + }), + }, + { + desc: "TLS certificates: same certificate stores merged with ResourceStrategyMerge", + configurations: map[string]*dynamic.Configuration{ + "provider1": { + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"}, + Stores: []string{"store1"}, + }, + }, + }, + }, + "provider2": { + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"}, + Stores: []string{"store2"}, + }, + }, + }, + }, + }, + strategy: ResourceStrategyMerge, + expected: buildExpectedConfiguration(func(c *dynamic.Configuration) { + c.TLS.Certificates = []*tls.CertAndStores{ + { + Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"}, + Stores: []string{"store1", "store2"}, + }, + } + }), + }, + { + desc: "TLS certificates: same certificate overlapping stores deduplicated", + configurations: map[string]*dynamic.Configuration{ + "provider1": { + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"}, + Stores: []string{"store1", "store2"}, + }, + }, + }, + }, + "provider2": { + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"}, + Stores: []string{"store2", "store3"}, + }, + }, + }, + }, + }, + strategy: ResourceStrategyMerge, + expected: buildExpectedConfiguration(func(c *dynamic.Configuration) { + c.TLS.Certificates = []*tls.CertAndStores{ + { + Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"}, + Stores: []string{"store1", "store2", "store3"}, + }, + } + }), + }, + { + desc: "TLS certificates: same certificate stores not merged with ResourceStrategySkipDuplicates", + configurations: map[string]*dynamic.Configuration{ + "provider1": { + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"}, + Stores: []string{"store1"}, + }, + }, + }, + }, + "provider2": { + TLS: &dynamic.TLSConfiguration{ + Certificates: []*tls.CertAndStores{ + { + Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"}, + Stores: []string{"store2"}, + }, + }, + }, + }, + }, + strategy: ResourceStrategySkipDuplicates, + expected: buildExpectedConfiguration(func(c *dynamic.Configuration) { + c.TLS.Certificates = []*tls.CertAndStores{ + { + Certificate: tls.Certificate{CertFile: "cert.pem", KeyFile: "key.pem"}, + Stores: []string{"store1"}, + }, + } + }), + }, + { + desc: "nil configuration from one provider", + configurations: map[string]*dynamic.Configuration{ + "provider1": { + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "router1": {Rule: "Host(`example.com`)"}, + }, + }, + }, + "provider2": { + // No HTTP configuration + }, + }, + strategy: ResourceStrategyMerge, + expected: buildExpectedConfiguration(func(c *dynamic.Configuration) { + c.HTTP.Routers["router1"] = &dynamic.Router{Rule: "Host(`example.com`)"} + }), + }, + { + desc: "empty configurations", + configurations: map[string]*dynamic.Configuration{}, + strategy: ResourceStrategyMerge, + expected: buildExpectedConfiguration(nil), + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + result := Merge(context.Background(), NameSortedConfigurations(test.configurations), test.strategy) + + assert.Equal(t, test.expected, result) + }) + } +} + +func buildExpectedConfiguration(modifier func(*dynamic.Configuration)) *dynamic.Configuration { + c := &dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: make(map[string]*dynamic.Router), + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + ServersTransports: make(map[string]*dynamic.ServersTransport), + }, + TCP: &dynamic.TCPConfiguration{ + Routers: make(map[string]*dynamic.TCPRouter), + Services: make(map[string]*dynamic.TCPService), + Middlewares: make(map[string]*dynamic.TCPMiddleware), + ServersTransports: make(map[string]*dynamic.TCPServersTransport), + }, + UDP: &dynamic.UDPConfiguration{ + Routers: make(map[string]*dynamic.UDPRouter), + Services: make(map[string]*dynamic.UDPService), + }, + TLS: &dynamic.TLSConfiguration{ + Stores: make(map[string]tls.Store), + }, + } + if modifier != nil { + modifier(c) + } + return c +} + +// assertMarkedForDeletion checks that toDelete contains an entry with the given key. +func assertMarkedForDeletion(t *testing.T, toDelete map[conflictKey]conflictInfo, key string) { + t.Helper() + for ck := range toDelete { + if ck.resourceKey == key { + return + } + } + t.Errorf("toDelete does not contain key %q", key) +} diff --git a/pkg/provider/nomad/config.go b/pkg/provider/nomad/config.go index a6fb4343b..69df3dfe8 100644 --- a/pkg/provider/nomad/config.go +++ b/pkg/provider/nomad/config.go @@ -84,7 +84,7 @@ func (p *Provider) buildConfig(ctx context.Context, items []item) *dynamic.Confi configurations[svcName] = config } - return provider.Merge(ctx, configurations) + return provider.Merge(ctx, provider.NameSortedConfigurations(configurations), provider.ResourceStrategyMerge) } func (p *Provider) buildTCPConfig(i item, configuration *dynamic.TCPConfiguration) error { diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index 8142f8359..747f476f6 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -28,6 +28,8 @@ type ConfigurationWatcher struct { requiredProvider string configurationListeners []func(dynamic.Configuration) + configurationTransformers []func(context.Context, dynamic.Configurations) dynamic.Configurations + routinesPool *safe.Pool } @@ -63,12 +65,14 @@ func (c *ConfigurationWatcher) Stop() { // AddListener adds a new listener function used when new configuration is provided. func (c *ConfigurationWatcher) AddListener(listener func(dynamic.Configuration)) { - if c.configurationListeners == nil { - c.configurationListeners = make([]func(dynamic.Configuration), 0) - } c.configurationListeners = append(c.configurationListeners, listener) } +// AddTransformer registers a function to modify configurations before they are applied. +func (c *ConfigurationWatcher) AddTransformer(transformer func(context.Context, dynamic.Configurations) dynamic.Configurations) { + c.configurationTransformers = append(c.configurationTransformers, transformer) +} + func (c *ConfigurationWatcher) startProviderAggregator() { log.Info().Msgf("Starting provider aggregator %T", c.providerAggregator) @@ -81,22 +85,24 @@ func (c *ConfigurationWatcher) startProviderAggregator() { } // receiveConfigurations receives configuration changes from the providers. -// The configuration message then gets passed along a series of check, notably +// The configuration message then gets passed along a series of checks, notably // to verify that, for a given provider, the configuration that was just received // is at least different from the previously received one. -// The full set of configurations is then sent to the throttling goroutine, -// (throttleAndApplyConfigurations) via a RingChannel, which ensures that we can -// constantly send in a non-blocking way to the throttling goroutine the last -// global state we are aware of. +// The full set of configurations is then sent to applyConfigurations +// via a channel in a non-blocking manner, ensuring the latest global state +// is always available for processing. func (c *ConfigurationWatcher) receiveConfigurations(ctx context.Context) { newConfigurations := make(dynamic.Configurations) + transformedConfigurations := make(dynamic.Configurations) + var output chan dynamic.Configurations + for { select { case <-ctx.Done(): return - // DeepCopy is necessary because newConfigurations gets modified later by the consumer of c.newConfigs - case output <- newConfigurations.DeepCopy(): + // DeepCopy is necessary because transformedConfigurations gets modified later by the consumer of c.newConfigs. + case output <- transformedConfigurations.DeepCopy(): output = nil default: @@ -123,28 +129,31 @@ func (c *ConfigurationWatcher) receiveConfigurations(ctx context.Context) { logConfiguration(logger, configMsg) if reflect.DeepEqual(newConfigurations[configMsg.ProviderName], configMsg.Configuration) { - // no change, do nothing + // no change, do nothing. logger.Debug().Msg("Skipping unchanged configuration") continue } newConfigurations[configMsg.ProviderName] = configMsg.Configuration.DeepCopy() + transformedConfigurations = newConfigurations + for _, transform := range c.configurationTransformers { + transformedConfigurations = transform(logger.WithContext(ctx), transformedConfigurations.DeepCopy()) + } + output = c.newConfigs - // DeepCopy is necessary because newConfigurations gets modified later by the consumer of c.newConfigs - case output <- newConfigurations.DeepCopy(): + // DeepCopy is necessary because newConfigurations gets modified later by the consumer of c.newConfigs. + case output <- transformedConfigurations.DeepCopy(): output = nil } } } } -// applyConfigurations blocks on a RingChannel that receives the new -// set of configurations that is compiled and sent by receiveConfigurations as soon -// as a provider change occurs. If the new set is different from the previous set -// that had been applied, the new set is applied, and we sleep for a while before -// listening on the channel again. +// applyConfigurations receives the full set of configurations from +// receiveConfigurations and applies them if they differ from the previous set. +// It waits for the required provider's configuration before applying any configs. func (c *ConfigurationWatcher) applyConfigurations(ctx context.Context) { var lastConfigurations dynamic.Configurations for { diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go index 99a196008..40df7b5aa 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -87,7 +87,7 @@ func TestNewConfigurationWatcher(t *testing.T) { th.WithServiceName("scv"), th.WithObservability())), th.WithMiddlewares(), - th.WithLoadBalancerServices(), + th.WithServices(), ), TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{}, @@ -126,7 +126,9 @@ func TestWaitForRequiredProvider(t *testing.T) { config := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), } @@ -170,14 +172,18 @@ func TestIgnoreTransientConfiguration(t *testing.T) { config := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), } expectedConfig := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability())), - th.WithLoadBalancerServices(th.WithService("bar@mock")), + th.WithServices( + th.WithService("bar@mock", th.WithServiceServersLoadBalancer()), + ), th.WithMiddlewares(), ), TCP: &dynamic.TCPConfiguration{ @@ -202,7 +208,9 @@ func TestIgnoreTransientConfiguration(t *testing.T) { expectedConfig3 := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability())), - th.WithLoadBalancerServices(th.WithService("bar-config3@mock")), + th.WithServices( + th.WithService("bar-config3@mock", th.WithServiceServersLoadBalancer()), + ), th.WithMiddlewares(), ), TCP: &dynamic.TCPConfiguration{ @@ -227,14 +235,18 @@ func TestIgnoreTransientConfiguration(t *testing.T) { config2 := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("baz", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("toto")), + th.WithServices( + th.WithService("toto", th.WithServiceServersLoadBalancer()), + ), ), } config3 := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar-config3")), + th.WithServices( + th.WithService("bar-config3", th.WithServiceServersLoadBalancer()), + ), ), } watcher := NewConfigurationWatcher(routinesPool, &mockProvider{}, []string{}, "") @@ -318,7 +330,9 @@ func TestListenProvidersThrottleProviderConfigReload(t *testing.T) { Configuration: &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i), th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), }, }) @@ -378,7 +392,9 @@ func TestListenProvidersSkipsSameConfigurationForProvider(t *testing.T) { Configuration: &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), }, } @@ -410,14 +426,18 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) { configuration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), } transientConfiguration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("bad", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bad")), + th.WithServices( + th.WithService("bad", th.WithServiceServersLoadBalancer()), + ), ), } @@ -449,7 +469,9 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) { expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability())), - th.WithLoadBalancerServices(th.WithService("bar@mock")), + th.WithServices( + th.WithService("bar@mock", th.WithServiceServersLoadBalancer()), + ), th.WithMiddlewares(), ), TCP: &dynamic.TCPConfiguration{ @@ -480,14 +502,18 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) { configuration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), } transientConfiguration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("bad", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bad")), + th.WithServices( + th.WithService("bad", th.WithServiceServersLoadBalancer()), + ), ), } @@ -540,7 +566,9 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) { expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability())), - th.WithLoadBalancerServices(th.WithService("bar@mock")), + th.WithServices( + th.WithService("bar@mock", th.WithServiceServersLoadBalancer()), + ), th.WithMiddlewares(), ), TCP: &dynamic.TCPConfiguration{ @@ -581,7 +609,9 @@ func TestApplyConfigUnderStress(t *testing.T) { case watcher.allProvidersConfigs <- dynamic.Message{ProviderName: "mock", Configuration: &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo"+strconv.Itoa(i), th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), }}: } @@ -616,28 +646,36 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) { configuration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), } transientConfiguration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("bad", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bad")), + th.WithServices( + th.WithService("bad", th.WithServiceServersLoadBalancer()), + ), ), } transientConfiguration2 := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("bad2", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bad2")), + th.WithServices( + th.WithService("bad2", th.WithServiceServersLoadBalancer()), + ), ), } finalConfiguration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("final", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("final")), + th.WithServices( + th.WithService("final", th.WithServiceServersLoadBalancer()), + ), ), } @@ -676,7 +714,9 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) { expected := dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("final@mock", th.WithEntryPoints("ep"), th.WithObservability())), - th.WithLoadBalancerServices(th.WithService("final@mock")), + th.WithServices( + th.WithService("final@mock", th.WithServiceServersLoadBalancer()), + ), th.WithMiddlewares(), ), TCP: &dynamic.TCPConfiguration{ @@ -709,7 +749,9 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { configuration := &dynamic.Configuration{ HTTP: th.BuildConfiguration( th.WithRouters(th.WithRouter("foo", th.WithEntryPoints("ep"))), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ), } @@ -742,9 +784,9 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability()), th.WithRouter("foo@mock2", th.WithEntryPoints("ep"), th.WithObservability()), ), - th.WithLoadBalancerServices( - th.WithService("bar@mock"), - th.WithService("bar@mock2"), + th.WithServices( + th.WithService("bar@mock", th.WithServiceServersLoadBalancer()), + th.WithService("bar@mock2", th.WithServiceServersLoadBalancer()), ), th.WithMiddlewares(), ), @@ -866,3 +908,87 @@ func TestPublishConfigUpdatedByConfigWatcherListener(t *testing.T) { assert.Equal(t, 1, publishedConfigCount) } + +func TestConfigurationWatcher_MultipleTransformers(t *testing.T) { + routinesPool := safe.NewPool(t.Context()) + t.Cleanup(routinesPool.Stop) + + pvd := &mockProvider{ + messages: []dynamic.Message{{ + ProviderName: "mock", + Configuration: &dynamic.Configuration{ + HTTP: th.BuildConfiguration( + th.WithRouters( + th.WithRouter("original", + th.WithEntryPoints("e"), + th.WithServiceName("scv"))), + ), + }, + }}, + } + + watcher := NewConfigurationWatcher(routinesPool, pvd, []string{}, "") + + var callOrder []string + + var callCount1, callCount2 int + + watcher.AddTransformer(func(_ context.Context, configs dynamic.Configurations) dynamic.Configurations { + callCount1++ + + callOrder = append(callOrder, "transformer1") + + for _, config := range configs { + if config != nil && config.HTTP != nil { + config.HTTP.Routers["from-transformer1"] = &dynamic.Router{ + EntryPoints: []string{"e"}, + Service: "svc1", + } + } + } + + return configs + }) + + watcher.AddTransformer(func(_ context.Context, configs dynamic.Configurations) dynamic.Configurations { + callCount2++ + + callOrder = append(callOrder, "transformer2") + + // Verify that transformer1's changes are visible. + for _, config := range configs { + if config != nil && config.HTTP != nil { + assert.Contains(t, config.HTTP.Routers, "from-transformer1") + config.HTTP.Routers["from-transformer2"] = &dynamic.Router{ + EntryPoints: []string{"e"}, + Service: "svc2", + } + } + } + + return configs + }) + + run := make(chan struct{}) + + watcher.AddListener(func(conf dynamic.Configuration) { + assert.NotNil(t, conf.HTTP) + assert.Contains(t, conf.HTTP.Routers, "original@mock") + assert.Contains(t, conf.HTTP.Routers, "from-transformer1@mock") + assert.Contains(t, conf.HTTP.Routers, "from-transformer2@mock") + close(run) + }) + + watcher.Start() + t.Cleanup(watcher.Stop) + + select { + case <-run: + case <-time.After(5 * time.Second): + t.Fatal("Timeout waiting for configuration") + } + + assert.Equal(t, []string{"transformer1", "transformer2"}, callOrder) + assert.Equal(t, 1, callCount1) + assert.Equal(t, 1, callCount2) +} diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index 77098760c..88707eb70 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -6,8 +6,6 @@ import ( "fmt" "net/http" "reflect" - "slices" - "strings" "github.com/containous/alice" "github.com/rs/zerolog/log" @@ -39,12 +37,7 @@ import ( "github.com/traefik/traefik/v3/pkg/middlewares/stripprefix" "github.com/traefik/traefik/v3/pkg/middlewares/stripprefixregex" "github.com/traefik/traefik/v3/pkg/server/provider" -) - -type middlewareStackType int - -const ( - middlewareStackKey middlewareStackType = iota + "github.com/traefik/traefik/v3/pkg/server/recursion" ) // Builder the middleware builder. @@ -63,8 +56,8 @@ func NewBuilder(configs map[string]*runtime.MiddlewareInfo, serviceBuilder servi return &Builder{configs: configs, serviceBuilder: serviceBuilder, pluginBuilder: pluginBuilder} } -// BuildChain creates a middleware chain. -func (b *Builder) BuildChain(ctx context.Context, middlewares []string) *alice.Chain { +// BuildMiddlewareChain creates a middleware chain. +func (b *Builder) BuildMiddlewareChain(ctx context.Context, middlewares []string) *alice.Chain { chain := alice.New() for _, name := range middlewares { middlewareName := provider.GetQualifiedName(ctx, name) @@ -76,7 +69,7 @@ func (b *Builder) BuildChain(ctx context.Context, middlewares []string) *alice.C } var err error - if constructorContext, err = checkRecursion(constructorContext, middlewareName); err != nil { + if constructorContext, err = recursion.CheckRecursion(constructorContext, "middleware", middlewareName); err != nil { b.configs[middlewareName].AddError(err, true) return nil, err } @@ -99,17 +92,6 @@ func (b *Builder) BuildChain(ctx context.Context, middlewares []string) *alice.C return &chain } -func checkRecursion(ctx context.Context, middlewareName string) (context.Context, error) { - currentStack, ok := ctx.Value(middlewareStackKey).([]string) - if !ok { - currentStack = []string{} - } - if slices.Contains(currentStack, middlewareName) { - return ctx, fmt.Errorf("could not instantiate middleware %s: recursion detected in %s", middlewareName, strings.Join(append(currentStack, middlewareName), "->")) - } - return context.WithValue(ctx, middlewareStackKey, append(currentStack, middlewareName)), nil -} - // it is the responsibility of the caller to make sure that b.configs[middlewareName].Middleware exists. func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (alice.Constructor, error) { config := b.configs[middlewareName] diff --git a/pkg/server/middleware/middlewares_test.go b/pkg/server/middleware/middlewares_test.go index 92cebc050..a972f33b2 100644 --- a/pkg/server/middleware/middlewares_test.go +++ b/pkg/server/middleware/middlewares_test.go @@ -19,7 +19,7 @@ func TestBuilder_BuildChainNilConfig(t *testing.T) { } middlewaresBuilder := NewBuilder(testConfig, nil, nil) - chain := middlewaresBuilder.BuildChain(t.Context(), []string{"empty"}) + chain := middlewaresBuilder.BuildMiddlewareChain(t.Context(), []string{"empty"}) _, err := chain.Then(nil) require.Error(t, err) } @@ -30,7 +30,7 @@ func TestBuilder_BuildChainNonExistentChain(t *testing.T) { } middlewaresBuilder := NewBuilder(testConfig, nil, nil) - chain := middlewaresBuilder.BuildChain(t.Context(), []string{"empty"}) + chain := middlewaresBuilder.BuildMiddlewareChain(t.Context(), []string{"empty"}) _, err := chain.Then(nil) require.Error(t, err) } @@ -172,7 +172,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { }, }, }, - expectedError: errors.New("could not instantiate middleware m1: recursion detected in m1->m2->m3->m1"), + expectedError: errors.New("could not instantiate middleware m1: recursion detected in middleware:m1->middleware:m2->middleware:m3->middleware:m1"), }, { desc: "Detects recursion in Middleware chain", @@ -197,9 +197,10 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { }, }, }, - expectedError: errors.New("could not instantiate middleware m1@provider: recursion detected in m1@provider->m2@provider2->m3@provider->m1@provider"), + expectedError: errors.New("could not instantiate middleware m1@provider: recursion detected in middleware:m1@provider->middleware:m2@provider2->middleware:m3@provider->middleware:m1@provider"), }, { + desc: "Detects recursion in Middleware chain", buildChain: []string{"ok", "m0"}, configuration: map[string]*dynamic.Middleware{ "ok": { @@ -211,7 +212,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { }, }, }, - expectedError: errors.New("could not instantiate middleware m0: recursion detected in m0->m0"), + expectedError: errors.New("could not instantiate middleware m0: recursion detected in middleware:m0->middleware:m0"), }, { desc: "Detects MiddlewareChain that references a Chain that references a Chain with a missing middleware", @@ -238,7 +239,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { }, }, }, - expectedError: errors.New("could not instantiate middleware m2: recursion detected in m0->m1->m2->m3->m2"), + expectedError: errors.New("could not instantiate middleware m2: recursion detected in middleware:m0->middleware:m1->middleware:m2->middleware:m3->middleware:m2"), }, { desc: "--", @@ -250,7 +251,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { }, }, }, - expectedError: errors.New("could not instantiate middleware m0: recursion detected in m0->m0"), + expectedError: errors.New("could not instantiate middleware m0: recursion detected in middleware:m0->middleware:m0"), }, } @@ -270,7 +271,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) { }) builder := NewBuilder(rtConf.Middlewares, nil, nil) - result := builder.BuildChain(ctx, test.buildChain) + result := builder.BuildMiddlewareChain(ctx, test.buildChain) handlers, err := result.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) if test.expectedError != nil { diff --git a/pkg/server/recursion/recursion.go b/pkg/server/recursion/recursion.go new file mode 100644 index 000000000..40998a37c --- /dev/null +++ b/pkg/server/recursion/recursion.go @@ -0,0 +1,26 @@ +package recursion + +import ( + "context" + "fmt" + "slices" + "strings" +) + +type stackType int + +const ( + stackKey stackType = iota +) + +func CheckRecursion(ctx context.Context, itemType, itemName string) (context.Context, error) { + currentStack, ok := ctx.Value(stackKey).([]string) + if !ok { + currentStack = []string{} + } + name := itemType + ":" + itemName + if slices.Contains(currentStack, name) { + return ctx, fmt.Errorf("could not instantiate %s %s: recursion detected in %s", itemType, itemName, strings.Join(append(currentStack, name), "->")) + } + return context.WithValue(ctx, stackKey, append(currentStack, name)), nil +} diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index fc9190e4b..594b77efe 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -28,8 +28,8 @@ import ( const maxUserPriority = math.MaxInt - 1000 -type middlewareBuilder interface { - BuildChain(ctx context.Context, names []string) *alice.Chain +type middlewareChainBuilder interface { + BuildMiddlewareChain(ctx context.Context, names []string) *alice.Chain } type serviceManager interface { @@ -42,7 +42,7 @@ type Manager struct { routerHandlers map[string]http.Handler serviceManager serviceManager observabilityMgr *middleware.ObservabilityMgr - middlewaresBuilder middlewareBuilder + middlewaresBuilder middlewareChainBuilder conf *runtime.Configuration tlsManager *tls.Manager parser httpmuxer.SyntaxParser @@ -51,7 +51,7 @@ type Manager struct { // NewManager creates a new Manager. func NewManager(conf *runtime.Configuration, serviceManager serviceManager, - middlewaresBuilder middlewareBuilder, + middlewaresBuilder middlewareChainBuilder, observabilityMgr *middleware.ObservabilityMgr, tlsManager *tls.Manager, parser httpmuxer.SyntaxParser, @@ -372,7 +372,7 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn }) } - mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares) + mHandler := m.middlewaresBuilder.BuildMiddlewareChain(ctx, router.Middlewares) return chain.Extend(*mHandler).Then(nextHandler) } diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index eb334d183..6b058c3ad 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -2000,7 +2000,7 @@ func (m *mockServiceManager) LaunchHealthCheck(_ context.Context) {} type mockMiddlewareBuilder struct{} -func (m *mockMiddlewareBuilder) BuildChain(_ context.Context, _ []string) *alice.Chain { +func (m *mockMiddlewareBuilder) BuildMiddlewareChain(_ context.Context, _ []string) *alice.Chain { chain := alice.New() return &chain } diff --git a/pkg/server/routerfactory.go b/pkg/server/routerfactory.go index 698de82b4..503a5c23a 100644 --- a/pkg/server/routerfactory.go +++ b/pkg/server/routerfactory.go @@ -104,6 +104,8 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, f.pluginBuilder) + serviceManager.SetMiddlewareChainBuilder(middlewaresBuilder) + routerManager := router.NewManager(rtConf, serviceManager, middlewaresBuilder, f.observabilityMgr, f.tlsManager, f.parser) routerManager.ParseRouterTree() diff --git a/pkg/server/routerfactory_test.go b/pkg/server/routerfactory_test.go index ba77f5e31..a287c22ee 100644 --- a/pkg/server/routerfactory_test.go +++ b/pkg/server/routerfactory_test.go @@ -46,8 +46,8 @@ func TestReuseService(t *testing.T) { th.WithMiddlewares(th.WithMiddleware("basicauth", th.WithBasicAuth(&dynamic.BasicAuth{Users: []string{"foo:bar"}}), )), - th.WithLoadBalancerServices(th.WithService("bar", - th.WithServers(th.WithServer(testServer.URL))), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer(testServer.URL)))), ), ) @@ -98,8 +98,8 @@ func TestServerResponseEmptyBackend(t *testing.T) { th.WithServiceName("bar"), th.WithRule(routeRule)), ), - th.WithLoadBalancerServices(th.WithService("bar", - th.WithServers(th.WithServer(testServerURL))), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer(th.WithServers(th.WithServer(testServerURL)))), ), ) }, @@ -121,7 +121,9 @@ func TestServerResponseEmptyBackend(t *testing.T) { th.WithServiceName("bar"), th.WithRule(routeRule)), ), - th.WithLoadBalancerServices(th.WithService("bar")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer()), + ), ) }, expectedStatusCode: http.StatusServiceUnavailable, @@ -135,27 +137,13 @@ func TestServerResponseEmptyBackend(t *testing.T) { th.WithServiceName("bar"), th.WithRule(routeRule)), ), - th.WithLoadBalancerServices(th.WithService("bar", - th.WithSticky("test")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer(th.WithSticky("test"))), ), ) }, expectedStatusCode: http.StatusServiceUnavailable, }, - { - desc: "Empty Backend LB", - config: func(testServerURL string) *dynamic.HTTPConfiguration { - return th.BuildConfiguration( - th.WithRouters(th.WithRouter("foo", - th.WithEntryPoints("web"), - th.WithServiceName("bar"), - th.WithRule(routeRule)), - ), - th.WithLoadBalancerServices(th.WithService("bar")), - ) - }, - expectedStatusCode: http.StatusServiceUnavailable, - }, { desc: "Empty Backend LB Sticky", config: func(testServerURL string) *dynamic.HTTPConfiguration { @@ -165,8 +153,8 @@ func TestServerResponseEmptyBackend(t *testing.T) { th.WithServiceName("bar"), th.WithRule(routeRule)), ), - th.WithLoadBalancerServices(th.WithService("bar", - th.WithSticky("test")), + th.WithServices( + th.WithService("bar", th.WithServiceServersLoadBalancer(th.WithSticky("test"))), ), ) }, @@ -256,6 +244,61 @@ func TestInternalServices(t *testing.T) { assert.Equal(t, http.StatusOK, responseRecorderOk.Result().StatusCode, "status code") } +func TestRecursionService(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + })) + defer testServer.Close() + + staticConfig := static.Configuration{ + EntryPoints: map[string]*static.EntryPoint{ + "web": {}, + }, + } + + dynamicConfigs := th.BuildConfiguration( + th.WithRouters( + th.WithRouter("foo@provider1", + th.WithEntryPoints("web"), + th.WithServiceName("bar"), + th.WithRule("Path(`/ok`)")), + ), + th.WithMiddlewares(th.WithMiddleware("customerror", + th.WithErrorPage(&dynamic.ErrorPage{Service: "bar"}), + )), + th.WithServices( + th.WithService("bar@provider1", th.WithServiceWRR(th.WithWRRServices(th.WithWRRService("foo")))), + th.WithService("foo@provider1", th.WithServiceWRR(th.WithWRRServices(th.WithWRRService("bar")))), + ), + ) + + transportManager := service.NewTransportManager(nil) + transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}}) + + managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, nil, nil) + tlsManager := tls.NewManager(nil) + + dialerManager := tcp.NewDialerManager(nil) + dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}}) + factory, err := NewRouterFactory(staticConfig, managerFactory, tlsManager, nil, nil, dialerManager) + require.NoError(t, err) + + rtConf := runtime.NewConfig(dynamic.Configuration{HTTP: dynamicConfigs}) + entryPointsHandlers, _ := factory.CreateRouters(rtConf) + + // Test that the /ok path returns a status 404. + responseRecorderOk := &httptest.ResponseRecorder{} + requestOk := httptest.NewRequest(http.MethodGet, testServer.URL+"/ok", nil) + entryPointsHandlers["web"].GetHTTPHandler().ServeHTTP(responseRecorderOk, requestOk) + + assert.Equal(t, http.StatusNotFound, responseRecorderOk.Result().StatusCode, "status code") + + require.NotNil(t, rtConf.Routers["foo@provider1"]) + assert.Contains(t, rtConf.Routers["foo@provider1"].Err, "building HTTP service \"foo\": building HTTP service \"bar\": could not instantiate service bar@provider1: recursion detected in service:bar@provider1->service:foo@provider1->service:bar@provider1") + require.NotNil(t, rtConf.Services["bar@provider1"]) + assert.Contains(t, rtConf.Services["bar@provider1"].Err, "could not instantiate service bar@provider1: recursion detected in service:bar@provider1->service:foo@provider1->service:bar@provider1") +} + type proxyBuilderMock struct{} func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) { diff --git a/pkg/server/service/loadbalancer/leasttime/leasttime_test.go b/pkg/server/service/loadbalancer/leasttime/leasttime_test.go index d444b314f..7c199eefc 100644 --- a/pkg/server/service/loadbalancer/leasttime/leasttime_test.go +++ b/pkg/server/service/loadbalancer/leasttime/leasttime_test.go @@ -800,53 +800,47 @@ func TestScoreCalculationWithWeights(t *testing.T) { } // TestScoreCalculationWithInflight tests that inflight requests are considered in score calculation. +// Uses direct manipulation of response times and nextServer() for deterministic results. func TestScoreCalculationWithInflight(t *testing.T) { balancer := New(nil, false) - // We'll manually control the inflight counters to test the score calculation. - // Add two servers with same response time. + // Add two servers with dummy handlers (we test selection logic directly). balancer.Add("server1", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - time.Sleep(10 * time.Millisecond) - rw.Header().Set("server", "server1") rw.WriteHeader(http.StatusOK) - httptrace.ContextClientTrace(req.Context()).GotFirstResponseByte() }), pointer(1), false) balancer.Add("server2", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - time.Sleep(10 * time.Millisecond) - rw.Header().Set("server", "server2") rw.WriteHeader(http.StatusOK) - httptrace.ContextClientTrace(req.Context()).GotFirstResponseByte() }), pointer(1), false) - // Build up response time averages for both servers. - for range 2 { - recorder := httptest.NewRecorder() - balancer.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil)) + // Pre-fill response times directly (10ms average for both servers). + for _, h := range balancer.handlers { + for i := range sampleSize { + h.responseTimes[i] = 10.0 + } + h.responseTimeSum = 10.0 * sampleSize + h.sampleCount = sampleSize } - // Now manually set server1 to have high inflight count. + // Set server1 to have high inflight count. balancer.handlers[0].inflightCount.Store(5) // Make requests - they should prefer server2 because: // Score for server1: (10 × (1 + 5)) / 1 = 60 // Score for server2: (10 × (1 + 0)) / 1 = 10 - recorder := &responseRecorder{save: map[string]int{}} + counts := map[string]int{"server1": 0, "server2": 0} for range 5 { - // Manually increment to simulate the ServeHTTP behavior. - server, _ := balancer.nextServer() + server, err := balancer.nextServer() + assert.NoError(t, err) + counts[server.name]++ + // Simulate ServeHTTP incrementing inflight count. server.inflightCount.Add(1) - - if server.name == "server1" { - recorder.save["server1"]++ - } else { - recorder.save["server2"]++ - } } - // Server2 should get all requests - assert.Equal(t, 5, recorder.save["server2"]) - assert.Zero(t, recorder.save["server1"]) + // Server2 should get all requests since its score (10-50) is always less than server1's (60). + // After each selection, server2's inflight grows: scores are 10, 20, 30, 40, 50 vs 60. + assert.Equal(t, 5, counts["server2"]) + assert.Zero(t, counts["server1"]) } // TestScoreCalculationColdStart tests that new servers (0ms avg) get fair selection @@ -930,28 +924,20 @@ func TestFastServerGetsMoreTraffic(t *testing.T) { // TestTrafficShiftsWhenPerformanceDegrades verifies that the load balancer // adapts to changing server performance by shifting traffic away from degraded servers. // This tests the adaptive behavior - the core value proposition of least-time load balancing. +// Uses nextServer() directly to avoid timing variations and ensure deterministic results. func TestTrafficShiftsWhenPerformanceDegrades(t *testing.T) { balancer := New(nil, false) - // Use atomic to dynamically control server1's response time. - server1Delay := atomic.Int64{} - server1Delay.Store(5) // Start with 5ms - + // Add two servers with dummy handlers (we'll test selection logic directly). balancer.Add("server1", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - time.Sleep(time.Duration(server1Delay.Load()) * time.Millisecond) - rw.Header().Set("server", "server1") rw.WriteHeader(http.StatusOK) - httptrace.ContextClientTrace(req.Context()).GotFirstResponseByte() }), pointer(1), false) balancer.Add("server2", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - time.Sleep(5 * time.Millisecond) // Static 5ms - rw.Header().Set("server", "server2") rw.WriteHeader(http.StatusOK) - httptrace.ContextClientTrace(req.Context()).GotFirstResponseByte() }), pointer(1), false) - // Pre-fill ring buffers to eliminate cold start effects and ensure deterministic equal performance state. + // Pre-fill ring buffers with equal response times (5ms each). for _, h := range balancer.handlers { for i := range sampleSize { h.responseTimes[i] = 5.0 @@ -960,35 +946,43 @@ func TestTrafficShiftsWhenPerformanceDegrades(t *testing.T) { h.sampleCount = sampleSize } - // Phase 1: Both servers perform equally (5ms each). - recorder := &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}} + // Phase 1: Both servers have equal performance (5ms each). + // With WRR tie-breaking, traffic should be distributed evenly. + counts := map[string]int{"server1": 0, "server2": 0} for range 50 { - balancer.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil)) + server, err := balancer.nextServer() + assert.NoError(t, err) + counts[server.name]++ } - // With equal performance and pre-filled buffers, distribution should be balanced via WRR tie-breaking. - total := recorder.save["server1"] + recorder.save["server2"] + total := counts["server1"] + counts["server2"] assert.Equal(t, 50, total) - assert.InDelta(t, 25, recorder.save["server1"], 10) // 25 ± 10 requests - assert.InDelta(t, 25, recorder.save["server2"], 10) // 25 ± 10 requests + assert.InDelta(t, 25, counts["server1"], 1) // Deterministic WRR: 25 ± 1 + assert.InDelta(t, 25, counts["server2"], 1) // Deterministic WRR: 25 ± 1 - // Phase 2: server1 degrades (simulating GC pause, CPU spike, or network latency). - server1Delay.Store(50) // Now 50ms (10x slower) - dramatic degradation for reliable detection - - // Make more requests to shift the moving average. - // Ring buffer has 100 samples, need significant new samples to shift average. - // server1's average will climb from ~5ms toward 50ms. - recorder2 := &responseRecorder{ResponseRecorder: httptest.NewRecorder(), save: map[string]int{}} - for range 60 { - balancer.ServeHTTP(recorder2, httptest.NewRequest(http.MethodGet, "/", nil)) + // Phase 2: Simulate server1 degradation by directly updating its ring buffer. + // Set server1's average response time to 50ms (10x slower than server2's 5ms). + for _, h := range balancer.handlers { + if h.name == "server1" { + for i := range sampleSize { + h.responseTimes[i] = 50.0 + } + h.responseTimeSum = 50.0 * sampleSize + } } - // server2 should get significantly more traffic - // With 10x performance difference, server2 should dominate. - total2 := recorder2.save["server1"] + recorder2.save["server2"] + // With 10x performance difference, server2 should get significantly more traffic. + counts2 := map[string]int{"server1": 0, "server2": 0} + for range 60 { + server, err := balancer.nextServer() + assert.NoError(t, err) + counts2[server.name]++ + } + + total2 := counts2["server1"] + counts2["server2"] assert.Equal(t, 60, total2) - assert.Greater(t, recorder2.save["server2"], 35) // At least ~60% (35/60) - assert.Less(t, recorder2.save["server1"], 25) // At most ~40% (25/60) + assert.Greater(t, counts2["server2"], 35) // At least ~60% (35/60) + assert.Less(t, counts2["server1"], 25) // At most ~40% (25/60) } // TestMultipleServersWithSameScore tests WRR tie-breaking when multiple servers have identical scores. diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 40b1ae474..599c943e0 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -27,6 +27,7 @@ import ( "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/recursion" "github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/failover" "github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/hrw" "github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/leasttime" @@ -47,6 +48,10 @@ type ServiceBuilder interface { BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) } +type middlewareChainBuilder interface { + BuildMiddlewareChain(ctx context.Context, middlewares []string) *alice.Chain +} + // Manager The service manager. type Manager struct { routinePool *safe.Pool @@ -55,10 +60,11 @@ type Manager struct { proxyBuilder ProxyBuilder serviceBuilders []ServiceBuilder - services map[string]http.Handler - configs map[string]*runtime.ServiceInfo - healthCheckers map[string]*healthcheck.ServiceHealthChecker - rand *rand.Rand // For the initial shuffling of load-balancers. + services map[string]http.Handler + configs map[string]*runtime.ServiceInfo + healthCheckers map[string]*healthcheck.ServiceHealthChecker + rand *rand.Rand // For the initial shuffling of load-balancers. + middlewareChainBuilder middlewareChainBuilder } // NewManager creates a new Manager. @@ -76,6 +82,11 @@ func NewManager(configs map[string]*runtime.ServiceInfo, observabilityMgr *middl } } +// SetMiddlewareChainBuilder sets the MiddlewareChainBuilder. +func (m *Manager) SetMiddlewareChainBuilder(middlewareChainBuilder middlewareChainBuilder) { + m.middlewareChainBuilder = middlewareChainBuilder +} + // BuildHTTP Creates a http.Handler for a service configuration. func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.Handler, error) { serviceName = provider.GetQualifiedName(rootCtx, serviceName) @@ -112,7 +123,7 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H value := reflect.ValueOf(*conf.Service) var count int for i := range value.NumField() { - if !value.Field(i).IsNil() { + if value.Type().Field(i).Name != "Middlewares" && !value.Field(i).IsNil() { count++ } } @@ -122,6 +133,12 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H return nil, err } + var errRecursion error + if ctx, errRecursion = recursion.CheckRecursion(ctx, "service", serviceName); errRecursion != nil { + conf.AddError(errRecursion, true) + return nil, errRecursion + } + var lb http.Handler switch { @@ -166,9 +183,22 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H return nil, sErr } - m.services[serviceName] = lb + if len(conf.Middlewares) > 0 { + if m.middlewareChainBuilder == nil { + // This should happen only in tests. + return nil, errors.New("chain builder not defined") + } + chain := m.middlewareChainBuilder.BuildMiddlewareChain(ctx, conf.Middlewares) + var err error + lb, err = chain.Then(lb) + if err != nil { + conf.AddError(err, true) + return nil, err + } + } - return lb, nil + m.services[serviceName] = lb + return m.services[serviceName], nil } // LaunchHealthCheck launches the health checks. @@ -319,7 +349,7 @@ func (m *Manager) getServiceHandler(ctx context.Context, service dynamic.WRRServ svcHandler, err := m.BuildHTTP(ctx, service.Name) if err != nil { - return nil, fmt.Errorf("building HTTP service: %w", err) + return nil, fmt.Errorf("building HTTP service %q: %w", service.Name, err) } if service.Headers != nil { diff --git a/pkg/testhelpers/config.go b/pkg/testhelpers/config.go index afd298599..344dab19a 100644 --- a/pkg/testhelpers/config.go +++ b/pkg/testhelpers/config.go @@ -66,32 +66,75 @@ func WithObservability() func(*dynamic.Router) { } } -// WithLoadBalancerServices is a helper to create a configuration. -func WithLoadBalancerServices(opts ...func(service *dynamic.ServersLoadBalancer) string) func(*dynamic.HTTPConfiguration) { +// WithServices is a helper to create a configuration. +func WithServices(opts ...func(service *dynamic.Service) string) func(*dynamic.HTTPConfiguration) { return func(c *dynamic.HTTPConfiguration) { c.Services = make(map[string]*dynamic.Service) for _, opt := range opts { - b := &dynamic.ServersLoadBalancer{} - b.SetDefaults() - + b := &dynamic.Service{} name := opt(b) - c.Services[name] = &dynamic.Service{ - LoadBalancer: b, - } + c.Services[name] = b } } } // WithService is a helper to create a configuration. -func WithService(name string, opts ...func(*dynamic.ServersLoadBalancer)) func(*dynamic.ServersLoadBalancer) string { - return func(r *dynamic.ServersLoadBalancer) string { +func WithService(name string, opts ...func(*dynamic.Service)) func(*dynamic.Service) string { + return func(s *dynamic.Service) string { for _, opt := range opts { - opt(r) + opt(s) } + return name } } +func WithServiceServersLoadBalancer(opts ...func(*dynamic.ServersLoadBalancer)) func(*dynamic.Service) { + return func(s *dynamic.Service) { + b := &dynamic.ServersLoadBalancer{} + b.SetDefaults() + + for _, opt := range opts { + opt(b) + } + + s.LoadBalancer = b + } +} + +func WithServiceWRR(opts ...func(*dynamic.WeightedRoundRobin)) func(*dynamic.Service) { + return func(s *dynamic.Service) { + b := &dynamic.WeightedRoundRobin{} + + for _, opt := range opts { + opt(b) + } + + s.Weighted = b + } +} + +// WithWRRServices is a helper to create a configuration. +func WithWRRServices(opts ...func(*dynamic.WRRService)) func(*dynamic.WeightedRoundRobin) { + return func(b *dynamic.WeightedRoundRobin) { + for _, opt := range opts { + service := dynamic.WRRService{} + opt(&service) + b.Services = append(b.Services, service) + } + } +} + +// WithWRRService is a helper to create a configuration. +func WithWRRService(name string, opts ...func(*dynamic.WRRService)) func(*dynamic.WRRService) { + return func(s *dynamic.WRRService) { + for _, opt := range opts { + opt(s) + } + s.Name = name + } +} + // WithMiddlewares is a helper to create a configuration. func WithMiddlewares(opts ...func(*dynamic.Middleware) string) func(*dynamic.HTTPConfiguration) { return func(c *dynamic.HTTPConfiguration) { @@ -121,6 +164,13 @@ func WithBasicAuth(auth *dynamic.BasicAuth) func(*dynamic.Middleware) { } } +// WithErrorPage is a helper to create a configuration. +func WithErrorPage(errorPage *dynamic.ErrorPage) func(*dynamic.Middleware) { + return func(r *dynamic.Middleware) { + r.Errors = errorPage + } +} + // WithEntryPoints is a helper to create a configuration. func WithEntryPoints(eps ...string) func(*dynamic.Router) { return func(f *dynamic.Router) { diff --git a/pkg/tls/certificate.go b/pkg/tls/certificate.go index f99796783..da1325a52 100644 --- a/pkg/tls/certificate.go +++ b/pkg/tls/certificate.go @@ -5,7 +5,6 @@ import ( "crypto/x509" "errors" "fmt" - "net/url" "os" "strings" @@ -160,37 +159,19 @@ func VerifyPeerCertificate(uri string, cfg *tls.Config, rawCerts [][]byte) error return nil } -// verifyServerCertMatchesURI is used on tls connections dialed to a server -// to ensure that the certificate it presented has the correct URI. +// verifyServerCertMatchesURI verifies that the given certificate contains the specified URI in its SANs. func verifyServerCertMatchesURI(uri string, cert *x509.Certificate) error { if cert == nil { return errors.New("peer certificate mismatch: no peer certificate presented") } - // Our certs will only ever have a single URI for now so only check that - if len(cert.URIs) < 1 { - return errors.New("peer certificate mismatch: peer certificate invalid") + for _, certURI := range cert.URIs { + if strings.EqualFold(certURI.String(), uri) { + return nil + } } - gotURI := cert.URIs[0] - - // Override the hostname since we rely on x509 constraints to limit ability to spoof the trust domain if needed - // (i.e. because a root is shared with other PKI or Consul clusters). - // This allows for seamless migrations between trust domains. - - expectURI := &url.URL{} - id, err := url.Parse(uri) - if err != nil { - return fmt.Errorf("%q is not a valid URI", uri) - } - *expectURI = *id - expectURI.Host = gotURI.Host - - if strings.EqualFold(gotURI.String(), expectURI.String()) { - return nil - } - - return fmt.Errorf("peer certificate mismatch got %s, want %s", gotURI, uri) + return fmt.Errorf("peer certificate mismatch: no SAN URI in peer certificate matches %s", uri) } // verifyChain performs standard TLS verification without enforcing remote hostname matching. diff --git a/pkg/tls/certificate_test.go b/pkg/tls/certificate_test.go new file mode 100644 index 000000000..c97c26531 --- /dev/null +++ b/pkg/tls/certificate_test.go @@ -0,0 +1,64 @@ +package tls + +import ( + "crypto/x509" + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_verifyServerCertMatchesURI(t *testing.T) { + tests := []struct { + desc string + uri string + cert *x509.Certificate + expErr require.ErrorAssertionFunc + }{ + { + desc: "returns error when certificate is nil", + uri: "spiffe://foo.com", + expErr: require.Error, + }, + { + desc: "returns error when certificate has no URIs", + uri: "spiffe://foo.com", + cert: &x509.Certificate{URIs: nil}, + expErr: require.Error, + }, + { + desc: "returns error when no URI matches", + uri: "spiffe://foo.com", + cert: &x509.Certificate{URIs: []*url.URL{ + {Scheme: "spiffe", Host: "other.org"}, + }}, + expErr: require.Error, + }, + { + desc: "returns nil when URI matches", + uri: "spiffe://foo.com", + cert: &x509.Certificate{URIs: []*url.URL{ + {Scheme: "spiffe", Host: "foo.com"}, + }}, + expErr: require.NoError, + }, + { + desc: "returns nil when one of the URI matches", + uri: "spiffe://foo.com", + cert: &x509.Certificate{URIs: []*url.URL{ + {Scheme: "spiffe", Host: "example.org"}, + {Scheme: "spiffe", Host: "foo.com"}, + }}, + expErr: require.NoError, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + err := verifyServerCertMatchesURI(test.uri, test.cert) + test.expErr(t, err) + }) + } +} diff --git a/pkg/tls/tls.go b/pkg/tls/tls.go index e3119e833..1a60a8427 100644 --- a/pkg/tls/tls.go +++ b/pkg/tls/tls.go @@ -4,6 +4,30 @@ import "github.com/traefik/traefik/v3/pkg/types" const certificateHeader = "-----BEGIN CERTIFICATE-----\n" +const ( + // NoClientCert indicates that no client certificate should be requested + // during the handshake, and if any certificates are sent they will not + // be verified. + NoClientCert = "NoClientCert" + // RequestClientCert indicates that a client certificate should be requested + // during the handshake, but does not require that the client send any + // certificates. + RequestClientCert = "RequestClientCert" + // RequireAnyClientCert indicates that a client certificate should be requested + // during the handshake, and that at least one certificate is required to be + // sent by the client, but that certificate is not required to be valid. + RequireAnyClientCert = "RequireAnyClientCert" + // VerifyClientCertIfGiven indicates that a client certificate should be requested + // during the handshake, but does not require that the client sends a + // certificate. If the client does send a certificate it is required to be + // valid. + VerifyClientCertIfGiven = "VerifyClientCertIfGiven" + // RequireAndVerifyClientCert indicates that a client certificate should be requested + // during the handshake, and that at least one valid certificate is required + // to be sent by the client. + RequireAndVerifyClientCert = "RequireAndVerifyClientCert" +) + // +k8s:deepcopy-gen=true // ClientAuth defines the parameters of the client authentication part of the TLS connection, if any. diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go index edd5987d9..9003c21f3 100644 --- a/pkg/tls/tlsmanager.go +++ b/pkg/tls/tlsmanager.go @@ -460,15 +460,15 @@ func buildTLSConfig(tlsOption Options) (*tls.Config, error) { } switch clientAuthType { - case "NoClientCert": + case NoClientCert: conf.ClientAuth = tls.NoClientCert - case "RequestClientCert": + case RequestClientCert: conf.ClientAuth = tls.RequestClientCert - case "RequireAnyClientCert": + case RequireAnyClientCert: conf.ClientAuth = tls.RequireAnyClientCert - case "VerifyClientCertIfGiven": + case VerifyClientCertIfGiven: conf.ClientAuth = tls.VerifyClientCertIfGiven - case "RequireAndVerifyClientCert": + case RequireAndVerifyClientCert: conf.ClientAuth = tls.RequireAndVerifyClientCert default: return nil, fmt.Errorf("unknown client auth type %q", clientAuthType)