diff --git a/.golangci.yml b/.golangci.yml index 359e07d7a..b43bbfcb6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -155,23 +155,16 @@ linters-settings: checks: - all - -SA1019 + errcheck: + exclude-functions: + - fmt.Fprintln linters: enable-all: true disable: - - deadcode # deprecated - - exhaustivestruct # deprecated - - golint # deprecated - - ifshort # deprecated - - interfacer # deprecated - - maligned # deprecated - - nosnakecase # deprecated - - scopelint # deprecated - - scopelint # deprecated - - structcheck # deprecated - - varcheck # deprecated + - execinquery # deprecated + - gomnd # deprecated - sqlclosecheck # not relevant (SQL) - rowserrcheck # not relevant (SQL) - - execinquery # not relevant (SQL) - cyclop # duplicate of gocyclo - lll # Not relevant - gocyclo # FIXME must be fixed @@ -185,14 +178,14 @@ linters: - gochecknoglobals - wsl # Too strict - nlreturn # Not relevant - - gomnd # Too strict + - mnd # Too strict - stylecheck # skip because report issues related to some generated files. - testpackage # Too strict - tparallel # Not relevant - paralleltest # Not relevant - exhaustive # Not relevant - exhaustruct # Not relevant - - goerr113 # Too strict + - err113 # Too strict - wrapcheck # Too strict - noctx # Too strict - bodyclose # too many false-positive @@ -208,7 +201,6 @@ linters: - gosmopolitan # not relevant - exportloopref # Useless with go1.22 - musttag - - intrange # bug (fixed in golangci-lint v1.58) issues: exclude-use-default: false @@ -226,6 +218,8 @@ issues: - goconst - funlen - godot + - canonicalheader + - fatcontext - path: '(.+)_test.go' text: ' always receives ' linters: diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index d59457ec7..068ddb294 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -25,7 +25,7 @@ global_job_config: - export "PATH=${GOPATH}/bin:${PATH}" - mkdir -vp "${SEMAPHORE_GIT_DIR}" "${GOPATH}/bin" - export GOPROXY=https://proxy.golang.org,direct - - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "${GOPATH}/bin" v1.57.0 + - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "${GOPATH}/bin" v1.59.0 - curl -sSfL https://gist.githubusercontent.com/traefiker/6d7ac019c11d011e4f131bb2cca8900e/raw/goreleaser.sh | bash -s -- -b "${GOPATH}/bin" - checkout - cache restore traefik-$(checksum go.sum) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4d550ccc..569a25c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +## [v3.0.2](https://github.com/traefik/traefik/tree/v3.0.2) (2024-06-10) +[All Commits](https://github.com/traefik/traefik/compare/v3.0.1...v3.0.2) + +**Bug fixes:** +- **[logs]** Bump OTel dependencies ([#10763](https://github.com/traefik/traefik/pull/10763) by [DrFaust92](https://github.com/DrFaust92)) +- **[logs]** Append to log file if it exists ([#10756](https://github.com/traefik/traefik/pull/10756) by [lbenguigui](https://github.com/lbenguigui)) +- **[metrics]** Fix service name label_replace in Grafana ([#10758](https://github.com/traefik/traefik/pull/10758) by [xdavidwu](https://github.com/xdavidwu)) +- **[middleware]** Forward the correct status code when compression is disabled within the Brotli handler ([#10780](https://github.com/traefik/traefik/pull/10780) by [rtribotte](https://github.com/rtribotte)) +- **[middleware]** Support Accept-Encoding header weights with Compress middleware ([#10777](https://github.com/traefik/traefik/pull/10777) by [ldez](https://github.com/ldez)) + +**Documentation:** +- Update v2 > v3 migration guide ([#10728](https://github.com/traefik/traefik/pull/10728) by [0anas01](https://github.com/0anas01)) + +**Misc:** +- Merge current v2.11 into v3.0 ([#10796](https://github.com/traefik/traefik/pull/10796) by [kevinpollet](https://github.com/kevinpollet)) +- Merge current v2.11 into v3.0 ([#10781](https://github.com/traefik/traefik/pull/10781) by [ldez](https://github.com/ldez)) + +## [v2.11.4](https://github.com/traefik/traefik/tree/v2.11.4) (2024-06-10) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.3...v2.11.4) + +**Bug fixes:** +- **[acme]** Update go-acme/lego to v4.17.3 ([#10768](https://github.com/traefik/traefik/pull/10768) by [ldez](https://github.com/ldez)) + +**Documentation:** +- **[acme]** Fix .com and .org domain examples ([#10635](https://github.com/traefik/traefik/pull/10635) by [rptaylor](https://github.com/rptaylor)) +- **[middleware]** Add a note about the Ratelimit middleware's behavior when the sourceCriterion header is missing ([#10752](https://github.com/traefik/traefik/pull/10752) by [dgutzmann](https://github.com/dgutzmann)) +- Add user guides link to getting started ([#10785](https://github.com/traefik/traefik/pull/10785) by [norlinhenrik](https://github.com/norlinhenrik)) +- Remove helm default repo warning as repo has been long deprecated ([#10772](https://github.com/traefik/traefik/pull/10772) by [corneliusroemer](https://github.com/corneliusroemer)) + ## [v3.0.1](https://github.com/traefik/traefik/tree/v3.0.1) (2024-05-22) [All Commits](https://github.com/traefik/traefik/compare/v3.0.0...v3.0.1) diff --git a/Dockerfile b/Dockerfile index fea809225..28f5e946a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,7 @@ # syntax=docker/dockerfile:1.2 -FROM alpine:3.19 +FROM alpine:3.20 -RUN apk --no-cache --no-progress add ca-certificates tzdata \ - && rm -rf /var/cache/apk/* +RUN apk add --no-cache --no-progress ca-certificates tzdata ARG TARGETPLATFORM COPY ./dist/$TARGETPLATFORM/traefik / diff --git a/Makefile b/Makefile index b006ce68e..8ba03926d 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,7 @@ crossbinary-default: generate generate-webui .PHONY: test #? test: Run the unit and integration tests -test: test-unit test-integration +test: test-ui-unit test-unit test-integration .PHONY: test-unit #? test-unit: Run the unit tests @@ -105,6 +105,13 @@ test-integration: binary test-gateway-api-conformance: build-image-dirty GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -v -test.run K8sConformanceSuite -k8sConformance $(TESTFLAGS) +.PHONY: test-ui-unit +#? test-ui-unit: Run the unit tests for the webui +test-ui-unit: + $(MAKE) build-webui-image + docker run --rm -v "$(PWD)/webui/static":'/src/webui/static' traefik-webui yarn --cwd webui install + docker run --rm -v "$(PWD)/webui/static":'/src/webui/static' traefik-webui yarn --cwd webui test:unit:ci + .PHONY: pull-images #? pull-images: Pull all Docker images to avoid timeout during integration tests pull-images: diff --git a/cmd/traefik/logger.go b/cmd/traefik/logger.go index 4a3a10893..58b7abbe3 100644 --- a/cmd/traefik/logger.go +++ b/cmd/traefik/logger.go @@ -49,7 +49,7 @@ func getLogWriter(staticConfiguration *static.Configuration) io.Writer { var w io.Writer = os.Stderr if staticConfiguration.Log != nil && len(staticConfiguration.Log.FilePath) > 0 { - _, _ = os.Create(staticConfiguration.Log.FilePath) + _, _ = os.OpenFile(staticConfiguration.Log.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666) w = &lumberjack.Logger{ Filename: staticConfiguration.Log.FilePath, MaxSize: staticConfiguration.Log.MaxSize, diff --git a/contrib/grafana/traefik-kubernetes.json b/contrib/grafana/traefik-kubernetes.json index 4e463b502..7d8f8ba4f 100644 --- a/contrib/grafana/traefik-kubernetes.json +++ b/contrib/grafana/traefik-kubernetes.json @@ -507,7 +507,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "topk(15,\n label_replace(\n traefik_service_request_duration_seconds_sum{service=~\"$service.*\",protocol=\"http\"} / \n traefik_service_request_duration_seconds_count{service=~\"$service.*\",protocol=\"http\"},\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)\n\n", + "expr": "topk(15,\n label_replace(\n traefik_service_request_duration_seconds_sum{service=~\"$service.*\",protocol=\"http\"} / \n traefik_service_request_duration_seconds_count{service=~\"$service.*\",protocol=\"http\"},\n \"service\", \"$1\", \"service\", \"([^@]+)@.*\")\n)\n\n", "legendFormat": "{{method}}[{{code}}] on {{service}}", "range": true, "refId": "A" @@ -606,7 +606,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "topk(15,\n label_replace(\n sum by (service,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", + "expr": "topk(15,\n label_replace(\n sum by (service,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^@]+)@.*\")\n)", "legendFormat": "[{{code}}] on {{service}}", "range": true, "refId": "A" @@ -711,7 +711,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "label_replace(\n 1 - (sum by (service)\n (rate(traefik_service_request_duration_seconds_bucket{le=\"1.2\",service=~\"$service.*\"}[5m])) / sum by (service) \n (rate(traefik_service_request_duration_seconds_count{service=~\"$service.*\"}[5m]))\n ) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\"\n)", + "expr": "label_replace(\n 1 - (sum by (service)\n (rate(traefik_service_request_duration_seconds_bucket{le=\"1.2\",service=~\"$service.*\"}[5m])) / sum by (service) \n (rate(traefik_service_request_duration_seconds_count{service=~\"$service.*\"}[5m]))\n ) > 0,\n \"service\", \"$1\", \"service\", \"([^@]+)@.*\"\n)", "legendFormat": "{{service}}", "range": true, "refId": "A" @@ -806,7 +806,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "label_replace(\n 1 - (sum by (service)\n (rate(traefik_service_request_duration_seconds_bucket{le=\"0.3\",service=~\"$service.*\"}[5m])) / sum by (service) \n (rate(traefik_service_request_duration_seconds_count{service=~\"$service.*\"}[5m]))\n ) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\"\n)", + "expr": "label_replace(\n 1 - (sum by (service)\n (rate(traefik_service_request_duration_seconds_bucket{le=\"0.3\",service=~\"$service.*\"}[5m])) / sum by (service) \n (rate(traefik_service_request_duration_seconds_count{service=~\"$service.*\"}[5m]))\n ) > 0,\n \"service\", \"$1\", \"service\", \"([^@]+)@.*\"\n)", "legendFormat": "{{service}}", "range": true, "refId": "A" @@ -922,7 +922,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code=~\"2..\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", + "expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code=~\"2..\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^@]+)@.*\")\n)", "legendFormat": "{{method}}[{{code}}] on {{service}}", "range": true, "refId": "A" @@ -1022,7 +1022,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code=~\"5..\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", + "expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code=~\"5..\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^@]+)@.*\")\n)", "legendFormat": "{{method}}[{{code}}] on {{service}}", "range": true, "refId": "A" @@ -1122,7 +1122,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code!~\"2..|5..\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", + "expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code!~\"2..|5..\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^@]+)@.*\")\n)", "legendFormat": "{{method}}[{{code}}] on {{service}}", "range": true, "refId": "A" @@ -1222,7 +1222,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "topk(15,\n label_replace(\n sum by (service,method) \n (rate(traefik_service_requests_bytes_total{service=~\"$service.*\",protocol=\"http\"}[1m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", + "expr": "topk(15,\n label_replace(\n sum by (service,method) \n (rate(traefik_service_requests_bytes_total{service=~\"$service.*\",protocol=\"http\"}[1m])) > 0,\n \"service\", \"$1\", \"service\", \"([^@]+)@.*\")\n)", "legendFormat": "{{method}} on {{service}}", "range": true, "refId": "A" @@ -1322,7 +1322,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "topk(15,\n label_replace(\n sum by (service,method) \n (rate(traefik_service_responses_bytes_total{service=~\"$service.*\",protocol=\"http\"}[1m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", + "expr": "topk(15,\n label_replace(\n sum by (service,method) \n (rate(traefik_service_responses_bytes_total{service=~\"$service.*\",protocol=\"http\"}[1m])) > 0,\n \"service\", \"$1\", \"service\", \"([^@]+)@.*\")\n)", "legendFormat": "{{method}} on {{service}}", "range": true, "refId": "A" @@ -1331,105 +1331,6 @@ "title": "Responses Size", "type": "timeseries" }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 39 - }, - "id": 2, - "options": { - "legend": { - "calcs": [ - "mean", - "max" - ], - "displayMode": "table", - "placement": "right", - "showLegend": true, - "sortBy": "Max", - "sortDesc": true - }, - "tooltip": { - "mode": "multi", - "sort": "desc" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "label_replace(\n sum(traefik_service_open_connections{service=~\"$service.*\"}) by (service),\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")", - "legendFormat": "{{service}}", - "range": true, - "refId": "A" - } - ], - "title": "Connections per Service", - "type": "timeseries" - }, { "datasource": { "type": "prometheus", @@ -1520,7 +1421,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "sum(traefik_entrypoint_open_connections{entrypoint=~\"$entrypoint\"}) by (entrypoint)\n", + "expr": "sum(traefik_open_connections{entrypoint=~\"$entrypoint\"}) by (entrypoint)\n", "legendFormat": "{{entrypoint}}", "range": true, "refId": "A" @@ -1560,14 +1461,14 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "definition": "label_values(traefik_entrypoint_open_connections, entrypoint)", + "definition": "label_values(traefik_open_connections, entrypoint)", "hide": 0, "includeAll": true, "multi": false, "name": "entrypoint", "options": [], "query": { - "query": "label_values(traefik_entrypoint_open_connections, entrypoint)", + "query": "label_values(traefik_open_connections, entrypoint)", "refId": "StandardVariableQuery" }, "refresh": 1, @@ -1582,18 +1483,18 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "definition": "label_values(traefik_service_open_connections, service)", + "definition": "label_values(traefik_service_requests_total, service)", "hide": 0, "includeAll": true, "multi": false, "name": "service", "options": [], "query": { - "query": "label_values(traefik_service_open_connections, service)", + "query": "label_values(traefik_service_requests_total, service)", "refId": "StandardVariableQuery" }, "refresh": 2, - "regex": "/([^-]+-[^-]+).*/", + "regex": "/([^@]+)@.*/", "skipUrlSync": false, "sort": 1, "type": "query" @@ -1608,6 +1509,6 @@ "timezone": "", "title": "Traefik Official Kubernetes Dashboard", "uid": "n5bu_kv4k", - "version": 6, + "version": 7, "weekStart": "" } diff --git a/contrib/grafana/traefik.json b/contrib/grafana/traefik.json index 5c3d140ee..2cda7d73a 100644 --- a/contrib/grafana/traefik.json +++ b/contrib/grafana/traefik.json @@ -1321,104 +1321,6 @@ "title": "Responses Size", "type": "timeseries" }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 39 - }, - "id": 2, - "options": { - "legend": { - "calcs": [ - "mean", - "max" - ], - "displayMode": "table", - "placement": "right", - "showLegend": true, - "sortBy": "Max", - "sortDesc": true - }, - "tooltip": { - "mode": "multi", - "sort": "desc" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "label_replace(\n sum(traefik_service_open_connections{service=~\"$service.*\"}) by (service),\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")", - "legendFormat": "{{service}}", - "range": true, - "refId": "A" - } - ], - "title": "Connections per Service", - "type": "timeseries" - }, { "datasource": { "type": "prometheus", @@ -1508,7 +1410,7 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "sum(traefik_entrypoint_open_connections{entrypoint=~\"$entrypoint\"}) by (entrypoint)\n", + "expr": "sum(traefik_open_connections{entrypoint=~\"$entrypoint\"}) by (entrypoint)\n", "legendFormat": "{{entrypoint}}", "range": true, "refId": "A" @@ -1552,14 +1454,14 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "definition": "label_values(traefik_entrypoint_open_connections, entrypoint)", + "definition": "label_values(traefik_open_connections, entrypoint)", "hide": 0, "includeAll": true, "multi": false, "name": "entrypoint", "options": [], "query": { - "query": "label_values(traefik_entrypoint_open_connections, entrypoint)", + "query": "label_values(traefik_open_connections, entrypoint)", "refId": "StandardVariableQuery" }, "refresh": 1, @@ -1574,14 +1476,14 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "definition": "label_values(traefik_service_open_connections, service)", + "definition": "label_values(traefik_service_requests_total, service)", "hide": 0, "includeAll": true, "multi": false, "name": "service", "options": [], "query": { - "query": "label_values(traefik_service_open_connections, service)", + "query": "label_values(traefik_service_requests_total, service)", "refId": "StandardVariableQuery" }, "refresh": 2, @@ -1600,6 +1502,6 @@ "timezone": "", "title": "Traefik Official Standalone Dashboard", "uid": "n5bu_kv45", - "version": 6, + "version": 7, "weekStart": "" } diff --git a/docs/check.Dockerfile b/docs/check.Dockerfile index d33a46af3..824a97081 100644 --- a/docs/check.Dockerfile +++ b/docs/check.Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.18 as alpine +FROM alpine:3.20 RUN apk --no-cache --no-progress add \ build-base \ diff --git a/docs/content/deprecation/releases.md b/docs/content/deprecation/releases.md index 3cfe9ddff..4ac402236 100644 --- a/docs/content/deprecation/releases.md +++ b/docs/content/deprecation/releases.md @@ -4,30 +4,26 @@ Below is a non-exhaustive list of versions and their maintenance status: -| Version | Release Date | Active Support | Security Support | -|---------|--------------|--------------------|------------------| -| 2.11 | Feb 12, 2024 | Yes | Yes | -| 2.10 | Apr 24, 2023 | Ended Feb 12, 2024 | No | -| 2.9 | Oct 03, 2022 | Ended Apr 24, 2023 | No | -| 2.8 | Jun 29, 2022 | Ended Oct 03, 2022 | No | -| 2.7 | May 24, 2022 | Ended Jun 29, 2022 | No | -| 2.6 | Jan 24, 2022 | Ended May 24, 2022 | No | -| 2.5 | Aug 17, 2021 | Ended Jan 24, 2022 | No | -| 2.4 | Jan 19, 2021 | Ended Aug 17, 2021 | No | -| 2.3 | Sep 23, 2020 | Ended Jan 19, 2021 | No | -| 2.2 | Mar 25, 2020 | Ended Sep 23, 2020 | No | -| 2.1 | Dec 11, 2019 | Ended Mar 25, 2020 | No | -| 2.0 | Sep 16, 2019 | Ended Dec 11, 2019 | No | -| 1.7 | Sep 24, 2018 | Ended Dec 31, 2021 | Contact Support | - -??? example "Active Support / Security Support" - - **Active support**: receives any bug fixes. - **Security support**: receives only critical bug and security fixes. +| Version | Release Date | Community Support | +|---------|--------------|--------------------| +| 3.0 | Apr 29, 2024 | Yes | +| 2.11 | Feb 12, 2024 | Ends Apr 29, 2025 | +| 2.10 | Apr 24, 2023 | Ended Feb 12, 2024 | +| 2.9 | Oct 03, 2022 | Ended Apr 24, 2023 | +| 2.8 | Jun 29, 2022 | Ended Oct 03, 2022 | +| 2.7 | May 24, 2022 | Ended Jun 29, 2022 | +| 2.6 | Jan 24, 2022 | Ended May 24, 2022 | +| 2.5 | Aug 17, 2021 | Ended Jan 24, 2022 | +| 2.4 | Jan 19, 2021 | Ended Aug 17, 2021 | +| 2.3 | Sep 23, 2020 | Ended Jan 19, 2021 | +| 2.2 | Mar 25, 2020 | Ended Sep 23, 2020 | +| 2.1 | Dec 11, 2019 | Ended Mar 25, 2020 | +| 2.0 | Sep 16, 2019 | Ended Dec 11, 2019 | +| 1.7 | Sep 24, 2018 | Ended Dec 31, 2021 | This page is maintained and updated periodically to reflect our roadmap and any decisions affecting the end of support for Traefik Proxy. -Please refer to our migration guides for specific instructions on upgrading between versions, an example is the [v1 to v2 migration guide](../migration/v1-to-v2.md). +Please refer to our migration guides for specific instructions on upgrading between versions, an example is the [v2 to v3 migration guide](../migration/v2-to-v3.md). !!! important "All target dates for end of support or feature removal announcements may be subject to change." diff --git a/docs/content/getting-started/install-traefik.md b/docs/content/getting-started/install-traefik.md index 57d15dd63..1ad17ff9b 100644 --- a/docs/content/getting-started/install-traefik.md +++ b/docs/content/getting-started/install-traefik.md @@ -35,11 +35,6 @@ For more details, go to the [Docker provider documentation](../providers/docker. ## Use the Helm Chart -!!! warning - - The Traefik Chart from - [Helm's default charts repository](https://github.com/helm/charts/tree/master/stable/traefik) is still using [Traefik v1.7](https://doc.traefik.io/traefik/v1.7). - Traefik can be installed in Kubernetes using the Helm chart from . Ensure that the following requirements are met: diff --git a/docs/content/getting-started/quick-start.md b/docs/content/getting-started/quick-start.md index 122ff530d..67a74036e 100644 --- a/docs/content/getting-started/quick-start.md +++ b/docs/content/getting-started/quick-start.md @@ -119,6 +119,6 @@ IP: 172.27.0.4 !!! question "Where to Go Next?" - Now that you have a basic understanding of how Traefik can automatically create the routes to your services and load balance them, it is time to dive into [the documentation](/ "Link to the docs landing page") and let Traefik work for you! + Now that you have a basic understanding of how Traefik can automatically create the routes to your services and load balance them, it is time to dive into [the user guides](../../user-guides/docker-compose/basic-example/ "Link to the user guides") and [the documentation](/ "Link to the docs landing page") and let Traefik work for you! {!traefik-for-business-applications.md!} diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index b012263ac..c4eafafd8 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -406,7 +406,7 @@ For complete details, refer to your provider's _Additional configuration_ link. | [Open Telekom Cloud](https://cloud.telekom.de) | `otc` | `OTC_DOMAIN_NAME`, `OTC_USER_NAME`, `OTC_PASSWORD`, `OTC_PROJECT_NAME`, `OTC_IDENTITY_ENDPOINT` | [Additional configuration](https://go-acme.github.io/lego/dns/otc) | | [Openstack Designate](https://docs.openstack.org/designate) | `designate` | `OS_AUTH_URL`, `OS_USERNAME`, `OS_PASSWORD`, `OS_TENANT_NAME`, `OS_REGION_NAME` | [Additional configuration](https://go-acme.github.io/lego/dns/designate) | | [Oracle Cloud](https://cloud.oracle.com/home) | `oraclecloud` | `OCI_COMPARTMENT_OCID`, `OCI_PRIVKEY_FILE`, `OCI_PRIVKEY_PASS`, `OCI_PUBKEY_FINGERPRINT`, `OCI_REGION`, `OCI_TENANCY_OCID`, `OCI_USER_OCID` | [Additional configuration](https://go-acme.github.io/lego/dns/oraclecloud) | -| [OVH](https://www.ovh.com) | `ovh` | `OVH_ENDPOINT`, `OVH_APPLICATION_KEY`, `OVH_APPLICATION_SECRET`, `OVH_CONSUMER_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/ovh) | +| [OVH](https://www.ovh.com) | `ovh` | `OVH_ENDPOINT`, `OVH_APPLICATION_KEY`, `OVH_APPLICATION_SECRET`, `OVH_CONSUMER_KEY`, `OVH_CLIENT_ID`, `OVH_CLIENT_SECRET` | [Additional configuration](https://go-acme.github.io/lego/dns/ovh) | | [Plesk](https://www.plesk.com) | `plesk` | `PLESK_SERVER_BASE_URL`, `PLESK_USERNAME`, `PLESK_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/plesk) | | [Porkbun](https://porkbun.com/) | `porkbun` | `PORKBUN_SECRET_API_KEY`, `PORKBUN_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/porkbun) | | [PowerDNS](https://www.powerdns.com) | `pdns` | `PDNS_API_KEY`, `PDNS_API_URL` | [Additional configuration](https://go-acme.github.io/lego/dns/pdns) | @@ -417,8 +417,9 @@ For complete details, refer to your provider's _Additional configuration_ link. | [RimuHosting](https://rimuhosting.com) | `rimuhosting` | `RIMUHOSTING_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/rimuhosting) | | [Route 53](https://aws.amazon.com/route53/) | `route53` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `[AWS_REGION]`, `[AWS_HOSTED_ZONE_ID]` or a configured user/instance IAM profile. | [Additional configuration](https://go-acme.github.io/lego/dns/route53) | | [Sakura Cloud](https://cloud.sakura.ad.jp/) | `sakuracloud` | `SAKURACLOUD_ACCESS_TOKEN`, `SAKURACLOUD_ACCESS_TOKEN_SECRET` | [Additional configuration](https://go-acme.github.io/lego/dns/sakuracloud) | -| [Scaleway](https://www.scaleway.com) | `scaleway` | `SCALEWAY_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/scaleway) | +| [Scaleway](https://www.scaleway.com) | `scaleway` | `SCW_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/scaleway) | | [Selectel](https://selectel.ru/en/) | `selectel` | `SELECTEL_API_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/selectel) | +| [Selectel v2](https://selectel.ru/en/) | `selectelv2` | `SELECTELV2_ACCOUNT_ID`, `SELECTELV2_PASSWORD`, `SELECTELV2_PROJECT_ID`, `SELECTELV2_USERNAME` | [Additional configuration](https://go-acme.github.io/lego/dns/selectelv2) | | [Servercow](https://servercow.de) | `servercow` | `SERVERCOW_USERNAME`, `SERVERCOW_PASSWORD` | [Additional configuration](https://go-acme.github.io/lego/dns/servercow) | | [Shellrent](https://www.shellrent.com) | `shellrent` | `SHELLRENT_USERNAME`, `SHELLRENT_TOKEN` | [Additional configuration](https://go-acme.github.io/lego/dns/shellrent) | | [Simply.com](https://www.simply.com/en/domains/) | `simply` | `SIMPLY_ACCOUNT_NAME`, `SIMPLY_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/simply) | diff --git a/docs/content/https/include-acme-multiple-domains-example.md b/docs/content/https/include-acme-multiple-domains-example.md index a904b8950..00f1aacd8 100644 --- a/docs/content/https/include-acme-multiple-domains-example.md +++ b/docs/content/https/include-acme-multiple-domains-example.md @@ -5,22 +5,10 @@ labels: - traefik.http.routers.blog.rule=Host(`example.com`) && Path(`/blog`) - traefik.http.routers.blog.tls=true - traefik.http.routers.blog.tls.certresolver=myresolver - - traefik.http.routers.blog.tls.domains[0].main=example.org + - traefik.http.routers.blog.tls.domains[0].main=example.com - traefik.http.routers.blog.tls.domains[0].sans=*.example.org ``` -```yaml tab="Docker (Swarm)" -## Dynamic configuration -deploy: - labels: - - traefik.http.routers.blog.rule=Host(`example.com`) && Path(`/blog`) - - traefik.http.services.blog-svc.loadbalancer.server.port=8080" - - traefik.http.routers.blog.tls=true - - traefik.http.routers.blog.tls.certresolver=myresolver - - traefik.http.routers.blog.tls.domains[0].main=example.org - - traefik.http.routers.blog.tls.domains[0].sans=*.example.org -``` - ```yaml tab="Kubernetes" apiVersion: traefik.io/v1alpha1 kind: IngressRoute @@ -38,7 +26,7 @@ spec: tls: certResolver: myresolver domains: - - main: example.org + - main: example.com sans: - '*.example.org' ``` @@ -52,7 +40,7 @@ http: tls: certResolver: myresolver domains: - - main: "example.org" + - main: "example.com" sans: - "*.example.org" ``` @@ -65,6 +53,6 @@ http: [http.routers.blog.tls] certResolver = "myresolver" # From static configuration [[http.routers.blog.tls.domains]] - main = "example.org" + main = "example.com" sans = ["*.example.org"] ``` diff --git a/docs/content/middlewares/http/compress.md b/docs/content/middlewares/http/compress.md index e98954660..f26fe622c 100644 --- a/docs/content/middlewares/http/compress.md +++ b/docs/content/middlewares/http/compress.md @@ -10,7 +10,7 @@ Compress Allows Compressing Responses before Sending them to the Client ![Compress](../../assets/img/middleware/compress.png) -The Compress middleware supports gzip and Brotli compression. +The Compress middleware supports Gzip, Brotli and Zstandard compression. The activation of compression, and the compression method choice rely (among other things) on the request's `Accept-Encoding` header. ## Configuration Examples @@ -54,8 +54,8 @@ http: Responses are compressed when the following criteria are all met: - * The `Accept-Encoding` request header contains `gzip`, `*`, and/or `br` with or without [quality values](https://developer.mozilla.org/en-US/docs/Glossary/Quality_values). - If the `Accept-Encoding` request header is absent, the response won't be encoded. + * The `Accept-Encoding` request header contains `gzip`, and/or `*`, and/or `br`, and/or `zstd` with or without [quality values](https://developer.mozilla.org/en-US/docs/Glossary/Quality_values). + If the `Accept-Encoding` request header is absent and no [defaultEncoding](#defaultencoding) is configured, the response won't be encoded. If it is present, but its value is the empty string, then compression is disabled. * The response is not already compressed, i.e. the `Content-Encoding` response header is not already set. * The response`Content-Type` header is not one among the [excludedContentTypes options](#excludedcontenttypes), or is one among the [includedContentTypes options](#includedcontenttypes). @@ -214,3 +214,44 @@ http: [http.middlewares.test-compress.compress] minResponseBodyBytes = 1200 ``` + +### `defaultEncoding` + +_Optional, Default=""_ + +`defaultEncoding` specifies the default encoding if the `Accept-Encoding` header is not in the request or contains a wildcard (`*`). + +There is no fallback on the `defaultEncoding` when the header value is empty or unsupported. + +```yaml tab="Docker & Swarm" +labels: + - "traefik.http.middlewares.test-compress.compress.defaultEncoding=gzip" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: test-compress +spec: + compress: + defaultEncoding: gzip +``` + +```yaml tab="Consul Catalog" +- "traefik.http.middlewares.test-compress.compress.defaultEncoding=gzip" +``` + +```yaml tab="File (YAML)" +http: + middlewares: + test-compress: + compress: + defaultEncoding: gzip +``` + +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.test-compress.compress] + defaultEncoding = "gzip" +``` diff --git a/docs/content/middlewares/http/headers.md b/docs/content/middlewares/http/headers.md index d0cb63672..afeb6891f 100644 --- a/docs/content/middlewares/http/headers.md +++ b/docs/content/middlewares/http/headers.md @@ -394,6 +394,10 @@ This overrides the `BrowserXssFilter` option. The `contentSecurityPolicy` option allows the `Content-Security-Policy` header value to be set with a custom value. +### `contentSecurityPolicyReportOnly` + +The `contentSecurityPolicyReportOnly` option allows the `Content-Security-Policy-Report-Only` header value to be set with a custom value. + ### `publicKey` The `publicKey` implements HPKP to prevent MITM attacks with forged certificates. diff --git a/docs/content/middlewares/http/ratelimit.md b/docs/content/middlewares/http/ratelimit.md index 98de1fc52..39a1f464f 100644 --- a/docs/content/middlewares/http/ratelimit.md +++ b/docs/content/middlewares/http/ratelimit.md @@ -359,6 +359,8 @@ http: Name of the header used to group incoming requests. +!!! important "If the header is not present, rate limiting will still be applied, but all requests without the specified header will be grouped together." + ```yaml tab="Docker & Swarm" labels: - "traefik.http.middlewares.test-ratelimit.ratelimit.sourcecriterion.requestheadername=username" diff --git a/docs/content/migration/v2-to-v3-details.md b/docs/content/migration/v2-to-v3-details.md new file mode 100644 index 000000000..57d221913 --- /dev/null +++ b/docs/content/migration/v2-to-v3-details.md @@ -0,0 +1,723 @@ +--- +title: "Traefik V3 Migration Details" +description: "Configuration changes and their details to successfully migrate from Traefik v2 to v3." +--- + +# Configuration Details for Migrating from Traefik v2 to v3 + +## Static Configuration Changes + +### SwarmMode + +In v3, the provider Docker has been split into 2 providers: + +- Docker provider (without Swarm support) +- Swarm provider (Swarm support only) + +??? example "An example usage of v2 Docker provider with Swarm" + + ```yaml tab="File (YAML)" + providers: + docker: + swarmMode: true + ``` + + ```toml tab="File (TOML)" + [providers.docker] + swarmMode=true + ``` + + ```bash tab="CLI" + --providers.docker.swarmMode=true + ``` + +This configuration is now unsupported and would prevent Traefik to start. + +#### Remediation + +In v3, the `swarmMode` should not be used with the Docker provider, and, to use Swarm, the Swarm provider should be used instead. + +??? example "An example usage of the Swarm provider" + + ```yaml tab="File (YAML)" + providers: + swarm: + endpoint: "tcp://127.0.0.1:2377" + ``` + + ```toml tab="File (TOML)" + [providers.swarm] + endpoint="tcp://127.0.0.1:2377" + ``` + + ```bash tab="CLI" + --providers.swarm.endpoint=tcp://127.0.0.1:2377 + ``` + +#### TLS.CAOptional + +Docker provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + docker: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.docker.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.docker.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the Docker provider static configuration. + +### Kubernetes Gateway API + +#### Experimental Channel Resources (TLSRoute and TCPRoute) + +In v3, the Kubernetes Gateway API provider does not enable support for the experimental channel API resources by default. + +##### Remediation + +The `experimentalChannel` option should be used to enable the support for the experimental channel API resources. + +??? example "An example usage of the Kubernetes Gateway API provider with experimental channel support enabled" + + ```yaml tab="File (YAML)" + providers: + kubernetesGateway: + experimentalChannel: true + ``` + + ```toml tab="File (TOML)" + [providers.kubernetesGateway] + experimentalChannel = true + # ... + ``` + + ```bash tab="CLI" + --providers.kubernetesgateway.experimentalchannel=true + ``` + +### Experimental Configuration + +#### HTTP3 + +In v3, HTTP/3 is no longer an experimental feature. +It can be enabled on entry points without the associated `experimental.http3` option, which is now removed. +It is now unsupported and would prevent Traefik to start. + +??? example "An example usage of v2 Experimental `http3` option" + + ```yaml tab="File (YAML)" + experimental: + http3: true + ``` + + ```toml tab="File (TOML)" + [experimental] + http3=true + ``` + + ```bash tab="CLI" + --experimental.http3=true + ``` + +##### Remediation + +The `http3` option should be removed from the static configuration experimental section. +To configure `http3`, please checkout the [entrypoint configuration documentation](https://doc.traefik.io/traefik/v3.0/routing/entrypoints/#http3_1). + +### Consul provider + +#### namespace + +The Consul provider `namespace` option was deprecated in v2 and is now removed in v3. +It is now unsupported and would prevent Traefik to start. + +??? example "An example usage of v2 Consul `namespace` option" + + ```yaml tab="File (YAML)" + consul: + namespace: foobar + ``` + + ```toml tab="File (TOML)" + [consul] + namespace=foobar + ``` + + ```bash tab="CLI" + --consul.namespace=foobar + ``` + +##### Remediation + +In v3, the `namespaces` option should be used instead of the `namespace` option. + +??? example "An example usage of Consul `namespaces` option" + + ```yaml tab="File (YAML)" + consul: + namespaces: + - foobar + ``` + + ```toml tab="File (TOML)" + [consul] + namespaces=["foobar"] + ``` + + ```bash tab="CLI" + --consul.namespaces=foobar + ``` + +#### TLS.CAOptional + +Consul provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + consul: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.consul.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.consul.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the Consul provider static configuration. + +### ConsulCatalog provider + +#### namespace + +The ConsulCatalog provider `namespace` option was deprecated in v2 and is now removed in v3. +It is now unsupported and would prevent Traefik to start. + +??? example "An example usage of v2 ConsulCatalog `namespace` option" + + ```yaml tab="File (YAML)" + consulCatalog: + namespace: foobar + ``` + + ```toml tab="File (TOML)" + [consulCatalog] + namespace=foobar + ``` + + ```bash tab="CLI" + --consulCatalog.namespace=foobar + ``` + +##### Remediation + +In v3, the `namespaces` option should be used instead of the `namespace` option. + +??? example "An example usage of ConsulCatalog `namespaces` option" + + ```yaml tab="File (YAML)" + consulCatalog: + namespaces: + - foobar + ``` + + ```toml tab="File (TOML)" + [consulCatalog] + namespaces=["foobar"] + ``` + + ```bash tab="CLI" + --consulCatalog.namespaces=foobar + ``` + +#### Endpoint.TLS.CAOptional + +ConsulCatalog provider `endpoint.tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the Endpoint.TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + consulCatalog: + endpoint: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.consulCatalog.endpoint.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.consulCatalog.endpoint.tls.caOptional=true + ``` + +##### Remediation + +The `endpoint.tls.caOptional` option should be removed from the ConsulCatalog provider static configuration. + +### Nomad provider + +#### namespace + +The Nomad provider `namespace` option was deprecated in v2 and is now removed in v3. +It is now unsupported and would prevent Traefik to start. + +??? example "An example usage of v2 Nomad `namespace` option" + + ```yaml tab="File (YAML)" + nomad: + namespace: foobar + ``` + + ```toml tab="File (TOML)" + [nomad] + namespace=foobar + ``` + + ```bash tab="CLI" + --nomad.namespace=foobar + ``` + +##### Remediation + +In v3, the `namespaces` option should be used instead of the `namespace` option. + +??? example "An example usage of Nomad `namespaces` option" + + ```yaml tab="File (YAML)" + nomad: + namespaces: + - foobar + ``` + + ```toml tab="File (TOML)" + [nomad] + namespaces=["foobar"] + ``` + + ```bash tab="CLI" + --nomad.namespaces=foobar + ``` + +#### Endpoint.TLS.CAOptional + +Nomad provider `endpoint.tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the Endpoint.TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + nomad: + endpoint: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.nomad.endpoint.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.nomad.endpoint.tls.caOptional=true + ``` + +##### Remediation + +The `endpoint.tls.caOptional` option should be removed from the Nomad provider static configuration. + +### Rancher v1 Provider + +In v3, the Rancher v1 provider has been removed because Rancher v1 is [no longer actively maintained](https://rancher.com/docs/os/v1.x/en/support/), +and Rancher v2 is supported as a standard Kubernetes provider. + +??? example "An example of Traefik v2 Rancher v1 configuration" + + ```yaml tab="File (YAML)" + providers: + rancher: {} + ``` + + ```toml tab="File (TOML)" + [providers.rancher] + ``` + + ```bash tab="CLI" + --providers.rancher=true + ``` + +This configuration is now unsupported and would prevent Traefik to start. + +#### Remediation + +Rancher 2.x requires Kubernetes and does not have a metadata endpoint of its own for Traefik to query. +As such, Rancher 2.x users should utilize the [Kubernetes CRD provider](../providers/kubernetes-crd.md) directly. + +Also, all Rancher provider related configuration should be removed from the static configuration. + +### Marathon provider + +Marathon maintenance [ended on October 31, 2021](https://github.com/mesosphere/marathon/blob/master/README.md). +In v3, the Marathon provider has been removed. + +??? example "An example of v2 Marathon provider configuration" + + ```yaml tab="File (YAML)" + providers: + marathon: {} + ``` + + ```toml tab="File (TOML)" + [providers.marathon] + ``` + + ```bash tab="CLI" + --providers.marathon=true + ``` + +This configuration is now unsupported and would prevent Traefik to start. + +#### Remediation + +All Marathon provider related configuration should be removed from the static configuration. + +### HTTP Provider + +#### TLS.CAOptional + +HTTP provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + http: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.http.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.http.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the HTTP provider static configuration. + +### ETCD Provider + +#### TLS.CAOptional + +ETCD provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + etcd: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.etcd.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.etcd.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the ETCD provider static configuration. + +### Redis Provider + +#### TLS.CAOptional + +Redis provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + redis: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.redis.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.redis.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the Redis provider static configuration. + +### InfluxDB v1 + +InfluxDB v1.x maintenance [ended in 2021](https://www.influxdata.com/blog/influxdb-oss-and-enterprise-roadmap-update-from-influxdays-emea/). +In v3, the InfluxDB v1 metrics provider has been removed. + +??? example "An example of Traefik v2 InfluxDB v1 metrics configuration" + + ```yaml tab="File (YAML)" + metrics: + influxDB: {} + ``` + + ```toml tab="File (TOML)" + [metrics.influxDB] + ``` + + ```bash tab="CLI" + --metrics.influxDB=true + ``` + +This configuration is now unsupported and would prevent Traefik to start. + +#### Remediation + +All InfluxDB v1 metrics provider related configuration should be removed from the static configuration. + +### Pilot + +Traefik Pilot is no longer available since October 4th, 2022. + +??? example "An example of v2 Pilot configuration" + + ```yaml tab="File (YAML)" + pilot: + token: foobar + ``` + + ```toml tab="File (TOML)" + [pilot] + token=foobar + ``` + + ```bash tab="CLI" + --pilot.token=foobar + ``` + +In v2, Pilot configuration was deprecated and ineffective, +it is now unsupported and would prevent Traefik to start. + +#### Remediation + +All Pilot related configuration should be removed from the static configuration. + +## Operations Changes + +### Traefik RBAC Update + +In v3, the support of `TCPServersTransport` has been introduced. +When using the KubernetesCRD provider, it is therefore necessary to update [RBAC](../reference/dynamic-configuration/kubernetes-crd.md#rbac) and [CRD](../reference/dynamic-configuration/kubernetes-crd.md) manifests. + +### Content-Type Auto-Detection + +In v3, the `Content-Type` header is not auto-detected anymore when it is not set by the backend. +One should use the `ContentType` middleware to enable the `Content-Type` header value auto-detection. + +### Observability + +#### gRPC Metrics + +In v3, the reported status code for gRPC requests is now the value of the `Grpc-Status` header. + +#### Tracing + +In v3, the tracing feature has been revamped and is now powered exclusively by [OpenTelemetry](https://opentelemetry.io/ "Link to website of OTel") (OTel). +!!! warning "Important" + Traefik v3 **no** longer supports direct output formats for specific vendors such as Instana, Jaeger, Zipkin, Haystack, Datadog, and Elastic. + Instead, it focuses on pure OpenTelemetry implementation, providing a unified and standardized approach for observability. + +Here are two possible transition strategies: + +1. OTLP Ingestion Endpoints: + + Most vendors now offer OpenTelemetry Protocol (OTLP) ingestion endpoints. + You can seamlessly integrate Traefik v3 with these endpoints to continue leveraging tracing capabilities. + +2. Legacy Stack Compatibility: + + For legacy stacks that cannot immediately upgrade to the latest vendor agents supporting OTLP ingestion, + using OpenTelemetry (OTel) collectors with appropriate exporters configuration is a viable solution. + This allows continued compatibility with the existing infrastructure. + +Please check the [OpenTelemetry Tracing provider documention](../observability/tracing/opentelemetry.md) for more information. + +#### Internal Resources Observability + +In v3, observability for internal routers or services (e.g.: `ping@internal`) is disabled by default. +To enable it one should use the new `addInternals` option for AccessLogs, Metrics or Tracing. +Please take a look at the observability documentation for more information: + +- [AccessLogs](../observability/access-logs.md#addinternals) +- [Metrics](../observability/metrics/overview.md#addinternals) +- [Tracing](../observability/tracing/overview.md#addinternals) + +## Dynamic Configuration Changes + +### Router Rule Matchers + +In v3, a new rule matchers syntax has been introduced for HTTP and TCP routers. +The default rule matchers syntax is now the v3 one, but for backward compatibility this can be configured. +The v2 rule matchers syntax is deprecated and its support will be removed in the next major version. +For this reason, we encourage migrating to the new syntax. + +By default, the `defaultRuleSyntax` static option is automatically set to `v3`, meaning that the default rule is the new one. + +#### New V3 Syntax Notable Changes + +The `Headers` and `HeadersRegexp` matchers have been renamed to `Header` and `HeaderRegexp` respectively. + +`PathPrefix` no longer uses regular expressions to match path prefixes. + +`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/). + +All matchers now take a single value (except `Header`, `HeaderRegexp`, `Query`, and `QueryRegexp` which take two) +and should be explicitly combined using logical operators to mimic previous behavior. + +`Query` can take a single value to match is the query value that has no value (e.g. `/search?mobile`). + +`HostHeader` has been removed, use `Host` instead. + +#### Remediation + +##### Configure the Default Syntax In Static Configuration + +The default rule matchers syntax is the expected syntax for any router that is not self opt-out from this default value. +It can be configured in the static configuration. + +??? example "An example configuration for the default rule matchers syntax" + + ```yaml tab="File (YAML)" + # static configuration + core: + defaultRuleSyntax: v2 + ``` + + ```toml tab="File (TOML)" + # static configuration + [core] + defaultRuleSyntax="v2" + ``` + + ```bash tab="CLI" + # static configuration + --core.defaultRuleSyntax=v2 + ``` + +##### Configure the Syntax Per Router + +The rule syntax can also be configured on a per-router basis. +This allows to have heterogeneous router configurations and ease migration. + +??? example "An example router with syntax configuration" + +```yaml tab="Docker & Swarm" +labels: + - "traefik.http.routers.test.ruleSyntax=v2" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + routes: + - match: PathPrefix(`/foo`, `/bar`) + syntax: v2 + kind: Rule +``` + +```yaml tab="Consul Catalog" +- "traefik.http.routers.test.ruleSyntax=v2" +``` + +```yaml tab="File (YAML)" +http: + routers: + test: + ruleSyntax: v2 +``` + +```toml tab="File (TOML)" +[http.routers] + [http.routers.test] + ruleSyntax = "v2" +``` + +### IPWhiteList + +In v3, we renamed the `IPWhiteList` middleware to `IPAllowList` without changing anything to the configuration. + +### Deprecated Options Removal + +- The `tracing.datadog.globaltag` option has been removed. +- The `tls.caOptional` option has been removed from the ForwardAuth middleware, as well as from the HTTP, Consul, Etcd, Redis, ZooKeeper, Consul Catalog, and Docker providers. +- `sslRedirect`, `sslTemporaryRedirect`, `sslHost`, `sslForceHost` and `featurePolicy` options of the Headers middleware have been removed. +- The `forceSlash` option of the StripPrefix middleware has been removed. +- The `preferServerCipherSuites` option has been removed. + +### TCP LoadBalancer `terminationDelay` option + +The TCP LoadBalancer `terminationDelay` option has been removed. +This option can now be configured directly on the `TCPServersTransport` level, please take a look at this [documentation](../routing/services/index.md#terminationdelay) + +### Kubernetes CRDs API Group `traefik.containo.us` + +In v3, the Kubernetes CRDs API Group `traefik.containo.us` has been removed. +Please use the API Group `traefik.io` instead. + +### Kubernetes Ingress API Group `networking.k8s.io/v1beta1` + +In v3, the Kubernetes Ingress API Group `networking.k8s.io/v1beta1` ([removed since Kubernetes v1.22](https://kubernetes.io/docs/reference/using-api/deprecation-guide/#ingress-v122)) support has been removed. + +Please use the API Group `networking.k8s.io/v1` instead. + +### Traefik CRD API Version `apiextensions.k8s.io/v1beta1` + +In v3, the Traefik CRD API Version `apiextensions.k8s.io/v1beta1` ([removed since Kubernetes v1.22](https://kubernetes.io/docs/reference/using-api/deprecation-guide/#customresourcedefinition-v122)) support has been removed. + +Please use the CRD definition with the API Version `apiextensions.k8s.io/v1` instead. diff --git a/docs/content/migration/v2-to-v3.md b/docs/content/migration/v2-to-v3.md index 6f80b2b15..1c548f7eb 100644 --- a/docs/content/migration/v2-to-v3.md +++ b/docs/content/migration/v2-to-v3.md @@ -8,729 +8,70 @@ description: "Migrate from Traefik Proxy v2 to v3 and update all the necessary c How to Migrate from Traefik v2 to Traefik v3. {: .subtitle } -The version 3 of Traefik introduces a number of breaking changes, -which require one to update their configuration when they migrate from v2 to v3. -The goal of this page is to recapitulate all of these changes, -and in particular to give examples, feature by feature, -of how the configuration looked like in v2, -and how it now looks like in v3. +With Traefik v3, we are introducing a streamlined transition process from v2. Minimal breaking changes have been made to specific options in the [static configuration](./v2-to-v3-details.md#static-configuration-changes "Link to static configuration changes"), and we are ensuring backward compatibility with v2 syntax in the [dynamic configuration](./v2-to-v3-details.md#dynamic-configuration-changes "Link to dynamic configuration changes"). This will offer a gradual path for adopting the v3 syntax, allowing users to progressively migrate their Kubernetes ingress resources, Docker labels, etc., to the new format. -## Static configuration +Here are the steps to progressively migrate from Traefik v2 to v3: -### Docker & Docker Swarm +1. [Prepare configurations and test v3](#step-1-prepare-configurations-and-test-v3) +1. [Migrate production instances to Traefik v3](#step-2-migrate-production-instances-to-traefik-v3) +1. [Progressively migrate dynamic configuration](#step-3-progressively-migrate-dynamic-configuration) -#### SwarmMode +## Step 1: Prepare Configurations and Test v3 -In v3, the provider Docker has been split into 2 providers: +Check the changes in [static configurations](./v2-to-v3-details.md#static-configuration-changes "Link to static configuration changes") and [operations](./v2-to-v3-details.md#operations-changes "Link to operations changes") brought by Traefik v3. +Modify your configurations accordingly. -- Docker provider (without Swarm support) -- Swarm provider (Swarm support only) +Then, add the following snippet to the static configuration: -??? example "An example usage of v2 Docker provider with Swarm" - - ```yaml tab="File (YAML)" - providers: - docker: - swarmMode: true - ``` - - ```toml tab="File (TOML)" - [providers.docker] - swarmMode=true - ``` - - ```bash tab="CLI" - --providers.docker.swarmMode=true - ``` - -This configuration is now unsupported and would prevent Traefik to start. - -##### Remediation - -In v3, the `swarmMode` should not be used with the Docker provider, and, to use Swarm, the Swarm provider should be used instead. - -??? example "An example usage of the Swarm provider" - - ```yaml tab="File (YAML)" - providers: - swarm: - endpoint: "tcp://127.0.0.1:2377" - ``` - - ```toml tab="File (TOML)" - [providers.swarm] - endpoint="tcp://127.0.0.1:2377" - ``` - - ```bash tab="CLI" - --providers.swarm.endpoint=tcp://127.0.0.1:2377 - ``` - -#### TLS.CAOptional - -Docker provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). - -??? example "An example usage of the TLS.CAOptional option" - - ```yaml tab="File (YAML)" - providers: - docker: - tls: - caOptional: true - ``` - - ```toml tab="File (TOML)" - [providers.docker.tls] - caOptional=true - ``` - - ```bash tab="CLI" - --providers.docker.tls.caOptional=true - ``` - -##### Remediation - -The `tls.caOptional` option should be removed from the Docker provider static configuration. - -### Kubernetes Gateway API - -#### Experimental Channel Resources (TLSRoute and TCPRoute) - -In v3, the Kubernetes Gateway API provider does not enable support for the experimental channel API resources by default. - -##### Remediation - -The `experimentalChannel` option should be used to enable the support for the experimental channel API resources. - -??? example "An example usage of the Kubernetes Gateway API provider with experimental channel support enabled" - - ```yaml tab="File (YAML)" - providers: - kubernetesGateway: - experimentalChannel: true - ``` - - ```toml tab="File (TOML)" - [providers.kubernetesGateway] - experimentalChannel = true - # ... - ``` - - ```bash tab="CLI" - --providers.kubernetesgateway.experimentalchannel=true - ``` - -### Experimental Configuration - -#### HTTP3 - -In v3, HTTP/3 is no longer an experimental feature. -It can be enabled on entry points without the associated `experimental.http3` option, which is now removed. -It is now unsupported and would prevent Traefik to start. - -??? example "An example usage of v2 Experimental `http3` option" - - ```yaml tab="File (YAML)" - experimental: - http3: true - ``` - - ```toml tab="File (TOML)" - [experimental] - http3=true - ``` - - ```bash tab="CLI" - --experimental.http3=true - ``` - -##### Remediation - -The `http3` option should be removed from the static configuration experimental section. -To configure `http3`, please checkout the [entrypoint configuration documentation](https://doc.traefik.io/traefik/v3.0/routing/entrypoints/#http3_1). - -### Consul provider - -#### namespace - -The Consul provider `namespace` option was deprecated in v2 and is now removed in v3. -It is now unsupported and would prevent Traefik to start. - -??? example "An example usage of v2 Consul `namespace` option" - - ```yaml tab="File (YAML)" - consul: - namespace: foobar - ``` - - ```toml tab="File (TOML)" - [consul] - namespace=foobar - ``` - - ```bash tab="CLI" - --consul.namespace=foobar - ``` - -##### Remediation - -In v3, the `namespaces` option should be used instead of the `namespace` option. - -??? example "An example usage of Consul `namespaces` option" - - ```yaml tab="File (YAML)" - consul: - namespaces: - - foobar - ``` - - ```toml tab="File (TOML)" - [consul] - namespaces=["foobar"] - ``` - - ```bash tab="CLI" - --consul.namespaces=foobar - ``` - -#### TLS.CAOptional - -Consul provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). - -??? example "An example usage of the TLS.CAOptional option" - - ```yaml tab="File (YAML)" - providers: - consul: - tls: - caOptional: true - ``` - - ```toml tab="File (TOML)" - [providers.consul.tls] - caOptional=true - ``` - - ```bash tab="CLI" - --providers.consul.tls.caOptional=true - ``` - -##### Remediation - -The `tls.caOptional` option should be removed from the Consul provider static configuration. - -### ConsulCatalog provider - -#### namespace - -The ConsulCatalog provider `namespace` option was deprecated in v2 and is now removed in v3. -It is now unsupported and would prevent Traefik to start. - -??? example "An example usage of v2 ConsulCatalog `namespace` option" - - ```yaml tab="File (YAML)" - consulCatalog: - namespace: foobar - ``` - - ```toml tab="File (TOML)" - [consulCatalog] - namespace=foobar - ``` - - ```bash tab="CLI" - --consulCatalog.namespace=foobar - ``` - -##### Remediation - -In v3, the `namespaces` option should be used instead of the `namespace` option. - -??? example "An example usage of ConsulCatalog `namespaces` option" - - ```yaml tab="File (YAML)" - consulCatalog: - namespaces: - - foobar - ``` - - ```toml tab="File (TOML)" - [consulCatalog] - namespaces=["foobar"] - ``` - - ```bash tab="CLI" - --consulCatalog.namespaces=foobar - ``` - -#### Endpoint.TLS.CAOptional - -ConsulCatalog provider `endpoint.tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). - -??? example "An example usage of the Endpoint.TLS.CAOptional option" - - ```yaml tab="File (YAML)" - providers: - consulCatalog: - endpoint: - tls: - caOptional: true - ``` - - ```toml tab="File (TOML)" - [providers.consulCatalog.endpoint.tls] - caOptional=true - ``` - - ```bash tab="CLI" - --providers.consulCatalog.endpoint.tls.caOptional=true - ``` - -##### Remediation - -The `endpoint.tls.caOptional` option should be removed from the ConsulCatalog provider static configuration. - -### Nomad provider - -#### namespace - -The Nomad provider `namespace` option was deprecated in v2 and is now removed in v3. -It is now unsupported and would prevent Traefik to start. - -??? example "An example usage of v2 Nomad `namespace` option" - - ```yaml tab="File (YAML)" - nomad: - namespace: foobar - ``` - - ```toml tab="File (TOML)" - [nomad] - namespace=foobar - ``` - - ```bash tab="CLI" - --nomad.namespace=foobar - ``` - -##### Remediation - -In v3, the `namespaces` option should be used instead of the `namespace` option. - -??? example "An example usage of Nomad `namespaces` option" - - ```yaml tab="File (YAML)" - nomad: - namespaces: - - foobar - ``` - - ```toml tab="File (TOML)" - [nomad] - namespaces=["foobar"] - ``` - - ```bash tab="CLI" - --nomad.namespaces=foobar - ``` - -#### Endpoint.TLS.CAOptional - -Nomad provider `endpoint.tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). - -??? example "An example usage of the Endpoint.TLS.CAOptional option" - - ```yaml tab="File (YAML)" - providers: - nomad: - endpoint: - tls: - caOptional: true - ``` - - ```toml tab="File (TOML)" - [providers.nomad.endpoint.tls] - caOptional=true - ``` - - ```bash tab="CLI" - --providers.nomad.endpoint.tls.caOptional=true - ``` - -##### Remediation - -The `endpoint.tls.caOptional` option should be removed from the Nomad provider static configuration. - -### Rancher v1 Provider - -In v3, the Rancher v1 provider has been removed because Rancher v1 is [no longer actively maintained](https://rancher.com/docs/os/v1.x/en/support/), -and Rancher v2 is supported as a standard Kubernetes provider. - -??? example "An example of Traefik v2 Rancher v1 configuration" - - ```yaml tab="File (YAML)" - providers: - rancher: {} - ``` - - ```toml tab="File (TOML)" - [providers.rancher] - ``` - - ```bash tab="CLI" - --providers.rancher=true - ``` - -This configuration is now unsupported and would prevent Traefik to start. - -#### Remediation - -Rancher 2.x requires Kubernetes and does not have a metadata endpoint of its own for Traefik to query. -As such, Rancher 2.x users should utilize the [Kubernetes CRD provider](../providers/kubernetes-crd.md) directly. - -Also, all Rancher provider related configuration should be removed from the static configuration. - -### Marathon provider - -Marathon maintenance [ended on October 31, 2021](https://github.com/mesosphere/marathon/blob/master/README.md). -In v3, the Marathon provider has been removed. - -??? example "An example of v2 Marathon provider configuration" - - ```yaml tab="File (YAML)" - providers: - marathon: {} - ``` - - ```toml tab="File (TOML)" - [providers.marathon] - ``` - - ```bash tab="CLI" - --providers.marathon=true - ``` - -This configuration is now unsupported and would prevent Traefik to start. - -#### Remediation - -All Marathon provider related configuration should be removed from the static configuration. - -### HTTP Provider - -#### TLS.CAOptional - -HTTP provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). - -??? example "An example usage of the TLS.CAOptional option" - - ```yaml tab="File (YAML)" - providers: - http: - tls: - caOptional: true - ``` - - ```toml tab="File (TOML)" - [providers.http.tls] - caOptional=true - ``` - - ```bash tab="CLI" - --providers.http.tls.caOptional=true - ``` - -##### Remediation - -The `tls.caOptional` option should be removed from the HTTP provider static configuration. - -### ETCD Provider - -#### TLS.CAOptional - -ETCD provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). - -??? example "An example usage of the TLS.CAOptional option" - - ```yaml tab="File (YAML)" - providers: - etcd: - tls: - caOptional: true - ``` - - ```toml tab="File (TOML)" - [providers.etcd.tls] - caOptional=true - ``` - - ```bash tab="CLI" - --providers.etcd.tls.caOptional=true - ``` - -##### Remediation - -The `tls.caOptional` option should be removed from the ETCD provider static configuration. - -### Redis Provider - -#### TLS.CAOptional - -Redis provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). - -??? example "An example usage of the TLS.CAOptional option" - - ```yaml tab="File (YAML)" - providers: - redis: - tls: - caOptional: true - ``` - - ```toml tab="File (TOML)" - [providers.redis.tls] - caOptional=true - ``` - - ```bash tab="CLI" - --providers.redis.tls.caOptional=true - ``` - -##### Remediation - -The `tls.caOptional` option should be removed from the Redis provider static configuration. - -### InfluxDB v1 - -InfluxDB v1.x maintenance [ended in 2021](https://www.influxdata.com/blog/influxdb-oss-and-enterprise-roadmap-update-from-influxdays-emea/). -In v3, the InfluxDB v1 metrics provider has been removed. - -??? example "An example of Traefik v2 InfluxDB v1 metrics configuration" - - ```yaml tab="File (YAML)" - metrics: - influxDB: {} - ``` - - ```toml tab="File (TOML)" - [metrics.influxDB] - ``` - - ```bash tab="CLI" - --metrics.influxDB=true - ``` - -This configuration is now unsupported and would prevent Traefik to start. - -#### Remediation - -All InfluxDB v1 metrics provider related configuration should be removed from the static configuration. - -### Pilot - -Traefik Pilot is no longer available since October 4th, 2022. - -??? example "An example of v2 Pilot configuration" - - ```yaml tab="File (YAML)" - pilot: - token: foobar - ``` - - ```toml tab="File (TOML)" - [pilot] - token=foobar - ``` - - ```bash tab="CLI" - --pilot.token=foobar - ``` - -In v2, Pilot configuration was deprecated and ineffective, -it is now unsupported and would prevent Traefik to start. - -#### Remediation - -All Pilot related configuration should be removed from the static configuration. - -## Dynamic configuration - -### Router Rule Matchers - -In v3, a new rule matchers syntax has been introduced for HTTP and TCP routers. -The default rule matchers syntax is now the v3 one, but for backward compatibility this can be configured. -The v2 rule matchers syntax is deprecated and its support will be removed in the next major version. -For this reason, we encourage migrating to the new syntax. - -By default, the `defaultRuleSyntax` static option is automatically set to `v3`, meaning that the default rule is the new one. - -#### New V3 Syntax Notable Changes - -The `Headers` and `HeadersRegexp` matchers have been renamed to `Header` and `HeaderRegexp` respectively. - -`PathPrefix` no longer uses regular expressions to match path prefixes. - -`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/). - -All matchers now take a single value (except `Header`, `HeaderRegexp`, `Query`, and `QueryRegexp` which take two) -and should be explicitly combined using logical operators to mimic previous behavior. - -`Query` can take a single value to match is the query value that has no value (e.g. `/search?mobile`). - -`HostHeader` has been removed, use `Host` instead. - -#### Remediation - -##### Configure the Default Syntax In Static Configuration - -The default rule matchers syntax is the expected syntax for any router that is not self opt-out from this default value. -It can be configured in the static configuration. - -??? example "An example configuration for the default rule matchers syntax" - - ```yaml tab="File (YAML)" - # static configuration - core: - defaultRuleSyntax: v2 - ``` - - ```toml tab="File (TOML)" - # static configuration - [core] - defaultRuleSyntax="v2" - ``` - - ```bash tab="CLI" - # static configuration - --core.defaultRuleSyntax=v2 - ``` - -##### Configure the Syntax Per Router - -The rule syntax can also be configured on a per-router basis. -This allows to have heterogeneous router configurations and ease migration. - -??? example "An example router with syntax configuration" - -```yaml tab="Docker & Swarm" -labels: - - "traefik.http.routers.test.ruleSyntax=v2" +```yaml +# static configuration +core: + defaultRuleSyntax: v2 ``` -```yaml tab="Kubernetes" -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: test.route - namespace: default +This snippet in the static configuration makes the [v2 format](https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/?ref=traefik.io#configure-the-default-syntax-in-static-configuration "Link to configure default syntax in static config") the default rule matchers syntax. -spec: - routes: - - match: PathPrefix(`/foo`, `/bar`) - syntax: v2 - kind: Rule +Start Traefik v3 with this new configuration to test it. + +If you don’t get any error logs while testing, you are good to go! +Otherwise, follow the remaining migration options highlighted in the logs. + +Once your Traefik test instances are starting and routing to your applications, proceed to the next step. + +## Step 2: Migrate Production Instances to Traefik v3 + +We strongly advise you to follow a progressive migration strategy ([Kubernetes rolling update mechanism](https://kubernetes.io/docs/tutorials/kubernetes-basics/update/update-intro/ "Link to the Kubernetes rolling update documentation"), for example) to migrate your production instances to v3. + +!!! Warning + Ensure you have a [real-time monitoring solution](https://traefik.io/blog/capture-traefik-metrics-for-apps-on-kubernetes-with-prometheus/ "Link to the blog on capturing Traefik metrics with Prometheus") for your ingress traffic to detect issues instantly. + +During the progressive migration, monitor your ingress traffic for any errors. Be prepared to rollback to a working state in case of any issues. + +If you encounter any issues, leverage debug and access logs provided by Traefik to understand what went wrong and how to fix it. + +Once every Traefik instance is updated, you will be on Traefik v3! + +## Step 3: Progressively Migrate Dynamic Configuration + +!!! info + This step can be done later in the process, as Traefik v3 is compatible with the v2 format for [dynamic configuration](./v2-to-v3-details.md#dynamic-configuration-changes "Link to dynamic configuration changes"). + Enable Traefik logs to get some help if any deprecated option is in use. + +Check the changes in [dynamic configuration](./v2-to-v3-details.md#dynamic-configuration-changes "Link to dynamic configuration changes"). + +Then, progressively [switch each router to the v3 syntax](./v2-to-v3-details.md#configure-the-syntax-per-router "Link to configuring the syntax per router"). + +Test and update each Ingress resource and ensure that ingress traffic is not impacted. + +Once a v3 Ingress resource migration is validated, deploy the resource and delete the v2 Ingress resource. +Repeat it until all Ingress resources are migrated. + +Now, remove the following snippet added to the static configuration in Step 1: + +```yaml +# static configuration +core: + defaultRuleSyntax: v2 ``` -```yaml tab="Consul Catalog" -- "traefik.http.routers.test.ruleSyntax=v2" -``` - -```yaml tab="File (YAML)" -http: - routers: - test: - ruleSyntax: v2 -``` - -```toml tab="File (TOML)" -[http.routers] - [http.routers.test] - ruleSyntax = "v2" -``` - -### IPWhiteList - -In v3, we renamed the `IPWhiteList` middleware to `IPAllowList` without changing anything to the configuration. - -### Deprecated Options Removal - -- The `tracing.datadog.globaltag` option has been removed. -- The `tls.caOptional` option has been removed from the ForwardAuth middleware, as well as from the HTTP, Consul, Etcd, Redis, ZooKeeper, Consul Catalog, and Docker providers. -- `sslRedirect`, `sslTemporaryRedirect`, `sslHost`, `sslForceHost` and `featurePolicy` options of the Headers middleware have been removed. -- The `forceSlash` option of the StripPrefix middleware has been removed. -- The `preferServerCipherSuites` option has been removed. - -### TCP LoadBalancer `terminationDelay` option - -The TCP LoadBalancer `terminationDelay` option has been removed. -This option can now be configured directly on the `TCPServersTransport` level, please take a look at this [documentation](../routing/services/index.md#terminationdelay) - -### Kubernetes CRDs API Group `traefik.containo.us` - -In v3, the Kubernetes CRDs API Group `traefik.containo.us` has been removed. -Please use the API Group `traefik.io` instead. - -### Kubernetes Ingress API Group `networking.k8s.io/v1beta1` - -In v3, the Kubernetes Ingress API Group `networking.k8s.io/v1beta1` ([removed since Kubernetes v1.22](https://kubernetes.io/docs/reference/using-api/deprecation-guide/#ingress-v122)) support has been removed. - -Please use the API Group `networking.k8s.io/v1` instead. - -### Traefik CRD API Version `apiextensions.k8s.io/v1beta1` - -In v3, the Traefik CRD API Version `apiextensions.k8s.io/v1beta1` ([removed since Kubernetes v1.22](https://kubernetes.io/docs/reference/using-api/deprecation-guide/#customresourcedefinition-v122)) support has been removed. - -Please use the CRD definition with the API Version `apiextensions.k8s.io/v1` instead. - -## Operations - -### Traefik RBAC Update - -In v3, the support of `TCPServersTransport` has been introduced. -When using the KubernetesCRD provider, it is therefore necessary to update [RBAC](../reference/dynamic-configuration/kubernetes-crd.md#rbac) and [CRD](../reference/dynamic-configuration/kubernetes-crd.md) manifests. - -### Content-Type Auto-Detection - -In v3, the `Content-Type` header is not auto-detected anymore when it is not set by the backend. -One should use the `ContentType` middleware to enable the `Content-Type` header value auto-detection. - -### Observability - -#### gRPC Metrics - -In v3, the reported status code for gRPC requests is now the value of the `Grpc-Status` header. - -#### Tracing - -In v3, the tracing feature has been revamped and is now powered exclusively by [OpenTelemetry](https://opentelemetry.io/ "Link to website of OTel") (OTel). -!!! warning "Important" - - Traefik v3 **no** longer supports direct output formats for specific vendors such as Instana, Jaeger, Zipkin, Haystack, Datadog, and Elastic. -Instead, it focuses on pure OpenTelemetry implementation, providing a unified and standardized approach for observability. - -Here are two possible transition strategies: - -1. OTLP Ingestion Endpoints: - - Most vendors now offer OpenTelemetry Protocol (OTLP) ingestion endpoints. - You can seamlessly integrate Traefik v3 with these endpoints to continue leveraging tracing capabilities. - -2. Legacy Stack Compatibility: - - For legacy stacks that cannot immediately upgrade to the latest vendor agents supporting OTLP ingestion, - using OpenTelemetry (OTel) collectors with appropriate exporters configuration is a viable solution. - This allows continued compatibility with the existing infrastructure. - -Please check the [OpenTelemetry Tracing provider documention](../observability/tracing/opentelemetry.md) for more information. - -#### Internal Resources Observability - -In v3, observability for internal routers or services (e.g.: `ping@internal`) is disabled by default. -To enable it one should use the new `addInternals` option for AccessLogs, Metrics or Tracing. -Please take a look at the observability documentation for more information: - -- [AccessLogs](../observability/access-logs.md#addinternals) -- [Metrics](../observability/metrics/overview.md#addinternals) -- [Tracing](../observability/tracing/overview.md#addinternals) +You are now fully migrated to Traefik v3 🎉 diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 5c21cab3d..a3e65baa2 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -18,6 +18,7 @@ - "traefik.http.middlewares.middleware05.circuitbreaker.recoveryduration=42s" - "traefik.http.middlewares.middleware05.circuitbreaker.responsecode=42" - "traefik.http.middlewares.middleware06.compress=true" +- "traefik.http.middlewares.middleware06.compress.defaultencoding=foobar" - "traefik.http.middlewares.middleware06.compress.excludedcontenttypes=foobar, foobar" - "traefik.http.middlewares.middleware06.compress.includedcontenttypes=foobar, foobar" - "traefik.http.middlewares.middleware06.compress.minresponsebodybytes=42" @@ -54,6 +55,7 @@ - "traefik.http.middlewares.middleware12.headers.allowedhosts=foobar, foobar" - "traefik.http.middlewares.middleware12.headers.browserxssfilter=true" - "traefik.http.middlewares.middleware12.headers.contentsecuritypolicy=foobar" +- "traefik.http.middlewares.middleware12.headers.contentsecuritypolicyreportonly=foobar" - "traefik.http.middlewares.middleware12.headers.contenttypenosniff=true" - "traefik.http.middlewares.middleware12.headers.custombrowserxssvalue=foobar" - "traefik.http.middlewares.middleware12.headers.customframeoptionsvalue=foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index d3cf0eaf2..cd0ffde60 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -143,6 +143,7 @@ excludedContentTypes = ["foobar", "foobar"] includedContentTypes = ["foobar", "foobar"] minResponseBodyBytes = 42 + defaultEncoding = "foobar" [http.middlewares.Middleware07] [http.middlewares.Middleware07.contentType] autoDetect = true @@ -197,6 +198,7 @@ browserXssFilter = true customBrowserXSSValue = "foobar" contentSecurityPolicy = "foobar" + contentSecurityPolicyReportOnly = "foobar" publicKey = "foobar" referrerPolicy = "foobar" permissionsPolicy = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index fdba1c332..d0de416ee 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -152,6 +152,7 @@ http: - foobar - foobar minResponseBodyBytes: 42 + defaultEncoding: foobar Middleware07: contentType: autoDetect: true @@ -241,6 +242,7 @@ http: browserXssFilter: true customBrowserXSSValue: foobar contentSecurityPolicy: foobar + contentSecurityPolicyReportOnly: foobar publicKey: foobar referrerPolicy: foobar permissionsPolicy: foobar 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 93f14adcf..761a8f928 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -98,6 +98,67 @@ spec: description: Service defines an upstream HTTP service to proxy traffic to. properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be + sent to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for + the health check endpoint. + type: string + port: + description: Port defines the server URL port for + the health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme + for the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: @@ -846,6 +907,11 @@ spec: This middleware compresses responses before sending them to the client, using gzip compression. More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/compress/ properties: + defaultEncoding: + description: DefaultEncoding specifies the default encoding if + the `Accept-Encoding` header is not in the request or contains + a wildcard (`*`). + type: string excludedContentTypes: description: |- ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing. @@ -919,6 +985,67 @@ spec: Service defines the reference to a Kubernetes Service that will serve the error page. More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/errorpages/#service properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname in + the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status code + of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: @@ -1182,6 +1309,10 @@ spec: description: ContentSecurityPolicy defines the Content-Security-Policy header value. type: string + contentSecurityPolicyReportOnly: + description: ContentSecurityPolicyReportOnly defines the Content-Security-Policy-Report-Only + header value. + type: string contentTypeNosniff: description: ContentTypeNosniff defines whether to add the X-Content-Type-Options header with the nosniff value. @@ -2295,6 +2426,67 @@ spec: mirroring: description: Mirroring defines the Mirroring service configuration. properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent to + the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname in the + Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the health + check endpoint. + type: string + port: + description: Port defines the server URL port for the health + check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for the + health check endpoint. + type: string + status: + description: Status defines the expected HTTP status code + of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: @@ -2314,6 +2506,67 @@ spec: items: description: MirrorService holds the mirror configuration. properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: @@ -2548,6 +2801,67 @@ spec: description: Service defines an upstream HTTP service to proxy traffic to. properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 254801508..10b9a8d00 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -21,6 +21,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/middlewares/Middleware05/circuitBreaker/fallbackDuration` | `42s` | | `traefik/http/middlewares/Middleware05/circuitBreaker/recoveryDuration` | `42s` | | `traefik/http/middlewares/Middleware05/circuitBreaker/responseCode` | `42` | +| `traefik/http/middlewares/Middleware06/compress/defaultEncoding` | `foobar` | | `traefik/http/middlewares/Middleware06/compress/excludedContentTypes/0` | `foobar` | | `traefik/http/middlewares/Middleware06/compress/excludedContentTypes/1` | `foobar` | | `traefik/http/middlewares/Middleware06/compress/includedContentTypes/0` | `foobar` | @@ -70,6 +71,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/middlewares/Middleware12/headers/allowedHosts/1` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/browserXssFilter` | `true` | | `traefik/http/middlewares/Middleware12/headers/contentSecurityPolicy` | `foobar` | +| `traefik/http/middlewares/Middleware12/headers/contentSecurityPolicyReportOnly` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/contentTypeNosniff` | `true` | | `traefik/http/middlewares/Middleware12/headers/customBrowserXSSValue` | `foobar` | | `traefik/http/middlewares/Middleware12/headers/customFrameOptionsValue` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml index 71d437491..7b23dba43 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml @@ -98,6 +98,67 @@ spec: description: Service defines an upstream HTTP service to proxy traffic to. properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be + sent to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for + the health check endpoint. + type: string + port: + description: Port defines the server URL port for + the health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme + for the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index 0a1891ea8..0d005e64d 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -183,6 +183,11 @@ spec: This middleware compresses responses before sending them to the client, using gzip compression. More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/compress/ properties: + defaultEncoding: + description: DefaultEncoding specifies the default encoding if + the `Accept-Encoding` header is not in the request or contains + a wildcard (`*`). + type: string excludedContentTypes: description: |- ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing. @@ -256,6 +261,67 @@ spec: Service defines the reference to a Kubernetes Service that will serve the error page. More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/errorpages/#service properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname in + the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status code + of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: @@ -519,6 +585,10 @@ spec: description: ContentSecurityPolicy defines the Content-Security-Policy header value. type: string + contentSecurityPolicyReportOnly: + description: ContentSecurityPolicyReportOnly defines the Content-Security-Policy-Report-Only + header value. + type: string contentTypeNosniff: description: ContentTypeNosniff defines whether to add the X-Content-Type-Options header with the nosniff value. diff --git a/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml b/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml index ee99b7b19..7a0f7daf3 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml @@ -47,6 +47,67 @@ spec: mirroring: description: Mirroring defines the Mirroring service configuration. properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent to + the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname in the + Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the health + check endpoint. + type: string + port: + description: Port defines the server URL port for the health + check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for the + health check endpoint. + type: string + status: + description: Status defines the expected HTTP status code + of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: @@ -66,6 +127,67 @@ spec: items: description: MirrorService holds the mirror configuration. properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: @@ -300,6 +422,67 @@ spec: description: Service defines an upstream HTTP service to proxy traffic to. properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index 2d325d204..950053a14 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -342,6 +342,9 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne flushInterval: 1ms scheme: https serversTransport: transport # [10] + healthCheck: # [11] + path: /health + interval: 15s sticky: cookie: httpOnly: true @@ -351,17 +354,17 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne maxAge: 42 strategy: RoundRobin weight: 10 - nativeLB: true # [11] - nodePortLB: true # [12] - tls: # [13] - secretName: supersecret # [14] - options: # [15] - name: opt # [16] - namespace: default # [17] - certResolver: foo # [18] - domains: # [19] - - main: example.net # [20] - sans: # [21] + nativeLB: true # [12] + nodePortLB: true # [13] + tls: # [14] + secretName: supersecret # [15] + options: # [16] + name: opt # [17] + namespace: default # [18] + certResolver: foo # [19] + domains: # [20] + - main: example.net # [21] + sans: # [22] - a.example.net - b.example.net ``` @@ -378,17 +381,18 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne | [8] | `routes[n].services` | List of any combination of [TraefikService](#kind-traefikservice) and reference to a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) (See below for `ExternalName Service` setup) | | [9] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. | | [10] | `services[n].serversTransport` | Defines the reference to a [ServersTransport](#kind-serverstransport). The ServersTransport namespace is assumed to be the [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) namespace (see [ServersTransport reference](#serverstransport-reference)). | -| [11] | `services[n].nativeLB` | Controls, when creating the load-balancer, whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. | -| [12] | `services[n].nodePortLB` | Controls, when creating the load-balancer, whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. | -| [13] | `tls` | Defines [TLS](../routers/index.md#tls) certificate configuration | -| [14] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) | -| [15] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) | -| [16] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name | -| [17] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace | -| [18] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver) | -| [19] | `tls.domains` | List of [domains](../routers/index.md#domains) | -| [20] | `domains[n].main` | Defines the main domain name | -| [21] | `domains[n].sans` | List of SANs (alternative domains) | +| [11] | `services[n].healthCheck` | Defines the HealthCheck when service references a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName. | +| [12] | `services[n].nativeLB` | Controls, when creating the load-balancer, whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. | +| [13] | `services[n].nodePortLB` | Controls, when creating the load-balancer, whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. | +| [14] | `tls` | Defines [TLS](../routers/index.md#tls) certificate configuration | +| [15] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) | +| [16] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) | +| [17] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name | +| [18] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace | +| [19] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver) | +| [20] | `tls.domains` | List of [domains](../routers/index.md#domains) | +| [21] | `domains[n].main` | Defines the main domain name | +| [22] | `domains[n].sans` | List of SANs (alternative domains) | ??? example "Declaring an IngressRoute" diff --git a/docs/docs.Dockerfile b/docs/docs.Dockerfile index ee10a5302..2ed4d0528 100644 --- a/docs/docs.Dockerfile +++ b/docs/docs.Dockerfile @@ -1,10 +1,12 @@ -FROM alpine:3.14 +FROM alpine:3.20 -ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/bin +ENV PATH="${PATH}:/venv/bin" COPY requirements.txt /mkdocs/ WORKDIR /mkdocs VOLUME /mkdocs RUN apk --no-cache --no-progress add py3-pip gcc musl-dev python3-dev \ - && pip3 install --user -r requirements.txt + && python3 -m venv /venv \ + && source /venv/bin/activate \ + && pip3 install -r requirements.txt diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index bbb3751ae..5cac2369f 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -172,7 +172,9 @@ nav: - 'HTTP Challenge': 'user-guides/docker-compose/acme-http/index.md' - 'DNS Challenge': 'user-guides/docker-compose/acme-dns/index.md' - 'Migration': - - 'Traefik v2 to v3': 'migration/v2-to-v3.md' + - 'Traefik v2 to v3': + - 'Migration guide': 'migration/v2-to-v3.md' + - 'Configuration changes for v3': 'migration/v2-to-v3-details.md' - 'Traefik v2 minor migrations': 'migration/v2.md' - 'Traefik v1 to v2': 'migration/v1-to-v2.md' - 'Contributing': diff --git a/docs/requirements.txt b/docs/requirements.txt index 8274f4d68..68126a411 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,45 +1,23 @@ -mkdocs==1.2.2 +markdown-include==0.5.1 +mkdocs==1.2.4 +mkdocs-exclude==1.0.2 mkdocs-traefiklabs>=100.0.7 -appdirs==1.4.4 -CacheControl==0.12.6 -certifi==2020.12.5 -chardet==4.0.0 -click==8.0.4 -colorama==0.4.4 -contextlib2==0.6.0 -distlib==0.3.1 -distro==1.5.0 -ghp-import==2.0.2 -html5lib==1.1 -idna==3.2 -importlib-metadata==4.11.3 -Jinja2==3.0.0 -lockfile==0.12.2 +click==8.1.7 +colorama==0.4.6 +ghp-import==2.1.0 +importlib_metadata==7.1.0 +Jinja2==3.1.3 Markdown==3.3.6 -markdown-include==0.5.1 -MarkupSafe==2.1.1 +MarkupSafe==2.1.5 mergedeep==1.3.4 -mkdocs-bootswatch==1.0 -mkdocs-exclude==1.0.2 -mkdocs-material-extensions==1.0.3 -msgpack==1.0.2 -ordered-set==4.0.2 -packaging==20.9 -pep517==0.10.0 -progress==1.5 -Pygments==2.11.2 +mkdocs-material-extensions==1.3.1 +packaging==24.0 +Pygments==2.18.0 pymdown-extensions==7.0 -pyparsing==2.4.7 -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 PyYAML==6.0.1 -pyyaml-env-tag==0.1 -requests==2.25.1 -retrying==1.3.3 -six==1.15.0 -toml==0.10.2 -urllib3==1.26.5 -watchdog==2.1.7 -webencodings==0.5.1 -zipp==3.7.0 - +pyyaml_env_tag==0.1 +six==1.16.0 +watchdog==4.0.0 +zipp==3.18.1 diff --git a/docs/runtime.txt b/docs/runtime.txt deleted file mode 100644 index 475ba515c..000000000 --- a/docs/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -3.7 diff --git a/go.mod b/go.mod index 6e12e68e9..09301931d 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module github.com/traefik/traefik/v3 go 1.22 require ( - github.com/BurntSushi/toml v1.3.2 + github.com/BurntSushi/toml v1.4.0 github.com/Masterminds/sprig/v3 v3.2.3 github.com/abbot/go-http-auth v0.0.0-00010101000000-000000000000 github.com/andybalholm/brotli v1.0.6 github.com/aws/aws-sdk-go v1.44.327 - github.com/cenkalti/backoff/v4 v4.2.1 + github.com/cenkalti/backoff/v4 v4.3.0 github.com/containous/alice v0.0.0-20181107144136-d83ebdd94cbd github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc @@ -17,16 +17,16 @@ require ( github.com/docker/go-connections v0.5.0 github.com/fatih/structs v1.1.0 github.com/fsnotify/fsnotify v1.7.0 - github.com/go-acme/lego/v4 v4.16.1 + github.com/go-acme/lego/v4 v4.17.4 github.com/go-kit/kit v0.10.1-0.20200915143503-439c4d2ed3ea - github.com/golang/protobuf v1.5.3 + github.com/golang/protobuf v1.5.4 github.com/google/go-github/v28 v28.1.1 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/hashicorp/consul/api v1.26.1 - github.com/hashicorp/go-hclog v1.5.0 + github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-multierror v1.1.1 - github.com/hashicorp/go-retryablehttp v0.7.5 + github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/nomad/api v0.0.0-20240122103822-8a4bd61caf74 github.com/http-wasm/http-wasm-host-go v0.6.0 @@ -39,7 +39,7 @@ require ( github.com/kvtools/valkeyrie v1.0.0 github.com/kvtools/zookeeper v1.0.2 github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f - github.com/miekg/dns v1.1.58 + github.com/miekg/dns v1.1.59 github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/hashstructure v1.0.0 github.com/mitchellh/mapstructure v1.5.0 @@ -68,25 +68,25 @@ require ( github.com/vulcand/oxy/v2 v2.0.0-20230427132221-be5cf38f3c1c github.com/vulcand/predicate v1.2.0 go.opentelemetry.io/collector/pdata v1.2.0 - go.opentelemetry.io/contrib/propagators/autoprop v0.49.0 - go.opentelemetry.io/otel v1.24.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.24.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.24.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 - go.opentelemetry.io/otel/metric v1.24.0 - go.opentelemetry.io/otel/sdk v1.24.0 - go.opentelemetry.io/otel/sdk/metric v1.24.0 - go.opentelemetry.io/otel/trace v1.24.0 - golang.org/x/exp v0.0.0-20231006140011-7918f672742d - golang.org/x/mod v0.17.0 - golang.org/x/net v0.24.0 - golang.org/x/sys v0.19.0 - golang.org/x/text v0.14.0 + go.opentelemetry.io/contrib/propagators/autoprop v0.52.0 + go.opentelemetry.io/otel v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 + go.opentelemetry.io/otel/metric v1.27.0 + go.opentelemetry.io/otel/sdk v1.27.0 + go.opentelemetry.io/otel/sdk/metric v1.27.0 + go.opentelemetry.io/otel/trace v1.27.0 + golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 + golang.org/x/mod v0.18.0 + golang.org/x/net v0.26.0 + golang.org/x/sys v0.21.0 + golang.org/x/text v0.16.0 golang.org/x/time v0.5.0 - golang.org/x/tools v0.20.0 - google.golang.org/grpc v1.61.1 + golang.org/x/tools v0.22.0 + google.golang.org/grpc v1.64.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.29.2 k8s.io/apiextensions-apiserver v0.28.3 @@ -99,27 +99,27 @@ require ( ) require ( - cloud.google.com/go/compute v1.23.3 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect dario.cat/mergo v1.0.0 // indirect github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.29 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect - github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect - github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect @@ -128,30 +128,30 @@ require ( github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect github.com/VividCortex/gohistogram v1.0.0 // indirect github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect - github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect - github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect + github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 // indirect github.com/armon/go-metrics v0.4.1 // indirect - github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect - github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect - github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 // indirect - github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect - github.com/aws/smithy-go v1.19.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.27.2 // indirect + github.com/aws/aws-sdk-go-v2/config v1.27.18 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.18 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 // indirect + github.com/aws/aws-sdk-go-v2/service/lightsail v1.38.3 // indirect + github.com/aws/aws-sdk-go-v2/service/route53 v1.40.10 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 // indirect + github.com/aws/smithy-go v1.20.2 // indirect + github.com/benbjohnson/clock v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/bytedance/sonic v1.10.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/civo/civogo v0.3.11 // indirect - github.com/cloudflare/cloudflare-go v0.86.0 // indirect + github.com/cloudflare/cloudflare-go v0.97.0 // indirect github.com/containerd/containerd v1.7.12 // indirect github.com/containerd/log v0.1.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect @@ -163,18 +163,18 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/distribution/reference v0.5.0 // indirect - github.com/dnsimple/dnsimple-go v1.2.0 // indirect + github.com/dnsimple/dnsimple-go v1.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.7.0 // indirect github.com/exoscale/egoscale v0.102.3 // indirect - github.com/fatih/color v1.15.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/gin-gonic/gin v1.9.1 // indirect github.com/go-errors/errors v1.0.1 // indirect - github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -183,15 +183,16 @@ require ( github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect - github.com/go-playground/validator/v10 v10.15.1 // indirect github.com/go-resty/resty/v2 v2.11.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/go-zookeeper/zk v1.0.3 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/gofrs/flock v0.8.1 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -201,16 +202,17 @@ require ( github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.0 // indirect - github.com/gophercloud/gophercloud v1.0.0 // indirect - github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae // indirect + github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/gophercloud/gophercloud v1.12.0 // indirect + github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/hashicorp/cronexpr v1.1.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/serf v0.10.1 // indirect github.com/huandu/xstrings v1.4.0 // indirect @@ -256,8 +258,8 @@ require ( github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect github.com/nrdcg/auroradns v1.1.0 // indirect - github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect - github.com/nrdcg/desec v0.7.0 // indirect + github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect + github.com/nrdcg/desec v0.8.0 // indirect github.com/nrdcg/dnspod-go v0.4.0 // indirect github.com/nrdcg/freemyip v0.2.0 // indirect github.com/nrdcg/goinwx v0.10.0 // indirect @@ -270,10 +272,11 @@ require ( github.com/onsi/ginkgo/v2 v2.17.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect - github.com/ovh/go-ovh v1.4.3 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect + github.com/oracle/oci-go-sdk/v65 v65.63.1 // indirect + github.com/ovh/go-ovh v1.5.1 // indirect github.com/pelletier/go-toml/v2 v2.0.9 // indirect - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/pquerna/otp v1.4.0 // indirect @@ -282,22 +285,25 @@ require ( github.com/quic-go/qpack v0.4.0 // indirect github.com/redis/go-redis/v9 v9.2.1 // indirect github.com/rs/cors v1.7.0 // indirect - github.com/sacloud/api-client-go v0.2.8 // indirect - github.com/sacloud/go-http v0.1.6 // indirect - github.com/sacloud/iaas-api-go v1.11.1 // indirect - github.com/sacloud/packages-go v0.0.9 // indirect - github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 // indirect + github.com/sacloud/api-client-go v0.2.10 // indirect + github.com/sacloud/go-http v0.1.8 // indirect + github.com/sacloud/iaas-api-go v1.12.0 // indirect + github.com/sacloud/packages-go v0.0.10 // indirect + github.com/scaleway/scaleway-sdk-go v1.0.0-beta.27 // indirect + github.com/selectel/domains-go v1.1.0 // indirect + github.com/selectel/go-selvpcclient/v3 v3.1.1 // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/shopspring/decimal v1.2.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect - github.com/softlayer/softlayer-go v1.1.3 // indirect + github.com/softlayer/softlayer-go v1.1.5 // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect + github.com/sony/gobreaker v0.5.0 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect @@ -306,8 +312,8 @@ require ( github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect github.com/vinyldns/go-vinyldns v0.9.16 // indirect github.com/vultr/govultr/v2 v2.17.2 // indirect - github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect - github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect + github.com/yandex-cloud/go-genproto v0.0.0-20240318083951-4fe6125f286e // indirect + github.com/yandex-cloud/go-sdk v0.0.0-20240318084659-dfa50323a0b4 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/zeebo/errs v1.2.2 // indirect go.etcd.io/etcd/api/v3 v3.5.9 // indirect @@ -315,31 +321,30 @@ require ( go.etcd.io/etcd/client/v3 v3.5.9 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/contrib/propagators/aws v1.24.0 // indirect - go.opentelemetry.io/contrib/propagators/b3 v1.24.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.24.0 // indirect - go.opentelemetry.io/contrib/propagators/ot v1.24.0 // indirect - go.opentelemetry.io/proto/otlp v1.1.0 // indirect + go.opentelemetry.io/contrib/propagators/aws v1.27.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.27.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.27.0 // indirect + go.opentelemetry.io/contrib/propagators/ot v1.27.0 // indirect + go.opentelemetry.io/proto/otlp v1.2.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/ratelimit v0.2.0 // indirect + go.uber.org/ratelimit v0.3.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/arch v0.4.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/term v0.19.0 // indirect - google.golang.org/api v0.149.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect - google.golang.org/protobuf v1.33.0 // indirect + golang.org/x/term v0.21.0 // indirect + google.golang.org/api v0.172.0 // indirect + google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/h2non/gock.v1 v1.0.16 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/ns1/ns1-go.v2 v2.7.13 // indirect + gopkg.in/ns1/ns1-go.v2 v2.9.1 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.110.1 // indirect diff --git a/go.sum b/go.sum index 2e610a138..9e8011f42 100644 --- a/go.sum +++ b/go.sum @@ -6,10 +6,8 @@ cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxK cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -23,34 +21,36 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 h1:8kDqDngH+DmVBiCtIjCFTGa7MBnsIOkF9IccInFEbjk= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 h1:8iR6OLffWWorFdzL2JFCab5xpD8VKEE2DUBBl+HNTDY= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0/go.mod h1:copqlcjMWc/wgQ1N2fzsJFQxDdqKGg1EQt8T5wJMOGE= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2/go.mod h1:FbdwsQ2EzwvXxOPcMFYO8ogEc9uMMIj3YkmCdXdAFmk= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 h1:rR8ZW79lE/ppfXTfiYSnMFv5EzmVuY4pfZWIkscIJ64= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0/go.mod h1:y2zXtLSMM/X5Mfawq0lOftpWn3f4V6OCsRdINsvWBPI= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 h1:H+U3Gk9zY56G3u872L82bk4thcsy2Gghb9ExT4Zvm1o= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0/go.mod h1:mgrmMSgaLp9hmax62XQTd0N4aAqSE5E0DulSpVYK7vc= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 h1:9Eih8XcEeQnFD0ntMlUDleKMzfeCeUfa+VbnDCI4AZs= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0/go.mod h1:wGPyTi+aURdqPAGMZDQqnNs9IrShADF8w2WZb6bKeq0= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc= github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= @@ -62,11 +62,11 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= @@ -100,10 +100,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 h1:J45/QHgrzUdqe/Vco/Vxk0wRvdS2nKUxmf/zLgvfass= -github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU= -github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= -github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 h1:lM7JnA9dEdDFH9XOgRNQMDTQnOjlLkDTNA7c0aWTQ30= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.712/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -123,37 +121,39 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.44.327 h1:ZS8oO4+7MOBLhkdwIhgtVeDzCeWOlTfKJS7EgggbIEY= github.com/aws/aws-sdk-go v1.44.327/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= -github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= -github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o= -github.com/aws/aws-sdk-go-v2/config v1.26.6/go.mod h1:uKU6cnDmYCvJ+pxO9S4cWDb2yWWIH5hra+32hVh1MI4= -github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8= -github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 h1:LvWkxBi/bsWHqj3bFTUuDLl4OAlbaM1HDZ9YPhj5+jg= -github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0/go.mod h1:35MKNS46RX7Lb9EIFP2bPy3WrJu+bxU6QgLis8K1aa4= -github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 h1:f3hBZWtpn9clZGXJoqahQeec9ZPZnu22g8pg+zNyif0= -github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0/go.mod h1:8qqfpG4mug2JLlEyWPSFhEGvJiaZ9iPmMDDMYc5Xtas= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= -github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= -github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/aws/aws-sdk-go-v2 v1.27.2 h1:pLsTXqX93rimAOZG2FIYraDQstZaaGVVN4tNw65v0h8= +github.com/aws/aws-sdk-go-v2 v1.27.2/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2/config v1.27.18 h1:wFvAnwOKKe7QAyIxziwSKjmer9JBMH1vzIL6W+fYuKk= +github.com/aws/aws-sdk-go-v2/config v1.27.18/go.mod h1:0xz6cgdX55+kmppvPm2IaKzIXOheGJhAufacPJaXZ7c= +github.com/aws/aws-sdk-go-v2/credentials v1.17.18 h1:D/ALDWqK4JdY3OFgA2thcPO1c9aYTT5STS/CvnkqY1c= +github.com/aws/aws-sdk-go-v2/credentials v1.17.18/go.mod h1:JuitCWq+F5QGUrmMPsk945rop6bB57jdscu+Glozdnc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 h1:dDgptDO9dxeFkXy+tEgVkzSClHZje/6JkPW5aZyEvrQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5/go.mod h1:gjvE2KBUgUQhcv89jqxrIxH9GaKs1JbZzWejj/DaHGA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 h1:cy8ahBJuhtM8GTTSyOkfy6WVPV1IE+SS5/wfXUYuulw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9/go.mod h1:CZBXGLaJnEZI6EVNcPd7a6B5IC5cA/GkRWtu9fp3S6Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 h1:A4SYk07ef04+vxZToz9LWvAXl9LW0NClpPpMsi31cz0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9/go.mod h1:5jJcHuwDagxN+ErjQ3PU3ocf6Ylc/p9x+BLO/+X4iXw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 h1:o4T+fKxA3gTMcluBNZZXE9DNaMkJuUL1O3mffCUjoJo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11/go.mod h1:84oZdJ+VjuJKs9v1UTC9NaodRZRseOXCTgku+vQJWR8= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.38.3 h1:YdA5QgoYa2wNblkWyZfPlLLYsAEKCwLfdMxpWu16wpM= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.38.3/go.mod h1:T0LiPG5vKHZ7DmOq4Cmw0Kku3tMkaR9AknskS2hUXvI= +github.com/aws/aws-sdk-go-v2/service/route53 v1.40.10 h1:J9uHribwEgHmesH5r0enxsZYyiGBWd2AaExSW2SydqE= +github.com/aws/aws-sdk-go-v2/service/route53 v1.40.10/go.mod h1:tdzmlLwRjsHJjd4XXoSSnubCkVdRa39y4jCp4RACMkY= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 h1:gEYM2GSpr4YNWc6hCd5nod4+d4kd9vWIAWrmGuLdlMw= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.11/go.mod h1:gVvwPdPNYehHSP9Rs7q27U1EU+3Or2ZpXvzAYJNh63w= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 h1:iXjh3uaH3vsVcnyZX7MqCoCfcyxIrVE9iOQruRaWPrQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5/go.mod h1:5ZXesEuy/QcO0WUnt+4sDkxhdXRHTu2yG0uCSH8B6os= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 h1:M/1u4HBpwLuMtjlxuI2y6HoVLzF5e2mfxHCg7ZVMYmk= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.12/go.mod h1:kcfd+eTdEi/40FIbLq4Hif3XMXnl5b/+t/KTfLt9xIk= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -174,8 +174,8 @@ github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7F github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -196,8 +196,8 @@ github.com/civo/civogo v0.3.11 h1:mON/fyrV946Sbk6paRtOSGsN+asCgCmHCgArf5xmGxM= github.com/civo/civogo v0.3.11/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/cloudflare-go v0.86.0 h1:jEKN5VHNYNYtfDL2lUFLTRo+nOVNPFxpXTstVx0rqHI= -github.com/cloudflare/cloudflare-go v0.86.0/go.mod h1:wYW/5UP02TUfBToa/yKbQHV+r6h1NnJ1Je7XjuGM4Jw= +github.com/cloudflare/cloudflare-go v0.97.0 h1:feZRGiRF1EbljnNIYdt8014FnOLtC3CCvgkLXu915ks= +github.com/cloudflare/cloudflare-go v0.97.0/go.mod h1:JXRwuTfHpe5xFg8xytc2w0XC6LcrFsBVMS4WlVaiGg8= 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/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= @@ -265,10 +265,8 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/dnsimple/dnsimple-go v1.2.0 h1:ddTGyLVKly5HKb5L65AkLqFqwZlWo3WnR0BlFZlIddM= -github.com/dnsimple/dnsimple-go v1.2.0/go.mod h1:z/cs26v/eiRvUyXsHQBLd8lWF8+cD6GbmkPH84plM4U= +github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY= +github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8= github.com/docker/cli v24.0.9+incompatible h1:OxbimnP/z+qVjDLpq9wbeFU3Nc30XhSe+LkwYQisD50= github.com/docker/cli v24.0.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= @@ -304,8 +302,8 @@ github.com/exoscale/egoscale v0.102.3/go.mod h1:RPf2Gah6up+6kAEayHTQwqapzXlm93f0 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -332,15 +330,15 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-acme/lego/v4 v4.16.1 h1:JxZ93s4KG0jL27rZ30UsIgxap6VGzKuREsSkkyzeoCQ= -github.com/go-acme/lego/v4 v4.16.1/go.mod h1:AVvwdPned/IWpD/ihHhMsKnveF7HHYAz/CmtXi7OZoE= +github.com/go-acme/lego/v4 v4.17.4 h1:h0nePd3ObP6o7kAkndtpTzCw8shOZuWckNYeUQwo36Q= +github.com/go-acme/lego/v4 v4.17.4/go.mod h1:dU94SvPNqimEeb7EVilGGSnS0nU1O5Exir0pQ4QFL4U= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= -github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.1-0.20200915143503-439c4d2ed3ea h1:CnEQOUv4ilElSwFB9g/lVmz206oLE4aNZDYngIY1Gvg= @@ -383,8 +381,8 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM= -github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= +github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -393,8 +391,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= @@ -409,9 +407,11 @@ github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/E github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= @@ -427,6 +427,8 @@ github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -453,8 +455,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -505,13 +507,13 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= -github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= -github.com/gophercloud/gophercloud v0.15.1-0.20210202035223-633d73521055/go.mod h1:wRtmUelyIIv3CSSDI47aUwbs075O6i+LY+pXsKCBsb4= -github.com/gophercloud/gophercloud v1.0.0 h1:9nTGx0jizmHxDobe4mck89FyQHVyA3CaXLIUSGJjP9k= -github.com/gophercloud/gophercloud v1.0.0/go.mod h1:Q8fZtyi5zZxPS/j9aj3sSxtvj41AdQMDwyo1myduD5c= -github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae h1:Hi3IgB9RQDE15Kfovd8MTZrcana+UlQqNbOif8dLpA0= -github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae/go.mod h1:wx8HMD8oQD0Ryhz6+6ykq75PJ79iPyEqYHfwZ4l7OsA= +github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= +github.com/gophercloud/gophercloud v1.12.0 h1:Jrz16vPAL93l80q16fp8NplrTCp93y7rZh2P3Q4Yq7g= +github.com/gophercloud/gophercloud v1.12.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= +github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 h1:sH7xkTfYzxIEgzq1tDHIMKRh1vThOEOGNsettdEeLbE= +github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56/go.mod h1:VSalo4adEk+3sNkmVJLnhHoOyOYYS8sTWLG4mv5BKto= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -527,8 +529,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= @@ -548,9 +550,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -562,8 +563,8 @@ github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= -github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= @@ -573,7 +574,6 @@ github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjG github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -619,8 +619,8 @@ github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1 github.com/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU= github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI= github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= -github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= +github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -632,7 +632,6 @@ github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -762,8 +761,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= -github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= +github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34= github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -832,10 +831,10 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uY github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo= github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk= -github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 h1:qpB3wZR4+MPK92cTC9zZPnndkJgDgPvQqPUAgVc1NXU= -github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9/go.mod h1:HUoHXDrFvidN1NK9Wb/mZKNOfDNutKkzF2Pg71M9hHA= -github.com/nrdcg/desec v0.7.0 h1:iuGhi4pstF3+vJWwt292Oqe2+AsSPKDynQna/eu1fDs= -github.com/nrdcg/desec v0.7.0/go.mod h1:e1uRqqKv1mJdd5+SQROAhmy75lKMphLzWIuASLkpeFY= +github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 h1:ouZ2JWDl8IW5k1qugYbmpbmW8hn85Ig6buSMBRlz3KI= +github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3/go.mod h1:ZwadWt7mVhMHMbAQ1w8IhDqtWO3eWqWq72W7trnaiE8= +github.com/nrdcg/desec v0.8.0 h1:FJbRWUAluTCUi9nHFnhqPhLSIHiNnB9elZVWYgFtIqA= +github.com/nrdcg/desec v0.8.0/go.mod h1:BsnYPtSlBttJL3Gyzv0kDH7zkk60obwThlnqiiKzn+o= github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U= github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= github.com/nrdcg/freemyip v0.2.0 h1:/GscavT4GVqAY13HExl5UyoB4wlchv6Cg5NYDGsUoJ8= @@ -875,8 +874,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= -github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= +github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -886,14 +885,16 @@ github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/oracle/oci-go-sdk v24.3.0+incompatible h1:x4mcfb4agelf1O4/1/auGlZ1lr97jXRSSN5MxTgG/zU= -github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= -github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0= -github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY= +github.com/oracle/oci-go-sdk/v65 v65.63.1 h1:dYL7sk9L1+C9LCmoq+zjPMNteuJJfk54YExq/4pV9xQ= +github.com/oracle/oci-go-sdk/v65 v65.63.1/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= +github.com/ovh/go-ovh v1.5.1 h1:P8O+7H+NQuFK9P/j4sFW5C0fvSS2DnHYGPwdVCp45wI= +github.com/ovh/go-ovh v1.5.1/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= @@ -910,8 +911,8 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pires/go-proxyproto v0.6.1 h1:EBupykFmo22SDjv4fQVQd2J9NOoLPmyZA/15ldOGkPw= github.com/pires/go-proxyproto v0.6.1/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -976,8 +977,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -985,21 +986,25 @@ github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sacloud/api-client-go v0.2.8 h1:tIY6PZNBX900K66TqEPa4d6UIbedUczfCBnPJkzi8kw= -github.com/sacloud/api-client-go v0.2.8/go.mod h1:0CV/kWNYlS1hCNdnk6Wx7Wdg8DPFCnv0zOIzdXjeAeY= -github.com/sacloud/go-http v0.1.6 h1:lJGXDt9xrxJiDszRPaN9NIP8MVj10YKMzmnyzdSfI8w= -github.com/sacloud/go-http v0.1.6/go.mod h1:oLAHoDJRkptf8sq4fE8oERLkdCh0kJWfWu+paoJY7I0= -github.com/sacloud/iaas-api-go v1.11.1 h1:2MsFZ4H1uRdRVx2nVXuERWQ3swoFc3XreIV5hJ3Nsws= -github.com/sacloud/iaas-api-go v1.11.1/go.mod h1:uBDSa06F/V0OnoR66jGdbH0PVnCJw+NeE9RVbVgMfss= -github.com/sacloud/packages-go v0.0.9 h1:GbinkBLC/eirFhHpLjoDW6JV7+95Rnd2d8RWj7Afeks= -github.com/sacloud/packages-go v0.0.9/go.mod h1:k+EEUMF2LlncjbNIJNOqLyZ9wjTESPIWIk1OA7x9j2Q= +github.com/sacloud/api-client-go v0.2.10 h1:+rv3jDohD+pkdYwOTBiB+jZsM0xK3AxadXRzhp3q66c= +github.com/sacloud/api-client-go v0.2.10/go.mod h1:Jj3CTy2+O4bcMedVDXlbHuqqche85HEPuVXoQFhLaRc= +github.com/sacloud/go-http v0.1.8 h1:ynreWA/vnM8G2ksbMlmefBHsXURKPz49qlPRqQ9IQdw= +github.com/sacloud/go-http v0.1.8/go.mod h1:7TL7TN1fnPKHsMifIqURDkGujnKViCgEz5Ei/LQdFK8= +github.com/sacloud/iaas-api-go v1.12.0 h1:kqXFn3HzCiawlX6hVJb1GVqcSJqcmiGHB4Zp14sxiI8= +github.com/sacloud/iaas-api-go v1.12.0/go.mod h1:SZLXeWOdXk3WReIS557sbU1gkOgrE4rseIBQV1B3b7o= +github.com/sacloud/packages-go v0.0.10 h1:UiQGjy8LretewkRhsuna1TBM9Vz/l9FoYpQx+D+AOck= +github.com/sacloud/packages-go v0.0.10/go.mod h1:f8QITBh9z4IZc4yE9j21Q8b0sXEMwRlRmhhjWeDVTYs= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 h1:wJrcTdddKOI8TFxs8cemnhKP2EmKy3yfUKHj3ZdfzYo= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.27 h1:yGAraK1uUjlhSXgNMIy8o/J4LFNcy7yeipBqt9N9mVg= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.27/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= +github.com/selectel/domains-go v1.1.0 h1:futG50J43ALLKQAnZk9H9yOtLGnSUh7c5hSvuC5gSHo= +github.com/selectel/domains-go v1.1.0/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA= +github.com/selectel/go-selvpcclient/v3 v3.1.1 h1:C1q2LqqosiapoLpnGITGmysg0YCSQYDo2Gh69CioevM= +github.com/selectel/go-selvpcclient/v3 v3.1.1/go.mod h1:NM7IXhh1IzqZ88DOw1Qc5Ez3tULLViXo95l5+rKPuyQ= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -1007,8 +1012,9 @@ github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -1023,12 +1029,14 @@ github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA= github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= -github.com/softlayer/softlayer-go v1.1.3 h1:dfFzt5eOKIAyB/b78fHMyDu5ICx0ZtxL9NRhBlf831A= -github.com/softlayer/softlayer-go v1.1.3/go.mod h1:Pc7F57OgUKaAam7TtpqkUeqL7QyKknfiUI4R49h41/U= +github.com/softlayer/softlayer-go v1.1.5 h1:UFFtgKxiw0yIuUw93XBCFIiIMYR5eLgmm4a5DqMHXGg= +github.com/softlayer/softlayer-go v1.1.5/go.mod h1:WeJrBLoTJcaT8nO1azeyHyNpo/fDLtbpbvh+pzts+Qw= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= @@ -1075,10 +1083,10 @@ github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154/go.mod h1:7jxm github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 h1:8rUlviSVOEe7TMk7W0gIPrW8MqEzYfZHpsNWSf8s2vg= github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 h1:mmz27tVi2r70JYnm5y0Zk8w0Qzsx+vfUw3oqSyrEfP8= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 h1:g9SWTaTy/rEuhMErC2jWq9Qt5ci+jBYSvXnJsLq4adg= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490/go.mod h1:l9q4vc1QiawUB1m3RU+87yLvrrxe54jc0w/kEl4DbSQ= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898 h1:ERwcXqhc94L9cFxtiI0pvt7IJtlHl/p/Jayl3mLw+ms= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.898/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898 h1:LoYv5u+gUoFpU/AmIuTRG/2KiEkdm9gCC0dTvk8WITQ= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.898/go.mod h1:c1j6YQ+vCbeA8kJ59Im4UnMd1GxovlpPBDhGZoewfn8= github.com/testcontainers/testcontainers-go v0.30.0 h1:jmn/XS22q4YRrcMwWg0pAwlClzs/abopbsBzrepyc4E= github.com/testcontainers/testcontainers-go v0.30.0/go.mod h1:K+kHNGiM5zjklKjgTtcrEetF3uhWbMUyqAQoyoh8Pf0= github.com/testcontainers/testcontainers-go/modules/k3s v0.30.0 h1:Mk47J0WcLoY2ig72lPl+/w8GTPYbRCdHoWcPjV2mVr8= @@ -1109,6 +1117,10 @@ github.com/transip/gotransip/v6 v6.23.0/go.mod h1:nzv9eN2tdsUrm5nG5ZX6AugYIU4qgs github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= @@ -1141,10 +1153,10 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f h1:cG+ehPRJSlqljSufLf1KXeXpUd1dLNjnzA18mZcB/O0= -github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= -github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 h1:2wzke3JH7OtN20WsNDZx2VH/TCmsbqtDEbXzjF+i05E= -github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997/go.mod h1:2CHKs/YGbCcNn/BPaCkEBwKz/FNCELi+MLILjR9RaTA= +github.com/yandex-cloud/go-genproto v0.0.0-20240318083951-4fe6125f286e h1:jLIqA7M9qY31g/Nw/5htVD0DFbxmLnlFZcHKJiG3osI= +github.com/yandex-cloud/go-genproto v0.0.0-20240318083951-4fe6125f286e/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= +github.com/yandex-cloud/go-sdk v0.0.0-20240318084659-dfa50323a0b4 h1:wtzLQJmghkSUb1YkeFphIh7ST7NNVDaVOJZSAJcjMdw= +github.com/yandex-cloud/go-sdk v0.0.0-20240318084659-dfa50323a0b4/go.mod h1:9d1MV6u4lK715YXnZceKqhP4L0bKBKmv4mSLnVSjJaM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -1173,43 +1185,44 @@ go.opentelemetry.io/collector/pdata v1.2.0 h1:N6VdyEFYJyoHIKqHd0F372eNVD5b+AbH0Z go.opentelemetry.io/collector/pdata v1.2.0/go.mod h1:mKXb6527Syb8PT4P9CZOJNbkuHOHjjGTZNNwSKESJhc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/contrib/propagators/autoprop v0.49.0 h1:Jbr/9/jv1QpINge/fvJD4kUkW9/TqRNWU7H2GSK/Vb8= -go.opentelemetry.io/contrib/propagators/autoprop v0.49.0/go.mod h1:aZTdrjEnMOr6ODgjCQ955njFMLRDo1IJdTNS+agSPjA= -go.opentelemetry.io/contrib/propagators/aws v1.24.0 h1:cuwQmy9nGJi99fbwUfZSygCL3d347ddnSCWRuiVjhJ8= -go.opentelemetry.io/contrib/propagators/aws v1.24.0/go.mod h1:7HbFx8Hiiuce72QONjbOtU+3QU+Scs9VOHZIrdmi1rw= -go.opentelemetry.io/contrib/propagators/b3 v1.24.0 h1:n4xwCdTx3pZqZs2CjS/CUZAs03y3dZcGhC/FepKtEUY= -go.opentelemetry.io/contrib/propagators/b3 v1.24.0/go.mod h1:k5wRxKRU2uXx2F8uNJ4TaonuEO/V7/5xoz7kdsDACT8= -go.opentelemetry.io/contrib/propagators/jaeger v1.24.0 h1:CKtIfwSgDvJmaWsZROcHzONZgmQdMYn9mVYWypOWT5o= -go.opentelemetry.io/contrib/propagators/jaeger v1.24.0/go.mod h1:Q5JA/Cfdy/ta+5VeEhrMJRWGyS6UNRwFbl+yS3W1h5I= -go.opentelemetry.io/contrib/propagators/ot v1.24.0 h1:6lf4HoYefKDOTUSCatwkpzliUYihAvlN0omfpOn5IDs= -go.opentelemetry.io/contrib/propagators/ot v1.24.0/go.mod h1:A406hNQ7A0EWsOFzWI1p53YaYQXe12C9f6wGHUxfh0g= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.24.0 h1:f2jriWfOdldanBwS9jNBdeOKAQN7b4ugAMaNu1/1k9g= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.24.0/go.mod h1:B+bcQI1yTY+N0vqMpoZbEN7+XU4tNM0DmUiOwebFJWI= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.24.0 h1:mM8nKi6/iFQ0iqst80wDHU2ge198Ye/TfN0WBS5U24Y= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.24.0/go.mod h1:0PrIIzDteLSmNyxqcGYRL4mDIo8OTuBAOI/Bn1URxac= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/sdk/metric v1.24.0 h1:yyMQrPzF+k88/DbH7o4FMAs80puqd+9osbiBrJrz/w8= -go.opentelemetry.io/otel/sdk/metric v1.24.0/go.mod h1:I6Y5FjH6rvEnTTAYQz3Mmv2kl6Ek5IIrmwTLqMrrOE0= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/contrib/propagators/autoprop v0.52.0 h1:xyRih6jMB0vroMSRdBE+uyKx20BclB/bybJt/LaCxmY= +go.opentelemetry.io/contrib/propagators/autoprop v0.52.0/go.mod h1:L67tQgHPIOrZEraNfzidjljS9o+yLha0Y3UY4jXfs5w= +go.opentelemetry.io/contrib/propagators/aws v1.27.0 h1:RJexJi4R0S9CpxzuhhzGlTCIpaaK9SJH9g9BFrCWfPE= +go.opentelemetry.io/contrib/propagators/aws v1.27.0/go.mod h1:bqU5Ma1dEQ7VtRbPMUsH8UDTuTMiLJN4W+eUmyNVayc= +go.opentelemetry.io/contrib/propagators/b3 v1.27.0 h1:IjgxbomVrV9za6bRi8fWCNXENs0co37SZedQilP2hm0= +go.opentelemetry.io/contrib/propagators/b3 v1.27.0/go.mod h1:Dv9obQz25lCisDvvs4dy28UPh974CxkahRDUPsY7y9E= +go.opentelemetry.io/contrib/propagators/jaeger v1.27.0 h1:tJPpZAEsihJgRTnXrPjY3rjED8Av3EJdi1kvKCi1yMc= +go.opentelemetry.io/contrib/propagators/jaeger v1.27.0/go.mod h1:5uPAMHJnlTktQbCCdWSX5PfK8CocD25mycIsZV/iFiU= +go.opentelemetry.io/contrib/propagators/ot v1.27.0 h1:xFPqk7ntRR87dqvl6RfeHiq9UlE8mPSuL6Dtr/zysL8= +go.opentelemetry.io/contrib/propagators/ot v1.27.0/go.mod h1:nVLTPrDlSZPoVdeWRmpWBwxA73TYL6XLkC4bj72jvmg= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 h1:bFgvUr3/O4PHj3VQcFEuYKvRZJX1SJDQ+11JXuSB3/w= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0/go.mod h1:xJntEd2KL6Qdg5lwp97HMLQDVeAhrYxmzFseAMDPQ8I= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 h1:CIHWikMsN3wO+wq1Tp5VGdVRTcON+DmOJSfDjXypKOc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0/go.mod h1:TNupZ6cxqyFEpLXAZW7On+mLFL0/g0TE3unIYL91xWc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= +go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= +go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= @@ -1222,8 +1235,8 @@ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= -go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= +go.uber.org/ratelimit v0.3.0 h1:IdZd9wqvFXnvLvSEBo0KPcGfkoBGNkpTHlrE3Rcjkjw= +go.uber.org/ratelimit v0.3.0/go.mod h1:So5LG7CV1zWpY1sHe+DXTJqQvOx+FFPFaAs2SnoyBaI= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= @@ -1251,14 +1264,14 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1267,8 +1280,8 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1290,8 +1303,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1333,14 +1346,14 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1404,7 +1417,6 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1426,8 +1438,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1436,8 +1448,9 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -1446,13 +1459,13 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1498,8 +1511,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1517,15 +1530,13 @@ google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= -google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= +google.golang.org/api v0.172.0 h1:/1OcMZGPmW1rX2LCu2CmGUD1KXK1+pfzxotxyRUCCdk= +google.golang.org/api v0.172.0/go.mod h1:+fJZq6QXWfa9pXhnIzsjx4yI22d4aI9ZpLb58gvXjis= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1540,12 +1551,12 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= -google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY= -google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM= -google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 h1:P8OJ/WCl/Xo4E4zoe4/bifHpSmmKwARqyqE4nW6J2GQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -1566,8 +1577,8 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY= -google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= google.golang.org/grpc/examples v0.0.0-20201130180447-c456688b1860/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1583,8 +1594,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1605,11 +1616,10 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ns1/ns1-go.v2 v2.7.13 h1:r07CLALg18f/L1KIK1ZJdbirBV349UtYT1rDWGjnaTk= -gopkg.in/ns1/ns1-go.v2 v2.7.13/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= +gopkg.in/ns1/ns1-go.v2 v2.9.1 h1:3/QYzUazRCSE49d3sh1Q+X7IrDp/I7OqR/M7dKA0Oks= +gopkg.in/ns1/ns1-go.v2 v2.9.1/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= @@ -1629,6 +1639,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= diff --git a/integration/docker_test.go b/integration/docker_test.go index daa6fa06b..d6a6819ff 100644 --- a/integration/docker_test.go +++ b/integration/docker_test.go @@ -82,7 +82,7 @@ func (s *DockerSuite) TestWRRServer() { require.NoError(s.T(), err) repartition := map[string]int{} - for i := 0; i < 4; i++ { + for range 4 { req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil) req.Host = "my.wrr.host" require.NoError(s.T(), err) diff --git a/integration/fixtures/k8s-conformance/02-traefik.yml b/integration/fixtures/k8s-conformance/02-traefik.yml index becc6dc5c..6f780affb 100644 --- a/integration/fixtures/k8s-conformance/02-traefik.yml +++ b/integration/fixtures/k8s-conformance/02-traefik.yml @@ -48,6 +48,8 @@ spec: - --api.insecure - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 + - --entrypoints.web8080.address=:8080 + - --entrypoints.traefik.address=:9000 - --experimental.kubernetesgateway - --providers.kubernetesgateway.experimentalChannel - --providers.kubernetesgateway.statusaddress.service.namespace=traefik @@ -55,10 +57,12 @@ spec: ports: - name: web containerPort: 80 - - name: admin - containerPort: 8080 - name: websecure containerPort: 443 + - name: web8080 + containerPort: 8080 + - name: traefik + containerPort: 9000 --- apiVersion: v1 @@ -78,5 +82,8 @@ spec: name: websecure targetPort: websecure - port: 8080 - name: admin - targetPort: admin + name: web8080 + targetPort: web8080 + - port: 9000 + name: traefik + targetPort: traefik diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 93f14adcf..761a8f928 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -98,6 +98,67 @@ spec: description: Service defines an upstream HTTP service to proxy traffic to. properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be + sent to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for + the health check endpoint. + type: string + port: + description: Port defines the server URL port for + the health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme + for the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: @@ -846,6 +907,11 @@ spec: This middleware compresses responses before sending them to the client, using gzip compression. More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/compress/ properties: + defaultEncoding: + description: DefaultEncoding specifies the default encoding if + the `Accept-Encoding` header is not in the request or contains + a wildcard (`*`). + type: string excludedContentTypes: description: |- ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing. @@ -919,6 +985,67 @@ spec: Service defines the reference to a Kubernetes Service that will serve the error page. More info: https://doc.traefik.io/traefik/v3.0/middlewares/http/errorpages/#service properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname in + the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status code + of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: @@ -1182,6 +1309,10 @@ spec: description: ContentSecurityPolicy defines the Content-Security-Policy header value. type: string + contentSecurityPolicyReportOnly: + description: ContentSecurityPolicyReportOnly defines the Content-Security-Policy-Report-Only + header value. + type: string contentTypeNosniff: description: ContentTypeNosniff defines whether to add the X-Content-Type-Options header with the nosniff value. @@ -2295,6 +2426,67 @@ spec: mirroring: description: Mirroring defines the Mirroring service configuration. properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent to + the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname in the + Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the health + check endpoint. + type: string + port: + description: Port defines the server URL port for the health + check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for the + health check endpoint. + type: string + status: + description: Status defines the expected HTTP status code + of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: @@ -2314,6 +2506,67 @@ spec: items: description: MirrorService holds the mirror configuration. properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: @@ -2548,6 +2801,67 @@ spec: description: Service defines an upstream HTTP service to proxy traffic to. properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + type: object kind: description: Kind defines the kind of the Service. enum: diff --git a/integration/k8s_conformance_test.go b/integration/k8s_conformance_test.go index 3ba562b2c..0e2b5e855 100644 --- a/integration/k8s_conformance_test.go +++ b/integration/k8s_conformance_test.go @@ -166,7 +166,7 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() { k3sContainerIP, err := s.k3sContainer.ContainerIP(context.Background()) require.NoError(s.T(), err) - err = try.GetRequest("http://"+k3sContainerIP+":8080/api/entrypoints", 10*time.Second, try.BodyContains(`"name":"web"`)) + err = try.GetRequest("http://"+k3sContainerIP+":9000/api/entrypoints", 10*time.Second, try.BodyContains(`"name":"web"`)) require.NoError(s.T(), err) opts := ksuite.Options{ @@ -195,17 +195,30 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() { LatestObservedGenerationSet: 5 * time.Second, RequiredConsecutiveSuccesses: 0, }, + SupportedFeatures: sets.New(ksuite.SupportGateway, + ksuite.SupportGatewayPort8080, + ksuite.SupportHTTPRoute, + ksuite.SupportHTTPRouteQueryParamMatching, + ksuite.SupportHTTPRouteMethodMatching, + ksuite.SupportHTTPRoutePortRedirect, + ksuite.SupportHTTPRouteSchemeRedirect, + ksuite.SupportHTTPRouteHostRewrite, + ksuite.SupportHTTPRoutePathRewrite, + ksuite.SupportHTTPRoutePathRedirect, + ), + ExemptFeatures: sets.New( + ksuite.SupportHTTPRouteRequestTimeout, + ksuite.SupportHTTPRouteBackendTimeout, + ksuite.SupportHTTPRouteResponseHeaderModification, + ksuite.SupportHTTPRouteRequestMirror, + ksuite.SupportHTTPRouteRequestMultipleMirrors, + ), EnableAllSupportedFeatures: false, RunTest: *k8sConformanceRunTest, // Until the feature are all supported, following tests are skipped. SkipTests: []string{ - tests.HTTPRouteListenerHostnameMatching.ShortName, - tests.HTTPRouteInvalidCrossNamespaceParentRef.ShortName, - tests.HTTPRouteMatchingAcrossRoutes.ShortName, - tests.HTTPRoutePartiallyInvalidViaInvalidReferenceGrant.ShortName, - tests.HTTPRoutePathMatchOrder.ShortName, - tests.HTTPRouteHeaderMatching.ShortName, - tests.HTTPRouteReferenceGrant.ShortName, + tests.HTTPRouteMethodMatching.ShortName, + tests.HTTPRouteQueryParamMatching.ShortName, }, } diff --git a/integration/resources/compose/etcd.yml b/integration/resources/compose/etcd.yml index fb1f3f7ec..6a3b34fe9 100644 --- a/integration/resources/compose/etcd.yml +++ b/integration/resources/compose/etcd.yml @@ -1,7 +1,7 @@ version: "3.8" services: etcd: - image: quay.io/coreos/etcd:v3.3.18 + image: quay.io/coreos/etcd:v3.5.14 command: - etcd - --listen-client-urls diff --git a/integration/simple_test.go b/integration/simple_test.go index c8ebb8d6c..018a4b67a 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -833,7 +833,7 @@ func (s *SimpleSuite) TestWRRServer() { require.NoError(s.T(), err) repartition := map[string]int{} - for i := 0; i < 4; i++ { + for range 4 { req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil) require.NoError(s.T(), err) diff --git a/integration/testdata/rawdata-gateway.json b/integration/testdata/rawdata-gateway.json index 08bd34802..d4dd467b0 100644 --- a/integration/testdata/rawdata-gateway.json +++ b/integration/testdata/rawdata-gateway.json @@ -30,27 +30,27 @@ "traefik" ] }, - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06@kubernetesgateway": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06@kubernetesgateway": { "entryPoints": [ "web" ], - "service": "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + "service": "default-http-app-1-my-gateway-web-0-wrr", "rule": "Host(`foo.com`) \u0026\u0026 Path(`/bar`)", "ruleSyntax": "v3", - "priority": 31, + "priority": 100008, "status": "enabled", "using": [ "web" ] }, - "default-http-app-1-my-https-gateway-websecure-1c0cf64bde37d9d0df06@kubernetesgateway": { + "default-http-app-1-my-https-gateway-websecure-0-1c0cf64bde37d9d0df06@kubernetesgateway": { "entryPoints": [ "websecure" ], - "service": "default-http-app-1-my-https-gateway-websecure-1c0cf64bde37d9d0df06-wrr", + "service": "default-http-app-1-my-https-gateway-websecure-0-wrr", "rule": "Host(`foo.com`) \u0026\u0026 Path(`/bar`)", "ruleSyntax": "v3", - "priority": 31, + "priority": 100008, "tls": {}, "status": "enabled", "using": [ @@ -96,7 +96,7 @@ "dashboard@internal" ] }, - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr@kubernetesgateway": { + "default-http-app-1-my-gateway-web-0-wrr@kubernetesgateway": { "weighted": { "services": [ { @@ -107,10 +107,10 @@ }, "status": "enabled", "usedBy": [ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06@kubernetesgateway" + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06@kubernetesgateway" ] }, - "default-http-app-1-my-https-gateway-websecure-1c0cf64bde37d9d0df06-wrr@kubernetesgateway": { + "default-http-app-1-my-https-gateway-websecure-0-wrr@kubernetesgateway": { "weighted": { "services": [ { @@ -121,7 +121,7 @@ }, "status": "enabled", "usedBy": [ - "default-http-app-1-my-https-gateway-websecure-1c0cf64bde37d9d0df06@kubernetesgateway" + "default-http-app-1-my-https-gateway-websecure-0-1c0cf64bde37d9d0df06@kubernetesgateway" ] }, "default-whoami-80@kubernetesgateway": { @@ -150,11 +150,11 @@ } }, "tcpRouters": { - "default-tcp-app-1-my-tcp-gateway-footcp-e3b0c44298fc1c149afb@kubernetesgateway": { + "default-tcp-app-1-my-tcp-gateway-footcp@kubernetesgateway": { "entryPoints": [ "footcp" ], - "service": "default-tcp-app-1-my-tcp-gateway-footcp-e3b0c44298fc1c149afb-wrr-0", + "service": "default-tcp-app-1-my-tcp-gateway-footcp-wrr-0", "rule": "HostSNI(`*`)", "ruleSyntax": "v3", "priority": -1, @@ -163,11 +163,11 @@ "footcp" ] }, - "default-tcp-app-1-my-tls-gateway-footlsterminate-e3b0c44298fc1c149afb@kubernetesgateway": { + "default-tcp-app-1-my-tls-gateway-footlsterminate@kubernetesgateway": { "entryPoints": [ "footlsterminate" ], - "service": "default-tcp-app-1-my-tls-gateway-footlsterminate-e3b0c44298fc1c149afb-wrr-0", + "service": "default-tcp-app-1-my-tls-gateway-footlsterminate-wrr-0", "rule": "HostSNI(`*`)", "ruleSyntax": "v3", "priority": -1, @@ -197,7 +197,7 @@ } }, "tcpServices": { - "default-tcp-app-1-my-tcp-gateway-footcp-e3b0c44298fc1c149afb-wrr-0@kubernetesgateway": { + "default-tcp-app-1-my-tcp-gateway-footcp-wrr-0@kubernetesgateway": { "weighted": { "services": [ { @@ -208,10 +208,10 @@ }, "status": "enabled", "usedBy": [ - "default-tcp-app-1-my-tcp-gateway-footcp-e3b0c44298fc1c149afb@kubernetesgateway" + "default-tcp-app-1-my-tcp-gateway-footcp@kubernetesgateway" ] }, - "default-tcp-app-1-my-tls-gateway-footlsterminate-e3b0c44298fc1c149afb-wrr-0@kubernetesgateway": { + "default-tcp-app-1-my-tls-gateway-footlsterminate-wrr-0@kubernetesgateway": { "weighted": { "services": [ { @@ -222,7 +222,7 @@ }, "status": "enabled", "usedBy": [ - "default-tcp-app-1-my-tls-gateway-footlsterminate-e3b0c44298fc1c149afb@kubernetesgateway" + "default-tcp-app-1-my-tls-gateway-footlsterminate@kubernetesgateway" ] }, "default-tls-app-1-my-tls-gateway-footlspassthrough-2279fe75c5156dc5eb26-wrr-0@kubernetesgateway": { diff --git a/pkg/cli/deprecation.go b/pkg/cli/deprecation.go index 98fc1d1c1..01fd64791 100644 --- a/pkg/cli/deprecation.go +++ b/pkg/cli/deprecation.go @@ -162,7 +162,7 @@ func findTypedField(rType reflect.Type, node *parser.Node) (reflect.StructField, return reflect.StructField{}, false } - for i := 0; i < rType.NumField(); i++ { + for i := range rType.NumField() { cField := rType.Field(i) // ignore unexported fields. diff --git a/pkg/config/dynamic/fixtures/sample.toml b/pkg/config/dynamic/fixtures/sample.toml index f6ab56cf1..c096381aa 100644 --- a/pkg/config/dynamic/fixtures/sample.toml +++ b/pkg/config/dynamic/fixtures/sample.toml @@ -330,6 +330,7 @@ browserXssFilter = true customBrowserXSSValue = "foobar" contentSecurityPolicy = "foobar" + contentSecurityPolicyReportOnly = "foobar" publicKey = "foobar" referrerPolicy = "foobar" isDevelopment = true diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index ecfd68c7e..b91de8413 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -254,7 +254,7 @@ type ServerHealthCheck struct { Interval ptypes.Duration `json:"interval,omitempty" toml:"interval,omitempty" yaml:"interval,omitempty" export:"true"` Timeout ptypes.Duration `json:"timeout,omitempty" toml:"timeout,omitempty" yaml:"timeout,omitempty" export:"true"` Hostname string `json:"hostname,omitempty" toml:"hostname,omitempty" yaml:"hostname,omitempty"` - FollowRedirects *bool `json:"followRedirects" toml:"followRedirects" yaml:"followRedirects" export:"true"` + FollowRedirects *bool `json:"followRedirects,omitempty" toml:"followRedirects,omitempty" yaml:"followRedirects,omitempty" export:"true"` Headers map[string]string `json:"headers,omitempty" toml:"headers,omitempty" yaml:"headers,omitempty" export:"true"` } diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 59c91814b..e26f0ca57 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -42,6 +42,8 @@ type Middleware struct { // Gateway API HTTPRoute filters middlewares. RequestHeaderModifier *RequestHeaderModifier `json:"requestHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` + RequestRedirect *RequestRedirect `json:"requestRedirect,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` + URLRewrite *URLRewrite `json:"URLRewrite,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` } // +k8s:deepcopy-gen=true @@ -174,6 +176,8 @@ type Compress struct { // MinResponseBodyBytes defines the minimum amount of bytes a response body must have to be compressed. // Default: 1024. MinResponseBodyBytes int `json:"minResponseBodyBytes,omitempty" toml:"minResponseBodyBytes,omitempty" yaml:"minResponseBodyBytes,omitempty" export:"true"` + // DefaultEncoding specifies the default encoding if the `Accept-Encoding` header is not in the request or contains a wildcard (`*`). + DefaultEncoding string `json:"defaultEncoding,omitempty" toml:"defaultEncoding,omitempty" yaml:"defaultEncoding,omitempty" export:"true"` } // +k8s:deepcopy-gen=true @@ -310,6 +314,8 @@ type Headers struct { CustomBrowserXSSValue string `json:"customBrowserXSSValue,omitempty" toml:"customBrowserXSSValue,omitempty" yaml:"customBrowserXSSValue,omitempty"` // ContentSecurityPolicy defines the Content-Security-Policy header value. ContentSecurityPolicy string `json:"contentSecurityPolicy,omitempty" toml:"contentSecurityPolicy,omitempty" yaml:"contentSecurityPolicy,omitempty"` + // ContentSecurityPolicyReportOnly defines the Content-Security-Policy-Report-Only header value. + ContentSecurityPolicyReportOnly string `json:"contentSecurityPolicyReportOnly,omitempty" toml:"contentSecurityPolicyReportOnly,omitempty" yaml:"contentSecurityPolicyReportOnly,omitempty"` // PublicKey is the public key that implements HPKP to prevent MITM attacks with forged certificates. PublicKey string `json:"publicKey,omitempty" toml:"publicKey,omitempty" yaml:"publicKey,omitempty"` // ReferrerPolicy defines the Referrer-Policy header value. @@ -373,6 +379,7 @@ func (h *Headers) HasSecureHeadersDefined() bool { h.BrowserXSSFilter || h.CustomBrowserXSSValue != "" || h.ContentSecurityPolicy != "" || + h.ContentSecurityPolicyReportOnly != "" || h.PublicKey != "" || h.ReferrerPolicy != "" || (h.FeaturePolicy != nil && *h.FeaturePolicy != "") || @@ -685,3 +692,24 @@ type RequestHeaderModifier struct { Add map[string]string `json:"add,omitempty"` Remove []string `json:"remove,omitempty"` } + +// +k8s:deepcopy-gen=true + +// RequestRedirect holds the request redirect middleware configuration. +type RequestRedirect struct { + Scheme *string `json:"scheme,omitempty"` + Hostname *string `json:"hostname,omitempty"` + Port *string `json:"port,omitempty"` + Path *string `json:"path,omitempty"` + PathPrefix *string `json:"pathPrefix,omitempty"` + StatusCode int `json:"statusCode,omitempty"` +} + +// +k8s:deepcopy-gen=true + +// URLRewrite holds the URL rewrite middleware configuration. +type URLRewrite struct { + Hostname *string `json:"hostname,omitempty"` + Path *string `json:"path,omitempty"` + PathPrefix *string `json:"pathPrefix,omitempty"` +} diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 719e1b049..7ad0cb525 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -864,6 +864,16 @@ func (in *Middleware) DeepCopyInto(out *Middleware) { *out = new(RequestHeaderModifier) (*in).DeepCopyInto(*out) } + if in.RequestRedirect != nil { + in, out := &in.RequestRedirect, &out.RequestRedirect + *out = new(RequestRedirect) + (*in).DeepCopyInto(*out) + } + if in.URLRewrite != nil { + in, out := &in.URLRewrite, &out.URLRewrite + *out = new(URLRewrite) + (*in).DeepCopyInto(*out) + } return } @@ -1107,6 +1117,47 @@ func (in *RequestHeaderModifier) DeepCopy() *RequestHeaderModifier { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RequestRedirect) DeepCopyInto(out *RequestRedirect) { + *out = *in + if in.Scheme != nil { + in, out := &in.Scheme, &out.Scheme + *out = new(string) + **out = **in + } + if in.Hostname != nil { + in, out := &in.Hostname, &out.Hostname + *out = new(string) + **out = **in + } + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(string) + **out = **in + } + if in.Path != nil { + in, out := &in.Path, &out.Path + *out = new(string) + **out = **in + } + if in.PathPrefix != nil { + in, out := &in.PathPrefix, &out.PathPrefix + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestRedirect. +func (in *RequestRedirect) DeepCopy() *RequestRedirect { + if in == nil { + return nil + } + out := new(RequestRedirect) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) { *out = *in @@ -2161,6 +2212,37 @@ func (in *UDPWeightedRoundRobin) DeepCopy() *UDPWeightedRoundRobin { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *URLRewrite) DeepCopyInto(out *URLRewrite) { + *out = *in + if in.Hostname != nil { + in, out := &in.Hostname, &out.Hostname + *out = new(string) + **out = **in + } + if in.Path != nil { + in, out := &in.Path, &out.Path + *out = new(string) + **out = **in + } + if in.PathPrefix != nil { + in, out := &in.PathPrefix, &out.PathPrefix + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new URLRewrite. +func (in *URLRewrite) DeepCopy() *URLRewrite { + if in == nil { + return nil + } + out := new(URLRewrite) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in Users) DeepCopyInto(out *Users) { { diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index e1c168820..c9989e1e3 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -63,6 +63,7 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.middlewares.Middleware8.headers.addvaryheader": "true", "traefik.http.middlewares.Middleware8.headers.browserxssfilter": "true", "traefik.http.middlewares.Middleware8.headers.contentsecuritypolicy": "foobar", + "traefik.http.middlewares.Middleware8.headers.contentsecuritypolicyreportonly": "foobar", "traefik.http.middlewares.Middleware8.headers.contenttypenosniff": "true", "traefik.http.middlewares.Middleware8.headers.custombrowserxssvalue": "foobar", "traefik.http.middlewares.Middleware8.headers.customframeoptionsvalue": "foobar", @@ -611,22 +612,23 @@ func TestDecodeConfiguration(t *testing.T) { "name0": "foobar", "name1": "foobar", }, - SSLForceHost: Bool(true), - STSSeconds: 42, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - CustomFrameOptionsValue: "foobar", - ContentTypeNosniff: true, - BrowserXSSFilter: true, - CustomBrowserXSSValue: "foobar", - ContentSecurityPolicy: "foobar", - PublicKey: "foobar", - ReferrerPolicy: "foobar", - FeaturePolicy: String("foobar"), - PermissionsPolicy: "foobar", - IsDevelopment: true, + SSLForceHost: Bool(true), + STSSeconds: 42, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foobar", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foobar", + ContentSecurityPolicy: "foobar", + ContentSecurityPolicyReportOnly: "foobar", + PublicKey: "foobar", + ReferrerPolicy: "foobar", + FeaturePolicy: String("foobar"), + PermissionsPolicy: "foobar", + IsDevelopment: true, }, }, "Middleware9": { @@ -1134,22 +1136,23 @@ func TestEncodeConfiguration(t *testing.T) { "name0": "foobar", "name1": "foobar", }, - SSLForceHost: Bool(true), - STSSeconds: 42, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - CustomFrameOptionsValue: "foobar", - ContentTypeNosniff: true, - BrowserXSSFilter: true, - CustomBrowserXSSValue: "foobar", - ContentSecurityPolicy: "foobar", - PublicKey: "foobar", - ReferrerPolicy: "foobar", - FeaturePolicy: String("foobar"), - PermissionsPolicy: "foobar", - IsDevelopment: true, + SSLForceHost: Bool(true), + STSSeconds: 42, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foobar", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foobar", + ContentSecurityPolicy: "foobar", + ContentSecurityPolicyReportOnly: "foobar", + PublicKey: "foobar", + ReferrerPolicy: "foobar", + FeaturePolicy: String("foobar"), + PermissionsPolicy: "foobar", + IsDevelopment: true, }, }, "Middleware9": { @@ -1299,6 +1302,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware8.Headers.AllowedHosts": "foobar, fiibar", "traefik.HTTP.Middlewares.Middleware8.Headers.BrowserXSSFilter": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.ContentSecurityPolicy": "foobar", + "traefik.HTTP.Middlewares.Middleware8.Headers.ContentSecurityPolicyReportOnly": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.ContentTypeNosniff": "true", "traefik.HTTP.Middlewares.Middleware8.Headers.CustomBrowserXSSValue": "foobar", "traefik.HTTP.Middlewares.Middleware8.Headers.CustomFrameOptionsValue": "foobar", diff --git a/pkg/metrics/opentelemetry_test.go b/pkg/metrics/opentelemetry_test.go index c778f2fec..54b87ce60 100644 --- a/pkg/metrics/opentelemetry_test.go +++ b/pkg/metrics/opentelemetry_test.go @@ -334,8 +334,8 @@ func TestOpenTelemetry(t *testing.T) { // TODO: the len of startUnixNano is no supposed to be 20, it should be 19 expectedConfig := []string{ `({"name":"traefik_config_reloads_total","description":"Config reloads","unit":"1","sum":{"dataPoints":\[{"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, - `({"name":"traefik_config_last_reload_success","description":"Last config reload success","unit":"ms","gauge":{"dataPoints":\[{"timeUnixNano":"[\d]{19}","asDouble":1}\]}})`, - `({"name":"traefik_open_connections","description":"How many open connections exist, by entryPoint and protocol","unit":"1","gauge":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test"}},{"key":"protocol","value":{"stringValue":"TCP"}}\],"timeUnixNano":"[\d]{19}","asDouble":1}\]}})`, + `({"name":"traefik_config_last_reload_success","description":"Last config reload success","unit":"ms","gauge":{"dataPoints":\[{"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\]}})`, + `({"name":"traefik_open_connections","description":"How many open connections exist, by entryPoint and protocol","unit":"1","gauge":{"dataPoints":\[{"attributes":\[{"key":"entrypoint","value":{"stringValue":"test"}},{"key":"protocol","value":{"stringValue":"TCP"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\]}})`, } registry.ConfigReloadsCounter().Add(1) @@ -345,7 +345,7 @@ func TestOpenTelemetry(t *testing.T) { tryAssertMessage(t, c, expectedConfig) expectedTLSCerts := []string{ - `({"name":"traefik_tls_certs_not_after","description":"Certificate expiration timestamp","unit":"ms","gauge":{"dataPoints":\[{"attributes":\[{"key":"key","value":{"stringValue":"value"}}\],"timeUnixNano":"[\d]{19}","asDouble":1}\]}})`, + `({"name":"traefik_tls_certs_not_after","description":"Certificate expiration timestamp","unit":"ms","gauge":{"dataPoints":\[{"attributes":\[{"key":"key","value":{"stringValue":"value"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\]}})`, } registry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1) @@ -389,7 +389,7 @@ func TestOpenTelemetry(t *testing.T) { `({"name":"traefik_service_requests_total","description":"How many HTTP requests processed on a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1},{"attributes":\[{"key":"code","value":{"stringValue":"(?:200|404)"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_service_requests_tls_total","description":"How many HTTP requests with TLS processed on a service, partitioned by TLS version and TLS cipher.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"service","value":{"stringValue":"test"}},{"key":"tls_cipher","value":{"stringValue":"bar"}},{"key":"tls_version","value":{"stringValue":"foo"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_service_request_duration_seconds","description":"How long it took to process the request on a service, partitioned by status code, protocol, and method.","unit":"ms","histogram":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"200"}},{"key":"service","value":{"stringValue":"test"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","count":"1","sum":10000,"bucketCounts":\["0","0","0","0","0","0","0","0","0","0","0","0","0","0","1"\],"explicitBounds":\[0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1,2.5,5,7.5,10\],"min":10000,"max":10000}\],"aggregationTemporality":2}})`, - `({"name":"traefik_service_server_up","description":"service server is up, described by gauge value of 0 or 1.","unit":"1","gauge":{"dataPoints":\[{"attributes":\[{"key":"service","value":{"stringValue":"test"}},{"key":"url","value":{"stringValue":"http://127.0.0.1"}}\],"timeUnixNano":"[\d]{19}","asDouble":1}\]}})`, + `({"name":"traefik_service_server_up","description":"service server is up, described by gauge value of 0 or 1.","unit":"1","gauge":{"dataPoints":\[{"attributes":\[{"key":"service","value":{"stringValue":"test"}},{"key":"url","value":{"stringValue":"http://127.0.0.1"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\]}})`, `({"name":"traefik_service_requests_bytes_total","description":"The total size of requests in bytes received by a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, `({"name":"traefik_service_responses_bytes_total","description":"The total size of responses in bytes returned by a service, partitioned by status code, protocol, and method.","unit":"1","sum":{"dataPoints":\[{"attributes":\[{"key":"code","value":{"stringValue":"404"}},{"key":"method","value":{"stringValue":"GET"}},{"key":"service","value":{"stringValue":"ServiceReqsCounter"}}\],"startTimeUnixNano":"[\d]{19}","timeUnixNano":"[\d]{19}","asDouble":1}\],"aggregationTemporality":2,"isMonotonic":true}})`, } @@ -463,7 +463,7 @@ func verifyMessage(msg string, expected []string) []error { re := regexp.MustCompile(pattern) match := re.FindStringSubmatch(msg) if len(match) != 2 { - errs = append(errs, fmt.Errorf("Got %q %v, want %q", msg, match, pattern)) + errs = append(errs, fmt.Errorf("got %q %v, want %q", msg, match, pattern)) } } return errs diff --git a/pkg/middlewares/auth/forward_test.go b/pkg/middlewares/auth/forward_test.go index ed3e2bb2b..e2f197b98 100644 --- a/pkg/middlewares/auth/forward_test.go +++ b/pkg/middlewares/auth/forward_test.go @@ -601,6 +601,7 @@ func (s *mockSpan) SetAttributes(kv ...attribute.KeyValue) { func (s *mockSpan) End(...trace.SpanEndOption) {} func (s *mockSpan) RecordError(_ error, _ ...trace.EventOption) {} func (s *mockSpan) AddEvent(_ string, _ ...trace.EventOption) {} +func (s *mockSpan) AddLink(_ trace.Link) {} func (s *mockSpan) SetName(name string) { s.name = name } diff --git a/pkg/middlewares/compress/acceptencoding.go b/pkg/middlewares/compress/acceptencoding.go new file mode 100644 index 000000000..3f9fc4f4a --- /dev/null +++ b/pkg/middlewares/compress/acceptencoding.go @@ -0,0 +1,138 @@ +package compress + +import ( + "slices" + "strconv" + "strings" +) + +const acceptEncodingHeader = "Accept-Encoding" + +const ( + brotliName = "br" + gzipName = "gzip" + zstdName = "zstd" + identityName = "identity" + wildcardName = "*" + notAcceptable = "not_acceptable" +) + +type Encoding struct { + Type string + Weight *float64 +} + +func getCompressionType(acceptEncoding []string, defaultType string) string { + if defaultType == "" { + // Keeps the pre-existing default inside Traefik. + defaultType = brotliName + } + + encodings, hasWeight := parseAcceptEncoding(acceptEncoding) + + if hasWeight { + if len(encodings) == 0 { + return identityName + } + + encoding := encodings[0] + + if encoding.Type == identityName && encoding.Weight != nil && *encoding.Weight == 0 { + return notAcceptable + } + + if encoding.Type == wildcardName && encoding.Weight != nil && *encoding.Weight == 0 { + return notAcceptable + } + + if encoding.Type == wildcardName { + return defaultType + } + + return encoding.Type + } + + for _, dt := range []string{zstdName, brotliName, gzipName} { + if slices.ContainsFunc(encodings, func(e Encoding) bool { return e.Type == dt }) { + return dt + } + } + + if slices.ContainsFunc(encodings, func(e Encoding) bool { return e.Type == wildcardName }) { + return defaultType + } + + return identityName +} + +func parseAcceptEncoding(acceptEncoding []string) ([]Encoding, bool) { + var encodings []Encoding + var hasWeight bool + + for _, line := range acceptEncoding { + for _, item := range strings.Split(strings.ReplaceAll(line, " ", ""), ",") { + parsed := strings.SplitN(item, ";", 2) + if len(parsed) == 0 { + continue + } + + switch parsed[0] { + case zstdName, brotliName, gzipName, identityName, wildcardName: + // supported encoding + default: + continue + } + + var weight *float64 + if len(parsed) > 1 && strings.HasPrefix(parsed[1], "q=") { + w, _ := strconv.ParseFloat(strings.TrimPrefix(parsed[1], "q="), 64) + + weight = &w + hasWeight = true + } + + encodings = append(encodings, Encoding{ + Type: parsed[0], + Weight: weight, + }) + } + } + + slices.SortFunc(encodings, compareEncoding) + + return encodings, hasWeight +} + +func compareEncoding(a, b Encoding) int { + lhs, rhs := a.Weight, b.Weight + + if lhs == nil && rhs == nil { + return 0 + } + + if lhs == nil && *rhs == 0 { + return -1 + } + + if lhs == nil { + return 1 + } + + if rhs == nil && *lhs == 0 { + return 1 + } + + if rhs == nil { + return -1 + } + + if *lhs < *rhs { + return 1 + } + + if *lhs > *rhs { + return -1 + } + + return 0 +} diff --git a/pkg/middlewares/compress/acceptencoding_test.go b/pkg/middlewares/compress/acceptencoding_test.go new file mode 100644 index 000000000..818c3e06e --- /dev/null +++ b/pkg/middlewares/compress/acceptencoding_test.go @@ -0,0 +1,156 @@ +package compress + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_getCompressionType(t *testing.T) { + testCases := []struct { + desc string + values []string + defaultType string + expected string + }{ + { + desc: "br > gzip (no weight)", + values: []string{"gzip, br"}, + expected: brotliName, + }, + { + desc: "zstd > br > gzip (no weight)", + values: []string{"zstd, gzip, br"}, + expected: zstdName, + }, + { + desc: "known compression type (no weight)", + values: []string{"compress, gzip"}, + expected: gzipName, + }, + { + desc: "unknown compression type (no weight), no encoding", + values: []string{"compress, rar"}, + expected: identityName, + }, + { + desc: "wildcard return the default compression type", + values: []string{"*"}, + expected: brotliName, + }, + { + desc: "wildcard return the custom default compression type", + values: []string{"*"}, + defaultType: "foo", + expected: "foo", + }, + { + desc: "follows weight", + values: []string{"br;q=0.8, gzip;q=1.0, *;q=0.1"}, + expected: gzipName, + }, + { + desc: "ignore unknown compression type", + values: []string{"compress;q=1.0, gzip;q=0.5"}, + expected: gzipName, + }, + { + desc: "fallback on non-zero compression type", + values: []string{"compress;q=1.0, gzip, identity;q=0"}, + expected: gzipName, + }, + { + desc: "not acceptable (identity)", + values: []string{"compress;q=1.0, identity;q=0"}, + expected: notAcceptable, + }, + { + desc: "not acceptable (wildcard)", + values: []string{"compress;q=1.0, *;q=0"}, + expected: notAcceptable, + }, + { + desc: "non-zero is higher than 0", + values: []string{"gzip, *;q=0"}, + expected: gzipName, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + encodingType := getCompressionType(test.values, test.defaultType) + + assert.Equal(t, test.expected, encodingType) + }) + } +} + +func Test_parseAcceptEncoding(t *testing.T) { + testCases := []struct { + desc string + values []string + expected []Encoding + assertWeight assert.BoolAssertionFunc + }{ + { + desc: "weight", + values: []string{"br;q=1.0, zstd;q=0.9, gzip;q=0.8, *;q=0.1"}, + expected: []Encoding{ + {Type: brotliName, Weight: ptr[float64](1)}, + {Type: zstdName, Weight: ptr(0.9)}, + {Type: gzipName, Weight: ptr(0.8)}, + {Type: wildcardName, Weight: ptr(0.1)}, + }, + assertWeight: assert.True, + }, + { + desc: "mixed", + values: []string{"zstd,gzip, br;q=1.0, *;q=0"}, + expected: []Encoding{ + {Type: brotliName, Weight: ptr[float64](1)}, + {Type: zstdName}, + {Type: gzipName}, + {Type: wildcardName, Weight: ptr[float64](0)}, + }, + assertWeight: assert.True, + }, + { + desc: "no weight", + values: []string{"zstd, gzip, br, *"}, + expected: []Encoding{ + {Type: zstdName}, + {Type: gzipName}, + {Type: brotliName}, + {Type: wildcardName}, + }, + assertWeight: assert.False, + }, + { + desc: "weight and identity", + values: []string{"gzip;q=1.0, identity; q=0.5, *;q=0"}, + expected: []Encoding{ + {Type: gzipName, Weight: ptr[float64](1)}, + {Type: identityName, Weight: ptr(0.5)}, + {Type: wildcardName, Weight: ptr[float64](0)}, + }, + assertWeight: assert.True, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + aes, hasWeight := parseAcceptEncoding(test.values) + + assert.Equal(t, test.expected, aes) + test.assertWeight(t, hasWeight) + }) + } +} + +func ptr[T any](t T) *T { + return &t +} diff --git a/pkg/middlewares/compress/compress.go b/pkg/middlewares/compress/compress.go index bde909978..3ccac9f2d 100644 --- a/pkg/middlewares/compress/compress.go +++ b/pkg/middlewares/compress/compress.go @@ -7,12 +7,10 @@ import ( "mime" "net/http" "slices" - "strings" "github.com/klauspost/compress/gzhttp" "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/middlewares" - "github.com/traefik/traefik/v3/pkg/middlewares/compress/brotli" "go.opentelemetry.io/otel/trace" ) @@ -24,14 +22,16 @@ const DefaultMinSize = 1024 // Compress is a middleware that allows to compress the response. type compress struct { - next http.Handler - name string - excludes []string - includes []string - minSize int + next http.Handler + name string + excludes []string + includes []string + minSize int + defaultEncoding string brotliHandler http.Handler gzipHandler http.Handler + zstdHandler http.Handler } // New creates a new compress middleware. @@ -68,15 +68,22 @@ func New(ctx context.Context, next http.Handler, conf dynamic.Compress, name str } c := &compress{ - next: next, - name: name, - excludes: excludes, - includes: includes, - minSize: minSize, + next: next, + name: name, + excludes: excludes, + includes: includes, + minSize: minSize, + defaultEncoding: conf.DefaultEncoding, } var err error - c.brotliHandler, err = c.newBrotliHandler() + + c.zstdHandler, err = c.newCompressionHandler(zstdName, name) + if err != nil { + return nil, err + } + + c.brotliHandler, err = c.newCompressionHandler(brotliName, name) if err != nil { return nil, err } @@ -109,25 +116,35 @@ func (c *compress) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } - // Client doesn't specify a preferred encoding, for compatibility don't encode the request - // See https://github.com/traefik/traefik/issues/9734 - acceptEncoding, ok := req.Header["Accept-Encoding"] + acceptEncoding, ok := req.Header[acceptEncodingHeader] if !ok { + if c.defaultEncoding != "" { + // RFC says: "If no Accept-Encoding header field is in the request, any content coding is considered acceptable by the user agent." + // https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding + c.chooseHandler(c.defaultEncoding, rw, req) + return + } + + // Client doesn't specify a preferred encoding, for compatibility don't encode the request + // See https://github.com/traefik/traefik/issues/9734 c.next.ServeHTTP(rw, req) return } - if encodingAccepts(acceptEncoding, "br") { + c.chooseHandler(getCompressionType(acceptEncoding, c.defaultEncoding), rw, req) +} + +func (c *compress) chooseHandler(typ string, rw http.ResponseWriter, req *http.Request) { + switch typ { + case zstdName: + c.zstdHandler.ServeHTTP(rw, req) + case brotliName: c.brotliHandler.ServeHTTP(rw, req) - return - } - - if encodingAccepts(acceptEncoding, "gzip") { + case gzipName: c.gzipHandler.ServeHTTP(rw, req) - return + default: + c.next.ServeHTTP(rw, req) } - - c.next.ServeHTTP(rw, req) } func (c *compress) GetTracingInformation() (string, string, trace.SpanKind) { @@ -157,34 +174,13 @@ func (c *compress) newGzipHandler() (http.Handler, error) { return wrapper(c.next), nil } -func (c *compress) newBrotliHandler() (http.Handler, error) { - cfg := brotli.Config{MinSize: c.minSize} +func (c *compress) newCompressionHandler(algo string, middlewareName string) (http.Handler, error) { + cfg := Config{MinSize: c.minSize, Algorithm: algo, MiddlewareName: middlewareName} if len(c.includes) > 0 { cfg.IncludedContentTypes = c.includes } else { cfg.ExcludedContentTypes = c.excludes } - wrapper, err := brotli.NewWrapper(cfg) - if err != nil { - return nil, fmt.Errorf("new brotli wrapper: %w", err) - } - - return wrapper(c.next), nil -} - -func encodingAccepts(acceptEncoding []string, typ string) bool { - for _, ae := range acceptEncoding { - for _, e := range strings.Split(ae, ",") { - parsed := strings.Split(strings.TrimSpace(e), ";") - if len(parsed) == 0 { - continue - } - if parsed[0] == typ || parsed[0] == "*" { - return true - } - } - } - - return false + return NewCompressionHandler(cfg, c.next) } diff --git a/pkg/middlewares/compress/compress_test.go b/pkg/middlewares/compress/compress_test.go index 1ea6afb45..af127df0c 100644 --- a/pkg/middlewares/compress/compress_test.go +++ b/pkg/middlewares/compress/compress_test.go @@ -18,12 +18,9 @@ import ( ) const ( - acceptEncodingHeader = "Accept-Encoding" contentEncodingHeader = "Content-Encoding" contentTypeHeader = "Content-Type" varyHeader = "Vary" - gzipValue = "gzip" - brotliValue = "br" ) func TestNegotiation(t *testing.T) { @@ -44,32 +41,52 @@ func TestNegotiation(t *testing.T) { { desc: "accept any header", acceptEncHeader: "*", - expEncoding: "br", + expEncoding: brotliName, }, { desc: "gzip accept header", acceptEncHeader: "gzip", - expEncoding: "gzip", + expEncoding: gzipName, }, { desc: "br accept header", acceptEncHeader: "br", - expEncoding: "br", + expEncoding: brotliName, }, { desc: "multi accept header, prefer br", acceptEncHeader: "br;q=0.8, gzip;q=0.6", - expEncoding: "br", + expEncoding: brotliName, }, { - desc: "multi accept header, prefer br", + desc: "multi accept header, prefer gzip", acceptEncHeader: "gzip;q=1.0, br;q=0.8", - expEncoding: "br", + expEncoding: gzipName, }, { desc: "multi accept header list, prefer br", acceptEncHeader: "gzip, br", - expEncoding: "br", + expEncoding: brotliName, + }, + { + desc: "zstd accept header", + acceptEncHeader: "zstd", + expEncoding: zstdName, + }, + { + desc: "multi accept header, prefer zstd", + acceptEncHeader: "zstd;q=0.9, br;q=0.8, gzip;q=0.6", + expEncoding: zstdName, + }, + { + desc: "multi accept header, prefer gzip", + acceptEncHeader: "gzip;q=1.0, br;q=0.8, zstd;q=0.7", + expEncoding: gzipName, + }, + { + desc: "multi accept header list, prefer zstd", + acceptEncHeader: "gzip, br, zstd", + expEncoding: zstdName, }, } @@ -98,7 +115,7 @@ func TestNegotiation(t *testing.T) { func TestShouldCompressWhenNoContentEncodingHeader(t *testing.T) { req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) - req.Header.Add(acceptEncodingHeader, gzipValue) + req.Header.Add(acceptEncodingHeader, gzipName) baseBody := generateBytes(gzhttp.DefaultMinSize) @@ -112,7 +129,7 @@ func TestShouldCompressWhenNoContentEncodingHeader(t *testing.T) { rw := httptest.NewRecorder() handler.ServeHTTP(rw, req) - assert.Equal(t, gzipValue, rw.Header().Get(contentEncodingHeader)) + assert.Equal(t, gzipName, rw.Header().Get(contentEncodingHeader)) assert.Equal(t, acceptEncodingHeader, rw.Header().Get(varyHeader)) gr, err := gzip.NewReader(rw.Body) @@ -125,11 +142,11 @@ func TestShouldCompressWhenNoContentEncodingHeader(t *testing.T) { func TestShouldNotCompressWhenContentEncodingHeader(t *testing.T) { req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) - req.Header.Add(acceptEncodingHeader, gzipValue) + req.Header.Add(acceptEncodingHeader, gzipName) fakeCompressedBody := generateBytes(gzhttp.DefaultMinSize) next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.Header().Add(contentEncodingHeader, gzipValue) + rw.Header().Add(contentEncodingHeader, gzipName) rw.Header().Add(varyHeader, acceptEncodingHeader) _, err := rw.Write(fakeCompressedBody) if err != nil { @@ -142,7 +159,7 @@ func TestShouldNotCompressWhenContentEncodingHeader(t *testing.T) { rw := httptest.NewRecorder() handler.ServeHTTP(rw, req) - assert.Equal(t, gzipValue, rw.Header().Get(contentEncodingHeader)) + assert.Equal(t, gzipName, rw.Header().Get(contentEncodingHeader)) assert.Equal(t, acceptEncodingHeader, rw.Header().Get(varyHeader)) assert.EqualValues(t, rw.Body.Bytes(), fakeCompressedBody) @@ -225,7 +242,7 @@ func TestShouldNotCompressWhenEmptyAcceptEncodingHeader(t *testing.T) { func TestShouldNotCompressHeadRequest(t *testing.T) { req := testhelpers.MustNewRequest(http.MethodHead, "http://localhost", nil) - req.Header.Add(acceptEncodingHeader, gzipValue) + req.Header.Add(acceptEncodingHeader, gzipName) fakeBody := generateBytes(gzhttp.DefaultMinSize) next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { @@ -301,7 +318,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) { t.Parallel() req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) - req.Header.Add(acceptEncodingHeader, gzipValue) + req.Header.Add(acceptEncodingHeader, gzipName) if test.reqContentType != "" { req.Header.Add(contentTypeHeader, test.reqContentType) } @@ -352,7 +369,7 @@ func TestShouldCompressWhenSpecificContentType(t *testing.T) { t.Parallel() req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) - req.Header.Add(acceptEncodingHeader, gzipValue) + req.Header.Add(acceptEncodingHeader, gzipName) next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { rw.Header().Set(contentTypeHeader, test.respContentType) @@ -368,7 +385,7 @@ func TestShouldCompressWhenSpecificContentType(t *testing.T) { rw := httptest.NewRecorder() handler.ServeHTTP(rw, req) - assert.Equal(t, gzipValue, rw.Header().Get(contentEncodingHeader)) + assert.Equal(t, gzipName, rw.Header().Get(contentEncodingHeader)) assert.Equal(t, acceptEncodingHeader, rw.Header().Get(varyHeader)) assert.NotEqualValues(t, rw.Body.Bytes(), baseBody) }) @@ -386,7 +403,7 @@ func TestIntegrationShouldNotCompress(t *testing.T) { { name: "when content already compressed", handler: http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.Header().Add(contentEncodingHeader, gzipValue) + rw.Header().Add(contentEncodingHeader, gzipName) rw.Header().Add(varyHeader, acceptEncodingHeader) _, err := rw.Write(fakeCompressedBody) if err != nil { @@ -398,7 +415,7 @@ func TestIntegrationShouldNotCompress(t *testing.T) { { name: "when content already compressed and status code Created", handler: http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.Header().Add(contentEncodingHeader, gzipValue) + rw.Header().Add(contentEncodingHeader, gzipName) rw.Header().Add(varyHeader, acceptEncodingHeader) rw.WriteHeader(http.StatusCreated) _, err := rw.Write(fakeCompressedBody) @@ -419,14 +436,14 @@ func TestIntegrationShouldNotCompress(t *testing.T) { defer ts.Close() req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil) - req.Header.Add(acceptEncodingHeader, gzipValue) + req.Header.Add(acceptEncodingHeader, gzipName) resp, err := http.DefaultClient.Do(req) require.NoError(t, err) assert.Equal(t, test.expectedStatusCode, resp.StatusCode) - assert.Equal(t, gzipValue, resp.Header.Get(contentEncodingHeader)) + assert.Equal(t, gzipName, resp.Header.Get(contentEncodingHeader)) assert.Equal(t, acceptEncodingHeader, resp.Header.Get(varyHeader)) body, err := io.ReadAll(resp.Body) @@ -438,7 +455,7 @@ func TestIntegrationShouldNotCompress(t *testing.T) { func TestShouldWriteHeaderWhenFlush(t *testing.T) { next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.Header().Add(contentEncodingHeader, gzipValue) + rw.Header().Add(contentEncodingHeader, gzipName) rw.Header().Add(varyHeader, acceptEncodingHeader) rw.WriteHeader(http.StatusUnauthorized) rw.(http.Flusher).Flush() @@ -454,14 +471,14 @@ func TestShouldWriteHeaderWhenFlush(t *testing.T) { defer ts.Close() req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil) - req.Header.Add(acceptEncodingHeader, gzipValue) + req.Header.Add(acceptEncodingHeader, gzipName) resp, err := http.DefaultClient.Do(req) require.NoError(t, err) assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - assert.Equal(t, gzipValue, resp.Header.Get(contentEncodingHeader)) + assert.Equal(t, gzipName, resp.Header.Get(contentEncodingHeader)) assert.Equal(t, acceptEncodingHeader, resp.Header.Get(varyHeader)) } @@ -505,14 +522,14 @@ func TestIntegrationShouldCompress(t *testing.T) { defer ts.Close() req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil) - req.Header.Add(acceptEncodingHeader, gzipValue) + req.Header.Add(acceptEncodingHeader, gzipName) resp, err := http.DefaultClient.Do(req) require.NoError(t, err) assert.Equal(t, test.expectedStatusCode, resp.StatusCode) - assert.Equal(t, gzipValue, resp.Header.Get(contentEncodingHeader)) + assert.Equal(t, gzipName, resp.Header.Get(contentEncodingHeader)) assert.Equal(t, acceptEncodingHeader, resp.Header.Get(varyHeader)) body, err := io.ReadAll(resp.Body) @@ -547,7 +564,7 @@ func TestMinResponseBodyBytes(t *testing.T) { t.Parallel() req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) - req.Header.Add(acceptEncodingHeader, gzipValue) + req.Header.Add(acceptEncodingHeader, gzipName) next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if _, err := rw.Write(fakeBody); err != nil { @@ -562,7 +579,7 @@ func TestMinResponseBodyBytes(t *testing.T) { handler.ServeHTTP(rw, req) if test.expectedCompression { - assert.Equal(t, gzipValue, rw.Header().Get(contentEncodingHeader)) + assert.Equal(t, gzipName, rw.Header().Get(contentEncodingHeader)) assert.NotEqualValues(t, rw.Body.Bytes(), fakeBody) return } @@ -636,7 +653,7 @@ func Test1xxResponses(t *testing.T) { }, } req, _ := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), http.MethodGet, server.URL, nil) - req.Header.Add(acceptEncodingHeader, gzipValue) + req.Header.Add(acceptEncodingHeader, gzipName) res, err := frontendClient.Do(req) assert.NoError(t, err) @@ -648,7 +665,7 @@ func Test1xxResponses(t *testing.T) { } checkLinkHeaders(t, []string{"; rel=preload; as=style", "; rel=preload; as=script", "; rel=preload; as=script"}, res.Header["Link"]) - assert.Equal(t, gzipValue, res.Header.Get(contentEncodingHeader)) + assert.Equal(t, gzipName, res.Header.Get(contentEncodingHeader)) body, _ := io.ReadAll(res.Body) assert.NotEqualValues(t, body, fakeBody) } @@ -730,7 +747,7 @@ func runBenchmark(b *testing.B, req *http.Request, handler http.Handler) { b.Fatalf("Expected 200 but got %d", code) } - assert.Equal(b, gzipValue, res.Header().Get(contentEncodingHeader)) + assert.Equal(b, gzipName, res.Header().Get(contentEncodingHeader)) } func generateBytes(length int) []byte { diff --git a/pkg/middlewares/compress/brotli/brotli.go b/pkg/middlewares/compress/compression_handler.go similarity index 71% rename from pkg/middlewares/compress/brotli/brotli.go rename to pkg/middlewares/compress/compression_handler.go index 17225f252..78214acc5 100644 --- a/pkg/middlewares/compress/brotli/brotli.go +++ b/pkg/middlewares/compress/compression_handler.go @@ -1,4 +1,4 @@ -package brotli +package compress import ( "bufio" @@ -10,6 +10,9 @@ import ( "net/http" "github.com/andybalholm/brotli" + "github.com/klauspost/compress/zstd" + "github.com/traefik/traefik/v3/pkg/middlewares" + "github.com/traefik/traefik/v3/pkg/middlewares/observability" ) const ( @@ -30,10 +33,26 @@ type Config struct { IncludedContentTypes []string // MinSize is the minimum size (in bytes) required to enable compression. MinSize int + // Algorithm used for the compression (currently Brotli and Zstandard) + Algorithm string + // MiddlewareName use for logging purposes + MiddlewareName string } -// NewWrapper returns a new Brotli compressing wrapper. -func NewWrapper(cfg Config) (func(http.Handler) http.HandlerFunc, error) { +// CompressionHandler handles Brolti and Zstd compression. +type CompressionHandler struct { + cfg Config + excludedContentTypes []parsedContentType + includedContentTypes []parsedContentType + next http.Handler +} + +// NewCompressionHandler returns a new compressing handler. +func NewCompressionHandler(cfg Config, next http.Handler) (http.Handler, error) { + if cfg.Algorithm == "" { + return nil, errors.New("compression algorithm undefined") + } + if cfg.MinSize < 0 { return nil, errors.New("minimum size must be greater than or equal to zero") } @@ -62,30 +81,89 @@ func NewWrapper(cfg Config) (func(http.Handler) http.HandlerFunc, error) { includedContentTypes = append(includedContentTypes, parsedContentType{mediaType, params}) } - return func(h http.Handler) http.HandlerFunc { - return func(rw http.ResponseWriter, r *http.Request) { - rw.Header().Add(vary, acceptEncoding) - - brw := &responseWriter{ - rw: rw, - bw: brotli.NewWriter(rw), - minSize: cfg.MinSize, - statusCode: http.StatusOK, - excludedContentTypes: excludedContentTypes, - includedContentTypes: includedContentTypes, - } - defer brw.close() - - h.ServeHTTP(brw, r) - } + return &CompressionHandler{ + cfg: cfg, + excludedContentTypes: excludedContentTypes, + includedContentTypes: includedContentTypes, + next: next, }, nil } +func (c *CompressionHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + rw.Header().Add(vary, acceptEncoding) + + compressionWriter, err := newCompressionWriter(c.cfg.Algorithm, rw) + if err != nil { + logger := middlewares.GetLogger(r.Context(), c.cfg.MiddlewareName, typeName) + logMessage := fmt.Sprintf("create compression handler: %v", err) + logger.Debug().Msg(logMessage) + observability.SetStatusErrorf(r.Context(), logMessage) + rw.WriteHeader(http.StatusInternalServerError) + return + } + + responseWriter := &responseWriter{ + rw: rw, + compressionWriter: compressionWriter, + minSize: c.cfg.MinSize, + statusCode: http.StatusOK, + excludedContentTypes: c.excludedContentTypes, + includedContentTypes: c.includedContentTypes, + } + defer responseWriter.close() + + c.next.ServeHTTP(responseWriter, r) +} + +type compression interface { + // Write data to the encoder. + // Input data will be buffered and as the buffer fills up + // content will be compressed and written to the output. + // When done writing, use Close to flush the remaining output + // and write CRC if requested. + Write(p []byte) (n int, err error) + // Flush will send the currently written data to output + // and block until everything has been written. + // This should only be used on rare occasions where pushing the currently queued data is critical. + Flush() error + // Close closes the underlying writers if/when appropriate. + // Note that the compressed writer should not be closed if we never used it, + // as it would otherwise send some extra "end of compression" bytes. + // Close also makes sure to flush whatever was left to write from the buffer. + Close() error +} + +type compressionWriter struct { + compression + alg string +} + +func newCompressionWriter(algo string, in io.Writer) (*compressionWriter, error) { + switch algo { + case brotliName: + return &compressionWriter{compression: brotli.NewWriter(in), alg: algo}, nil + + case zstdName: + writer, err := zstd.NewWriter(in) + if err != nil { + return nil, fmt.Errorf("creating zstd writer: %w", err) + } + return &compressionWriter{compression: writer, alg: algo}, nil + + default: + return nil, fmt.Errorf("unknown compression algo: %s", algo) + } +} + +func (c *compressionWriter) ContentEncoding() string { + return c.alg +} + // TODO: check whether we want to implement content-type sniffing (as gzip does) // TODO: check whether we should support Accept-Ranges (as gzip does, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges) type responseWriter struct { - rw http.ResponseWriter - bw *brotli.Writer + rw http.ResponseWriter + compressionWriter *compressionWriter minSize int excludedContentTypes []parsedContentType @@ -133,7 +211,7 @@ func (r *responseWriter) Write(p []byte) (int, error) { // We are now in compression cruise mode until the end of times. if r.compressionStarted { // If compressionStarted we assume we have sent headers already - return r.bw.Write(p) + return r.compressionWriter.Write(p) } // If we detect a contentEncoding, we know we are never going to compress. @@ -160,6 +238,7 @@ func (r *responseWriter) Write(p []byte) (int, error) { } if !found { r.compressionDisabled = true + r.rw.WriteHeader(r.statusCode) return r.rw.Write(p) } } @@ -167,6 +246,7 @@ func (r *responseWriter) Write(p []byte) (int, error) { for _, excludedContentType := range r.excludedContentTypes { if excludedContentType.equals(mediaType, params) { r.compressionDisabled = true + r.rw.WriteHeader(r.statusCode) return r.rw.Write(p) } } @@ -185,13 +265,13 @@ func (r *responseWriter) Write(p []byte) (int, error) { // Since we know we are going to compress we will never be able to know the actual length. r.rw.Header().Del(contentLength) - r.rw.Header().Set(contentEncoding, "br") + r.rw.Header().Set(contentEncoding, r.compressionWriter.ContentEncoding()) r.rw.WriteHeader(r.statusCode) r.headersSent = true // Start with sending what we have previously buffered, before actually writing // the bytes in argument. - n, err := r.bw.Write(r.buf) + n, err := r.compressionWriter.Write(r.buf) if err != nil { r.buf = r.buf[n:] // Return zero because we haven't taken care of the bytes in argument yet. @@ -210,7 +290,7 @@ func (r *responseWriter) Write(p []byte) (int, error) { r.buf = r.buf[:0] // Now that we emptied the buffer, we can actually write the given bytes. - return r.bw.Write(p) + return r.compressionWriter.Write(p) } // Flush flushes data to the appropriate underlying writer(s), although it does @@ -248,7 +328,7 @@ func (r *responseWriter) Flush() { // we have to do it ourselves. defer func() { // because we also ignore the error returned by Write anyway - _ = r.bw.Flush() + _ = r.compressionWriter.Flush() if rw, ok := r.rw.(http.Flusher); ok { rw.Flush() @@ -256,7 +336,7 @@ func (r *responseWriter) Flush() { }() // We empty whatever is left of the buffer that Write never took care of. - n, err := r.bw.Write(r.buf) + n, err := r.compressionWriter.Write(r.buf) if err != nil { return } @@ -311,7 +391,7 @@ func (r *responseWriter) close() error { if len(r.buf) == 0 { // If we got here we know compression has started, so we can safely flush on bw. - return r.bw.Close() + return r.compressionWriter.Close() } // There is still data in the buffer, because we never reached minSize (to @@ -329,16 +409,16 @@ func (r *responseWriter) close() error { // There is still data in the buffer, simply because Write did not take care of it all. // We flush it to the compressed writer. - n, err := r.bw.Write(r.buf) + n, err := r.compressionWriter.Write(r.buf) if err != nil { - r.bw.Close() + r.compressionWriter.Close() return err } if n < len(r.buf) { - r.bw.Close() + r.compressionWriter.Close() return io.ErrShortWrite } - return r.bw.Close() + return r.compressionWriter.Close() } // parsedContentType is the parsed representation of one of the inputs to ContentTypes. diff --git a/pkg/middlewares/compress/brotli/brotli_test.go b/pkg/middlewares/compress/compression_handler_test.go similarity index 57% rename from pkg/middlewares/compress/brotli/brotli_test.go rename to pkg/middlewares/compress/compression_handler_test.go index 2e1ef3e17..1df9e9588 100644 --- a/pkg/middlewares/compress/brotli/brotli_test.go +++ b/pkg/middlewares/compress/compression_handler_test.go @@ -1,4 +1,4 @@ -package brotli +package compress import ( "bytes" @@ -9,6 +9,7 @@ import ( "testing" "github.com/andybalholm/brotli" + "github.com/klauspost/compress/zstd" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -19,44 +20,107 @@ var ( ) func Test_Vary(t *testing.T) { - h := newTestHandler(t, smallTestBody) + testCases := []struct { + desc string + h http.Handler + acceptEncoding string + }{ + { + desc: "brotli", + h: newTestBrotliHandler(t, smallTestBody), + acceptEncoding: "br", + }, + { + desc: "zstd", + h: newTestZstandardHandler(t, smallTestBody), + acceptEncoding: "zstd", + }, + } - req, _ := http.NewRequest(http.MethodGet, "/whatever", nil) - req.Header.Set(acceptEncoding, "br") + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() - rw := httptest.NewRecorder() - h.ServeHTTP(rw, req) + req, _ := http.NewRequest(http.MethodGet, "/whatever", nil) + req.Header.Set(acceptEncoding, test.acceptEncoding) - assert.Equal(t, http.StatusAccepted, rw.Code) - assert.Equal(t, acceptEncoding, rw.Header().Get(vary)) + rw := httptest.NewRecorder() + test.h.ServeHTTP(rw, req) + + assert.Equal(t, http.StatusAccepted, rw.Code) + assert.Equal(t, acceptEncoding, rw.Header().Get(vary)) + }) + } } func Test_SmallBodyNoCompression(t *testing.T) { - h := newTestHandler(t, smallTestBody) + testCases := []struct { + desc string + h http.Handler + acceptEncoding string + }{ + { + desc: "brotli", + h: newTestBrotliHandler(t, smallTestBody), + acceptEncoding: "br", + }, + { + desc: "zstd", + h: newTestZstandardHandler(t, smallTestBody), + acceptEncoding: "zstd", + }, + } - req, _ := http.NewRequest(http.MethodGet, "/whatever", nil) - req.Header.Set(acceptEncoding, "br") + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() - rw := httptest.NewRecorder() - h.ServeHTTP(rw, req) + req, _ := http.NewRequest(http.MethodGet, "/whatever", nil) + req.Header.Set(acceptEncoding, test.acceptEncoding) - // With less than 1024 bytes the response should not be compressed. - assert.Equal(t, http.StatusAccepted, rw.Code) - assert.Empty(t, rw.Header().Get(contentEncoding)) - assert.Equal(t, smallTestBody, rw.Body.Bytes()) + rw := httptest.NewRecorder() + test.h.ServeHTTP(rw, req) + + // With less than 1024 bytes the response should not be compressed. + assert.Equal(t, http.StatusAccepted, rw.Code) + assert.Empty(t, rw.Header().Get(contentEncoding)) + assert.Equal(t, smallTestBody, rw.Body.Bytes()) + }) + } } func Test_AlreadyCompressed(t *testing.T) { - h := newTestHandler(t, bigTestBody) + testCases := []struct { + desc string + h http.Handler + acceptEncoding string + }{ + { + desc: "brotli", + h: newTestBrotliHandler(t, bigTestBody), + acceptEncoding: "br", + }, + { + desc: "zstd", + h: newTestZstandardHandler(t, bigTestBody), + acceptEncoding: "zstd", + }, + } - req, _ := http.NewRequest(http.MethodGet, "/compressed", nil) - req.Header.Set(acceptEncoding, "br") + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() - rw := httptest.NewRecorder() - h.ServeHTTP(rw, req) + req, _ := http.NewRequest(http.MethodGet, "/compressed", nil) + req.Header.Set(acceptEncoding, test.acceptEncoding) - assert.Equal(t, http.StatusAccepted, rw.Code) - assert.Equal(t, bigTestBody, rw.Body.Bytes()) + rw := httptest.NewRecorder() + test.h.ServeHTTP(rw, req) + + assert.Equal(t, http.StatusAccepted, rw.Code) + assert.Equal(t, bigTestBody, rw.Body.Bytes()) + }) + } } func Test_NoBody(t *testing.T) { @@ -91,15 +155,17 @@ func Test_NoBody(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - h := mustNewWrapper(t, Config{MinSize: 1024})(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(test.statusCode) _, err := rw.Write(test.body) require.NoError(t, err) - })) + }) + + h := mustNewCompressionHandler(t, Config{MinSize: 1024, Algorithm: zstdName}, next) req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set(acceptEncoding, "br") + req.Header.Set(acceptEncoding, "zstd") rw := httptest.NewRecorder() h.ServeHTTP(rw, req) @@ -115,24 +181,26 @@ func Test_NoBody(t *testing.T) { func Test_MinSize(t *testing.T) { cfg := Config{ - MinSize: 128, + MinSize: 128, + Algorithm: zstdName, } var bodySize int - h := mustNewWrapper(t, cfg)(http.HandlerFunc( - func(rw http.ResponseWriter, req *http.Request) { - for i := 0; i < bodySize; i++ { - // We make sure to Write at least once less than minSize so that both - // cases below go through the same algo: i.e. they start buffering - // because they haven't reached minSize. - _, err := rw.Write([]byte{'x'}) - require.NoError(t, err) - } - }, - )) + + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + for range bodySize { + // We make sure to Write at least once less than minSize so that both + // cases below go through the same algo: i.e. they start buffering + // because they haven't reached minSize. + _, err := rw.Write([]byte{'x'}) + require.NoError(t, err) + } + }) + + h := mustNewCompressionHandler(t, cfg, next) req, _ := http.NewRequest(http.MethodGet, "/whatever", &bytes.Buffer{}) - req.Header.Add(acceptEncoding, "br") + req.Header.Add(acceptEncoding, "zstd") // Short response is not compressed bodySize = cfg.MinSize - 1 @@ -146,18 +214,20 @@ func Test_MinSize(t *testing.T) { rw = httptest.NewRecorder() h.ServeHTTP(rw, req) - assert.Equal(t, "br", rw.Result().Header.Get(contentEncoding)) + assert.Equal(t, "zstd", rw.Result().Header.Get(contentEncoding)) } func Test_MultipleWriteHeader(t *testing.T) { - h := mustNewWrapper(t, Config{MinSize: 1024})(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { // We ensure that the subsequent call to WriteHeader is a noop. rw.WriteHeader(http.StatusInternalServerError) rw.WriteHeader(http.StatusNotFound) - })) + }) + + h := mustNewCompressionHandler(t, Config{MinSize: 1024, Algorithm: zstdName}, next) req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set(acceptEncoding, "br") + req.Header.Set(acceptEncoding, "zstd") rw := httptest.NewRecorder() h.ServeHTTP(rw, req) @@ -166,121 +236,255 @@ func Test_MultipleWriteHeader(t *testing.T) { } func Test_FlushBeforeWrite(t *testing.T) { - srv := httptest.NewServer(mustNewWrapper(t, Config{MinSize: 1024})(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) - rw.(http.Flusher).Flush() + testCases := []struct { + desc string + cfg Config + readerBuilder func(io.Reader) (io.Reader, error) + acceptEncoding string + }{ + { + desc: "brotli", + cfg: Config{MinSize: 1024, Algorithm: brotliName, MiddlewareName: "Test"}, + readerBuilder: func(reader io.Reader) (io.Reader, error) { + return brotli.NewReader(reader), nil + }, + acceptEncoding: "br", + }, + { + desc: "zstd", + cfg: Config{MinSize: 1024, Algorithm: zstdName, MiddlewareName: "Test"}, + readerBuilder: func(reader io.Reader) (io.Reader, error) { + return zstd.NewReader(reader) + }, + acceptEncoding: "zstd", + }, + } - _, err := rw.Write(bigTestBody) - require.NoError(t, err) - }))) - defer srv.Close() + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() - req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) - require.NoError(t, err) + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + rw.(http.Flusher).Flush() - req.Header.Set(acceptEncoding, "br") + _, err := rw.Write(bigTestBody) + require.NoError(t, err) + }) - res, err := http.DefaultClient.Do(req) - require.NoError(t, err) + srv := httptest.NewServer(mustNewCompressionHandler(t, test.cfg, next)) + defer srv.Close() - defer res.Body.Close() + req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) + require.NoError(t, err) - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, "br", res.Header.Get(contentEncoding)) + req.Header.Set(acceptEncoding, test.acceptEncoding) - got, err := io.ReadAll(brotli.NewReader(res.Body)) - require.NoError(t, err) - assert.Equal(t, bigTestBody, got) + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + defer res.Body.Close() + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, test.acceptEncoding, res.Header.Get(contentEncoding)) + + reader, err := test.readerBuilder(res.Body) + require.NoError(t, err) + + got, err := io.ReadAll(reader) + require.NoError(t, err) + assert.Equal(t, bigTestBody, got) + }) + } } func Test_FlushAfterWrite(t *testing.T) { - srv := httptest.NewServer(mustNewWrapper(t, Config{MinSize: 1024})(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) + testCases := []struct { + desc string + cfg Config + readerBuilder func(io.Reader) (io.Reader, error) + acceptEncoding string + }{ + { + desc: "brotli", + cfg: Config{MinSize: 1024, Algorithm: brotliName, MiddlewareName: "Test"}, + readerBuilder: func(reader io.Reader) (io.Reader, error) { + return brotli.NewReader(reader), nil + }, + acceptEncoding: "br", + }, + { + desc: "zstd", + cfg: Config{MinSize: 1024, Algorithm: zstdName, MiddlewareName: "Test"}, + readerBuilder: func(reader io.Reader) (io.Reader, error) { + return zstd.NewReader(reader) + }, + acceptEncoding: "zstd", + }, + } - _, err := rw.Write(bigTestBody[0:1]) - require.NoError(t, err) + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) - rw.(http.Flusher).Flush() - for _, b := range bigTestBody[1:] { - _, err := rw.Write([]byte{b}) + _, err := rw.Write(bigTestBody[0:1]) + require.NoError(t, err) + + rw.(http.Flusher).Flush() + for _, b := range bigTestBody[1:] { + _, err := rw.Write([]byte{b}) + require.NoError(t, err) + } + }) + + srv := httptest.NewServer(mustNewCompressionHandler(t, test.cfg, next)) + defer srv.Close() + + req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) require.NoError(t, err) - } - }))) - defer srv.Close() - req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) - require.NoError(t, err) + req.Header.Set(acceptEncoding, test.acceptEncoding) - req.Header.Set(acceptEncoding, "br") + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) - res, err := http.DefaultClient.Do(req) - require.NoError(t, err) + defer res.Body.Close() - defer res.Body.Close() + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, test.acceptEncoding, res.Header.Get(contentEncoding)) - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, "br", res.Header.Get(contentEncoding)) + reader, err := test.readerBuilder(res.Body) + require.NoError(t, err) - got, err := io.ReadAll(brotli.NewReader(res.Body)) - require.NoError(t, err) - assert.Equal(t, bigTestBody, got) + got, err := io.ReadAll(reader) + require.NoError(t, err) + assert.Equal(t, bigTestBody, got) + }) + } } func Test_FlushAfterWriteNil(t *testing.T) { - srv := httptest.NewServer(mustNewWrapper(t, Config{MinSize: 1024})(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) + testCases := []struct { + desc string + cfg Config + readerBuilder func(io.Reader) (io.Reader, error) + acceptEncoding string + }{ + { + desc: "brotli", + cfg: Config{MinSize: 1024, Algorithm: brotliName, MiddlewareName: "Test"}, + readerBuilder: func(reader io.Reader) (io.Reader, error) { + return brotli.NewReader(reader), nil + }, + acceptEncoding: "br", + }, + { + desc: "zstd", + cfg: Config{MinSize: 1024, Algorithm: zstdName, MiddlewareName: "Test"}, + readerBuilder: func(reader io.Reader) (io.Reader, error) { + return zstd.NewReader(reader) + }, + acceptEncoding: "zstd", + }, + } - _, err := rw.Write(nil) - require.NoError(t, err) + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) - rw.(http.Flusher).Flush() - }))) - defer srv.Close() + _, err := rw.Write(nil) + require.NoError(t, err) - req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) - require.NoError(t, err) + rw.(http.Flusher).Flush() + }) - req.Header.Set(acceptEncoding, "br") + srv := httptest.NewServer(mustNewCompressionHandler(t, test.cfg, next)) + defer srv.Close() - res, err := http.DefaultClient.Do(req) - require.NoError(t, err) + req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) + require.NoError(t, err) - defer res.Body.Close() + req.Header.Set(acceptEncoding, test.acceptEncoding) - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Empty(t, res.Header.Get(contentEncoding)) + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) - got, err := io.ReadAll(brotli.NewReader(res.Body)) - require.NoError(t, err) - assert.Empty(t, got) + defer res.Body.Close() + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Empty(t, res.Header.Get(contentEncoding)) + + reader, err := test.readerBuilder(res.Body) + require.NoError(t, err) + + got, err := io.ReadAll(reader) + require.NoError(t, err) + assert.Empty(t, got) + }) + } } func Test_FlushAfterAllWrites(t *testing.T) { - srv := httptest.NewServer(mustNewWrapper(t, Config{MinSize: 1024})(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - for i := range bigTestBody { - _, err := rw.Write(bigTestBody[i : i+1]) + testCases := []struct { + desc string + cfg Config + readerBuilder func(io.Reader) (io.Reader, error) + acceptEncoding string + }{ + { + desc: "brotli", + cfg: Config{MinSize: 1024, Algorithm: brotliName, MiddlewareName: "Test"}, + readerBuilder: func(reader io.Reader) (io.Reader, error) { + return brotli.NewReader(reader), nil + }, + acceptEncoding: "br", + }, + { + desc: "zstd", + cfg: Config{MinSize: 1024, Algorithm: zstdName, MiddlewareName: "Test"}, + readerBuilder: func(reader io.Reader) (io.Reader, error) { + return zstd.NewReader(reader) + }, + acceptEncoding: "zstd", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + for i := range bigTestBody { + _, err := rw.Write(bigTestBody[i : i+1]) + require.NoError(t, err) + } + rw.(http.Flusher).Flush() + }) + + srv := httptest.NewServer(mustNewCompressionHandler(t, test.cfg, next)) + defer srv.Close() + + req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) require.NoError(t, err) - } - rw.(http.Flusher).Flush() - }))) - defer srv.Close() - req, err := http.NewRequest(http.MethodGet, srv.URL, http.NoBody) - require.NoError(t, err) + req.Header.Set(acceptEncoding, test.acceptEncoding) - req.Header.Set(acceptEncoding, "br") + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) - res, err := http.DefaultClient.Do(req) - require.NoError(t, err) + defer res.Body.Close() - defer res.Body.Close() + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, test.acceptEncoding, res.Header.Get(contentEncoding)) - assert.Equal(t, http.StatusOK, res.StatusCode) - assert.Equal(t, "br", res.Header.Get(contentEncoding)) + reader, err := test.readerBuilder(res.Body) + require.NoError(t, err) - got, err := io.ReadAll(brotli.NewReader(res.Body)) - require.NoError(t, err) - assert.Equal(t, bigTestBody, got) + got, err := io.ReadAll(reader) + require.NoError(t, err) + assert.Equal(t, bigTestBody, got) + }) + } } func Test_ExcludedContentTypes(t *testing.T) { @@ -352,32 +556,39 @@ func Test_ExcludedContentTypes(t *testing.T) { cfg := Config{ MinSize: 1024, ExcludedContentTypes: test.excludedContentTypes, + Algorithm: zstdName, } - h := mustNewWrapper(t, cfg)(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set(contentType, test.contentType) - rw.WriteHeader(http.StatusOK) + rw.WriteHeader(http.StatusAccepted) _, err := rw.Write(bigTestBody) require.NoError(t, err) - })) + }) + + h := mustNewCompressionHandler(t, cfg, next) req, _ := http.NewRequest(http.MethodGet, "/whatever", nil) - req.Header.Set(acceptEncoding, "br") + req.Header.Set(acceptEncoding, zstdName) rw := httptest.NewRecorder() h.ServeHTTP(rw, req) - assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, http.StatusAccepted, rw.Code) if test.expCompression { - assert.Equal(t, "br", rw.Header().Get(contentEncoding)) + assert.Equal(t, zstdName, rw.Header().Get(contentEncoding)) - got, err := io.ReadAll(brotli.NewReader(rw.Body)) + reader, err := zstd.NewReader(rw.Body) + require.NoError(t, err) + + got, err := io.ReadAll(reader) assert.NoError(t, err) assert.Equal(t, bigTestBody, got) } else { - assert.NotEqual(t, "br", rw.Header().Get("Content-Encoding")) + assert.NotEqual(t, zstdName, rw.Header().Get("Content-Encoding")) got, err := io.ReadAll(rw.Body) assert.NoError(t, err) @@ -456,32 +667,39 @@ func Test_IncludedContentTypes(t *testing.T) { cfg := Config{ MinSize: 1024, IncludedContentTypes: test.includedContentTypes, + Algorithm: zstdName, } - h := mustNewWrapper(t, cfg)(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set(contentType, test.contentType) - rw.WriteHeader(http.StatusOK) + rw.WriteHeader(http.StatusAccepted) _, err := rw.Write(bigTestBody) require.NoError(t, err) - })) + }) + + h := mustNewCompressionHandler(t, cfg, next) req, _ := http.NewRequest(http.MethodGet, "/whatever", nil) - req.Header.Set(acceptEncoding, "br") + req.Header.Set(acceptEncoding, zstdName) rw := httptest.NewRecorder() h.ServeHTTP(rw, req) - assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, http.StatusAccepted, rw.Code) if test.expCompression { - assert.Equal(t, "br", rw.Header().Get(contentEncoding)) + assert.Equal(t, zstdName, rw.Header().Get(contentEncoding)) - got, err := io.ReadAll(brotli.NewReader(rw.Body)) + reader, err := zstd.NewReader(rw.Body) + require.NoError(t, err) + + got, err := io.ReadAll(reader) assert.NoError(t, err) assert.Equal(t, bigTestBody, got) } else { - assert.NotEqual(t, "br", rw.Header().Get("Content-Encoding")) + assert.NotEqual(t, zstdName, rw.Header().Get("Content-Encoding")) got, err := io.ReadAll(rw.Body) assert.NoError(t, err) @@ -560,8 +778,10 @@ func Test_FlushExcludedContentTypes(t *testing.T) { cfg := Config{ MinSize: 1024, ExcludedContentTypes: test.excludedContentTypes, + Algorithm: zstdName, } - h := mustNewWrapper(t, cfg)(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set(contentType, test.contentType) rw.WriteHeader(http.StatusOK) @@ -581,10 +801,12 @@ func Test_FlushExcludedContentTypes(t *testing.T) { rw.(http.Flusher).Flush() tb = tb[toWrite:] } - })) + }) + + h := mustNewCompressionHandler(t, cfg, next) req, _ := http.NewRequest(http.MethodGet, "/whatever", nil) - req.Header.Set(acceptEncoding, "br") + req.Header.Set(acceptEncoding, zstdName) // This doesn't allow checking flushes, but we validate if content is correct. rw := httptest.NewRecorder() @@ -593,13 +815,16 @@ func Test_FlushExcludedContentTypes(t *testing.T) { assert.Equal(t, http.StatusOK, rw.Code) if test.expCompression { - assert.Equal(t, "br", rw.Header().Get(contentEncoding)) + assert.Equal(t, zstdName, rw.Header().Get(contentEncoding)) - got, err := io.ReadAll(brotli.NewReader(rw.Body)) + reader, err := zstd.NewReader(rw.Body) + require.NoError(t, err) + + got, err := io.ReadAll(reader) assert.NoError(t, err) assert.Equal(t, bigTestBody, got) } else { - assert.NotEqual(t, "br", rw.Header().Get(contentEncoding)) + assert.NotEqual(t, zstdName, rw.Header().Get(contentEncoding)) got, err := io.ReadAll(rw.Body) assert.NoError(t, err) @@ -678,8 +903,10 @@ func Test_FlushIncludedContentTypes(t *testing.T) { cfg := Config{ MinSize: 1024, IncludedContentTypes: test.includedContentTypes, + Algorithm: zstdName, } - h := mustNewWrapper(t, cfg)(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.Header().Set(contentType, test.contentType) rw.WriteHeader(http.StatusOK) @@ -699,10 +926,12 @@ func Test_FlushIncludedContentTypes(t *testing.T) { rw.(http.Flusher).Flush() tb = tb[toWrite:] } - })) + }) + + h := mustNewCompressionHandler(t, cfg, next) req, _ := http.NewRequest(http.MethodGet, "/whatever", nil) - req.Header.Set(acceptEncoding, "br") + req.Header.Set(acceptEncoding, zstdName) // This doesn't allow checking flushes, but we validate if content is correct. rw := httptest.NewRecorder() @@ -711,13 +940,16 @@ func Test_FlushIncludedContentTypes(t *testing.T) { assert.Equal(t, http.StatusOK, rw.Code) if test.expCompression { - assert.Equal(t, "br", rw.Header().Get(contentEncoding)) + assert.Equal(t, zstdName, rw.Header().Get(contentEncoding)) - got, err := io.ReadAll(brotli.NewReader(rw.Body)) + reader, err := zstd.NewReader(rw.Body) + require.NoError(t, err) + + got, err := io.ReadAll(reader) assert.NoError(t, err) assert.Equal(t, bigTestBody, got) } else { - assert.NotEqual(t, "br", rw.Header().Get(contentEncoding)) + assert.NotEqual(t, zstdName, rw.Header().Get(contentEncoding)) got, err := io.ReadAll(rw.Body) assert.NoError(t, err) @@ -727,32 +959,48 @@ func Test_FlushIncludedContentTypes(t *testing.T) { } } -func mustNewWrapper(t *testing.T, cfg Config) func(http.Handler) http.HandlerFunc { +func mustNewCompressionHandler(t *testing.T, cfg Config, next http.Handler) http.Handler { t.Helper() - w, err := NewWrapper(cfg) + w, err := NewCompressionHandler(cfg, next) require.NoError(t, err) return w } -func newTestHandler(t *testing.T, body []byte) http.Handler { +func newTestBrotliHandler(t *testing.T, body []byte) http.Handler { t.Helper() - return mustNewWrapper(t, Config{MinSize: 1024})( - http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - if req.URL.Path == "/compressed" { - rw.Header().Set("Content-Encoding", "br") - } + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/compressed" { + rw.Header().Set("Content-Encoding", brotliName) + } - rw.WriteHeader(http.StatusAccepted) - _, err := rw.Write(body) - require.NoError(t, err) - }), - ) + rw.WriteHeader(http.StatusAccepted) + _, err := rw.Write(body) + require.NoError(t, err) + }) + + return mustNewCompressionHandler(t, Config{MinSize: 1024, Algorithm: brotliName, MiddlewareName: "Compress"}, next) } -func TestParseContentType_equals(t *testing.T) { +func newTestZstandardHandler(t *testing.T, body []byte) http.Handler { + t.Helper() + + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/compressed" { + rw.Header().Set("Content-Encoding", zstdName) + } + + rw.WriteHeader(http.StatusAccepted) + _, err := rw.Write(body) + require.NoError(t, err) + }) + + return mustNewCompressionHandler(t, Config{MinSize: 1024, Algorithm: zstdName, MiddlewareName: "Compress"}, next) +} + +func Test_ParseContentType_equals(t *testing.T) { testCases := []struct { desc string pct parsedContentType diff --git a/pkg/middlewares/headermodifier/request_header_modifier.go b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go similarity index 93% rename from pkg/middlewares/headermodifier/request_header_modifier.go rename to pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go index 3b197c2d5..1f4536362 100644 --- a/pkg/middlewares/headermodifier/request_header_modifier.go +++ b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go @@ -22,7 +22,7 @@ type requestHeaderModifier struct { } // NewRequestHeaderModifier creates a new request header modifier middleware. -func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.RequestHeaderModifier, name string) (http.Handler, error) { +func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.RequestHeaderModifier, name string) http.Handler { logger := middlewares.GetLogger(ctx, name, typeName) logger.Debug().Msg("Creating middleware") @@ -32,7 +32,7 @@ func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dyn set: config.Set, add: config.Add, remove: config.Remove, - }, nil + } } func (r *requestHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) { diff --git a/pkg/middlewares/headermodifier/request_header_modifier_test.go b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go similarity index 94% rename from pkg/middlewares/headermodifier/request_header_modifier_test.go rename to pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go index 60c446ecc..d522d9850 100644 --- a/pkg/middlewares/headermodifier/request_header_modifier_test.go +++ b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/testhelpers" ) @@ -104,8 +103,7 @@ func TestRequestHeaderModifier(t *testing.T) { gotHeaders = r.Header }) - handler, err := NewRequestHeaderModifier(context.Background(), next, test.config, "foo-request-header-modifier") - require.NoError(t, err) + handler := NewRequestHeaderModifier(context.Background(), next, test.config, "foo-request-header-modifier") req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) if test.requestHeaders != nil { diff --git a/pkg/middlewares/gatewayapi/redirect/request_redirect.go b/pkg/middlewares/gatewayapi/redirect/request_redirect.go new file mode 100644 index 000000000..9258b652f --- /dev/null +++ b/pkg/middlewares/gatewayapi/redirect/request_redirect.go @@ -0,0 +1,103 @@ +package redirect + +import ( + "context" + "fmt" + "net" + "net/http" + "path" + "strings" + + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/middlewares" + "go.opentelemetry.io/otel/trace" +) + +const ( + typeName = "RequestRedirect" +) + +type redirect struct { + name string + next http.Handler + + scheme *string + hostname *string + port *string + path *string + pathPrefix *string + statusCode int +} + +// NewRequestRedirect creates a redirect middleware. +func NewRequestRedirect(ctx context.Context, next http.Handler, conf dynamic.RequestRedirect, name string) (http.Handler, error) { + logger := middlewares.GetLogger(ctx, name, typeName) + logger.Debug().Msg("Creating middleware") + + statusCode := conf.StatusCode + if statusCode == 0 { + statusCode = http.StatusFound + } + if statusCode != http.StatusMovedPermanently && statusCode != http.StatusFound { + return nil, fmt.Errorf("unsupported status code: %d", statusCode) + } + + return redirect{ + name: name, + next: next, + scheme: conf.Scheme, + hostname: conf.Hostname, + port: conf.Port, + path: conf.Path, + pathPrefix: conf.PathPrefix, + statusCode: statusCode, + }, nil +} + +func (r redirect) GetTracingInformation() (string, string, trace.SpanKind) { + return r.name, typeName, trace.SpanKindInternal +} + +func (r redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + redirectURL := *req.URL + redirectURL.Host = req.Host + + if r.scheme != nil { + redirectURL.Scheme = *r.scheme + } + + host := redirectURL.Hostname() + if r.hostname != nil { + host = *r.hostname + } + + port := redirectURL.Port() + if r.port != nil { + port = *r.port + } + + if port != "" { + host = net.JoinHostPort(host, port) + } + redirectURL.Host = host + + if r.path != nil && r.pathPrefix == nil { + redirectURL.Path = *r.path + } + + if r.path != nil && r.pathPrefix != nil { + redirectURL.Path = path.Join(*r.path, strings.TrimPrefix(req.URL.Path, *r.pathPrefix)) + + // add the trailing slash if needed, as path.Join removes trailing slashes. + if strings.HasSuffix(req.URL.Path, "/") && !strings.HasSuffix(redirectURL.Path, "/") { + redirectURL.Path += "/" + } + } + + rw.Header().Set("Location", redirectURL.String()) + + rw.WriteHeader(r.statusCode) + if _, err := rw.Write([]byte(http.StatusText(r.statusCode))); err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + } +} diff --git a/pkg/middlewares/gatewayapi/redirect/request_redirect_test.go b/pkg/middlewares/gatewayapi/redirect/request_redirect_test.go new file mode 100644 index 000000000..68eb19bd3 --- /dev/null +++ b/pkg/middlewares/gatewayapi/redirect/request_redirect_test.go @@ -0,0 +1,216 @@ +package redirect + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "k8s.io/utils/ptr" +) + +func TestRequestRedirectHandler(t *testing.T) { + testCases := []struct { + desc string + config dynamic.RequestRedirect + url string + wantURL string + wantStatus int + wantErr bool + }{ + { + desc: "wrong status code", + config: dynamic.RequestRedirect{ + Path: ptr.To("/baz"), + StatusCode: http.StatusOK, + }, + url: "http://foo.com:80/foo/bar", + wantErr: true, + }, + { + desc: "replace path", + config: dynamic.RequestRedirect{ + Path: ptr.To("/baz"), + }, + url: "http://foo.com:80/foo/bar", + wantURL: "http://foo.com:80/baz", + wantStatus: http.StatusFound, + }, + { + desc: "replace path without trailing slash", + config: dynamic.RequestRedirect{ + Path: ptr.To("/baz"), + }, + url: "http://foo.com:80/foo/bar/", + wantURL: "http://foo.com:80/baz", + wantStatus: http.StatusFound, + }, + { + desc: "replace path with trailing slash", + config: dynamic.RequestRedirect{ + Path: ptr.To("/baz/"), + }, + url: "http://foo.com:80/foo/bar", + wantURL: "http://foo.com:80/baz/", + wantStatus: http.StatusFound, + }, + { + desc: "only hostname", + config: dynamic.RequestRedirect{ + Hostname: ptr.To("bar.com"), + }, + url: "http://foo.com:8080/foo/", + wantURL: "http://bar.com:8080/foo/", + wantStatus: http.StatusFound, + }, + { + desc: "replace prefix path", + config: dynamic.RequestRedirect{ + Path: ptr.To("/baz"), + PathPrefix: ptr.To("/foo"), + }, + url: "http://foo.com:80/foo/bar", + wantURL: "http://foo.com:80/baz/bar", + wantStatus: http.StatusFound, + }, + { + desc: "replace prefix path with trailing slash", + config: dynamic.RequestRedirect{ + Path: ptr.To("/baz"), + PathPrefix: ptr.To("/foo"), + }, + url: "http://foo.com:80/foo/bar/", + wantURL: "http://foo.com:80/baz/bar/", + wantStatus: http.StatusFound, + }, + { + desc: "replace prefix path without slash prefix", + config: dynamic.RequestRedirect{ + Path: ptr.To("baz"), + PathPrefix: ptr.To("/foo"), + }, + url: "http://foo.com:80/foo/bar", + wantURL: "http://foo.com:80/baz/bar", + wantStatus: http.StatusFound, + }, + { + desc: "replace prefix path without slash prefix", + config: dynamic.RequestRedirect{ + Path: ptr.To("/baz"), + PathPrefix: ptr.To("/foo/"), + }, + url: "http://foo.com:80/foo/bar", + wantURL: "http://foo.com:80/baz/bar", + wantStatus: http.StatusFound, + }, + { + desc: "simple redirection", + config: dynamic.RequestRedirect{ + Scheme: ptr.To("https"), + Hostname: ptr.To("foobar.com"), + Port: ptr.To("443"), + }, + url: "http://foo.com:80", + wantURL: "https://foobar.com:443", + wantStatus: http.StatusFound, + }, + { + desc: "HTTP to HTTPS permanent", + config: dynamic.RequestRedirect{ + Scheme: ptr.To("https"), + StatusCode: http.StatusMovedPermanently, + }, + url: "http://foo", + wantURL: "https://foo", + wantStatus: http.StatusMovedPermanently, + }, + { + desc: "HTTPS to HTTP permanent", + config: dynamic.RequestRedirect{ + Scheme: ptr.To("http"), + StatusCode: http.StatusMovedPermanently, + }, + url: "https://foo", + wantURL: "http://foo", + wantStatus: http.StatusMovedPermanently, + }, + { + desc: "HTTP to HTTPS", + config: dynamic.RequestRedirect{ + Scheme: ptr.To("https"), + Port: ptr.To("443"), + }, + url: "http://foo:80", + wantURL: "https://foo:443", + wantStatus: http.StatusFound, + }, + { + desc: "HTTP to HTTPS, with X-Forwarded-Proto", + config: dynamic.RequestRedirect{ + Scheme: ptr.To("https"), + Port: ptr.To("443"), + }, + url: "http://foo:80", + wantURL: "https://foo:443", + wantStatus: http.StatusFound, + }, + { + desc: "HTTPS to HTTP", + config: dynamic.RequestRedirect{ + Scheme: ptr.To("http"), + Port: ptr.To("80"), + }, + url: "https://foo:443", + wantURL: "http://foo:80", + wantStatus: http.StatusFound, + }, + { + desc: "HTTP to HTTP", + config: dynamic.RequestRedirect{ + Scheme: ptr.To("http"), + Port: ptr.To("88"), + }, + url: "http://foo:80", + wantURL: "http://foo:88", + wantStatus: http.StatusFound, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + handler, err := NewRequestRedirect(context.Background(), next, test.config, "traefikTest") + if test.wantErr { + require.Error(t, err) + require.Nil(t, handler) + return + } + + require.NoError(t, err) + require.NotNil(t, handler) + + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, test.url, nil) + + handler.ServeHTTP(recorder, req) + + assert.Equal(t, test.wantStatus, recorder.Code) + switch test.wantStatus { + case http.StatusMovedPermanently, http.StatusFound: + location, err := recorder.Result().Location() + require.NoError(t, err) + + assert.Equal(t, test.wantURL, location.String()) + default: + location, err := recorder.Result().Location() + require.Errorf(t, err, "Location %v", location) + } + }) + } +} diff --git a/pkg/middlewares/gatewayapi/urlrewrite/url_rewrite.go b/pkg/middlewares/gatewayapi/urlrewrite/url_rewrite.go new file mode 100644 index 000000000..2960a5583 --- /dev/null +++ b/pkg/middlewares/gatewayapi/urlrewrite/url_rewrite.go @@ -0,0 +1,68 @@ +package urlrewrite + +import ( + "context" + "net/http" + "path" + "strings" + + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/middlewares" + "go.opentelemetry.io/otel/trace" +) + +const ( + typeName = "URLRewrite" +) + +type urlRewrite struct { + name string + next http.Handler + + hostname *string + path *string + pathPrefix *string +} + +// NewURLRewrite creates a URL rewrite middleware. +func NewURLRewrite(ctx context.Context, next http.Handler, conf dynamic.URLRewrite, name string) http.Handler { + logger := middlewares.GetLogger(ctx, name, typeName) + logger.Debug().Msg("Creating middleware") + + return urlRewrite{ + name: name, + next: next, + hostname: conf.Hostname, + path: conf.Path, + pathPrefix: conf.PathPrefix, + } +} + +func (u urlRewrite) GetTracingInformation() (string, string, trace.SpanKind) { + return u.name, typeName, trace.SpanKindInternal +} + +func (u urlRewrite) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + newPath := req.URL.Path + if u.path != nil && u.pathPrefix == nil { + newPath = *u.path + } + if u.path != nil && u.pathPrefix != nil { + newPath = path.Join(*u.path, strings.TrimPrefix(req.URL.Path, *u.pathPrefix)) + + // add the trailing slash if needed, as path.Join removes trailing slashes. + if strings.HasSuffix(req.URL.Path, "/") && !strings.HasSuffix(newPath, "/") { + newPath += "/" + } + } + + req.URL.Path = newPath + req.URL.RawPath = req.URL.EscapedPath() + req.RequestURI = req.URL.RequestURI() + + if u.hostname != nil { + req.Host = *u.hostname + } + + u.next.ServeHTTP(rw, req) +} diff --git a/pkg/middlewares/gatewayapi/urlrewrite/url_rewrite_test.go b/pkg/middlewares/gatewayapi/urlrewrite/url_rewrite_test.go new file mode 100644 index 000000000..6b2b9edab --- /dev/null +++ b/pkg/middlewares/gatewayapi/urlrewrite/url_rewrite_test.go @@ -0,0 +1,126 @@ +package urlrewrite + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "k8s.io/utils/ptr" +) + +func TestURLRewriteHandler(t *testing.T) { + testCases := []struct { + desc string + config dynamic.URLRewrite + url string + wantURL string + wantHost string + }{ + { + desc: "replace path", + config: dynamic.URLRewrite{ + Path: ptr.To("/baz"), + }, + url: "http://foo.com/foo/bar", + wantURL: "http://foo.com/baz", + wantHost: "foo.com", + }, + { + desc: "replace path without trailing slash", + config: dynamic.URLRewrite{ + Path: ptr.To("/baz"), + }, + url: "http://foo.com/foo/bar/", + wantURL: "http://foo.com/baz", + wantHost: "foo.com", + }, + { + desc: "replace path with trailing slash", + config: dynamic.URLRewrite{ + Path: ptr.To("/baz/"), + }, + url: "http://foo.com/foo/bar", + wantURL: "http://foo.com/baz/", + wantHost: "foo.com", + }, + { + desc: "only host", + config: dynamic.URLRewrite{ + Hostname: ptr.To("bar.com"), + }, + url: "http://foo.com/foo/", + wantURL: "http://foo.com/foo/", + wantHost: "bar.com", + }, + { + desc: "host and path", + config: dynamic.URLRewrite{ + Hostname: ptr.To("bar.com"), + Path: ptr.To("/baz/"), + }, + url: "http://foo.com/foo/", + wantURL: "http://foo.com/baz/", + wantHost: "bar.com", + }, + { + desc: "replace prefix path", + config: dynamic.URLRewrite{ + Path: ptr.To("/baz"), + PathPrefix: ptr.To("/foo"), + }, + url: "http://foo.com/foo/bar", + wantURL: "http://foo.com/baz/bar", + wantHost: "foo.com", + }, + { + desc: "replace prefix path with trailing slash", + config: dynamic.URLRewrite{ + Path: ptr.To("/baz"), + PathPrefix: ptr.To("/foo"), + }, + url: "http://foo.com/foo/bar/", + wantURL: "http://foo.com/baz/bar/", + wantHost: "foo.com", + }, + { + desc: "replace prefix path without slash prefix", + config: dynamic.URLRewrite{ + Path: ptr.To("baz"), + PathPrefix: ptr.To("/foo"), + }, + url: "http://foo.com/foo/bar", + wantURL: "http://foo.com/baz/bar", + wantHost: "foo.com", + }, + { + desc: "replace prefix path without slash prefix", + config: dynamic.URLRewrite{ + Path: ptr.To("/baz"), + PathPrefix: ptr.To("/foo/"), + }, + url: "http://foo.com/foo/bar", + wantURL: "http://foo.com/baz/bar", + wantHost: "foo.com", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + handler := NewURLRewrite(context.Background(), next, test.config, "traefikTest") + + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, test.url, nil) + handler.ServeHTTP(recorder, req) + + assert.Equal(t, test.wantURL, req.URL.String()) + assert.Equal(t, test.wantHost, req.Host) + }) + } +} diff --git a/pkg/middlewares/headers/secure.go b/pkg/middlewares/headers/secure.go index 1766e6356..9769627d1 100644 --- a/pkg/middlewares/headers/secure.go +++ b/pkg/middlewares/headers/secure.go @@ -17,24 +17,25 @@ type secureHeader struct { // newSecure constructs a new secure instance with supplied options. func newSecure(next http.Handler, cfg dynamic.Headers, contextKey string) *secureHeader { opt := secure.Options{ - BrowserXssFilter: cfg.BrowserXSSFilter, - ContentTypeNosniff: cfg.ContentTypeNosniff, - ForceSTSHeader: cfg.ForceSTSHeader, - FrameDeny: cfg.FrameDeny, - IsDevelopment: cfg.IsDevelopment, - STSIncludeSubdomains: cfg.STSIncludeSubdomains, - STSPreload: cfg.STSPreload, - ContentSecurityPolicy: cfg.ContentSecurityPolicy, - CustomBrowserXssValue: cfg.CustomBrowserXSSValue, - CustomFrameOptionsValue: cfg.CustomFrameOptionsValue, - PublicKey: cfg.PublicKey, - ReferrerPolicy: cfg.ReferrerPolicy, - AllowedHosts: cfg.AllowedHosts, - HostsProxyHeaders: cfg.HostsProxyHeaders, - SSLProxyHeaders: cfg.SSLProxyHeaders, - STSSeconds: cfg.STSSeconds, - PermissionsPolicy: cfg.PermissionsPolicy, - SecureContextKey: contextKey, + BrowserXssFilter: cfg.BrowserXSSFilter, + ContentTypeNosniff: cfg.ContentTypeNosniff, + ForceSTSHeader: cfg.ForceSTSHeader, + FrameDeny: cfg.FrameDeny, + IsDevelopment: cfg.IsDevelopment, + STSIncludeSubdomains: cfg.STSIncludeSubdomains, + STSPreload: cfg.STSPreload, + ContentSecurityPolicy: cfg.ContentSecurityPolicy, + ContentSecurityPolicyReportOnly: cfg.ContentSecurityPolicyReportOnly, + CustomBrowserXssValue: cfg.CustomBrowserXSSValue, + CustomFrameOptionsValue: cfg.CustomFrameOptionsValue, + PublicKey: cfg.PublicKey, + ReferrerPolicy: cfg.ReferrerPolicy, + AllowedHosts: cfg.AllowedHosts, + HostsProxyHeaders: cfg.HostsProxyHeaders, + SSLProxyHeaders: cfg.SSLProxyHeaders, + STSSeconds: cfg.STSSeconds, + PermissionsPolicy: cfg.PermissionsPolicy, + SecureContextKey: contextKey, } return &secureHeader{ diff --git a/pkg/middlewares/observability/mock_tracing_test.go b/pkg/middlewares/observability/mock_tracing_test.go index 31ddbc642..4a70b77bc 100644 --- a/pkg/middlewares/observability/mock_tracing_test.go +++ b/pkg/middlewares/observability/mock_tracing_test.go @@ -58,6 +58,7 @@ func (s *mockSpan) SetAttributes(kv ...attribute.KeyValue) { func (s *mockSpan) End(...trace.SpanEndOption) {} func (s *mockSpan) RecordError(_ error, _ ...trace.EventOption) {} func (s *mockSpan) AddEvent(_ string, _ ...trace.EventOption) {} +func (s *mockSpan) AddLink(_ trace.Link) {} func (s *mockSpan) SetName(name string) { s.name = name } diff --git a/pkg/muxer/http/matcher.go b/pkg/muxer/http/matcher.go index 9b6861d2f..fb455803b 100644 --- a/pkg/muxer/http/matcher.go +++ b/pkg/muxer/http/matcher.go @@ -264,7 +264,7 @@ func queryRegexp(tree *matchersTree, queries ...string) error { // IsASCII checks if the given string contains only ASCII characters. func IsASCII(s string) bool { - for i := 0; i < len(s); i++ { + for i := range len(s) { if s[i] >= utf8.RuneSelf { return false } diff --git a/pkg/muxer/tcp/matcher.go b/pkg/muxer/tcp/matcher.go index 0ac0ed23a..9e8959928 100644 --- a/pkg/muxer/tcp/matcher.go +++ b/pkg/muxer/tcp/matcher.go @@ -124,7 +124,7 @@ func hostSNIRegexp(tree *matchersTree, templates ...string) error { // isASCII checks if the given string contains only ASCII characters. func isASCII(s string) bool { - for i := 0; i < len(s); i++ { + for i := range len(s) { if s[i] >= utf8.RuneSelf { return false } diff --git a/pkg/muxer/tcp/matcher_v2.go b/pkg/muxer/tcp/matcher_v2.go index b473f87a4..4fd0e3368 100644 --- a/pkg/muxer/tcp/matcher_v2.go +++ b/pkg/muxer/tcp/matcher_v2.go @@ -219,7 +219,7 @@ func varGroupName(idx int) string { func braceIndices(s string) ([]int, error) { var level, idx int var idxs []int - for i := 0; i < len(s); i++ { + for i := range len(s) { switch s[i] { case '{': if level++; level == 1 { diff --git a/pkg/provider/consulcatalog/config_test.go b/pkg/provider/consulcatalog/config_test.go index d96867919..a9a2e1379 100644 --- a/pkg/provider/consulcatalog/config_test.go +++ b/pkg/provider/consulcatalog/config_test.go @@ -3878,7 +3878,7 @@ func TestFilterHealthStatuses(t *testing.T) { err := p.Init() require.NoError(t, err) - for i := 0; i < len(test.items); i++ { + for i := range len(test.items) { var err error test.items[i].ExtraConf, err = p.getExtraConf(test.items[i].Labels) require.NoError(t, err) diff --git a/pkg/provider/kubernetes/crd/fixtures/with_one_external_service_and_health_check.yml b/pkg/provider/kubernetes/crd/fixtures/with_one_external_service_and_health_check.yml new file mode 100644 index 000000000..b0a56d1d1 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_one_external_service_and_health_check.yml @@ -0,0 +1,22 @@ +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: Host(`foo.com`) && PathPrefix(`/foo`) + kind: Rule + priority: 12 + + services: + - name: external-svc + port: 443 + healthCheck: + path: /health + interval: 15s \ No newline at end of file diff --git a/pkg/provider/kubernetes/crd/fixtures/with_one_external_svc_and_regular_svc_health_check.yml b/pkg/provider/kubernetes/crd/fixtures/with_one_external_svc_and_regular_svc_health_check.yml new file mode 100644 index 000000000..194d04c39 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_one_external_svc_and_regular_svc_health_check.yml @@ -0,0 +1,27 @@ +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: Host(`foo.com`) && PathPrefix(`/foo`) + kind: Rule + priority: 12 + + services: + - name: external-svc + port: 443 + healthCheck: + path: /health1 + interval: 15s + - name: whoami2 + port: 443 + healthCheck: + path: /health3 + interval: 30s \ No newline at end of file diff --git a/pkg/provider/kubernetes/crd/fixtures/with_two_external_services_and_health_check.yml b/pkg/provider/kubernetes/crd/fixtures/with_two_external_services_and_health_check.yml new file mode 100644 index 000000000..10bdf4ee0 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_two_external_services_and_health_check.yml @@ -0,0 +1,27 @@ +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: Host(`foo.com`) && PathPrefix(`/foo`) + kind: Rule + priority: 12 + + services: + - name: external-svc + port: 443 + healthCheck: + path: /health1 + interval: 15s + - name: external-svc-with-https + port: 443 + healthCheck: + path: /health2 + interval: 20s \ No newline at end of file diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 760efb1a8..1d5eab777 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -306,6 +306,36 @@ func (c configBuilder) buildServersLB(namespace string, svc traefikv1alpha1.Load lb.SetDefaults() lb.Servers = servers + if svc.HealthCheck != nil { + lb.HealthCheck = &dynamic.ServerHealthCheck{ + Scheme: svc.HealthCheck.Scheme, + Path: svc.HealthCheck.Path, + Method: svc.HealthCheck.Method, + Status: svc.HealthCheck.Status, + Port: svc.HealthCheck.Port, + Hostname: svc.HealthCheck.Hostname, + Headers: svc.HealthCheck.Headers, + } + lb.HealthCheck.SetDefaults() + + if svc.HealthCheck.FollowRedirects != nil { + lb.HealthCheck.FollowRedirects = svc.HealthCheck.FollowRedirects + } + if svc.HealthCheck.Mode != "http" { + lb.HealthCheck.Mode = svc.HealthCheck.Mode + } + if svc.HealthCheck.Interval != nil { + if err := lb.HealthCheck.Interval.Set(svc.HealthCheck.Interval.String()); err != nil { + return nil, err + } + } + if svc.HealthCheck.Timeout != nil { + if err := lb.HealthCheck.Timeout.Set(svc.HealthCheck.Timeout.String()); err != nil { + return nil, err + } + } + } + conf := svc lb.PassHostHeader = conf.PassHostHeader if lb.PassHostHeader == nil { @@ -380,6 +410,10 @@ func (c configBuilder) loadServers(parentNamespace string, svc traefikv1alpha1.L } var servers []dynamic.Server + if service.Spec.Type != corev1.ServiceTypeExternalName && svc.HealthCheck != nil { + return nil, fmt.Errorf("HealthCheck allowed only for ExternalName services: %s/%s", namespace, sanitizedName) + } + if service.Spec.Type == corev1.ServiceTypeExternalName { if !c.allowExternalNameServices { return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, sanitizedName) diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index ce7cc9cb4..4b886b7e8 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -2432,6 +2432,182 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, }, + { + desc: "with one external service and health check", + paths: []string{"services.yml", "with_one_external_service_and_health_check.yml"}, + 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{ + "default-test-route-77c62dfe9517144aeeaa": { + EntryPoints: []string{"foo"}, + Service: "default-test-route-77c62dfe9517144aeeaa", + Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-test-route-77c62dfe9517144aeeaa": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "https://external.domain:443", + }, + }, + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + HealthCheck: &dynamic.ServerHealthCheck{ + Path: "/health", + Timeout: 5000000000, + Interval: 15000000000, + FollowRedirects: Bool(true), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "with two external services and health check", + paths: []string{"services.yml", "with_two_external_services_and_health_check.yml"}, + 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{ + "default-test-route-77c62dfe9517144aeeaa": { + EntryPoints: []string{"foo"}, + Service: "default-test-route-77c62dfe9517144aeeaa", + Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-test-route-77c62dfe9517144aeeaa": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-external-svc-443", + Weight: func(i int) *int { return &i }(1), + }, + { + Name: "default-external-svc-with-https-443", + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-external-svc-443": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "https://external.domain:443", + }, + }, + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + HealthCheck: &dynamic.ServerHealthCheck{ + Path: "/health1", + Timeout: 5000000000, + Interval: 15000000000, + FollowRedirects: Bool(true), + }, + }, + }, + "default-external-svc-with-https-443": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "https://external.domain:443", + }, + }, + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + HealthCheck: &dynamic.ServerHealthCheck{ + Path: "/health2", + Timeout: 5000000000, + Interval: 20000000000, + FollowRedirects: Bool(true), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "with one external service and one regular service and health check", + paths: []string{"services.yml", "with_one_external_svc_and_regular_svc_health_check.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-external-svc-443": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "https://external.domain:443", + }, + }, + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + HealthCheck: &dynamic.ServerHealthCheck{ + Path: "/health1", + Timeout: 5000000000, + Interval: 15000000000, + FollowRedirects: Bool(true), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, { desc: "services lb, servers lb, and mirror service, all in a wrr with different namespaces", allowCrossNamespace: true, diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go index 33fc9c687..ecc777dfa 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go @@ -131,6 +131,8 @@ type LoadBalancerSpec struct { // It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. // By default, NodePortLB is false. NodePortLB bool `json:"nodePortLB,omitempty"` + // Healthcheck defines health checks for ExternalName services. + HealthCheck *ServerHealthCheck `json:"healthCheck,omitempty"` } type ResponseForwarding struct { @@ -142,6 +144,36 @@ type ResponseForwarding struct { FlushInterval string `json:"flushInterval,omitempty"` } +type ServerHealthCheck struct { + // Scheme replaces the server URL scheme for the health check endpoint. + Scheme string `json:"scheme,omitempty"` + // Mode defines the health check mode. + // If defined to grpc, will use the gRPC health check protocol to probe the server. + // Default: http + Mode string `json:"mode,omitempty"` + // Path defines the server URL path for the health check endpoint. + Path string `json:"path,omitempty"` + // Method defines the healthcheck method. + Method string `json:"method,omitempty"` + // Status defines the expected HTTP status code of the response to the health check request. + Status int `json:"status,omitempty"` + // Port defines the server URL port for the health check endpoint. + Port int `json:"port,omitempty"` + // Interval defines the frequency of the health check calls. + // Default: 30s + Interval *intstr.IntOrString `json:"interval,omitempty"` + // Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + // Default: 5s + Timeout *intstr.IntOrString `json:"timeout,omitempty"` + // Hostname defines the value of hostname in the Host header of the health check request. + Hostname string `json:"hostname,omitempty"` + // FollowRedirects defines whether redirects should be followed during the health check calls. + // Default: true + FollowRedirects *bool `json:"followRedirects,omitempty"` + // Headers defines custom headers to be sent to the health check endpoint. + Headers map[string]string `json:"headers,omitempty"` +} + // Service defines an upstream HTTP service to proxy traffic to. type Service struct { LoadBalancerSpec `json:",inline"` 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 a17ff5484..8ff42f478 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go @@ -582,6 +582,11 @@ func (in *LoadBalancerSpec) DeepCopyInto(out *LoadBalancerSpec) { *out = new(bool) **out = **in } + if in.HealthCheck != nil { + in, out := &in.HealthCheck, &out.HealthCheck + *out = new(ServerHealthCheck) + (*in).DeepCopyInto(*out) + } return } @@ -1110,6 +1115,44 @@ func (in *RouteUDP) DeepCopy() *RouteUDP { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerHealthCheck) DeepCopyInto(out *ServerHealthCheck) { + *out = *in + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(intstr.IntOrString) + **out = **in + } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(intstr.IntOrString) + **out = **in + } + if in.FollowRedirects != nil { + in, out := &in.FollowRedirects, &out.FollowRedirects + *out = new(bool) + **out = **in + } + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerHealthCheck. +func (in *ServerHealthCheck) DeepCopy() *ServerHealthCheck { + if in == nil { + return nil + } + out := new(ServerHealthCheck) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServersTransport) DeepCopyInto(out *ServersTransport) { *out = *in diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https.yml index de4cdf9ec..5e03018a3 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https.yml @@ -39,12 +39,10 @@ spec: hostnames: - "example.org" rules: - - backendRefs: - - name: whoami - port: 80 - weight: 1 - kind: Service - group: "" + - matches: + - path: + type: PathPrefix + value: / filters: - type: RequestRedirect requestRedirect: diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https_with_hostname_and_port.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https_with_hostname_and_port.yml index f1aba4c62..5ad86c1b8 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https_with_hostname_and_port.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_http_to_https_with_hostname_and_port.yml @@ -39,12 +39,10 @@ spec: hostnames: - "example.org" rules: - - backendRefs: - - name: whoami - port: 80 - weight: 1 - kind: Service - group: "" + - matches: + - path: + type: PathPrefix + value: / filters: - type: RequestRedirect requestRedirect: diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_request_header_modifier.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_request_header_modifier.yml index d5cc3d0b6..5b2d00cf9 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_request_header_modifier.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_request_header_modifier.yml @@ -39,7 +39,11 @@ spec: hostnames: - "example.org" rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_url_rewrite_combined.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_url_rewrite_combined.yml new file mode 100644 index 000000000..90d432572 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_url_rewrite_combined.yml @@ -0,0 +1,58 @@ +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: 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: + - "example.com" + rules: + - matches: + - path: + type: PathPrefix + value: /foo + backendRefs: + - name: whoami + port: 80 + weight: 1 + kind: Service + group: "" + filters: + - type: URLRewrite + urlRewrite: + hostname: www.foo.bar + path: + type: ReplacePrefixMatch + replacePrefixMatch: /xyz diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_url_rewrite_fullpath.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_url_rewrite_fullpath.yml new file mode 100644 index 000000000..f1183610a --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_url_rewrite_fullpath.yml @@ -0,0 +1,57 @@ +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: 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: + - "example.com" + rules: + - matches: + - path: + type: PathPrefix + value: /foo + backendRefs: + - name: whoami + port: 80 + weight: 1 + kind: Service + group: "" + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplaceFullPath + replaceFullPath: /bar diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_url_rewrite_hostname.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_url_rewrite_hostname.yml new file mode 100644 index 000000000..854d2eac2 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/filter_url_rewrite_hostname.yml @@ -0,0 +1,55 @@ +--- +kind: GatewayClass +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: 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: + - "example.com" + rules: + - matches: + - path: + type: PathPrefix + value: /foo + backendRefs: + - name: whoami + port: 80 + weight: 1 + kind: Service + group: "" + filters: + - type: URLRewrite + urlRewrite: + hostname: www.foo.bar diff --git a/pkg/provider/kubernetes/gateway/fixtures/httproute/with_multiple_host.yml b/pkg/provider/kubernetes/gateway/fixtures/httproute/with_multiple_host.yml index 1f35bc7bc..248144586 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/httproute/with_multiple_host.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/httproute/with_multiple_host.yml @@ -37,7 +37,11 @@ spec: - "foo.com" - "bar.com" rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/mixed/simple.yml b/pkg/provider/kubernetes/gateway/fixtures/mixed/simple.yml index ac15a9934..0fd6ee36e 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/mixed/simple.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/mixed/simple.yml @@ -107,7 +107,11 @@ spec: kind: Gateway group: gateway.networking.k8s.io rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 @@ -131,7 +135,11 @@ spec: kind: Gateway group: gateway.networking.k8s.io rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoamitcp port: 9000 weight: 1 @@ -151,7 +159,11 @@ spec: kind: Gateway group: gateway.networking.k8s.io rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoamitcp port: 9000 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_bad_listener_protocol.yml b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_bad_listener_protocol.yml index 3c11ca130..72a7584be 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_bad_listener_protocol.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_bad_listener_protocol.yml @@ -33,7 +33,11 @@ metadata: namespace: default spec: rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_bad_listener_route_kind.yml b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_bad_listener_route_kind.yml index 1e54ea49c..b6507d251 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_bad_listener_route_kind.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_bad_listener_route_kind.yml @@ -33,7 +33,11 @@ metadata: namespace: default spec: rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_core_group.yml b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_core_group.yml index d9a0c0a00..dcef7966d 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_core_group.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_core_group.yml @@ -89,7 +89,11 @@ spec: kind: Gateway group: gateway.networking.k8s.io rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_incompatible_protocol_and_route_kind.yml b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_incompatible_protocol_and_route_kind.yml index 6f983f1ec..58b2f4e6c 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_incompatible_protocol_and_route_kind.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_incompatible_protocol_and_route_kind.yml @@ -33,7 +33,11 @@ metadata: namespace: default spec: rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_multiple_listeners_using_same_hostname_port_protocol.yml b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_multiple_listeners_using_same_hostname_port_protocol.yml index b57edc456..272e363d7 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_multiple_listeners_using_same_hostname_port_protocol.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_multiple_listeners_using_same_hostname_port_protocol.yml @@ -119,7 +119,11 @@ metadata: namespace: default spec: rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 @@ -134,7 +138,11 @@ metadata: namespace: default spec: rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_namespace_all.yml b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_namespace_all.yml index a05cf0c73..c2b525044 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_namespace_all.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_namespace_all.yml @@ -107,7 +107,11 @@ spec: kind: Gateway group: gateway.networking.k8s.io rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 @@ -177,7 +181,11 @@ spec: kind: Gateway group: gateway.networking.k8s.io rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami-bar port: 80 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_namespace_same.yml b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_namespace_same.yml index 78ce26173..8d470e749 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_namespace_same.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_namespace_same.yml @@ -107,7 +107,11 @@ spec: kind: Gateway group: gateway.networking.k8s.io rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 @@ -177,7 +181,11 @@ spec: kind: Gateway group: gateway.networking.k8s.io rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami-bar port: 80 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_namespace_selector.yml b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_namespace_selector.yml index 1178ad391..a46e3dab6 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/mixed/with_namespace_selector.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/mixed/with_namespace_selector.yml @@ -130,7 +130,11 @@ spec: kind: Gateway group: gateway.networking.k8s.io rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 @@ -200,7 +204,11 @@ spec: kind: Gateway group: gateway.networking.k8s.io rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami-bar port: 80 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_service.yml b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_service.yml new file mode 100644 index 000000000..19b1cf14c --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_service.yml @@ -0,0 +1,65 @@ +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: backend-from-bar + namespace: bar +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: default + to: + - group: "" + kind: Service + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: http + protocol: HTTP + port: 80 + hostname: foo.example.com + allowedRoutes: + kinds: + - kind: HTTPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: whoami-bar + port: 80 + weight: 1 + kind: Service + group: "" diff --git a/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_service_missing.yml b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_service_missing.yml new file mode 100644 index 000000000..fe238b5d5 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_service_missing.yml @@ -0,0 +1,50 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: http + protocol: HTTP + port: 80 + hostname: foo.example.com + allowedRoutes: + kinds: + - kind: HTTPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: whoami-bar + port: 80 + weight: 1 + kind: Service + group: "" diff --git a/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_service_not_matching_from.yml b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_service_not_matching_from.yml new file mode 100644 index 000000000..5f29ee339 --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_service_not_matching_from.yml @@ -0,0 +1,65 @@ +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: backend-from-bar + namespace: bar +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: anothernamespce + to: + - group: "" + kind: Service + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: http + protocol: HTTP + port: 80 + hostname: foo.example.com + allowedRoutes: + kinds: + - kind: HTTPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: whoami-bar + port: 80 + weight: 1 + kind: Service + group: "" diff --git a/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_service_not_matching_to.yml b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_service_not_matching_to.yml new file mode 100644 index 000000000..903d9038a --- /dev/null +++ b/pkg/provider/kubernetes/gateway/fixtures/referencegrant/for_service_not_matching_to.yml @@ -0,0 +1,66 @@ +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: backend-from-bar + namespace: bar +spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: default + to: + - group: "" + kind: Service + name: differentservice + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: my-gateway-class +spec: + controllerName: traefik.io/gateway-controller + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: my-gateway + namespace: default +spec: + gatewayClassName: my-gateway-class + listeners: # Use GatewayClass defaults for listener definition. + - name: http + protocol: HTTP + port: 80 + hostname: foo.example.com + allowedRoutes: + kinds: + - kind: HTTPRoute + group: gateway.networking.k8s.io + namespaces: + from: Same + +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-app-1 + namespace: default +spec: + parentRefs: + - name: my-gateway + kind: Gateway + group: gateway.networking.k8s.io + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: whoami-bar + port: 80 + weight: 1 + kind: Service + group: "" diff --git a/pkg/provider/kubernetes/gateway/fixtures/with_two_hosts_one_wildcard.yml b/pkg/provider/kubernetes/gateway/fixtures/with_two_hosts_one_wildcard.yml index 33b2cad7e..36fbb14f6 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/with_two_hosts_one_wildcard.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/with_two_hosts_one_wildcard.yml @@ -37,7 +37,11 @@ spec: - "foo.com" - "*.bar.com" rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/fixtures/with_two_hosts_wildcard.yml b/pkg/provider/kubernetes/gateway/fixtures/with_two_hosts_wildcard.yml index f069e9b3b..2ce58c6f0 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/with_two_hosts_wildcard.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/with_two_hosts_wildcard.yml @@ -37,7 +37,11 @@ spec: - "foo.com" - "*.foo.com" rules: - - backendRefs: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: - name: whoami port: 80 weight: 1 diff --git a/pkg/provider/kubernetes/gateway/httproute.go b/pkg/provider/kubernetes/gateway/httproute.go index 3410764d9..4d47869bf 100644 --- a/pkg/provider/kubernetes/gateway/httproute.go +++ b/pkg/provider/kubernetes/gateway/httproute.go @@ -20,8 +20,8 @@ import ( gatev1 "sigs.k8s.io/gateway-api/apis/v1" ) -func (p *Provider) loadHTTPRoutes(ctx context.Context, client Client, gatewayListeners []gatewayListener, conf *dynamic.Configuration) { - routes, err := client.ListHTTPRoutes() +func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) { + routes, err := p.client.ListHTTPRoutes() if err != nil { log.Ctx(ctx).Error().Err(err).Msg("Unable to list HTTPRoutes") return @@ -41,64 +41,45 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, client Client, gatewayLis Conditions: []metav1.Condition{ { Type: string(gatev1.RouteConditionAccepted), - Status: metav1.ConditionTrue, + Status: metav1.ConditionFalse, ObservedGeneration: route.Generation, LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteReasonAccepted), + Reason: string(gatev1.RouteReasonNoMatchingParent), }, }, } - var attachedListeners bool - notAcceptedReason := gatev1.RouteReasonNoMatchingParent for _, listener := range gatewayListeners { if !matchListener(listener, route.Namespace, parentRef) { continue } + accepted := true if !allowRoute(listener, route.Namespace, kindHTTPRoute) { - notAcceptedReason = gatev1.RouteReasonNotAllowedByListeners - continue + parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNotAllowedByListeners)) + accepted = false } - hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames) if !ok { - notAcceptedReason = gatev1.RouteReasonNoMatchingListenerHostname - continue + parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNoMatchingListenerHostname)) + accepted = false } - listener.Status.AttachedRoutes++ - - // TODO should we build the conf if the listener is not attached - // only consider the route attached if the listener is in an "attached" state. - if listener.Attached { - attachedListeners = true + if accepted { + // Gateway listener should have AttachedRoutes set even when Gateway has unresolved refs. + listener.Status.AttachedRoutes++ + // Only consider the route attached if the listener is in an "attached" state. + if listener.Attached { + parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonAccepted)) + } } - resolveConditions := p.loadHTTPRoute(logger.WithContext(ctx), client, listener, route, hostnames, conf) - // TODO: handle more accurately route conditions (in case of multiple listener matching). - for _, condition := range resolveConditions { - parentStatus.Conditions = appendCondition(parentStatus.Conditions, condition) + routeConf, resolveRefCondition := p.loadHTTPRoute(logger.WithContext(ctx), listener, route, hostnames) + if accepted && listener.Attached { + mergeHTTPConfiguration(routeConf, conf) } - } - if !attachedListeners { - parentStatus.Conditions = []metav1.Condition{ - { - Type: string(gatev1.RouteConditionAccepted), - Status: metav1.ConditionFalse, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(notAcceptedReason), - }, - { - Type: string(gatev1.RouteConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteReasonRefNotPermitted), - }, - } + parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition) } parentStatuses = append(parentStatuses, *parentStatus) @@ -109,7 +90,7 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, client Client, gatewayLis Parents: parentStatuses, }, } - if err := client.UpdateHTTPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil { + if err := p.client.UpdateHTTPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil { logger.Error(). Err(err). Msg("Unable to update HTTPRoute status") @@ -117,130 +98,146 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, client Client, gatewayLis } } -func (p *Provider) loadHTTPRoute(ctx context.Context, client Client, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname, conf *dynamic.Configuration) []metav1.Condition { - routeConditions := []metav1.Condition{ - { - Type: string(gatev1.RouteConditionResolvedRefs), - Status: metav1.ConditionTrue, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteConditionResolvedRefs), +func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) { + conf := &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), }, } - hostRule := hostRule(hostnames) + condition := metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteConditionResolvedRefs), + } - for _, routeRule := range route.Spec.Rules { - router := dynamic.Router{ - RuleSyntax: "v3", - Rule: routerRule(routeRule, hostRule), - EntryPoints: []string{listener.EPName}, - } - if listener.Protocol == gatev1.HTTPSProtocolType { - router.TLS = &dynamic.RouterTLSConfig{} - } - - // Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes. - routerName := route.Name + "-" + listener.GWName + "-" + listener.EPName - routerKey := makeRouterKey(router.Rule, makeID(route.Namespace, routerName)) - - var wrr dynamic.WeightedRoundRobin - wrrName := provider.Normalize(routerKey + "-wrr") - - middlewares, err := p.loadMiddlewares(listener.Protocol, route.Namespace, routerKey, routeRule.Filters) - if err != nil { - log.Ctx(ctx).Error(). - Err(err). - Msg("Unable to load HTTPRoute filters") - - wrr.Services = append(wrr.Services, dynamic.WRRService{ + errWrr := dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { Name: "invalid-httproute-filter", Status: ptr.To(500), Weight: ptr.To(1), - }) - - conf.HTTP.Services[wrrName] = &dynamic.Service{Weighted: &wrr} - router.Service = wrrName - } else { - for name, middleware := range middlewares { - // If the middleware config is nil in the return of the loadMiddlewares function, - // it means that we just need a reference to that middleware. - if middleware != nil { - conf.HTTP.Middlewares[name] = middleware - } - - router.Middlewares = append(router.Middlewares, name) - } - - // Traefik internal service can be used only if there is only one BackendRef service reference. - if len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0].BackendRef) { - router.Service = string(routeRule.BackendRefs[0].Name) - } else { - for _, backendRef := range routeRule.BackendRefs { - name, svc, errCondition := p.loadHTTPService(client, route, backendRef) - weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1))) - if errCondition != nil { - routeConditions = appendCondition(routeConditions, *errCondition) - wrr.Services = append(wrr.Services, dynamic.WRRService{ - Name: name, - Status: ptr.To(500), - Weight: weight, - }) - continue - } - - if svc != nil { - conf.HTTP.Services[name] = svc - } - - wrr.Services = append(wrr.Services, dynamic.WRRService{ - Name: name, - Weight: weight, - }) - } - - conf.HTTP.Services[wrrName] = &dynamic.Service{Weighted: &wrr} - router.Service = wrrName - } - } - - rt := &router - p.applyRouterTransform(ctx, rt, route) - - routerKey = provider.Normalize(routerKey) - conf.HTTP.Routers[routerKey] = rt + }, + }, } - return routeConditions + for ri, routeRule := range route.Spec.Rules { + // Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes. + routeKey := provider.Normalize(fmt.Sprintf("%s-%s-%s-%s-%d", route.Namespace, route.Name, listener.GWName, listener.EPName, ri)) + + for _, match := range routeRule.Matches { + rule, priority := buildMatchRule(hostnames, match) + router := dynamic.Router{ + RuleSyntax: "v3", + Rule: rule, + Priority: priority + len(route.Spec.Rules) - ri, + EntryPoints: []string{listener.EPName}, + } + if listener.Protocol == gatev1.HTTPSProtocolType { + router.TLS = &dynamic.RouterTLSConfig{} + } + + var err error + routerName := makeRouterName(rule, routeKey) + router.Middlewares, err = p.loadMiddlewares(conf, route.Namespace, routerName, routeRule.Filters, match.Path) + switch { + case err != nil: + log.Ctx(ctx).Error().Err(err).Msg("Unable to load HTTPRoute filters") + + errWrrName := routerName + "-err-wrr" + conf.HTTP.Services[errWrrName] = &dynamic.Service{Weighted: &errWrr} + router.Service = errWrrName + + case len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0].BackendRef): + router.Service = string(routeRule.BackendRefs[0].Name) + + default: + var serviceCondition *metav1.Condition + router.Service, serviceCondition = p.loadService(conf, routeKey, routeRule, route) + if serviceCondition != nil { + condition = *serviceCondition + } + } + + p.applyRouterTransform(ctx, &router, route) + + conf.HTTP.Routers[routerName] = &router + } + } + + return conf, condition +} + +func (p *Provider) loadService(conf *dynamic.Configuration, routeKey string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute) (string, *metav1.Condition) { + name := routeKey + "-wrr" + if _, ok := conf.HTTP.Services[name]; ok { + return name, nil + } + + var wrr dynamic.WeightedRoundRobin + var condition *metav1.Condition + for _, backendRef := range routeRule.BackendRefs { + svcName, svc, errCondition := p.loadHTTPService(route, backendRef) + weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1))) + if errCondition != nil { + condition = errCondition + wrr.Services = append(wrr.Services, dynamic.WRRService{ + Name: svcName, + Status: ptr.To(500), + Weight: weight, + }) + continue + } + + if svc != nil { + conf.HTTP.Services[svcName] = svc + } + + wrr.Services = append(wrr.Services, dynamic.WRRService{ + Name: svcName, + Weight: weight, + }) + } + + conf.HTTP.Services[name] = &dynamic.Service{Weighted: &wrr} + return name, condition } // loadHTTPService 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) loadHTTPService(client Client, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef) (string, *dynamic.Service, *metav1.Condition) { +func (p *Provider) loadHTTPService(route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef) (string, *dynamic.Service, *metav1.Condition) { + kind := ptr.Deref(backendRef.Kind, "Service") + group := groupCore if backendRef.Group != nil && *backendRef.Group != "" { group = string(*backendRef.Group) } - kind := ptr.Deref(backendRef.Kind, "Service") - namespace := ptr.Deref(backendRef.Namespace, gatev1.Namespace(route.Namespace)) - namespaceStr := string(namespace) - serviceName := provider.Normalize(makeID(namespaceStr, string(backendRef.Name))) + namespace := route.Namespace + if backendRef.Namespace != nil && *backendRef.Namespace != "" { + namespace = string(*backendRef.Namespace) + } - // TODO support cross namespace through ReferenceGrant. - if namespaceStr != route.Namespace { + serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name)) + + if err := p.isReferenceGranted(groupGateway, kindHTTPRoute, route.Namespace, group, string(kind), string(backendRef.Name), namespace); err != nil { return serviceName, nil, &metav1.Condition{ Type: string(gatev1.RouteConditionResolvedRefs), Status: metav1.ConditionFalse, ObservedGeneration: route.Generation, LastTransitionTime: metav1.Now(), Reason: string(gatev1.RouteReasonRefNotPermitted), - Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s namespace not allowed", group, kind, namespace, backendRef.Name), + Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: %s", group, kind, namespace, backendRef.Name, err), } } if group != groupCore || kind != "Service" { - name, service, err := p.loadHTTPBackendRef(namespaceStr, backendRef) + name, service, err := p.loadHTTPBackendRef(namespace, backendRef) if err != nil { return serviceName, nil, &metav1.Condition{ Type: string(gatev1.RouteConditionResolvedRefs), @@ -270,7 +267,7 @@ func (p *Provider) loadHTTPService(client Client, route *gatev1.HTTPRoute, backe portStr := strconv.FormatInt(int64(port), 10) serviceName = provider.Normalize(serviceName + "-" + portStr) - lb, err := loadHTTPServers(client, namespaceStr, backendRef) + lb, err := p.loadHTTPServers(namespace, backendRef) if err != nil { return serviceName, nil, &metav1.Condition{ Type: string(gatev1.RouteConditionResolvedRefs), @@ -303,18 +300,21 @@ func (p *Provider) loadHTTPBackendRef(namespace string, backendRef gatev1.HTTPBa return backendFunc(string(backendRef.Name), namespace) } -func (p *Provider) loadMiddlewares(listenerProtocol gatev1.ProtocolType, namespace, prefix string, filters []gatev1.HTTPRouteFilter) (map[string]*dynamic.Middleware, error) { - middlewares := make(map[string]*dynamic.Middleware) +func (p *Provider) loadMiddlewares(conf *dynamic.Configuration, namespace, routerName string, filters []gatev1.HTTPRouteFilter, pathMatch *gatev1.HTTPPathMatch) ([]string, error) { + pm := ptr.Deref(pathMatch, gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchPathPrefix), + Value: ptr.To("/"), + }) + middlewares := make(map[string]*dynamic.Middleware) for i, filter := range filters { + name := fmt.Sprintf("%s-%s-%d", routerName, strings.ToLower(string(filter.Type)), i) switch filter.Type { case gatev1.HTTPRouteFilterRequestRedirect: - middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i)) - middlewares[middlewareName] = createRedirectRegexMiddleware(listenerProtocol, filter.RequestRedirect) + middlewares[name] = createRequestRedirect(filter.RequestRedirect, pm) case gatev1.HTTPRouteFilterRequestHeaderModifier: - middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i)) - middlewares[middlewareName] = createRequestHeaderModifier(filter.RequestHeaderModifier) + middlewares[name] = createRequestHeaderModifier(filter.RequestHeaderModifier) case gatev1.HTTPRouteFilterExtensionRef: name, middleware, err := p.loadHTTPRouteFilterExtensionRef(namespace, filter.ExtensionRef) @@ -324,6 +324,14 @@ func (p *Provider) loadMiddlewares(listenerProtocol gatev1.ProtocolType, namespa middlewares[name] = middleware + case gatev1.HTTPRouteFilterURLRewrite: + var err error + middleware, err := createURLRewrite(filter.URLRewrite, pm) + if err != nil { + return nil, fmt.Errorf("invalid filter %s: %w", filter.Type, err) + } + middlewares[name] = middleware + default: // As per the spec: https://gateway-api.sigs.k8s.io/api-types/httproute/#filters-optional // In all cases where incompatible or unsupported filters are @@ -333,7 +341,16 @@ func (p *Provider) loadMiddlewares(listenerProtocol gatev1.ProtocolType, namespa } } - return middlewares, nil + var middlewareNames []string + for name, middleware := range middlewares { + if middleware != nil { + conf.HTTP.Middlewares[name] = middleware + } + + middlewareNames = append(middlewareNames, name) + } + + return middlewareNames, nil } func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRef *gatev1.LocalObjectReference) (string, *dynamic.Middleware, error) { @@ -352,9 +369,8 @@ func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRe return filterFunc(string(extensionRef.Name), namespace) } -// TODO support cross namespace through ReferencePolicy. -func loadHTTPServers(client Client, namespace string, backendRef gatev1.HTTPBackendRef) (*dynamic.ServersLoadBalancer, error) { - service, exists, err := client.GetService(namespace, string(backendRef.Name)) +func (p *Provider) loadHTTPServers(namespace string, backendRef gatev1.HTTPBackendRef) (*dynamic.ServersLoadBalancer, error) { + service, exists, err := p.client.GetService(namespace, string(backendRef.Name)) if err != nil { return nil, fmt.Errorf("getting service: %w", err) } @@ -376,7 +392,7 @@ func loadHTTPServers(client Client, namespace string, backendRef gatev1.HTTPBack return nil, errors.New("service port not found") } - endpoints, endpointsExists, err := client.GetEndpoints(namespace, string(backendRef.Name)) + endpoints, endpointsExists, err := p.client.GetEndpoints(namespace, string(backendRef.Name)) if err != nil { return nil, fmt.Errorf("getting endpoints: %w", err) } @@ -418,12 +434,17 @@ func loadHTTPServers(client Client, namespace string, backendRef gatev1.HTTPBack return lb, nil } -func hostRule(hostnames []gatev1.Hostname) string { +func buildHostRule(hostnames []gatev1.Hostname) (string, int) { var rules []string + var priority int for _, hostname := range hostnames { host := string(hostname) + if priority < len(host) { + priority = len(host) + } + wildcard := strings.Count(host, "*") if wildcard == 0 { rules = append(rules, fmt.Sprintf("Host(`%s`)", host)) @@ -436,86 +457,97 @@ func hostRule(hostnames []gatev1.Hostname) string { switch len(rules) { case 0: - return "" + return "", 0 case 1: - return rules[0] + return rules[0], priority default: - return fmt.Sprintf("(%s)", strings.Join(rules, " || ")) + return fmt.Sprintf("(%s)", strings.Join(rules, " || ")), priority } } -func routerRule(routeRule gatev1.HTTPRouteRule, hostRule string) string { - var rule string - var matchesRules []string +// buildMatchRule builds the route rule and computes its priority. +// The current priority computing is rather naive but aims to fulfill Conformance tests suite requirement. +// The priority is computed to match the following precedence order: +// +// * "Exact" path match. (+100000) +// * "Prefix" path match with largest number of characters. (+10000) PathRegex (+1000) +// * Method match. (not implemented) +// * Largest number of header matches. (+100 each) or with PathRegex (+10 each) +// * Largest number of query param matches. (not implemented) +// +// In case of multiple matches for a route, the maximum priority among all matches is retain. +func buildMatchRule(hostnames []gatev1.Hostname, match gatev1.HTTPRouteMatch) (string, int) { + path := ptr.Deref(match.Path, gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchPathPrefix), + Value: ptr.To("/"), + }) - for _, match := range routeRule.Matches { - path := ptr.Deref(match.Path, gatev1.HTTPPathMatch{ - Type: ptr.To(gatev1.PathMatchPathPrefix), - Value: ptr.To("/"), - }) - pathType := ptr.Deref(path.Type, gatev1.PathMatchPathPrefix) - pathValue := ptr.Deref(path.Value, "/") + var priority int + var matchRules []string - var matchRules []string - switch pathType { - case gatev1.PathMatchExact: - matchRules = append(matchRules, fmt.Sprintf("Path(`%s`)", pathValue)) - case gatev1.PathMatchPathPrefix: - matchRules = append(matchRules, buildPathMatchPathPrefixRule(pathValue)) - case gatev1.PathMatchRegularExpression: - matchRules = append(matchRules, fmt.Sprintf("PathRegexp(`%s`)", pathValue)) - } + pathRule, pathPriority := buildPathRule(path) + matchRules = append(matchRules, pathRule) + priority += pathPriority - matchRules = append(matchRules, headerRules(match.Headers)...) - matchesRules = append(matchesRules, strings.Join(matchRules, " && ")) + headerRules, headersPriority := buildHeaderRules(match.Headers) + matchRules = append(matchRules, headerRules...) + priority += headersPriority + + matchRulesStr := strings.Join(matchRules, " && ") + + hostRule, hostPriority := buildHostRule(hostnames) + + if hostRule == "" { + return matchRulesStr, priority } - // If no matches are specified, the default is a prefix - // path match on "/", which has the effect of matching every - // HTTP request. - if len(routeRule.Matches) == 0 { - matchesRules = append(matchesRules, "PathPrefix(`/`)") - } - - if hostRule != "" { - if len(matchesRules) == 0 { - return hostRule - } - rule += hostRule + " && " - } - - if len(matchesRules) == 1 { - return rule + matchesRules[0] - } - - if len(rule) == 0 { - return strings.Join(matchesRules, " || ") - } - - return rule + "(" + strings.Join(matchesRules, " || ") + ")" + // A route with a host should match over the same route with no host. + priority += hostPriority + return hostRule + " && " + matchRulesStr, priority } -func headerRules(headers []gatev1.HTTPHeaderMatch) []string { - var headerRules []string +func buildPathRule(pathMatch gatev1.HTTPPathMatch) (string, int) { + pathType := ptr.Deref(pathMatch.Type, gatev1.PathMatchPathPrefix) + pathValue := ptr.Deref(pathMatch.Value, "/") + + switch pathType { + case gatev1.PathMatchExact: + return fmt.Sprintf("Path(`%s`)", pathValue), 100000 + + case gatev1.PathMatchPathPrefix: + // PathPrefix(`/`) rule is a catch-all, + // here we ensure it would be evaluated last. + if pathValue == "/" { + return "PathPrefix(`/`)", 1 + } + + pv := strings.TrimSuffix(pathValue, "/") + return fmt.Sprintf("(Path(`%[1]s`) || PathPrefix(`%[1]s/`))", pv), 10000 + len(pathValue)*100 + + case gatev1.PathMatchRegularExpression: + return fmt.Sprintf("PathRegexp(`%s`)", pathValue), 1000 + len(pathValue)*100 + + default: + return "PathPrefix(`/`)", 1 + } +} + +func buildHeaderRules(headers []gatev1.HTTPHeaderMatch) ([]string, int) { + var rules []string + var priority int for _, header := range headers { typ := ptr.Deref(header.Type, gatev1.HeaderMatchExact) switch typ { case gatev1.HeaderMatchExact: - headerRules = append(headerRules, fmt.Sprintf("Header(`%s`,`%s`)", header.Name, header.Value)) + rules = append(rules, fmt.Sprintf("Header(`%s`,`%s`)", header.Name, header.Value)) + priority += 100 case gatev1.HeaderMatchRegularExpression: - headerRules = append(headerRules, fmt.Sprintf("HeaderRegexp(`%s`,`%s`)", header.Name, header.Value)) + rules = append(rules, fmt.Sprintf("HeaderRegexp(`%s`,`%s`)", header.Name, header.Value)) + priority += 10 } } - return headerRules -} -func buildPathMatchPathPrefixRule(path string) string { - if path == "/" { - return "PathPrefix(`/`)" - } - - path = strings.TrimSuffix(path, "/") - return fmt.Sprintf("(Path(`%[1]s`) || PathPrefix(`%[1]s/`))", path) + return rules, priority } // createRequestHeaderModifier does not enforce/check the configuration, @@ -540,31 +572,76 @@ func createRequestHeaderModifier(filter *gatev1.HTTPHeaderFilter) *dynamic.Middl } } -func createRedirectRegexMiddleware(listenerProtocol gatev1.ProtocolType, filter *gatev1.HTTPRequestRedirectFilter) *dynamic.Middleware { - // The spec allows for an empty string in which case we should use the - // scheme of the request which in this case is the listener scheme. - filterScheme := ptr.Deref(filter.Scheme, strings.ToLower(string(listenerProtocol))) - statusCode := ptr.Deref(filter.StatusCode, http.StatusFound) - - port := "${port}" - if filter.Port != nil { - port = fmt.Sprintf(":%d", *filter.Port) +func createRequestRedirect(filter *gatev1.HTTPRequestRedirectFilter, pathMatch gatev1.HTTPPathMatch) *dynamic.Middleware { + var hostname *string + if filter.Hostname != nil { + hostname = ptr.To(string(*filter.Hostname)) } - hostname := "${hostname}" - if filter.Hostname != nil && *filter.Hostname != "" { - hostname = string(*filter.Hostname) + var port *string + filterScheme := ptr.Deref(filter.Scheme, "") + if filterScheme == "http" || filterScheme == "https" { + port = ptr.To("") + } + if filter.Port != nil { + port = ptr.To(fmt.Sprintf("%d", *filter.Port)) + } + + var path *string + var pathPrefix *string + if filter.Path != nil { + switch filter.Path.Type { + case gatev1.FullPathHTTPPathModifier: + path = filter.Path.ReplaceFullPath + case gatev1.PrefixMatchHTTPPathModifier: + path = filter.Path.ReplacePrefixMatch + pathPrefix = pathMatch.Value + } } return &dynamic.Middleware{ - RedirectRegex: &dynamic.RedirectRegex{ - Regex: `^[a-z]+:\/\/(?P.+@)?(?P\[[\w:\.]+\]|[\w\._-]+)(?P:\d+)?\/(?P.*)`, - Replacement: fmt.Sprintf("%s://${userinfo}%s%s/${path}", filterScheme, hostname, port), - Permanent: statusCode == http.StatusMovedPermanently, + RequestRedirect: &dynamic.RequestRedirect{ + Scheme: filter.Scheme, + Hostname: hostname, + Port: port, + Path: path, + PathPrefix: pathPrefix, + StatusCode: ptr.Deref(filter.StatusCode, http.StatusFound), }, } } +func createURLRewrite(filter *gatev1.HTTPURLRewriteFilter, pathMatch gatev1.HTTPPathMatch) (*dynamic.Middleware, error) { + if filter.Path == nil && filter.Hostname == nil { + return nil, errors.New("empty configuration") + } + + var host *string + if filter.Hostname != nil { + host = ptr.To(string(*filter.Hostname)) + } + + var path *string + var pathPrefix *string + if filter.Path != nil { + switch filter.Path.Type { + case gatev1.FullPathHTTPPathModifier: + path = filter.Path.ReplaceFullPath + case gatev1.PrefixMatchHTTPPathModifier: + path = filter.Path.ReplacePrefixMatch + pathPrefix = pathMatch.Value + } + } + + return &dynamic.Middleware{ + URLRewrite: &dynamic.URLRewrite{ + Hostname: host, + Path: path, + PathPrefix: pathPrefix, + }, + }, nil +} + func getProtocol(portSpec corev1.ServicePort) string { protocol := "http" if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") { @@ -573,3 +650,35 @@ func getProtocol(portSpec corev1.ServicePort) string { return protocol } + +func mergeHTTPConfiguration(from, to *dynamic.Configuration) { + if from == nil || from.HTTP == nil || to == nil { + return + } + + if to.HTTP == nil { + to.HTTP = from.HTTP + return + } + + if to.HTTP.Routers == nil { + to.HTTP.Routers = map[string]*dynamic.Router{} + } + for routerName, router := range from.HTTP.Routers { + to.HTTP.Routers[routerName] = router + } + + if to.HTTP.Middlewares == nil { + to.HTTP.Middlewares = map[string]*dynamic.Middleware{} + } + for middlewareName, middleware := range from.HTTP.Middlewares { + to.HTTP.Middlewares[middlewareName] = middleware + } + + if to.HTTP.Services == nil { + to.HTTP.Services = map[string]*dynamic.Service{} + } + for serviceName, service := range from.HTTP.Services { + to.HTTP.Services[serviceName] = service + } +} diff --git a/pkg/provider/kubernetes/gateway/httproute_test.go b/pkg/provider/kubernetes/gateway/httproute_test.go index 2698acfdb..bd74a04d0 100644 --- a/pkg/provider/kubernetes/gateway/httproute_test.go +++ b/pkg/provider/kubernetes/gateway/httproute_test.go @@ -8,15 +8,16 @@ import ( gatev1 "sigs.k8s.io/gateway-api/apis/v1" ) -func Test_hostRule(t *testing.T) { +func Test_buildHostRule(t *testing.T) { testCases := []struct { - desc string - hostnames []gatev1.Hostname - expectedRule string - expectErr bool + desc string + hostnames []gatev1.Hostname + expectedRule string + expectedPriority int + expectErr bool }{ { - desc: "Empty rule and matches", + desc: "Empty (should not happen)", expectedRule: "", }, { @@ -24,7 +25,8 @@ func Test_hostRule(t *testing.T) { hostnames: []gatev1.Hostname{ "Foo", }, - expectedRule: "Host(`Foo`)", + expectedRule: "Host(`Foo`)", + expectedPriority: 3, }, { desc: "Multiple Hosts", @@ -33,7 +35,8 @@ func Test_hostRule(t *testing.T) { "Bar", "Bir", }, - expectedRule: "(Host(`Foo`) || Host(`Bar`) || Host(`Bir`))", + expectedRule: "(Host(`Foo`) || Host(`Bar`) || Host(`Bir`))", + expectedPriority: 3, }, { desc: "Several Host and wildcard", @@ -42,14 +45,16 @@ func Test_hostRule(t *testing.T) { "bar.foo", "foo.foo", }, - expectedRule: "(HostRegexp(`^[a-z0-9-\\.]+\\.bar\\.foo$`) || Host(`bar.foo`) || Host(`foo.foo`))", + expectedRule: "(HostRegexp(`^[a-z0-9-\\.]+\\.bar\\.foo$`) || Host(`bar.foo`) || Host(`foo.foo`))", + expectedPriority: 9, }, { desc: "Host with wildcard", hostnames: []gatev1.Hostname{ "*.bar.foo", }, - expectedRule: "HostRegexp(`^[a-z0-9-\\.]+\\.bar\\.foo$`)", + expectedRule: "HostRegexp(`^[a-z0-9-\\.]+\\.bar\\.foo$`)", + expectedPriority: 9, }, } @@ -57,216 +62,134 @@ func Test_hostRule(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - rule := hostRule(test.hostnames) + rule, priority := buildHostRule(test.hostnames) assert.Equal(t, test.expectedRule, rule) + assert.Equal(t, test.expectedPriority, priority) }) } } -func Test_routerRule(t *testing.T) { +func Test_buildMatchRule(t *testing.T) { testCases := []struct { - desc string - routeRule gatev1.HTTPRouteRule - hostRule string - expectedRule string - expectedError bool + desc string + routeMatch gatev1.HTTPRouteMatch + hostnames []gatev1.Hostname + expectedRule string + expectedPriority int + expectedError bool }{ { - desc: "Empty rule and matches", - expectedRule: "PathPrefix(`/`)", + desc: "Empty rule and matches ", + expectedRule: "PathPrefix(`/`)", + expectedPriority: 1, }, { - desc: "One Host rule without matches", - hostRule: "Host(`foo.com`)", - expectedRule: "Host(`foo.com`) && PathPrefix(`/`)", + desc: "One Host rule without match", + hostnames: []gatev1.Hostname{"foo.com"}, + expectedRule: "Host(`foo.com`) && PathPrefix(`/`)", + expectedPriority: 8, }, { desc: "One HTTPRouteMatch with nil HTTPHeaderMatch", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: ptr.To(gatev1.HTTPPathMatch{ - Type: ptr.To(gatev1.PathMatchPathPrefix), - Value: ptr.To("/"), - }), - Headers: nil, - }, - }, + routeMatch: gatev1.HTTPRouteMatch{ + Path: ptr.To(gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchPathPrefix), + Value: ptr.To("/"), + }), + Headers: nil, }, - expectedRule: "PathPrefix(`/`)", + expectedRule: "PathPrefix(`/`)", + expectedPriority: 1, }, { desc: "One HTTPRouteMatch with nil HTTPHeaderMatch Type", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: ptr.To(gatev1.HTTPPathMatch{ - Type: ptr.To(gatev1.PathMatchPathPrefix), - Value: ptr.To("/"), - }), - Headers: []gatev1.HTTPHeaderMatch{ - {Name: "foo", Value: "bar"}, - }, - }, + routeMatch: gatev1.HTTPRouteMatch{ + Path: ptr.To(gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchPathPrefix), + Value: ptr.To("/"), + }), + Headers: []gatev1.HTTPHeaderMatch{ + {Name: "foo", Value: "bar"}, }, }, - expectedRule: "PathPrefix(`/`) && Header(`foo`,`bar`)", + expectedRule: "PathPrefix(`/`) && Header(`foo`,`bar`)", + expectedPriority: 101, }, { - desc: "One HTTPRouteMatch with nil HTTPPathMatch", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - {Path: nil}, - }, - }, - expectedRule: "PathPrefix(`/`)", + desc: "One HTTPRouteMatch with nil HTTPPathMatch", + routeMatch: gatev1.HTTPRouteMatch{Path: nil}, + expectedRule: "PathPrefix(`/`)", + expectedPriority: 1, }, { desc: "One HTTPRouteMatch with nil HTTPPathMatch Type", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: nil, - Value: ptr.To("/foo/"), - }, - }, + routeMatch: gatev1.HTTPRouteMatch{ + Path: &gatev1.HTTPPathMatch{ + Type: nil, + Value: ptr.To("/foo/"), }, }, - expectedRule: "(Path(`/foo`) || PathPrefix(`/foo/`))", + expectedRule: "(Path(`/foo`) || PathPrefix(`/foo/`))", + expectedPriority: 10500, }, { desc: "One HTTPRouteMatch with nil HTTPPathMatch Values", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: ptr.To(gatev1.PathMatchExact), - Value: nil, - }, - }, + routeMatch: gatev1.HTTPRouteMatch{ + Path: &gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchExact), + Value: nil, }, }, - expectedRule: "Path(`/`)", + expectedRule: "Path(`/`)", + expectedPriority: 100000, }, { - desc: "One Path in matches", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: ptr.To(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, - }, + desc: "One Path", + routeMatch: gatev1.HTTPRouteMatch{ + Path: &gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchExact), + Value: ptr.To("/foo/"), }, }, - expectedRule: "Path(`/foo/`)", + expectedRule: "Path(`/foo/`)", + expectedPriority: 100000, }, { - desc: "One Path in matches and another empty", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ + desc: "Path && Header", + routeMatch: gatev1.HTTPRouteMatch{ + Path: &gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchExact), + Value: ptr.To("/foo/"), + }, + Headers: []gatev1.HTTPHeaderMatch{ { - Path: &gatev1.HTTPPathMatch{ - Type: ptr.To(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, + Type: ptr.To(gatev1.HeaderMatchExact), + Name: "my-header", + Value: "foo", }, - {}, }, }, - expectedRule: "Path(`/foo/`) || PathPrefix(`/`)", + expectedRule: "Path(`/foo/`) && Header(`my-header`,`foo`)", + expectedPriority: 100100, }, { - desc: "Path OR Header rules", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ + desc: "Host && Path && Header", + hostnames: []gatev1.Hostname{"foo.com"}, + routeMatch: gatev1.HTTPRouteMatch{ + Path: &gatev1.HTTPPathMatch{ + Type: ptr.To(gatev1.PathMatchExact), + Value: ptr.To("/foo/"), + }, + Headers: []gatev1.HTTPHeaderMatch{ { - Path: &gatev1.HTTPPathMatch{ - Type: ptr.To(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, - }, - { - Headers: []gatev1.HTTPHeaderMatch{ - { - Type: ptr.To(gatev1.HeaderMatchExact), - Name: "my-header", - Value: "foo", - }, - }, + Type: ptr.To(gatev1.HeaderMatchExact), + Name: "my-header", + Value: "foo", }, }, }, - expectedRule: "Path(`/foo/`) || PathPrefix(`/`) && Header(`my-header`,`foo`)", - }, - { - desc: "Path && Header rules", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: ptr.To(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, - Headers: []gatev1.HTTPHeaderMatch{ - { - Type: ptr.To(gatev1.HeaderMatchExact), - Name: "my-header", - Value: "foo", - }, - }, - }, - }, - }, - expectedRule: "Path(`/foo/`) && Header(`my-header`,`foo`)", - }, - { - desc: "Host && Path && Header rules", - hostRule: "Host(`foo.com`)", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: ptr.To(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, - Headers: []gatev1.HTTPHeaderMatch{ - { - Type: ptr.To(gatev1.HeaderMatchExact), - Name: "my-header", - Value: "foo", - }, - }, - }, - }, - }, - expectedRule: "Host(`foo.com`) && Path(`/foo/`) && Header(`my-header`,`foo`)", - }, - { - desc: "Host && (Path || Header) rules", - hostRule: "Host(`foo.com`)", - routeRule: gatev1.HTTPRouteRule{ - Matches: []gatev1.HTTPRouteMatch{ - { - Path: &gatev1.HTTPPathMatch{ - Type: ptr.To(gatev1.PathMatchExact), - Value: ptr.To("/foo/"), - }, - }, - { - Headers: []gatev1.HTTPHeaderMatch{ - { - Type: ptr.To(gatev1.HeaderMatchExact), - Name: "my-header", - Value: "foo", - }, - }, - }, - }, - }, - expectedRule: "Host(`foo.com`) && (Path(`/foo/`) || PathPrefix(`/`) && Header(`my-header`,`foo`))", + expectedRule: "Host(`foo.com`) && Path(`/foo/`) && Header(`my-header`,`foo`)", + expectedPriority: 100107, }, } @@ -274,8 +197,9 @@ func Test_routerRule(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - rule := routerRule(test.routeRule, test.hostRule) + rule, priority := buildMatchRule(test.hostnames, test.routeMatch) assert.Equal(t, test.expectedRule, rule) + assert.Equal(t, test.expectedPriority, priority) }) } } diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index e6cea12a1..6af304e0c 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -37,7 +37,8 @@ const ( providerName = "kubernetesgateway" controllerName = "traefik.io/gateway-controller" - groupCore = "core" + groupCore = "core" + groupGateway = "gateway.networking.k8s.io" kindGateway = "Gateway" kindTraefikService = "TraefikService" @@ -67,6 +68,7 @@ type Provider struct { lastConfiguration safe.Safe routerTransform k8s.RouterTransform + client *clientWrapper } // Entrypoint defines the available entry points. @@ -193,6 +195,14 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) { // Init the provider. func (p *Provider) Init() error { + logger := log.With().Str(logs.ProviderName, providerName).Logger() + + var err error + p.client, err = p.newK8sClient(logger.WithContext(context.Background())) + if err != nil { + return fmt.Errorf("creating k8s client: %w", err) + } + return nil } @@ -201,14 +211,9 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. logger := log.With().Str(logs.ProviderName, providerName).Logger() ctxLog := logger.WithContext(context.Background()) - k8sClient, err := p.newK8sClient(ctxLog) - if err != nil { - return err - } - pool.GoCtx(func(ctxPool context.Context) { operation := func() error { - eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done()) + eventsChan, err := p.client.WatchAll(p.Namespaces, ctxPool.Done()) if err != nil { logger.Error().Err(err).Msg("Error watching kubernetes events") timer := time.NewTimer(1 * time.Second) @@ -234,7 +239,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. // Note that event is the *first* event that came in during this throttling interval -- if we're hitting our throttle, we may have dropped events. // This is fine, because we don't treat different event types differently. // But if we do in the future, we'll need to track more information about the dropped events. - conf := p.loadConfigurationFromGateways(ctxLog, k8sClient) + conf := p.loadConfigurationFromGateways(ctxLog) confHash, err := hashstructure.Hash(conf, nil) switch { @@ -271,7 +276,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. } // TODO Handle errors and update resources statuses (gatewayClass, gateway). -func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Client) *dynamic.Configuration { +func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.Configuration { conf := &dynamic.Configuration{ HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{}, @@ -292,13 +297,13 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Cli TLS: &dynamic.TLSConfiguration{}, } - addresses, err := p.gatewayAddresses(client) + addresses, err := p.gatewayAddresses() if err != nil { log.Ctx(ctx).Error().Err(err).Msg("Unable to get Gateway status addresses") return nil } - gatewayClasses, err := client.ListGatewayClasses() + gatewayClasses, err := p.client.ListGatewayClasses() if err != nil { log.Ctx(ctx).Error().Err(err).Msg("Unable to list GatewayClasses") return nil @@ -312,7 +317,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Cli gatewayClassNames[gatewayClass.Name] = struct{}{} - err := client.UpdateGatewayClassStatus(gatewayClass, metav1.Condition{ + err := p.client.UpdateGatewayClassStatus(gatewayClass, metav1.Condition{ Type: string(gatev1.GatewayClassConditionStatusAccepted), Status: metav1.ConditionTrue, ObservedGeneration: gatewayClass.Generation, @@ -329,7 +334,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Cli } } - gateways := client.ListGateways() + gateways := p.client.ListGateways() var gatewayListeners []gatewayListener for _, gateway := range gateways { @@ -342,14 +347,14 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Cli continue } - gatewayListeners = append(gatewayListeners, p.loadGatewayListeners(logger.WithContext(ctx), client, gateway, conf)...) + gatewayListeners = append(gatewayListeners, p.loadGatewayListeners(logger.WithContext(ctx), gateway, conf)...) } - p.loadHTTPRoutes(ctx, client, gatewayListeners, conf) + p.loadHTTPRoutes(ctx, gatewayListeners, conf) if p.ExperimentalChannel { - p.loadTCPRoutes(ctx, client, gatewayListeners, conf) - p.loadTLSRoutes(ctx, client, gatewayListeners, conf) + p.loadTCPRoutes(ctx, gatewayListeners, conf) + p.loadTLSRoutes(ctx, gatewayListeners, conf) } for _, gateway := range gateways { @@ -366,7 +371,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Cli } gatewayStatus, errG := p.makeGatewayStatus(gateway, listeners, addresses) - if err = client.UpdateGatewayStatus(gateway, gatewayStatus); err != nil { + if err = p.client.UpdateGatewayStatus(gateway, gatewayStatus); err != nil { logger.Error(). Err(err). Msg("Unable to update Gateway status") @@ -381,7 +386,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Cli return conf } -func (p *Provider) loadGatewayListeners(ctx context.Context, client Client, gateway *gatev1.Gateway, conf *dynamic.Configuration) []gatewayListener { +func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gateway, conf *dynamic.Configuration) []gatewayListener { tlsConfigs := make(map[string]*tls.CertAndStores) allocatedListeners := make(map[string]struct{}) gatewayListeners := make([]gatewayListener, len(gateway.Spec.Listeners)) @@ -419,7 +424,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, client Client, gate gatewayListeners[i].EPName = ep allowedRoutes := ptr.Deref(listener.AllowedRoutes, gatev1.AllowedRoutes{Namespaces: &gatev1.RouteNamespaces{From: ptr.To(gatev1.NamespacesFromSame)}}) - gatewayListeners[i].AllowedNamespaces, err = allowedNamespaces(client, gateway.Namespace, allowedRoutes.Namespaces) + gatewayListeners[i].AllowedNamespaces, err = p.allowedNamespaces(gateway.Namespace, allowedRoutes.Namespaces) if err != nil { // update "ResolvedRefs" status true with "InvalidRoutesRef" reason gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ @@ -564,40 +569,22 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, client Client, gate certificateNamespace = string(*certificateRef.Namespace) } - if certificateNamespace != gateway.Namespace { - referenceGrants, err := client.ListReferenceGrants(certificateNamespace) - if err != nil { - gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.ListenerReasonRefNotPermitted), - Message: fmt.Sprintf("Cannot find any ReferenceGrant: %v", err), - }) + if err := p.isReferenceGranted(groupGateway, kindGateway, gateway.Namespace, groupCore, "Secret", string(certificateRef.Name), certificateNamespace); err != nil { + gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ + Type: string(gatev1.ListenerConditionResolvedRefs), + Status: metav1.ConditionFalse, + ObservedGeneration: gateway.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.ListenerReasonRefNotPermitted), + Message: fmt.Sprintf("Cannot load CertificateRef %s/%s: %s", certificateNamespace, certificateRef.Name, err), + }) - continue - } - - referenceGrants = filterReferenceGrantsFrom(referenceGrants, "gateway.networking.k8s.io", "Gateway", gateway.Namespace) - referenceGrants = filterReferenceGrantsTo(referenceGrants, groupCore, "Secret", string(certificateRef.Name)) - if len(referenceGrants) == 0 { - gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{ - Type: string(gatev1.ListenerConditionResolvedRefs), - Status: metav1.ConditionFalse, - ObservedGeneration: gateway.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.ListenerReasonRefNotPermitted), - Message: "Required ReferenceGrant for cross namespace secret reference is missing", - }) - - continue - } + continue } configKey := certificateNamespace + "/" + string(certificateRef.Name) if _, tlsExists := tlsConfigs[configKey]; !tlsExists { - tlsConf, err := getTLS(client, certificateRef.Name, certificateNamespace) + tlsConf, err := p.getTLS(certificateRef.Name, certificateNamespace) if err != nil { // update "ResolvedRefs" status false with "InvalidCertificateRef" reason // update "Programmed" status false with "Invalid" reason @@ -720,7 +707,7 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listeners []gatewa return gatewayStatus, nil } -func (p *Provider) gatewayAddresses(client Client) ([]gatev1.GatewayStatusAddress, error) { +func (p *Provider) gatewayAddresses() ([]gatev1.GatewayStatusAddress, error) { if p.StatusAddress == nil { return nil, nil } @@ -741,7 +728,7 @@ func (p *Provider) gatewayAddresses(client Client) ([]gatev1.GatewayStatusAddres svcRef := p.StatusAddress.Service if svcRef.Name != "" && svcRef.Namespace != "" { - svc, exists, err := client.GetService(svcRef.Namespace, svcRef.Name) + svc, exists, err := p.client.GetService(svcRef.Namespace, svcRef.Name) if err != nil { return nil, fmt.Errorf("unable to get service: %w", err) } @@ -789,6 +776,71 @@ func (p *Provider) entryPointName(port gatev1.PortNumber, protocol gatev1.Protoc return "", fmt.Errorf("no matching entryPoint for port %d and protocol %q", port, protocol) } +func (p *Provider) isReferenceGranted(fromGroup, fromKind, fromNamespace, toGroup, toKind, toName, toNamespace string) error { + if toNamespace == fromNamespace { + return nil + } + + refGrants, err := p.client.ListReferenceGrants(toNamespace) + if err != nil { + return fmt.Errorf("listing ReferenceGrant: %w", err) + } + + refGrants = filterReferenceGrantsFrom(refGrants, fromGroup, fromKind, fromNamespace) + refGrants = filterReferenceGrantsTo(refGrants, toGroup, toKind, toName) + if len(refGrants) == 0 { + return errors.New("missing ReferenceGrant") + } + + return nil +} + +func (p *Provider) getTLS(secretName gatev1.ObjectName, namespace string) (*tls.CertAndStores, error) { + secret, exists, err := p.client.GetSecret(namespace, string(secretName)) + if err != nil { + return nil, fmt.Errorf("failed to fetch secret %s/%s: %w", namespace, secretName, err) + } + if !exists { + return nil, fmt.Errorf("secret %s/%s does not exist", namespace, secretName) + } + + cert, key, err := getCertificateBlocks(secret, namespace, string(secretName)) + if err != nil { + return nil, err + } + + return &tls.CertAndStores{ + Certificate: tls.Certificate{ + CertFile: types.FileOrContent(cert), + KeyFile: types.FileOrContent(key), + }, + }, nil +} + +func (p *Provider) allowedNamespaces(gatewayNamespace string, routeNamespaces *gatev1.RouteNamespaces) ([]string, error) { + if routeNamespaces == nil || routeNamespaces.From == nil { + return []string{gatewayNamespace}, nil + } + + switch *routeNamespaces.From { + case gatev1.NamespacesFromAll: + return []string{metav1.NamespaceAll}, nil + + case gatev1.NamespacesFromSame: + return []string{gatewayNamespace}, nil + + case gatev1.NamespacesFromSelector: + selector, err := metav1.LabelSelectorAsSelector(routeNamespaces.Selector) + if err != nil { + return nil, fmt.Errorf("malformed selector: %w", err) + } + + return p.client.ListNamespaces(selector) + } + + return nil, fmt.Errorf("unsupported RouteSelectType: %q", *routeNamespaces.From) +} + func supportedRouteKinds(protocol gatev1.ProtocolType, experimentalChannel bool) ([]gatev1.RouteGroupKind, []metav1.Condition) { group := gatev1.Group(gatev1.GroupName) @@ -873,30 +925,6 @@ func allowedRouteKinds(gateway *gatev1.Gateway, listener gatev1.Listener, suppor return routeKinds, conditions } -func allowedNamespaces(client Client, gatewayNamespace string, routeNamespaces *gatev1.RouteNamespaces) ([]string, error) { - if routeNamespaces == nil || routeNamespaces.From == nil { - return []string{gatewayNamespace}, nil - } - - switch *routeNamespaces.From { - case gatev1.NamespacesFromAll: - return []string{metav1.NamespaceAll}, nil - - case gatev1.NamespacesFromSame: - return []string{gatewayNamespace}, nil - - case gatev1.NamespacesFromSelector: - selector, err := metav1.LabelSelectorAsSelector(routeNamespaces.Selector) - if err != nil { - return nil, fmt.Errorf("malformed selector: %w", err) - } - - return client.ListNamespaces(selector) - } - - return nil, fmt.Errorf("unsupported RouteSelectType: %q", *routeNamespaces.From) -} - func findMatchingHostnames(listenerHostname *gatev1.Hostname, routeHostnames []gatev1.Hostname) ([]gatev1.Hostname, bool) { if listenerHostname == nil { return routeHostnames, true @@ -988,7 +1016,7 @@ func matchListener(listener gatewayListener, routeNamespace string, parentRef ga return true } -func makeRouterKey(rule, name string) string { +func makeRouterName(rule, name string) string { h := sha256.New() // As explained in https://pkg.go.dev/hash#Hash, @@ -998,36 +1026,6 @@ func makeRouterKey(rule, name string) string { return fmt.Sprintf("%s-%.10x", name, h.Sum(nil)) } -func makeID(namespace, name string) string { - if namespace == "" { - return name - } - - return namespace + "-" + name -} - -func getTLS(k8sClient Client, secretName gatev1.ObjectName, namespace string) (*tls.CertAndStores, error) { - secret, exists, err := k8sClient.GetSecret(namespace, string(secretName)) - if err != nil { - return nil, fmt.Errorf("failed to fetch secret %s/%s: %w", namespace, secretName, err) - } - if !exists { - return nil, fmt.Errorf("secret %s/%s does not exist", namespace, secretName) - } - - cert, key, err := getCertificateBlocks(secret, namespace, string(secretName)) - if err != nil { - return nil, err - } - - return &tls.CertAndStores{ - Certificate: tls.Certificate{ - CertFile: types.FileOrContent(cert), - KeyFile: types.FileOrContent(key), - }, - }, nil -} - func getTLSConfig(tlsConfigs map[string]*tls.CertAndStores) []*tls.CertAndStores { var secretNames []string for secretName := range tlsConfigs { @@ -1193,13 +1191,38 @@ func kindToString(p *gatev1.Kind) string { return string(*p) } -func appendCondition(conditions []metav1.Condition, condition metav1.Condition) []metav1.Condition { - res := []metav1.Condition{condition} +func updateRouteConditionAccepted(conditions []metav1.Condition, reason string) []metav1.Condition { + var conds []metav1.Condition for _, c := range conditions { - if c.Type != condition.Type { - res = append(res, c) + if c.Type == string(gatev1.RouteConditionAccepted) && c.Status != metav1.ConditionTrue { + c.Reason = reason + c.LastTransitionTime = metav1.Now() + + if reason == string(gatev1.RouteReasonAccepted) { + c.Status = metav1.ConditionTrue + } } + + conds = append(conds, c) } - return res + return conds +} + +func upsertRouteConditionResolvedRefs(conditions []metav1.Condition, condition metav1.Condition) []metav1.Condition { + var ( + curr *metav1.Condition + conds []metav1.Condition + ) + for _, c := range conditions { + if c.Type == string(gatev1.RouteConditionResolvedRefs) { + curr = &c + continue + } + conds = append(conds, c) + } + if curr != nil && curr.Status == metav1.ConditionFalse && condition.Status == metav1.ConditionTrue { + return append(conds, *curr) + } + return append(conds, condition) } diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 033f19420..af87073df 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -3,6 +3,7 @@ package gateway import ( "context" "errors" + "net/http" "os" "path/filepath" "testing" @@ -198,16 +199,17 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -566,16 +568,17 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -626,10 +629,11 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, Service: "api@internal", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, }, @@ -659,17 +663,18 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-websecure-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-websecure-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"websecure"}, - Service: "default-http-app-1-my-gateway-websecure-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-websecure-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-websecure-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-websecure-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -729,16 +734,17 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-66e726cd8903b49727ae": { + "default-http-app-1-my-gateway-web-0-66e726cd8903b49727ae": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-66e726cd8903b49727ae-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "(Host(`foo.com`) || Host(`bar.com`)) && PathPrefix(`/`)", + Priority: 9, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-66e726cd8903b49727ae-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -789,16 +795,17 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-baa117c0219e3878749f": { + "default-http-app-1-my-gateway-web-0-baa117c0219e3878749f": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-baa117c0219e3878749f-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "(Host(`foo.com`) || HostRegexp(`^[a-z0-9-\\.]+\\.bar\\.com$`)) && PathPrefix(`/`)", + Priority: 11, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-baa117c0219e3878749f-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -849,16 +856,17 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-45eba2eaf40ac792e036": { + "default-http-app-1-my-gateway-web-0-45eba2eaf40ac792e036": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-45eba2eaf40ac792e036-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "(Host(`foo.com`) || HostRegexp(`^[a-z0-9-\\.]+\\.foo\\.com$`)) && PathPrefix(`/`)", + Priority: 11, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-45eba2eaf40ac792e036-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -909,22 +917,24 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100009, RuleSyntax: "v3", - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", }, - "default-http-app-1-my-gateway-web-d737b4933fa88e68ab8a": { + "default-http-app-1-my-gateway-web-1-d737b4933fa88e68ab8a": { EntryPoints: []string{"web"}, Rule: "Host(`foo.com`) && Path(`/bir`)", + Priority: 100008, RuleSyntax: "v3", - Service: "default-http-app-1-my-gateway-web-d737b4933fa88e68ab8a-wrr", + Service: "default-http-app-1-my-gateway-web-1-wrr", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -934,7 +944,7 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, }, - "default-http-app-1-my-gateway-web-d737b4933fa88e68ab8a-wrr": { + "default-http-app-1-my-gateway-web-1-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1001,16 +1011,17 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1086,23 +1097,25 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-http-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-http-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-http-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-http-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, - "default-http-app-1-my-gateway-https-websecure-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-https-websecure-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"websecure"}, - Service: "default-http-app-1-my-gateway-https-websecure-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-https-websecure-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-http-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-http-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1112,7 +1125,7 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, }, - "default-http-app-1-my-gateway-https-websecure-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-https-websecure-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1177,23 +1190,25 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, - "default-http-app-1-my-gateway-websecure-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-websecure-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"websecure"}, - Service: "default-http-app-1-my-gateway-websecure-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-websecure-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1203,7 +1218,7 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, }, - "default-http-app-1-my-gateway-websecure-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-websecure-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1263,28 +1278,31 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-6cf37fa71907768d925c": { + "default-http-app-1-my-gateway-web-0-6cf37fa71907768d925c": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-6cf37fa71907768d925c-wrr", Rule: "Host(`foo.com`) && (Path(`/bar`) || PathPrefix(`/bar/`)) && Header(`my-header`,`foo`) && Header(`my-header2`,`bar`)", + Priority: 10610, RuleSyntax: "v3", + Service: "default-http-app-1-my-gateway-web-0-wrr", }, - "default-http-app-1-my-gateway-web-aaba0f24fd26e1ca2276": { + "default-http-app-1-my-gateway-web-2-d23f7039bc8036fb918c": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-aaba0f24fd26e1ca2276-wrr", - Rule: "Host(`foo.com`) && Path(`/bar`) && Header(`my-header`,`bar`)", - RuleSyntax: "v3", - }, - "default-http-app-1-my-gateway-web-d23f7039bc8036fb918c": { - EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-d23f7039bc8036fb918c-wrr", Rule: "Host(`foo.com`) && PathRegexp(`^/buzz/[0-9]+$`)", + Priority: 2408, RuleSyntax: "v3", + Service: "default-http-app-1-my-gateway-web-2-wrr", + }, + "default-http-app-1-my-gateway-web-1-aaba0f24fd26e1ca2276": { + EntryPoints: []string{"web"}, + Rule: "Host(`foo.com`) && Path(`/bar`) && Header(`my-header`,`bar`)", + Priority: 100109, + RuleSyntax: "v3", + Service: "default-http-app-1-my-gateway-web-1-wrr", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-6cf37fa71907768d925c-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1294,7 +1312,7 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, }, - "default-http-app-1-my-gateway-web-aaba0f24fd26e1ca2276-wrr": { + "default-http-app-1-my-gateway-web-2-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1304,7 +1322,7 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, }, - "default-http-app-1-my-gateway-web-d23f7039bc8036fb918c-wrr": { + "default-http-app-1-my-gateway-web-1-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1355,16 +1373,17 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-default-my-gateway-web-efde1997778109a1f6eb": { + "default-http-app-default-my-gateway-web-0-efde1997778109a1f6eb": { EntryPoints: []string{"web"}, - Service: "default-http-app-default-my-gateway-web-efde1997778109a1f6eb-wrr", + Service: "default-http-app-default-my-gateway-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/foo`)", + Priority: 100008, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-default-my-gateway-web-efde1997778109a1f6eb-wrr": { + "default-http-app-default-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1415,22 +1434,24 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-default-my-gateway-web-efde1997778109a1f6eb": { + "default-http-app-default-my-gateway-web-0-efde1997778109a1f6eb": { EntryPoints: []string{"web"}, - Service: "default-http-app-default-my-gateway-web-efde1997778109a1f6eb-wrr", + Service: "default-http-app-default-my-gateway-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/foo`)", + Priority: 100008, RuleSyntax: "v3", }, - "bar-http-app-bar-my-gateway-web-66f5c78d03d948e36597": { + "bar-http-app-bar-my-gateway-web-0-66f5c78d03d948e36597": { EntryPoints: []string{"web"}, - Service: "bar-http-app-bar-my-gateway-web-66f5c78d03d948e36597-wrr", + Service: "bar-http-app-bar-my-gateway-web-0-wrr", Rule: "Host(`bar.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-default-my-gateway-web-efde1997778109a1f6eb-wrr": { + "default-http-app-default-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1440,7 +1461,7 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, }, - "bar-http-app-bar-my-gateway-web-66f5c78d03d948e36597-wrr": { + "bar-http-app-bar-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1507,16 +1528,17 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "bar-http-app-bar-my-gateway-web-66f5c78d03d948e36597": { + "bar-http-app-bar-my-gateway-web-0-66f5c78d03d948e36597": { EntryPoints: []string{"web"}, - Service: "bar-http-app-bar-my-gateway-web-66f5c78d03d948e36597-wrr", + Service: "bar-http-app-bar-my-gateway-web-0-wrr", Rule: "Host(`bar.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "bar-http-app-bar-my-gateway-web-66f5c78d03d948e36597-wrr": { + "bar-http-app-bar-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1567,16 +1589,17 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4": { + "default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "Host(`example.org`) && PathPrefix(`/`)", + Priority: 13, RuleSyntax: "v3", - Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestheadermodifier-0"}, + Middlewares: []string{"default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-requestheadermodifier-0"}, }, }, Middlewares: map[string]*dynamic.Middleware{ - "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestheadermodifier-0": { + "default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-requestheadermodifier-0": { RequestHeaderModifier: &dynamic.RequestHeaderModifier{ Set: map[string]string{"X-Foo": "Bar"}, Add: map[string]string{"X-Bar": "Foo"}, @@ -1585,7 +1608,7 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1636,30 +1659,123 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4": { + "default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "Host(`example.org`) && PathPrefix(`/`)", + Priority: 13, RuleSyntax: "v3", - Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0"}, + Middlewares: []string{"default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-requestredirect-0"}, }, }, Middlewares: map[string]*dynamic.Middleware{ - "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0": { - RedirectRegex: &dynamic.RedirectRegex{ - Regex: "^[a-z]+:\\/\\/(?P.+@)?(?P\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P:\\d+)?\\/(?P.*)", - Replacement: "https://${userinfo}${hostname}${port}/${path}", - Permanent: true, + "default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-requestredirect-0": { + RequestRedirect: &dynamic.RequestRedirect{ + Scheme: ptr.To("https"), + Port: ptr.To(""), + StatusCode: http.StatusMovedPermanently, }, }, }, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { + Weighted: &dynamic.WeightedRoundRobin{}, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "Simple HTTPRoute, redirect HTTP to HTTPS with hostname", + paths: []string{"services.yml", "httproute/filter_http_to_https_with_hostname_and_port.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{ + "default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4": { + EntryPoints: []string{"web"}, + Service: "default-http-app-1-my-gateway-web-0-wrr", + Rule: "Host(`example.org`) && PathPrefix(`/`)", + Priority: 13, + RuleSyntax: "v3", + Middlewares: []string{"default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-requestredirect-0"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-http-app-1-my-gateway-web-0-364ce6ec04c3d49b19c4-requestredirect-0": { + RequestRedirect: &dynamic.RequestRedirect{ + Hostname: ptr.To("example.com"), + Port: ptr.To("443"), + StatusCode: http.StatusFound, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-http-app-1-my-gateway-web-0-wrr": { + Weighted: &dynamic.WeightedRoundRobin{}, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "Simple HTTPRoute URL rewrite FullPath", + paths: []string{"services.yml", "httproute/filter_url_rewrite_fullpath.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{ + "default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8": { + EntryPoints: []string{"web"}, + Service: "default-http-app-1-my-gateway-web-0-wrr", + Rule: "Host(`example.com`) && (Path(`/foo`) || PathPrefix(`/foo/`))", + RuleSyntax: "v3", + Priority: 10412, + Middlewares: []string{"default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8-urlrewrite-0"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8-urlrewrite-0": { + URLRewrite: &dynamic.URLRewrite{ + Path: ptr.To("/bar"), + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: ptr.To(1), + Weight: func(i int) *int { return &i }(1), }, }, }, @@ -1687,8 +1803,8 @@ func TestLoadHTTPRoutes(t *testing.T) { }, }, { - desc: "Simple HTTPRoute, redirect HTTP to HTTPS with hostname", - paths: []string{"services.yml", "httproute/filter_http_to_https_with_hostname_and_port.yml"}, + desc: "Simple HTTPRoute URL rewrite Hostname", + paths: []string{"services.yml", "httproute/filter_url_rewrite_hostname.yml"}, entryPoints: map[string]Entrypoint{"web": { Address: ":80", }}, @@ -1705,29 +1821,99 @@ func TestLoadHTTPRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4": { + "default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr", - Rule: "Host(`example.org`) && PathPrefix(`/`)", + Service: "default-http-app-1-my-gateway-web-0-wrr", + Rule: "Host(`example.com`) && (Path(`/foo`) || PathPrefix(`/foo/`))", RuleSyntax: "v3", - Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0"}, + Priority: 10412, + Middlewares: []string{"default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8-urlrewrite-0"}, }, }, Middlewares: map[string]*dynamic.Middleware{ - "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0": { - RedirectRegex: &dynamic.RedirectRegex{ - Regex: "^[a-z]+:\\/\\/(?P.+@)?(?P\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P:\\d+)?\\/(?P.*)", - Replacement: "http://${userinfo}example.com:443/${path}", + "default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8-urlrewrite-0": { + URLRewrite: &dynamic.URLRewrite{ + Hostname: ptr.To("www.foo.bar"), }, }, }, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { Name: "default-whoami-80", - Weight: ptr.To(1), + Weight: func(i int) *int { return &i }(1), + }, + }, + }, + }, + "default-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + 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 URL rewrite Combined", + paths: []string{"services.yml", "httproute/filter_url_rewrite_combined.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{ + "default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8": { + EntryPoints: []string{"web"}, + Service: "default-http-app-1-my-gateway-web-0-wrr", + Rule: "Host(`example.com`) && (Path(`/foo`) || PathPrefix(`/foo/`))", + RuleSyntax: "v3", + Priority: 10412, + Middlewares: []string{"default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8-urlrewrite-0"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8-urlrewrite-0": { + URLRewrite: &dynamic.URLRewrite{ + Hostname: ptr.To("www.foo.bar"), + Path: ptr.To("/xyz"), + PathPrefix: ptr.To("/foo"), + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-http-app-1-my-gateway-web-0-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-80", + Weight: func(i int) *int { return &i }(1), }, }, }, @@ -1764,11 +1950,6 @@ func TestLoadHTTPRoutes(t *testing.T) { return } - p := Provider{ - EntryPoints: test.entryPoints, - ExperimentalChannel: test.experimentalChannel, - } - k8sObjects, gwObjects := readResources(t, test.paths) kubeClient := kubefake.NewSimpleClientset(k8sObjects...) @@ -1784,7 +1965,13 @@ func TestLoadHTTPRoutes(t *testing.T) { <-eventCh } - conf := p.loadConfigurationFromGateways(context.Background(), client) + p := Provider{ + EntryPoints: test.entryPoints, + ExperimentalChannel: test.experimentalChannel, + client: client, + } + + conf := p.loadConfigurationFromGateways(context.Background()) assert.Equal(t, test.expected, conf) }) } @@ -1822,16 +2009,17 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1871,16 +2059,17 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1922,16 +2111,17 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -1972,16 +2162,17 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -2023,16 +2214,17 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -2078,8 +2270,6 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { return } - p := Provider{EntryPoints: test.entryPoints} - k8sObjects, gwObjects := readResources(t, test.paths) kubeClient := kubefake.NewSimpleClientset(k8sObjects...) @@ -2095,12 +2285,17 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) { <-eventCh } + p := Provider{ + EntryPoints: test.entryPoints, + client: client, + } + for group, kindFuncs := range test.groupKindBackendFuncs { for kind, backendFunc := range kindFuncs { p.RegisterBackendFuncs(group, kind, backendFunc) } } - conf := p.loadConfigurationFromGateways(context.Background(), client) + conf := p.loadConfigurationFromGateways(context.Background()) assert.Equal(t, test.expected, conf) }) } @@ -2136,17 +2331,18 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", Middlewares: []string{"default-my-middleware"}, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -2201,10 +2397,11 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", Middlewares: []string{"default-my-middleware"}, }, @@ -2213,7 +2410,7 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) { "default-my-middleware": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"Test-Header": "Test"}}}, }, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -2263,16 +2460,17 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06-err-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06-err-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -2312,16 +2510,17 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr", + Service: "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06-err-wrr", Rule: "Host(`foo.com`) && Path(`/bar`)", + Priority: 100008, RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-1c0cf64bde37d9d0df06-wrr": { + "default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06-err-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -2348,8 +2547,6 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) { return } - p := Provider{EntryPoints: test.entryPoints} - k8sObjects, gwObjects := readResources(t, []string{"services.yml", "httproute/filter_extension_ref.yml"}) kubeClient := kubefake.NewSimpleClientset(k8sObjects...) @@ -2365,12 +2562,17 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) { <-eventCh } + p := Provider{ + EntryPoints: test.entryPoints, + client: client, + } + for group, kindFuncs := range test.groupKindFilterFuncs { for kind, filterFunc := range kindFuncs { p.RegisterFilterFuncs(group, kind, filterFunc) } } - conf := p.loadConfigurationFromGateways(context.Background(), client) + conf := p.loadConfigurationFromGateways(context.Background()) assert.Equal(t, test.expected, conf) }) } @@ -2575,16 +2777,16 @@ func TestLoadTCPRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-1-my-tcp-gateway-tcp-e3b0c44298fc1c149afb": { + "default-tcp-app-1-my-tcp-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "default-tcp-app-1-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-1-my-tcp-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-1-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-1-my-tcp-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -2633,22 +2835,22 @@ func TestLoadTCPRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-1-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb": { + "default-tcp-app-1-my-tcp-gateway-tcp-1": { EntryPoints: []string{"tcp-1"}, - Service: "default-tcp-app-1-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-1-my-tcp-gateway-tcp-1-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, - "default-tcp-app-2-my-tcp-gateway-tcp-2-e3b0c44298fc1c149afb": { + "default-tcp-app-2-my-tcp-gateway-tcp-2": { EntryPoints: []string{"tcp-2"}, - Service: "default-tcp-app-2-my-tcp-gateway-tcp-2-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-2-my-tcp-gateway-tcp-2-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-1-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-1-my-tcp-gateway-tcp-1-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -2658,7 +2860,7 @@ func TestLoadTCPRoutes(t *testing.T) { }, }, }, - "default-tcp-app-2-my-tcp-gateway-tcp-2-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-2-my-tcp-gateway-tcp-2-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -2719,30 +2921,30 @@ func TestLoadTCPRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb": { + "default-tcp-app-my-tcp-gateway-tcp-1": { EntryPoints: []string{"tcp-1"}, - Service: "default-tcp-app-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr", + Service: "default-tcp-app-my-tcp-gateway-tcp-1-wrr", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr": { + "default-tcp-app-my-tcp-gateway-tcp-1-wrr": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { - Name: "default-tcp-app-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr-0", + Name: "default-tcp-app-my-tcp-gateway-tcp-1-wrr-0", Weight: ptr.To(1), }, { - Name: "default-tcp-app-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr-1", + Name: "default-tcp-app-my-tcp-gateway-tcp-1-wrr-1", Weight: ptr.To(1), }, }, }, }, - "default-tcp-app-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-my-tcp-gateway-tcp-1-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -2752,7 +2954,7 @@ func TestLoadTCPRoutes(t *testing.T) { }, }, }, - "default-tcp-app-my-tcp-gateway-tcp-1-e3b0c44298fc1c149afb-wrr-1": { + "default-tcp-app-my-tcp-gateway-tcp-1-wrr-1": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -2811,16 +3013,16 @@ func TestLoadTCPRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-1-my-gateway-tcp-e3b0c44298fc1c149afb": { + "default-tcp-app-1-my-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "default-tcp-app-1-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-1-my-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-1-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-1-my-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -2871,9 +3073,9 @@ func TestLoadTCPRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb": { + "default-tcp-app-1-my-gateway-tls": { EntryPoints: []string{"tls"}, - Service: "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-1-my-gateway-tls-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, @@ -2881,7 +3083,7 @@ func TestLoadTCPRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-1-my-gateway-tls-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{{ Name: "default-whoamitcp-9000", @@ -2935,16 +3137,16 @@ func TestLoadTCPRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-default-my-tcp-gateway-tcp-e3b0c44298fc1c149afb": { + "default-tcp-app-default-my-tcp-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "default-tcp-app-default-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-default-my-tcp-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-default-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-default-my-tcp-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -2991,22 +3193,22 @@ func TestLoadTCPRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-default-my-tcp-gateway-tcp-e3b0c44298fc1c149afb": { + "default-tcp-app-default-my-tcp-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "default-tcp-app-default-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-default-my-tcp-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, - "bar-tcp-app-bar-my-tcp-gateway-tcp-e3b0c44298fc1c149afb": { + "bar-tcp-app-bar-my-tcp-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "bar-tcp-app-bar-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "bar-tcp-app-bar-my-tcp-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-default-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-default-my-tcp-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -3016,7 +3218,7 @@ func TestLoadTCPRoutes(t *testing.T) { }, }, }, - "bar-tcp-app-bar-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "bar-tcp-app-bar-my-tcp-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -3075,16 +3277,16 @@ func TestLoadTCPRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "bar-tcp-app-bar-my-tcp-gateway-tcp-e3b0c44298fc1c149afb": { + "bar-tcp-app-bar-my-tcp-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "bar-tcp-app-bar-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "bar-tcp-app-bar-my-tcp-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "bar-tcp-app-bar-my-tcp-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "bar-tcp-app-bar-my-tcp-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -3128,11 +3330,6 @@ func TestLoadTCPRoutes(t *testing.T) { return } - p := Provider{ - EntryPoints: test.entryPoints, - ExperimentalChannel: true, - } - k8sObjects, gwObjects := readResources(t, test.paths) kubeClient := kubefake.NewSimpleClientset(k8sObjects...) @@ -3149,7 +3346,13 @@ func TestLoadTCPRoutes(t *testing.T) { <-eventCh } - conf := p.loadConfigurationFromGateways(context.Background(), client) + p := Provider{ + EntryPoints: test.entryPoints, + ExperimentalChannel: true, + client: client, + } + + conf := p.loadConfigurationFromGateways(context.Background()) assert.Equal(t, test.expected, conf) }) } @@ -3371,9 +3574,9 @@ func TestLoadTLSRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-1-my-tls-gateway-tcp-e3b0c44298fc1c149afb": { + "default-tcp-app-1-my-tls-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "default-tcp-app-1-my-tls-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-1-my-tls-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, @@ -3381,7 +3584,7 @@ func TestLoadTLSRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-1-my-tls-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-1-my-tls-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -3437,9 +3640,9 @@ func TestLoadTLSRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-1-my-tls-gateway-tcp-e3b0c44298fc1c149afb": { + "default-tcp-app-1-my-tls-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "default-tcp-app-1-my-tls-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-1-my-tls-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{ @@ -3449,7 +3652,7 @@ func TestLoadTLSRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-1-my-tls-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-1-my-tls-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -3556,9 +3759,9 @@ func TestLoadTLSRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-1-my-tls-gateway-tls-e3b0c44298fc1c149afb": { + "default-tcp-app-1-my-tls-gateway-tls": { EntryPoints: []string{"tls"}, - Service: "default-tcp-app-1-my-tls-gateway-tls-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-1-my-tls-gateway-tls-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, @@ -3575,7 +3778,7 @@ func TestLoadTLSRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-1-my-tls-gateway-tls-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-1-my-tls-gateway-tls-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -3653,9 +3856,9 @@ func TestLoadTLSRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb": { + "default-tcp-app-1-my-gateway-tls": { EntryPoints: []string{"tls"}, - Service: "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-1-my-gateway-tls-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, @@ -3663,7 +3866,7 @@ func TestLoadTLSRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-1-my-gateway-tls-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -4259,11 +4462,6 @@ func TestLoadTLSRoutes(t *testing.T) { return } - p := Provider{ - EntryPoints: test.entryPoints, - ExperimentalChannel: true, - } - k8sObjects, gwObjects := readResources(t, test.paths) kubeClient := kubefake.NewSimpleClientset(k8sObjects...) @@ -4280,7 +4478,13 @@ func TestLoadTLSRoutes(t *testing.T) { <-eventCh } - conf := p.loadConfigurationFromGateways(context.Background(), client) + p := Provider{ + EntryPoints: test.entryPoints, + ExperimentalChannel: true, + client: client, + } + + conf := p.loadConfigurationFromGateways(context.Background()) assert.Equal(t, test.expected, conf) }) } @@ -4413,15 +4617,15 @@ func TestLoadMixedRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-1-my-gateway-tcp-e3b0c44298fc1c149afb": { + "default-tcp-app-1-my-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "default-tcp-app-1-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-1-my-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, - "default-tcp-app-1-my-gateway-tls-1-e3b0c44298fc1c149afb": { + "default-tcp-app-1-my-gateway-tls-1": { EntryPoints: []string{"tls-1"}, - Service: "default-tcp-app-1-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-1-my-gateway-tls-1-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, @@ -4438,7 +4642,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-1-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-1-my-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -4448,7 +4652,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "default-tcp-app-1-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-1-my-gateway-tls-1-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -4485,23 +4689,25 @@ func TestLoadMixedRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-1-my-gateway-web-a431b128267aabc954fd": { + "default-http-app-1-my-gateway-web-0-a431b128267aabc954fd": { EntryPoints: []string{"web"}, - Service: "default-http-app-1-my-gateway-web-a431b128267aabc954fd-wrr", + Service: "default-http-app-1-my-gateway-web-0-wrr", Rule: "PathPrefix(`/`)", + Priority: 2, RuleSyntax: "v3", }, - "default-http-app-1-my-gateway-websecure-a431b128267aabc954fd": { + "default-http-app-1-my-gateway-websecure-0-a431b128267aabc954fd": { EntryPoints: []string{"websecure"}, - Service: "default-http-app-1-my-gateway-websecure-a431b128267aabc954fd-wrr", + Service: "default-http-app-1-my-gateway-websecure-0-wrr", Rule: "PathPrefix(`/`)", + Priority: 2, RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-1-my-gateway-web-a431b128267aabc954fd-wrr": { + "default-http-app-1-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -4511,7 +4717,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "default-http-app-1-my-gateway-websecure-a431b128267aabc954fd-wrr": { + "default-http-app-1-my-gateway-websecure-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -4598,15 +4804,15 @@ func TestLoadMixedRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb": { + "default-tcp-app-default-my-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-default-my-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, - "default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb": { + "default-tcp-app-default-my-gateway-tls-1": { EntryPoints: []string{"tls-1"}, - Service: "default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-default-my-gateway-tls-1-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, @@ -4623,7 +4829,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-default-my-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -4633,7 +4839,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-default-my-gateway-tls-1-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -4670,23 +4876,25 @@ func TestLoadMixedRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-default-my-gateway-web-a431b128267aabc954fd": { + "default-http-app-default-my-gateway-web-0-a431b128267aabc954fd": { EntryPoints: []string{"web"}, - Service: "default-http-app-default-my-gateway-web-a431b128267aabc954fd-wrr", + Service: "default-http-app-default-my-gateway-web-0-wrr", Rule: "PathPrefix(`/`)", + Priority: 2, RuleSyntax: "v3", }, - "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd": { + "default-http-app-default-my-gateway-websecure-0-a431b128267aabc954fd": { EntryPoints: []string{"websecure"}, - Service: "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd-wrr", + Service: "default-http-app-default-my-gateway-websecure-0-wrr", Rule: "PathPrefix(`/`)", + Priority: 2, RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-default-my-gateway-web-a431b128267aabc954fd-wrr": { + "default-http-app-default-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -4696,7 +4904,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd-wrr": { + "default-http-app-default-my-gateway-websecure-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -4755,15 +4963,15 @@ func TestLoadMixedRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb": { + "default-tcp-app-default-my-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-default-my-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, - "default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb": { + "default-tcp-app-default-my-gateway-tls-1": { EntryPoints: []string{"tls-1"}, - Service: "default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-default-my-gateway-tls-1-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, @@ -4777,15 +4985,15 @@ func TestLoadMixedRoutes(t *testing.T) { Passthrough: true, }, }, - "bar-tcp-app-bar-my-gateway-tcp-e3b0c44298fc1c149afb": { + "bar-tcp-app-bar-my-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "bar-tcp-app-bar-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "bar-tcp-app-bar-my-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, - "bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb": { + "bar-tcp-app-bar-my-gateway-tls-1": { EntryPoints: []string{"tls-1"}, - Service: "bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0", + Service: "bar-tcp-app-bar-my-gateway-tls-1-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, @@ -4793,7 +5001,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-default-my-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -4803,7 +5011,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "default-tcp-app-default-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-default-my-gateway-tls-1-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -4847,7 +5055,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "bar-tcp-app-bar-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "bar-tcp-app-bar-my-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -4857,7 +5065,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0": { + "bar-tcp-app-bar-my-gateway-tls-1-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -4872,36 +5080,40 @@ func TestLoadMixedRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-default-my-gateway-web-a431b128267aabc954fd": { + "default-http-app-default-my-gateway-web-0-a431b128267aabc954fd": { EntryPoints: []string{"web"}, - Service: "default-http-app-default-my-gateway-web-a431b128267aabc954fd-wrr", + Service: "default-http-app-default-my-gateway-web-0-wrr", Rule: "PathPrefix(`/`)", + Priority: 2, RuleSyntax: "v3", }, - "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd": { + "default-http-app-default-my-gateway-websecure-0-a431b128267aabc954fd": { EntryPoints: []string{"websecure"}, - Service: "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd-wrr", + Service: "default-http-app-default-my-gateway-websecure-0-wrr", Rule: "PathPrefix(`/`)", + Priority: 2, RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, - "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd": { + "bar-http-app-bar-my-gateway-web-0-a431b128267aabc954fd": { EntryPoints: []string{"web"}, - Service: "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr", + Service: "bar-http-app-bar-my-gateway-web-0-wrr", Rule: "PathPrefix(`/`)", + Priority: 2, RuleSyntax: "v3", }, - "bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd": { + "bar-http-app-bar-my-gateway-websecure-0-a431b128267aabc954fd": { EntryPoints: []string{"websecure"}, - Service: "bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd-wrr", + Service: "bar-http-app-bar-my-gateway-websecure-0-wrr", Rule: "PathPrefix(`/`)", + Priority: 2, RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-default-my-gateway-web-a431b128267aabc954fd-wrr": { + "default-http-app-default-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -4911,7 +5123,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd-wrr": { + "default-http-app-default-my-gateway-websecure-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -4953,7 +5165,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr": { + "bar-http-app-bar-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -4963,7 +5175,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd-wrr": { + "bar-http-app-bar-my-gateway-websecure-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -5006,15 +5218,15 @@ func TestLoadMixedRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "bar-tcp-app-bar-my-gateway-tcp-e3b0c44298fc1c149afb": { + "bar-tcp-app-bar-my-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "bar-tcp-app-bar-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "bar-tcp-app-bar-my-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, - "bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb": { + "bar-tcp-app-bar-my-gateway-tls-1": { EntryPoints: []string{"tls-1"}, - Service: "bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0", + Service: "bar-tcp-app-bar-my-gateway-tls-1-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, @@ -5043,7 +5255,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "bar-tcp-app-bar-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "bar-tcp-app-bar-my-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -5053,7 +5265,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "bar-tcp-app-bar-my-gateway-tls-1-e3b0c44298fc1c149afb-wrr-0": { + "bar-tcp-app-bar-my-gateway-tls-1-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -5078,16 +5290,18 @@ func TestLoadMixedRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd": { + "bar-http-app-bar-my-gateway-web-0-a431b128267aabc954fd": { EntryPoints: []string{"web"}, - Service: "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr", + Service: "bar-http-app-bar-my-gateway-web-0-wrr", Rule: "PathPrefix(`/`)", + Priority: 2, RuleSyntax: "v3", }, - "bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd": { + "bar-http-app-bar-my-gateway-websecure-0-a431b128267aabc954fd": { EntryPoints: []string{"websecure"}, - Service: "bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd-wrr", + Service: "bar-http-app-bar-my-gateway-websecure-0-wrr", Rule: "PathPrefix(`/`)", + Priority: 2, RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, @@ -5110,7 +5324,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "bar-http-app-bar-my-gateway-web-a431b128267aabc954fd-wrr": { + "bar-http-app-bar-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -5120,7 +5334,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "bar-http-app-bar-my-gateway-websecure-a431b128267aabc954fd-wrr": { + "bar-http-app-bar-my-gateway-websecure-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -5162,15 +5376,15 @@ func TestLoadMixedRoutes(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb": { + "default-tcp-app-default-my-gateway-tcp": { EntryPoints: []string{"tcp"}, - Service: "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-default-my-gateway-tcp-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", }, - "default-tcp-app-default-my-gateway-tls-e3b0c44298fc1c149afb": { + "default-tcp-app-default-my-gateway-tls": { EntryPoints: []string{"tls"}, - Service: "default-tcp-app-default-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-default-my-gateway-tls-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, @@ -5178,7 +5392,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-default-my-gateway-tcp-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-default-my-gateway-tcp-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -5188,7 +5402,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "default-tcp-app-default-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-default-my-gateway-tls-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{ { @@ -5215,23 +5429,25 @@ func TestLoadMixedRoutes(t *testing.T) { }, HTTP: &dynamic.HTTPConfiguration{ Routers: map[string]*dynamic.Router{ - "default-http-app-default-my-gateway-web-a431b128267aabc954fd": { + "default-http-app-default-my-gateway-web-0-a431b128267aabc954fd": { EntryPoints: []string{"web"}, - Service: "default-http-app-default-my-gateway-web-a431b128267aabc954fd-wrr", + Service: "default-http-app-default-my-gateway-web-0-wrr", Rule: "PathPrefix(`/`)", + Priority: 2, RuleSyntax: "v3", }, - "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd": { + "default-http-app-default-my-gateway-websecure-0-a431b128267aabc954fd": { EntryPoints: []string{"websecure"}, - Service: "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd-wrr", + Service: "default-http-app-default-my-gateway-websecure-0-wrr", Rule: "PathPrefix(`/`)", + Priority: 2, RuleSyntax: "v3", TLS: &dynamic.RouterTLSConfig{}, }, }, Middlewares: map[string]*dynamic.Middleware{}, Services: map[string]*dynamic.Service{ - "default-http-app-default-my-gateway-web-a431b128267aabc954fd-wrr": { + "default-http-app-default-my-gateway-web-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -5241,7 +5457,7 @@ func TestLoadMixedRoutes(t *testing.T) { }, }, }, - "default-http-app-default-my-gateway-websecure-a431b128267aabc954fd-wrr": { + "default-http-app-default-my-gateway-websecure-0-wrr": { Weighted: &dynamic.WeightedRoundRobin{ Services: []dynamic.WRRService{ { @@ -5292,11 +5508,6 @@ func TestLoadMixedRoutes(t *testing.T) { return } - p := Provider{ - EntryPoints: test.entryPoints, - ExperimentalChannel: test.experimentalChannel, - } - k8sObjects, gwObjects := readResources(t, test.paths) kubeClient := kubefake.NewSimpleClientset(k8sObjects...) @@ -5313,7 +5524,13 @@ func TestLoadMixedRoutes(t *testing.T) { <-eventCh } - conf := p.loadConfigurationFromGateways(context.Background(), client) + p := Provider{ + EntryPoints: test.entryPoints, + ExperimentalChannel: test.experimentalChannel, + client: client, + } + + conf := p.loadConfigurationFromGateways(context.Background()) assert.Equal(t, test.expected, conf) }) } @@ -5377,7 +5594,7 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) { }, }, { - desc: "Empty because ReferenceGrant spec.from does not match", + desc: "Empty because ReferenceGrant spec.from does not match secret", paths: []string{"services.yml", "referencegrant/for_secret_not_matching_from.yml"}, entryPoints: map[string]Entrypoint{ "tls": {Address: ":9000"}, @@ -5403,7 +5620,7 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) { }, }, { - desc: "Empty because ReferenceGrant spec.to does not match", + desc: "Empty because ReferenceGrant spec.to does not match secret", paths: []string{"services.yml", "referencegrant/for_secret_not_matching_to.yml"}, entryPoints: map[string]Entrypoint{ "tls": {Address: ":9000"}, @@ -5442,9 +5659,9 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) { }, TCP: &dynamic.TCPConfiguration{ Routers: map[string]*dynamic.TCPRouter{ - "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb": { + "default-tcp-app-1-my-gateway-tls": { EntryPoints: []string{"tls"}, - Service: "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0", + Service: "default-tcp-app-1-my-gateway-tls-wrr-0", Rule: "HostSNI(`*`)", RuleSyntax: "v3", TLS: &dynamic.RouterTCPTLSConfig{}, @@ -5452,7 +5669,7 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) { }, Middlewares: map[string]*dynamic.TCPMiddleware{}, Services: map[string]*dynamic.TCPService{ - "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0": { + "default-tcp-app-1-my-gateway-tls-wrr-0": { Weighted: &dynamic.TCPWeightedRoundRobin{ Services: []dynamic.TCPWRRService{{ Name: "default-whoamitcp-9000", @@ -5493,6 +5710,130 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) { }, }, }, + { + desc: "Empty because ReferenceGrant for Service is missing", + paths: []string{"services.yml", "referencegrant/for_secret_missing.yml"}, + entryPoints: map[string]Entrypoint{ + "tls": {Address: ":9000"}, + }, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "Empty because ReferenceGrant spec.from does not match service", + paths: []string{"services.yml", "referencegrant/for_service_not_matching_from.yml"}, + entryPoints: map[string]Entrypoint{ + "tls": {Address: ":9000"}, + }, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "Empty because ReferenceGrant spec.to does not match service", + paths: []string{"services.yml", "referencegrant/for_service_not_matching_to.yml"}, + entryPoints: map[string]Entrypoint{ + "tls": {Address: ":9000"}, + }, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "For Service", + paths: []string{"services.yml", "referencegrant/for_service.yml"}, + entryPoints: map[string]Entrypoint{ + "http": {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{ + "default-http-app-1-my-gateway-http-0-d40286ed9f4652ca2108": { + EntryPoints: []string{"http"}, + Rule: "Host(`foo.example.com`) && PathPrefix(`/`)", + Service: "default-http-app-1-my-gateway-http-0-wrr", + RuleSyntax: "v3", + Priority: 17, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-http-app-1-my-gateway-http-0-wrr": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-bar-80", + Weight: ptr.To(1), + Status: ptr.To(500), + }, + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, } for _, test := range testCases { @@ -5503,11 +5844,6 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) { return } - p := Provider{ - EntryPoints: test.entryPoints, - ExperimentalChannel: test.experimentalChannel, - } - k8sObjects, gwObjects := readResources(t, test.paths) kubeClient := kubefake.NewSimpleClientset(k8sObjects...) @@ -5524,7 +5860,13 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) { <-eventCh } - conf := p.loadConfigurationFromGateways(context.Background(), client) + p := Provider{ + EntryPoints: test.entryPoints, + ExperimentalChannel: test.experimentalChannel, + client: client, + } + + conf := p.loadConfigurationFromGateways(context.Background()) assert.Equal(t, test.expected, conf) }) } @@ -6363,8 +6705,6 @@ func Test_gatewayAddresses(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - p := Provider{StatusAddress: test.statusAddress} - k8sObjects, gwObjects := readResources(t, test.paths) kubeClient := kubefake.NewSimpleClientset(k8sObjects...) @@ -6380,7 +6720,12 @@ func Test_gatewayAddresses(t *testing.T) { <-eventCh } - got, err := p.gatewayAddresses(client) + p := Provider{ + StatusAddress: test.statusAddress, + client: client, + } + + got, err := p.gatewayAddresses() test.wantErr(t, err) assert.Equal(t, test.want, got) @@ -6388,6 +6733,136 @@ func Test_gatewayAddresses(t *testing.T) { } } +func Test_upsertRouteConditionResolvedRefs(t *testing.T) { + testCases := []struct { + desc string + conditions []metav1.Condition + condition metav1.Condition + wantConditions []metav1.Condition + }{ + { + desc: "True to False", + conditions: []metav1.Condition{ + { + Type: "foo", + Status: "bar", + Reason: "baz", + Message: "foobarbaz", + }, + { + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + Reason: "foo", + Message: "foo", + }, + }, + condition: metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + Reason: "bar", + Message: "bar", + }, + wantConditions: []metav1.Condition{ + { + Type: "foo", + Status: "bar", + Reason: "baz", + Message: "foobarbaz", + }, + { + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + Reason: "bar", + Message: "bar", + }, + }, + }, + { + desc: "False to False", + conditions: []metav1.Condition{ + { + Type: "foo", + Status: "bar", + Reason: "baz", + Message: "foobarbaz", + }, + { + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + Reason: "foo", + Message: "foo", + }, + }, + condition: metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + Reason: "bar", + Message: "bar", + }, + wantConditions: []metav1.Condition{ + { + Type: "foo", + Status: "bar", + Reason: "baz", + Message: "foobarbaz", + }, + { + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + Reason: "bar", + Message: "bar", + }, + }, + }, + { + desc: "False to True: no upsert", + conditions: []metav1.Condition{ + { + Type: "foo", + Status: "bar", + Reason: "baz", + Message: "foobarbaz", + }, + { + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + Reason: "foo", + Message: "foo", + }, + }, + condition: metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + Reason: "bar", + Message: "bar", + }, + wantConditions: []metav1.Condition{ + { + Type: "foo", + Status: "bar", + Reason: "baz", + Message: "foobarbaz", + }, + { + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + Reason: "foo", + Message: "foo", + }, + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := upsertRouteConditionResolvedRefs(test.conditions, test.condition) + assert.Equal(t, test.wantConditions, got) + }) + } +} + // We cannot use the gateway-api fake.NewSimpleClientset due to Gateway being pluralized as "gatewaies" instead of "gateways". func newGatewaySimpleClientSet(t *testing.T, objects ...runtime.Object) *gatefake.Clientset { t.Helper() diff --git a/pkg/provider/kubernetes/gateway/tcproute.go b/pkg/provider/kubernetes/gateway/tcproute.go index 118ae8f1d..cdbb155ff 100644 --- a/pkg/provider/kubernetes/gateway/tcproute.go +++ b/pkg/provider/kubernetes/gateway/tcproute.go @@ -18,11 +18,12 @@ import ( gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) -func (p *Provider) loadTCPRoutes(ctx context.Context, client Client, gatewayListeners []gatewayListener, conf *dynamic.Configuration) { +func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) { logger := log.Ctx(ctx) - routes, err := client.ListTCPRoutes() + routes, err := p.client.ListTCPRoutes() if err != nil { - logger.Error().Err(err).Msgf("Get TCPRoutes: %s", err) + logger.Error().Err(err).Msgf("Unable to list TCPRoutes") + return } for _, route := range routes { @@ -34,39 +35,6 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, client Client, gatewayList ParentRef: parentRef, ControllerName: controllerName, Conditions: []metav1.Condition{ - { - Type: string(gatev1.RouteConditionAccepted), - Status: metav1.ConditionTrue, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteReasonAccepted), - }, - }, - } - - var attachedListeners bool - for _, listener := range gatewayListeners { - if !matchListener(listener, route.Namespace, parentRef) { - continue - } - - if !allowRoute(listener, route.Namespace, kindTCPRoute) { - continue - } - - listener.Status.AttachedRoutes++ - attachedListeners = true - - resolveConditions := p.loadTCPRoute(client, listener, route, conf) - - // TODO: handle more accurately route conditions (in case of multiple listener matching). - for _, condition := range resolveConditions { - parentStatus.Conditions = appendCondition(parentStatus.Conditions, condition) - } - } - - if !attachedListeners { - parentStatus.Conditions = []metav1.Condition{ { Type: string(gatev1.RouteConditionAccepted), Status: metav1.ConditionFalse, @@ -74,7 +42,33 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, client Client, gatewayList LastTransitionTime: metav1.Now(), Reason: string(gatev1.RouteReasonNoMatchingParent), }, + }, + } + + for _, listener := range gatewayListeners { + if !matchListener(listener, route.Namespace, parentRef) { + continue } + + accepted := true + if !allowRoute(listener, route.Namespace, kindTCPRoute) { + parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNotAllowedByListeners)) + accepted = false + } + + if accepted { + listener.Status.AttachedRoutes++ + // only consider the route attached if the listener is in an "attached" state. + if listener.Attached { + parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonAccepted)) + } + } + + routeConf, resolveRefCondition := p.loadTCPRoute(listener, route) + if accepted && listener.Attached { + mergeTCPConfiguration(routeConf, conf) + } + parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition) } parentStatuses = append(parentStatuses, *parentStatus) @@ -85,7 +79,7 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, client Client, gatewayList Parents: parentStatuses, }, } - if err := client.UpdateTCPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil { + if err := p.client.UpdateTCPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil { logger.Error(). Err(err). Msg("Unable to update TCPRoute status") @@ -93,17 +87,24 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, client Client, gatewayList } } -func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route *gatev1alpha2.TCPRoute, conf *dynamic.Configuration) []metav1.Condition { - routeConditions := []metav1.Condition{ - { - Type: string(gatev1.RouteConditionResolvedRefs), - Status: metav1.ConditionTrue, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteConditionResolvedRefs), +func (p *Provider) loadTCPRoute(listener gatewayListener, route *gatev1alpha2.TCPRoute) (*dynamic.Configuration, metav1.Condition) { + conf := &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: make(map[string]*dynamic.TCPRouter), + Middlewares: make(map[string]*dynamic.TCPMiddleware), + Services: make(map[string]*dynamic.TCPService), + ServersTransports: make(map[string]*dynamic.TCPServersTransport), }, } + condition := metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteConditionResolvedRefs), + } + router := dynamic.TCPRouter{ Rule: "HostSNI(`*`)", EntryPoints: []string{listener.EPName}, @@ -118,8 +119,7 @@ func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route * } // Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes. - routerName := route.Name + "-" + listener.GWName + "-" + listener.EPName - routerKey := provider.Normalize(makeRouterKey("", makeID(route.Namespace, routerName))) + routerName := provider.Normalize(route.Namespace + "-" + route.Name + "-" + listener.GWName + "-" + listener.EPName) var ruleServiceNames []string for i, rule := range route.Spec.Rules { @@ -129,24 +129,23 @@ func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route * continue } - wrrService, subServices, err := loadTCPServices(client, route.Namespace, rule.BackendRefs) + wrrService, subServices, err := p.loadTCPServices(route.Namespace, rule.BackendRefs) if err != nil { - routeConditions = appendCondition(routeConditions, metav1.Condition{ + return conf, metav1.Condition{ Type: string(gatev1.RouteConditionResolvedRefs), Status: metav1.ConditionFalse, ObservedGeneration: route.Generation, LastTransitionTime: metav1.Now(), Reason: string(gatev1.RouteReasonBackendNotFound), Message: fmt.Sprintf("Cannot load TCPRoute service %s/%s: %v", route.Namespace, route.Name, err), - }) - return routeConditions + } } for svcName, svc := range subServices { conf.TCP.Services[svcName] = svc } - serviceName := fmt.Sprintf("%s-wrr-%d", routerKey, i) + serviceName := fmt.Sprintf("%s-wrr-%d", routerName, i) conf.TCP.Services[serviceName] = wrrService ruleServiceNames = append(ruleServiceNames, serviceName) @@ -154,11 +153,11 @@ func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route * if len(ruleServiceNames) == 1 { router.Service = ruleServiceNames[0] - conf.TCP.Routers[routerKey] = &router - return routeConditions + conf.TCP.Routers[routerName] = &router + return conf, condition } - routeServiceKey := routerKey + "-wrr" + routeServiceKey := routerName + "-wrr" routeService := &dynamic.TCPService{Weighted: &dynamic.TCPWeightedRoundRobin{}} for _, name := range ruleServiceNames { @@ -171,13 +170,13 @@ func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route * conf.TCP.Services[routeServiceKey] = routeService router.Service = routeServiceKey - conf.TCP.Routers[routerKey] = &router + conf.TCP.Routers[routerName] = &router - return routeConditions + return conf, condition } // loadTCPServices is generating a WRR service, even when there is only one target. -func loadTCPServices(client Client, namespace string, backendRefs []gatev1.BackendRef) (*dynamic.TCPService, map[string]*dynamic.TCPService, error) { +func (p *Provider) loadTCPServices(namespace string, backendRefs []gatev1.BackendRef) (*dynamic.TCPService, map[string]*dynamic.TCPService, error) { services := map[string]*dynamic.TCPService{} wrrSvc := &dynamic.TCPService{ @@ -211,7 +210,7 @@ func loadTCPServices(client Client, namespace string, backendRefs []gatev1.Backe LoadBalancer: &dynamic.TCPServersLoadBalancer{}, } - service, exists, err := client.GetService(namespace, string(backendRef.Name)) + service, exists, err := p.client.GetService(namespace, string(backendRef.Name)) if err != nil { return nil, nil, err } @@ -247,7 +246,7 @@ func loadTCPServices(client Client, namespace string, backendRefs []gatev1.Backe return nil, nil, errors.New("service port not found") } - endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, string(backendRef.Name)) + endpoints, endpointsExists, endpointsErr := p.client.GetEndpoints(namespace, string(backendRef.Name)) if endpointsErr != nil { return nil, nil, endpointsErr } @@ -282,7 +281,7 @@ func loadTCPServices(client Client, namespace string, backendRefs []gatev1.Backe } } - serviceName := provider.Normalize(makeID(service.Namespace, service.Name) + "-" + portStr) + serviceName := provider.Normalize(service.Namespace + "-" + service.Name + "-" + portStr) services[serviceName] = &svc wrrSvc.Weighted.Services = append(wrrSvc.Weighted.Services, dynamic.TCPWRRService{Name: serviceName, Weight: &weight}) @@ -294,3 +293,35 @@ func loadTCPServices(client Client, namespace string, backendRefs []gatev1.Backe return wrrSvc, services, nil } + +func mergeTCPConfiguration(from, to *dynamic.Configuration) { + if from == nil || from.TCP == nil || to == nil { + return + } + + if to.TCP == nil { + to.TCP = from.TCP + return + } + + if to.TCP.Routers == nil { + to.TCP.Routers = map[string]*dynamic.TCPRouter{} + } + for routerName, router := range from.TCP.Routers { + to.TCP.Routers[routerName] = router + } + + if to.TCP.Middlewares == nil { + to.TCP.Middlewares = map[string]*dynamic.TCPMiddleware{} + } + for middlewareName, middleware := range from.TCP.Middlewares { + to.TCP.Middlewares[middlewareName] = middleware + } + + if to.TCP.Services == nil { + to.TCP.Services = map[string]*dynamic.TCPService{} + } + for serviceName, service := range from.TCP.Services { + to.TCP.Services[serviceName] = service + } +} diff --git a/pkg/provider/kubernetes/gateway/tlsroute.go b/pkg/provider/kubernetes/gateway/tlsroute.go index 1eb30c788..131fb1d92 100644 --- a/pkg/provider/kubernetes/gateway/tlsroute.go +++ b/pkg/provider/kubernetes/gateway/tlsroute.go @@ -15,11 +15,12 @@ import ( gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) -func (p *Provider) loadTLSRoutes(ctx context.Context, client Client, gatewayListeners []gatewayListener, conf *dynamic.Configuration) { +func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) { logger := log.Ctx(ctx) - routes, err := client.ListTLSRoutes() + routes, err := p.client.ListTLSRoutes() if err != nil { - logger.Error().Err(err).Msgf("Get TLSRoutes: %s", err) + logger.Error().Err(err).Msgf("Unable to list TLSRoute") + return } for _, route := range routes { @@ -31,44 +32,6 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, client Client, gatewayList ParentRef: parentRef, ControllerName: controllerName, Conditions: []metav1.Condition{ - { - Type: string(gatev1.RouteConditionAccepted), - Status: metav1.ConditionTrue, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteReasonAccepted), - }, - }, - } - - var attachedListeners bool - for _, listener := range gatewayListeners { - if !matchListener(listener, route.Namespace, parentRef) { - continue - } - - if !allowRoute(listener, route.Namespace, kindTLSRoute) { - continue - } - - hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames) - if !ok { - continue - } - - listener.Status.AttachedRoutes++ - attachedListeners = true - - resolveConditions := p.loadTLSRoute(client, listener, route, hostnames, conf) - - // TODO: handle more accurately route conditions (in case of multiple listener matching). - for _, condition := range resolveConditions { - parentStatus.Conditions = appendCondition(parentStatus.Conditions, condition) - } - } - - if !attachedListeners { - parentStatus.Conditions = []metav1.Condition{ { Type: string(gatev1.RouteConditionAccepted), Status: metav1.ConditionFalse, @@ -76,7 +39,38 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, client Client, gatewayList LastTransitionTime: metav1.Now(), Reason: string(gatev1.RouteReasonNoMatchingParent), }, + }, + } + + for _, listener := range gatewayListeners { + if !matchListener(listener, route.Namespace, parentRef) { + continue } + + accepted := true + if !allowRoute(listener, route.Namespace, kindTLSRoute) { + parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNotAllowedByListeners)) + accepted = false + } + hostnames, ok := findMatchingHostnames(listener.Hostname, route.Spec.Hostnames) + if !ok { + parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonNoMatchingListenerHostname)) + accepted = false + } + + if accepted { + listener.Status.AttachedRoutes++ + // only consider the route attached if the listener is in an "attached" state. + if listener.Attached { + parentStatus.Conditions = updateRouteConditionAccepted(parentStatus.Conditions, string(gatev1.RouteReasonAccepted)) + } + } + + routeConf, resolveRefCondition := p.loadTLSRoute(listener, route, hostnames) + if accepted && listener.Attached { + mergeTCPConfiguration(routeConf, conf) + } + parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition) } parentStatuses = append(parentStatuses, *parentStatus) @@ -87,7 +81,7 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, client Client, gatewayList Parents: parentStatuses, }, } - if err := client.UpdateTLSRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil { + if err := p.client.UpdateTLSRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil { logger.Error(). Err(err). Msg("Unable to update TLSRoute status") @@ -95,29 +89,36 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, client Client, gatewayList } } -func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route *gatev1alpha2.TLSRoute, hostnames []gatev1.Hostname, conf *dynamic.Configuration) []metav1.Condition { - routeConditions := []metav1.Condition{ - { - Type: string(gatev1.RouteConditionResolvedRefs), - Status: metav1.ConditionTrue, - ObservedGeneration: route.Generation, - LastTransitionTime: metav1.Now(), - Reason: string(gatev1.RouteConditionResolvedRefs), +func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1alpha2.TLSRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) { + conf := &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: make(map[string]*dynamic.TCPRouter), + Middlewares: make(map[string]*dynamic.TCPMiddleware), + Services: make(map[string]*dynamic.TCPService), + ServersTransports: make(map[string]*dynamic.TCPServersTransport), }, } + condition := metav1.Condition{ + Type: string(gatev1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: route.Generation, + LastTransitionTime: metav1.Now(), + Reason: string(gatev1.RouteConditionResolvedRefs), + } + router := dynamic.TCPRouter{ RuleSyntax: "v3", Rule: hostSNIRule(hostnames), EntryPoints: []string{listener.EPName}, TLS: &dynamic.RouterTCPTLSConfig{ - Passthrough: listener.TLS.Mode != nil && *listener.TLS.Mode == gatev1.TLSModePassthrough, + Passthrough: listener.TLS != nil && listener.TLS.Mode != nil && *listener.TLS.Mode == gatev1.TLSModePassthrough, }, } // Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes. - routerName := route.Name + "-" + listener.GWName + "-" + listener.EPName - routerKey := provider.Normalize(makeRouterKey(router.Rule, makeID(route.Namespace, routerName))) + routeKey := provider.Normalize(route.Namespace + "-" + route.Name + "-" + listener.GWName + "-" + listener.EPName) + routerName := makeRouterName(router.Rule, routeKey) var ruleServiceNames []string for i, routeRule := range route.Spec.Rules { @@ -127,17 +128,17 @@ func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route * continue } - wrrService, subServices, err := loadTCPServices(client, route.Namespace, routeRule.BackendRefs) + wrrService, subServices, err := p.loadTCPServices(route.Namespace, routeRule.BackendRefs) if err != nil { // update "ResolvedRefs" status true with "InvalidBackendRefs" reason - routeConditions = appendCondition(routeConditions, metav1.Condition{ + condition = metav1.Condition{ Type: string(gatev1.RouteConditionResolvedRefs), Status: metav1.ConditionFalse, ObservedGeneration: route.Generation, LastTransitionTime: metav1.Now(), Reason: string(gatev1.RouteReasonBackendNotFound), Message: fmt.Sprintf("Cannot load TLSRoute service %s/%s: %v", route.Namespace, route.Name, err), - }) + } continue } @@ -145,7 +146,7 @@ func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route * conf.TCP.Services[svcName] = svc } - serviceName := fmt.Sprintf("%s-wrr-%d", routerKey, i) + serviceName := fmt.Sprintf("%s-wrr-%d", routerName, i) conf.TCP.Services[serviceName] = wrrService ruleServiceNames = append(ruleServiceNames, serviceName) @@ -153,12 +154,12 @@ func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route * if len(ruleServiceNames) == 1 { router.Service = ruleServiceNames[0] - conf.TCP.Routers[routerKey] = &router + conf.TCP.Routers[routerName] = &router - return routeConditions + return conf, condition } - routeServiceKey := routerKey + "-wrr" + routeServiceKey := routerName + "-wrr" routeService := &dynamic.TCPService{Weighted: &dynamic.TCPWeightedRoundRobin{}} for _, name := range ruleServiceNames { @@ -171,9 +172,9 @@ func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route * conf.TCP.Services[routeServiceKey] = routeService router.Service = routeServiceKey - conf.TCP.Routers[routerKey] = &router + conf.TCP.Routers[routerName] = &router - return routeConditions + return conf, condition } func hostSNIRule(hostnames []gatev1.Hostname) string { diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 18084b881..b952c32d7 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -235,7 +235,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl certConfigs := make(map[string]*tls.CertAndStores) for _, ingress := range ingresses { logger := log.Ctx(ctx).With().Str("ingress", ingress.Name).Str("namespace", ingress.Namespace).Logger() - ctx = logger.WithContext(ctx) + ctxIngress := logger.WithContext(ctx) if !p.shouldProcessIngress(ingress, ingressClasses) { continue @@ -247,7 +247,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl continue } - err = getCertificates(ctx, ingress, client, certConfigs) + err = getCertificates(ctxIngress, ingress, client, certConfigs) if err != nil { logger.Error().Err(err).Msg("Error configuring TLS") } @@ -289,7 +289,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl rt.TLS = rtConfig.Router.TLS } - p.applyRouterTransform(ctx, rt, ingress) + p.applyRouterTransform(ctxIngress, rt, ingress) conf.HTTP.Routers["default-router"] = rt conf.HTTP.Services["default-backend"] = service @@ -336,7 +336,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl rt := loadRouter(rule, pa, rtConfig, serviceName) - p.applyRouterTransform(ctx, rt, ingress) + p.applyRouterTransform(ctxIngress, rt, ingress) routerKey := strings.TrimPrefix(provider.Normalize(ingress.Namespace+"-"+ingress.Name+"-"+rule.Host+pa.Path), "-") diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index 25de18e03..b4493c2d5 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -139,6 +139,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/middlewares/Middleware09/headers/accessControlExposeHeaders/0": "foobar", "traefik/http/middlewares/Middleware09/headers/accessControlExposeHeaders/1": "foobar", "traefik/http/middlewares/Middleware09/headers/contentSecurityPolicy": "foobar", + "traefik/http/middlewares/Middleware09/headers/contentSecurityPolicyReportOnly": "foobar", "traefik/http/middlewares/Middleware09/headers/publicKey": "foobar", "traefik/http/middlewares/Middleware09/headers/customRequestHeaders/name0": "foobar", "traefik/http/middlewares/Middleware09/headers/customRequestHeaders/name1": "foobar", @@ -601,22 +602,23 @@ func Test_buildConfiguration(t *testing.T) { "name1": "foobar", "name0": "foobar", }, - SSLForceHost: Bool(true), - STSSeconds: 42, - STSIncludeSubdomains: true, - STSPreload: true, - ForceSTSHeader: true, - FrameDeny: true, - CustomFrameOptionsValue: "foobar", - ContentTypeNosniff: true, - BrowserXSSFilter: true, - CustomBrowserXSSValue: "foobar", - ContentSecurityPolicy: "foobar", - PublicKey: "foobar", - ReferrerPolicy: "foobar", - FeaturePolicy: String("foobar"), - PermissionsPolicy: "foobar", - IsDevelopment: true, + SSLForceHost: Bool(true), + STSSeconds: 42, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foobar", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foobar", + ContentSecurityPolicy: "foobar", + ContentSecurityPolicyReportOnly: "foobar", + PublicKey: "foobar", + ReferrerPolicy: "foobar", + FeaturePolicy: String("foobar"), + PermissionsPolicy: "foobar", + IsDevelopment: true, }, }, "Middleware17": { diff --git a/pkg/redactor/redactor_config_test.go b/pkg/redactor/redactor_config_test.go index ed498721f..ee653b28a 100644 --- a/pkg/redactor/redactor_config_test.go +++ b/pkg/redactor/redactor_config_test.go @@ -214,6 +214,7 @@ func init() { BrowserXSSFilter: true, CustomBrowserXSSValue: "foo", ContentSecurityPolicy: "foo", + ContentSecurityPolicyReportOnly: "foo", PublicKey: "foo", ReferrerPolicy: "foo", PermissionsPolicy: "foo", diff --git a/pkg/redactor/testdata/anonymized-dynamic-config.json b/pkg/redactor/testdata/anonymized-dynamic-config.json index f797b3800..66668fe85 100644 --- a/pkg/redactor/testdata/anonymized-dynamic-config.json +++ b/pkg/redactor/testdata/anonymized-dynamic-config.json @@ -170,6 +170,7 @@ "browserXssFilter": true, "customBrowserXSSValue": "xxxx", "contentSecurityPolicy": "xxxx", + "contentSecurityPolicyReportOnly": "xxxx", "publicKey": "xxxx", "referrerPolicy": "foo", "permissionsPolicy": "foo", diff --git a/pkg/redactor/testdata/secured-dynamic-config.json b/pkg/redactor/testdata/secured-dynamic-config.json index 8ff3d0789..75c70ae25 100644 --- a/pkg/redactor/testdata/secured-dynamic-config.json +++ b/pkg/redactor/testdata/secured-dynamic-config.json @@ -173,6 +173,7 @@ "browserXssFilter": true, "customBrowserXSSValue": "foo", "contentSecurityPolicy": "foo", + "contentSecurityPolicyReportOnly": "foo", "publicKey": "foo", "referrerPolicy": "foo", "permissionsPolicy": "foo", diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go index 53143c5bc..2caa36dd0 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -345,7 +345,7 @@ func TestListenProvidersThrottleProviderConfigReload(t *testing.T) { // To load 5 new configs it would require 150ms (5 configs * 30ms). // In 100ms, we should only have time to load 3 configs. assert.LessOrEqual(t, publishedConfigCount, 3, "config was applied too many times") - assert.Greater(t, publishedConfigCount, 0, "config was not applied at least once") + assert.Positive(t, publishedConfigCount, "config was not applied at least once") } func TestListenProvidersSkipsEmptyConfigs(t *testing.T) { diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index 2217dc58a..8b8ee16b9 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -20,8 +20,10 @@ import ( "github.com/traefik/traefik/v3/pkg/middlewares/compress" "github.com/traefik/traefik/v3/pkg/middlewares/contenttype" "github.com/traefik/traefik/v3/pkg/middlewares/customerrors" + "github.com/traefik/traefik/v3/pkg/middlewares/gatewayapi/headermodifier" + gapiredirect "github.com/traefik/traefik/v3/pkg/middlewares/gatewayapi/redirect" + "github.com/traefik/traefik/v3/pkg/middlewares/gatewayapi/urlrewrite" "github.com/traefik/traefik/v3/pkg/middlewares/grpcweb" - "github.com/traefik/traefik/v3/pkg/middlewares/headermodifier" "github.com/traefik/traefik/v3/pkg/middlewares/headers" "github.com/traefik/traefik/v3/pkg/middlewares/inflightreq" "github.com/traefik/traefik/v3/pkg/middlewares/ipallowlist" @@ -391,7 +393,25 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( return nil, badConf } middleware = func(next http.Handler) (http.Handler, error) { - return headermodifier.NewRequestHeaderModifier(ctx, next, *config.RequestHeaderModifier, middlewareName) + return headermodifier.NewRequestHeaderModifier(ctx, next, *config.RequestHeaderModifier, middlewareName), nil + } + } + + if config.RequestRedirect != nil { + if middleware != nil { + return nil, badConf + } + middleware = func(next http.Handler) (http.Handler, error) { + return gapiredirect.NewRequestRedirect(ctx, next, *config.RequestRedirect, middlewareName) + } + } + + if config.URLRewrite != nil { + if middleware != nil { + return nil, badConf + } + middleware = func(next http.Handler) (http.Handler, error) { + return urlrewrite.NewURLRewrite(ctx, next, *config.URLRewrite, middlewareName), nil } } diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index b6c09ed2b..f88cb01d7 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v3.0.1 +# example new bugfix v3.0.2 CurrentRef = "v3.0" -PreviousRef = "v3.0.0" +PreviousRef = "v3.0.1" BaseBranch = "v3.0" -FutureCurrentRefName = "v3.0.1" +FutureCurrentRefName = "v3.0.2" ThresholdPreviousRef = 10 ThresholdCurrentRef = 10 diff --git a/webui/Dockerfile b/webui/Dockerfile index 3939d56b0..df6ca6167 100644 --- a/webui/Dockerfile +++ b/webui/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.11 +FROM node:20.14 # Current Active LTS release according to (https://nodejs.org/en/about/releases/) ENV WEBUI_DIR /src/webui diff --git a/webui/package.json b/webui/package.json index 3e367b9fd..23cbbba7c 100644 --- a/webui/package.json +++ b/webui/package.json @@ -8,10 +8,10 @@ "scripts": { "transfer": "node dev/scripts/transfer.js", "lint": "eslint --ext .js,.vue src", - "dev": "export APP_ENV='development' && quasar dev", + "dev": "APP_ENV=development quasar dev", "build-quasar": "quasar build", - "build-staging": "export NODE_ENV='production' && export APP_ENV='development' && yarn build-quasar", - "build": "export NODE_ENV='production' && export APP_ENV='production' && yarn build-quasar && yarn transfer spa", + "build-staging": "NODE_ENV=production APP_ENV=development yarn build-quasar", + "build": "NODE_ENV=production APP_ENV=production yarn build-quasar && yarn transfer spa", "build:nc": "yarn build", "test": "echo \"See package.json => scripts for available tests.\" && exit 0", "test:unit": "vitest", @@ -56,6 +56,7 @@ "engines": { "node": "^20 || ^18 || ^16", "npm": ">= 6.13.4", - "yarn": ">= 1.21.1" - } + "yarn": ">= 1.22.22" + }, + "packageManager": "yarn@1.22.22" } diff --git a/webui/readme.md b/webui/readme.md index e621e4080..812cbdf5a 100644 --- a/webui/readme.md +++ b/webui/readme.md @@ -20,7 +20,7 @@ make clean-webui generate-webui # Generate static contents in `webui/static/` fo ## How to build (only for frontend developer) -- prerequisite: [Node 20.11+](https://nodejs.org) [Yarn 1.22.19](https://yarnpkg.com/) +- prerequisite: [Node 20.14+](https://nodejs.org) [Yarn 1.22.22](https://yarnpkg.com/) - Go to the `webui/` directory diff --git a/webui/src/components/_commons/AvatarState.vue b/webui/src/components/_commons/AvatarState.vue index e74e73104..1b99f976e 100644 --- a/webui/src/components/_commons/AvatarState.vue +++ b/webui/src/components/_commons/AvatarState.vue @@ -24,7 +24,7 @@ import { defineComponent } from 'vue' export default defineComponent({ name: 'AvatarState', props: { - state: String + state: { type: String, default: undefined, required: false } } }) diff --git a/webui/src/components/_commons/MainTable.vue b/webui/src/components/_commons/MainTable.vue index 1907215eb..468f21d31 100644 --- a/webui/src/components/_commons/MainTable.vue +++ b/webui/src/components/_commons/MainTable.vue @@ -128,13 +128,14 @@ export default defineComponent({ QPageScroller }, props: { - data: Object, + data: { type: Object, default: undefined, required: false }, columns: Array[Object], loading: Boolean, - onLoadMore: Function, + onLoadMore: { type: Function, default: undefined, required: false }, endReached: Boolean, - onRowClick: Function + onRowClick: { type: Function, default: undefined, required: false } }, + emits: ['update:currentSort', 'update:currentSortDir'], data () { return { currentSort: 'name', diff --git a/webui/src/components/_commons/PanelHealthCheck.vue b/webui/src/components/_commons/PanelHealthCheck.vue index 7513cded0..0d1689d0d 100644 --- a/webui/src/components/_commons/PanelHealthCheck.vue +++ b/webui/src/components/_commons/PanelHealthCheck.vue @@ -137,7 +137,7 @@ export default { filters: { }, props: { - data: Object, + data: { type: Object, default: undefined, required: false }, dense: Boolean }, computed: { diff --git a/webui/src/components/_commons/PanelMiddlewares.vue b/webui/src/components/_commons/PanelMiddlewares.vue index 98eff915a..78ce95fb2 100644 --- a/webui/src/components/_commons/PanelMiddlewares.vue +++ b/webui/src/components/_commons/PanelMiddlewares.vue @@ -75,8 +75,8 @@ ERRORS {{ errorMsg }} @@ -817,6 +817,22 @@ + + +
+
+
+ Content Security Policy (Report Only) +
+ + {{ exData(middleware).contentSecurityPolicyReportOnly }} + +
+
+
diff --git a/webui/src/components/_commons/PanelMirroringServices.vue b/webui/src/components/_commons/PanelMirroringServices.vue index 3a3fa3319..a526f3f0f 100644 --- a/webui/src/components/_commons/PanelMirroringServices.vue +++ b/webui/src/components/_commons/PanelMirroringServices.vue @@ -70,7 +70,7 @@ export default { name: 'PanelMirroringServices', props: { - data: Object, + data: { type: Object, default: undefined, required: false }, dense: Boolean }, computed: { diff --git a/webui/src/components/_commons/PanelRouterDetails.vue b/webui/src/components/_commons/PanelRouterDetails.vue index 7a17fe5de..a605cd393 100644 --- a/webui/src/components/_commons/PanelRouterDetails.vue +++ b/webui/src/components/_commons/PanelRouterDetails.vue @@ -146,8 +146,8 @@ export default defineComponent({ AvatarState }, props: { - data: Object, - protocol: String + data: { type: Object, default: undefined, required: false }, + protocol: { type: String, default: undefined, required: false } }, computed: { getProviderLogoPath () { diff --git a/webui/src/components/_commons/PanelServers.vue b/webui/src/components/_commons/PanelServers.vue index 5b07eda44..5c3e8f54b 100644 --- a/webui/src/components/_commons/PanelServers.vue +++ b/webui/src/components/_commons/PanelServers.vue @@ -102,7 +102,7 @@ export default defineComponent({ AvatarState }, props: { - data: Object, + data: { type: Object, default: undefined, required: false }, dense: Boolean, hasStatus: Boolean }, diff --git a/webui/src/components/_commons/PanelServiceDetails.vue b/webui/src/components/_commons/PanelServiceDetails.vue index 98d26d19e..0eddba463 100644 --- a/webui/src/components/_commons/PanelServiceDetails.vue +++ b/webui/src/components/_commons/PanelServiceDetails.vue @@ -155,7 +155,7 @@ export default defineComponent({ StickyServiceDetails }, props: { - data: Object, + data: { type: Object, default: undefined, required: false }, dense: Boolean }, computed: { diff --git a/webui/src/components/_commons/PanelTLS.vue b/webui/src/components/_commons/PanelTLS.vue index eb7af91a8..905d8878f 100644 --- a/webui/src/components/_commons/PanelTLS.vue +++ b/webui/src/components/_commons/PanelTLS.vue @@ -77,12 +77,12 @@ {{ domain.main }} - {{ domain }} + {{ sanDomain }}
@@ -130,8 +130,8 @@ export default defineComponent({ BooleanState }, props: { - data: Object, - protocol: String + data: { type: Object, default: undefined, required: false }, + protocol: { type: String, default: undefined, required: false } } }) diff --git a/webui/src/components/_commons/PanelWeightedServices.vue b/webui/src/components/_commons/PanelWeightedServices.vue index 178bc8d4b..b8eeaa72a 100644 --- a/webui/src/components/_commons/PanelWeightedServices.vue +++ b/webui/src/components/_commons/PanelWeightedServices.vue @@ -66,7 +66,7 @@ export default defineComponent({ name: 'PanelWeightedServices', components: {}, props: { - data: Object, + data: { type: Object, default: undefined, required: false }, dense: Boolean }, computed: { diff --git a/webui/src/components/_commons/ProviderIcon.vue b/webui/src/components/_commons/ProviderIcon.vue index 9be76c746..4511b696b 100644 --- a/webui/src/components/_commons/ProviderIcon.vue +++ b/webui/src/components/_commons/ProviderIcon.vue @@ -9,7 +9,7 @@ import { defineComponent } from 'vue' export default defineComponent({ props: { - name: String + name: { type: String, default: undefined, required: false } }, computed: { getLogoPath () { diff --git a/webui/src/components/_commons/SidePanel.vue b/webui/src/components/_commons/SidePanel.vue index b13979739..f20d5bfe6 100644 --- a/webui/src/components/_commons/SidePanel.vue +++ b/webui/src/components/_commons/SidePanel.vue @@ -23,6 +23,7 @@ export default defineComponent({ props: { isOpen: Boolean }, + emits: ['onClose'], methods: { close () { this.$emit('onClose') diff --git a/webui/src/components/_commons/SkeletonBox.vue b/webui/src/components/_commons/SkeletonBox.vue index de6f3d4dd..f51612923 100644 --- a/webui/src/components/_commons/SkeletonBox.vue +++ b/webui/src/components/_commons/SkeletonBox.vue @@ -10,20 +10,20 @@ export default { name: 'SkeletonBox', props: { maxWidth: { - default: 100, - type: Number + type: Number, + default: 100 }, minWidth: { - default: 80, - type: Number + type: Number, + default: 80 }, height: { - default: '2em', - type: String + type: String, + default: '2em' }, width: { - default: null, - type: String + type: String, + default: null } }, computed: { diff --git a/webui/src/components/_commons/StickyServiceDetails.vue b/webui/src/components/_commons/StickyServiceDetails.vue index f2e53f4a9..de81f1119 100644 --- a/webui/src/components/_commons/StickyServiceDetails.vue +++ b/webui/src/components/_commons/StickyServiceDetails.vue @@ -55,7 +55,7 @@ export default defineComponent({ BooleanState }, props: { - sticky: Object, + sticky: { type: Object, default: undefined, required: false }, dense: Boolean } }) diff --git a/webui/src/components/_commons/ToolBarTable.vue b/webui/src/components/_commons/ToolBarTable.vue index 03d4b6a52..65a6e3d6b 100644 --- a/webui/src/components/_commons/ToolBarTable.vue +++ b/webui/src/components/_commons/ToolBarTable.vue @@ -42,9 +42,10 @@ import Helps from '../../_helpers/Helps' export default defineComponent({ name: 'ToolBarTable', props: { - status: String, - filter: String + status: { type: String, default: undefined, required: false }, + filter: { type: String, default: undefined, required: false } }, + emits: ['update:status', 'update:filter'], computed: { getStatus: { get () { diff --git a/webui/src/components/dashboard/PanelChart.vue b/webui/src/components/dashboard/PanelChart.vue index 411de01e6..3f2bc0a8f 100644 --- a/webui/src/components/dashboard/PanelChart.vue +++ b/webui/src/components/dashboard/PanelChart.vue @@ -118,9 +118,9 @@ export default defineComponent({ AvatarState }, props: { - name: String, - data: Object, - type: String + name: { type: String, default: undefined, required: false }, + data: { type: Object, default: undefined, required: false }, + type: { type: String, default: undefined, required: false } }, data () { return { diff --git a/webui/src/components/dashboard/PanelEntry.vue b/webui/src/components/dashboard/PanelEntry.vue index 969c321b3..3077ce157 100644 --- a/webui/src/components/dashboard/PanelEntry.vue +++ b/webui/src/components/dashboard/PanelEntry.vue @@ -28,11 +28,11 @@ import { defineComponent } from 'vue' export default defineComponent({ name: 'PanelEntry', props: { - address: String, - name: String, - type: String, + address: { type: String, default: undefined, required: false }, + name: { type: String, default: undefined, required: false }, + type: { type: String, default: undefined, required: false }, focus: Boolean, - exSize: Number + exSize: { type: Number, default: undefined, required: false } } }) diff --git a/webui/src/components/dashboard/PanelFeature.vue b/webui/src/components/dashboard/PanelFeature.vue index 9edff71c8..1b3ee5b64 100644 --- a/webui/src/components/dashboard/PanelFeature.vue +++ b/webui/src/components/dashboard/PanelFeature.vue @@ -28,7 +28,10 @@