├── .github ├── dependabot.yml ├── renovate.json └── workflows │ ├── docker.yml │ ├── go.yml │ └── renovate-auto-approve.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── istio-config-validator │ └── main.go ├── docs └── test-cases.md ├── examples ├── delegate_virtualservice.yml ├── destination.yml ├── multidocument_virtualservice.yml ├── virtualservice.yml ├── virtualservice_delegate_test.yml └── virtualservice_test.yml ├── go.mod ├── go.sum ├── hack └── istio-router-check │ ├── Dockerfile │ ├── README.md │ ├── examples │ ├── envoy-tests │ │ └── test.yml │ └── virtualservices │ │ └── virtualservice.yml │ └── main.go └── internal └── pkg ├── istio-router-check ├── cmd │ ├── root.go │ ├── root_test.go │ └── testdata │ │ ├── test.yml │ │ └── virtualservice.yml ├── envoy │ ├── options.go │ ├── routes.go │ ├── routes_test.go │ ├── testdata │ │ ├── bookinfo │ │ │ ├── details │ │ │ │ └── virtualservice.yaml │ │ │ └── reviews │ │ │ │ ├── file.txt │ │ │ │ └── virtualservice.yml │ │ ├── tests │ │ │ ├── details │ │ │ │ ├── details_test.yml │ │ │ │ └── file.txt │ │ │ └── reviews │ │ │ │ └── reviews_test.yaml │ │ └── virtualservice.yml │ ├── testfile.go │ └── testfile_test.go └── helpers │ ├── namespacedname.go │ └── walkdir.go ├── parser ├── testcase.go ├── testcase_test.go ├── testdata │ ├── invalid_test.yml │ └── invalid_vs.yml ├── virtualservice.go └── virtualservice_test.go └── unit ├── match_request.go ├── match_request_test.go ├── unit.go └── unit_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | open-pull-requests-limit: 0 # only enable security updates 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | time: "08:00" 9 | - package-ecosystem: docker 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | time: "08:00" 14 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", ":disableDependencyDashboard"], 3 | "enabledManagers": ["gomod"], 4 | "postUpdateOptions": ["gomodUpdateImportPaths", "gomodMassage", "gomodTidy"], 5 | "automergeSchedule": ["after 9am and before 6pm every weekday"], 6 | "packageRules": [ 7 | { 8 | "schedule": ["after 9am and before 6pm on monday"], 9 | "automerge": true, 10 | "groupName": "Go minor dependencies updates (auto-merge)", 11 | "updateTypes": ["minor", "patch", "digest", "pin", "pinDigest"] 12 | }, 13 | { 14 | "schedule": ["after 9am and before 6pm on monday"], 15 | "updateTypes": ["major"] 16 | } 17 | ], 18 | "ignoreDeps": ["istio.io/istio", "github.com/imdario/mergo"] 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}${{ github.ref_name != github.event.repository.default_branch && github.ref || github.run_id }} 11 | cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }} 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v2 21 | 22 | - name: Login to Docker Hub 23 | uses: docker/login-action@v3 24 | with: 25 | username: ${{ secrets.DOCKER_USERNAME }} 26 | password: ${{ secrets.DOCKER_ACCESS_TOKEN }} 27 | 28 | - name: Build and push docker image 29 | uses: docker/build-push-action@v4 30 | with: 31 | push: true 32 | tags: getyourguide/istio-config-validator:latest 33 | 34 | - name: Build and push istio-router-check 35 | uses: docker/build-push-action@v4 36 | with: 37 | pull: true 38 | file: hack/istio-router-check/Dockerfile 39 | context: . 40 | push: true 41 | tags: getyourguide/istio-router-check:release-1.22 42 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: [push] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}${{ github.ref_name != github.event.repository.default_branch && github.ref || github.run_id }} 7 | cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }} 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | platform: [ubuntu-latest, macos-latest] 14 | runs-on: ${{ matrix.platform }} 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v3 18 | - name: Install Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version-file: go.mod 22 | - name: Environment information 23 | run: | 24 | uname -a 25 | go version 26 | go env 27 | - name: golangci-lint 28 | if: matrix.platform == 'ubuntu-latest' 29 | uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6.5.2 30 | - name: Test 31 | run: go test -vet=off -count=1 ./... 32 | - name: Test with -race 33 | run: go test -vet=off -race -count=1 ./... 34 | - name: Run 35 | run: make run 36 | -------------------------------------------------------------------------------- /.github/workflows/renovate-auto-approve.yml: -------------------------------------------------------------------------------- 1 | name: auto-approve 2 | on: pull_request 3 | 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}${{ github.ref_name != github.event.repository.default_branch && github.ref || github.run_id }} 7 | cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }} 8 | permissions: 9 | pull-requests: write 10 | contents: write 11 | 12 | jobs: 13 | auto-approve: 14 | runs-on: ubuntu-latest 15 | if: ${{ github.actor == 'renovate[bot]' || github.actor == 'dependabot[bot]' }} 16 | steps: 17 | - name: Approve Renovate PR 18 | run: gh pr review --approve "$PR_URL" 19 | env: 20 | PR_URL: ${{github.event.pull_request.html_url}} 21 | GITHUB_TOKEN: ${{ secrets.GYGROBOT_TOKEN }} 22 | - name: auto-merge Dependabot PRs 23 | if: ${{ github.actor == 'dependabot[bot]' }} 24 | run: gh pr merge --auto --squash "$PR_URL" 25 | env: 26 | PR_URL: ${{github.event.pull_request.html_url}} 27 | GITHUB_TOKEN: ${{ github.token }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # VIM swap files 18 | [._]*.s[a-v][a-z] 19 | # !*.svg # uncomment if you need vector files 20 | [._]*.sw[a-p] 21 | [._]s[a-rt-v][a-z] 22 | [._]ss[a-gi-z] 23 | [._]sw[a-p] 24 | 25 | vendor/ 26 | 27 | istio-config-validator 28 | build/ 29 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 10m 3 | linters: 4 | disable-all: true 5 | enable: 6 | - errcheck 7 | - gofumpt 8 | - gosimple 9 | - govet 10 | - ineffassign 11 | - staticcheck 12 | - stylecheck 13 | - typecheck 14 | - unused 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.1.0 4 | 5 | The API for test cases does not cover all aspects of VirtualServices. 6 | 7 | - Supported [HTTPMatchRequests](https://istio.io/docs/reference/config/networking/virtual-service/#HTTPMatchRequest) fields to match requests against are: `authority`, `method`, `headers` and `uri`. 8 | - Not supported ones: `scheme`, `port`, `queryParams`, etc. 9 | 10 | - Supported assert against [HTTPRouteDestination](https://istio.io/docs/reference/config/networking/virtual-service/#HTTPRouteDestination) 11 | - Not supported ones: [HTTPRedirect](https://istio.io/docs/reference/config/networking/virtual-service/#HTTPRedirect), [HTTPRewrite](https://istio.io/docs/reference/config/networking/virtual-service/#HTTPRewrite), etc. 12 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @cainelli @x-way @gygrobot 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, we encourage to first discuss the change you wish to make via issue before submitting a change. We're not strict about this and for small changes feel free to PR directly. 4 | 5 | ## Development Environment 6 | 7 | Compilation and building is handled in the Docker container: 8 | - checkout the git repo 9 | - in the repo folder, run `make run` 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | RUN apk update && apk add --no-cache git 3 | WORKDIR $GOPATH/src/istio-config-validator/ 4 | COPY . . 5 | RUN CGO_ENABLED=0 go build -a -ldflags '-extldflags "-static"' -o /go/bin/istio-config-validator ./cmd/istio-config-validator/ 6 | 7 | FROM busybox 8 | COPY --from=builder /go/bin/istio-config-validator /go/bin/istio-config-validator 9 | 10 | ENTRYPOINT ["/go/bin/istio-config-validator"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 GetYourGuide AG 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CURRENTPATH = $(shell echo $(PWD)) 2 | WORKDIR = /src/github.com/getyourguide.com/istio-config-validator 3 | 4 | 5 | run: 6 | go run cmd/istio-config-validator/main.go -t examples/ examples/ 7 | 8 | build: 9 | go build -o istio-config-validator cmd/istio-config-validator/main.go 10 | 11 | install: 12 | go install cmd/istio-config-validator/main.go 13 | 14 | test: 15 | go test -race -count=1 ./... 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # istio-config-validator 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/getyourguide/istio-config-validator)](https://goreportcard.com/report/github.com/getyourguide/istio-config-validator) 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/6bee3a704e8648949523cdcfcefacc1f)](https://www.codacy.com?utm_source=github.com&utm_medium=referral&utm_content=getyourguide/istio-config-validator&utm_campaign=Badge_Grade) 5 | [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/6bee3a704e8648949523cdcfcefacc1f)](https://www.codacy.com?utm_source=github.com&utm_medium=referral&utm_content=getyourguide/istio-config-validator&utm_campaign=Badge_Coverage) 6 | 7 | > The `istio-config-validator` tool is a **Work In Progress** project. 8 | 9 | It provides to developers and cluster operators a way to test their changes in VirtualServices. We do it by mocking Istio/Envoy behavior to decide to which destination the request would go to. Eg: 10 | 11 | ```yaml 12 | apiVersion: networking.istio.io/v1alpha3 13 | kind: VirtualService 14 | metadata: 15 | name: example 16 | namespace: example 17 | spec: 18 | hosts: 19 | - www.example.com 20 | - example.com 21 | http: 22 | - match: 23 | - uri: 24 | regex: /users(/.*)? 25 | headers: 26 | x-user-type: 27 | exact: qa 28 | route: 29 | - destination: 30 | host: users.users.svc.cluster.local 31 | port: 32 | number: 80 33 | - route: 34 | - destination: 35 | host: monolith.monolith.svc.cluster.local 36 | ``` 37 | 38 | Given the above `VirtualService`, developers can introduce test cases that covers the intended behavior as the following: 39 | 40 | ```yaml 41 | testCases: 42 | - description: Only QA users should go to the new Users microservice. 43 | wantMatch: true 44 | request: 45 | authority: ["www.example.com", "example.com"] 46 | method: ["GET", "OPTIONS", "POST"] 47 | uri: ["/users", "/users/"] 48 | headers: 49 | x-user-type: qa 50 | route: 51 | - destination: 52 | host: users.users.svc.cluster.local 53 | port: 54 | number: 80 55 | - description: Fallback other user types to the monolith 56 | wantMatch: true 57 | request: 58 | authority: ["www.example.com", "example.com"] 59 | method: ["GET", "OPTIONS", "POST"] 60 | uri: ["/users", "/users/"] 61 | route: 62 | - destination: 63 | host: monolith.monolith.svc.cluster.local 64 | ``` 65 | 66 | Have a look in the [TestCase Reference](docs/test-cases.md) to learn more how to define the tests. 67 | 68 | ## Installation 69 | 70 | Either install the go package 71 | 72 | ``` 73 | # go install github.com/getyourguide/istio-config-validator/cmd/istio-config-validator@latest 74 | ``` 75 | 76 | Or alternatively install the docker image 77 | 78 | ``` 79 | # docker pull getyourguide/istio-config-validator:latest 80 | ``` 81 | 82 | ## Usage 83 | 84 | ``` 85 | # istio-config-validator -h 86 | Usage: istio-config-validator -t [-t ...] [ ...] 87 | 88 | -t value 89 | Testcase files/folders 90 | ``` 91 | 92 | ``` 93 | # istio-config-validator -t examples/virtualservice_test.yml examples/virtualservice.yml 94 | 2020-05-29T18:45:39.261018Z info running test: happy path users 95 | 2020-05-29T18:45:39.261106Z info PASS input:[{www.example.com GET /users map[x-user-id:abc123]}] 96 | 2020-05-29T18:45:39.261128Z info PASS input:[{www.example.com GET /users/ map[x-user-id:abc123]}] 97 | 2020-05-29T18:45:39.261141Z info PASS input:[{www.example.com POST /users map[x-user-id:abc123]}] 98 | 2020-05-29T18:45:39.261157Z info PASS input:[{www.example.com POST /users/ map[x-user-id:abc123]}] 99 | 2020-05-29T18:45:39.261169Z info PASS input:[{example.com GET /users map[x-user-id:abc123]}] 100 | 2020-05-29T18:45:39.261184Z info PASS input:[{example.com GET /users/ map[x-user-id:abc123]}] 101 | 2020-05-29T18:45:39.261207Z info PASS input:[{example.com POST /users map[x-user-id:abc123]}] 102 | 2020-05-29T18:45:39.261220Z info PASS input:[{example.com POST /users/ map[x-user-id:abc123]}] 103 | =========================== 104 | 2020-05-29T18:45:39.261228Z info running test: Partner service only accepts GET or OPTIONS 105 | 2020-05-29T18:45:39.261256Z info PASS input:[{example.com PUT /partners map[]}] 106 | 2020-05-29T18:45:39.261274Z info PASS input:[{example.com PUT /partners/1 map[]}] 107 | 2020-05-29T18:45:39.261284Z info PASS input:[{example.com POST /partners map[]}] 108 | 2020-05-29T18:45:39.261900Z info PASS input:[{example.com POST /partners/1 map[]}] 109 | 2020-05-29T18:45:39.261940Z info PASS input:[{example.com PATCH /partners map[]}] 110 | 2020-05-29T18:45:39.261984Z info PASS input:[{example.com PATCH /partners/1 map[]}] 111 | =========================== 112 | ``` 113 | 114 | ## Contributing 115 | 116 | If you're interested in contributing to this project or running a dev version, have a look into the [CONTRIBUTING](CONTRIBUTING.md) document 117 | 118 | ## Known Limitations 119 | 120 | The API for test cases does not cover all aspects of VirtualServices. 121 | 122 | - Supported [HTTPMatchRequests](https://istio.io/docs/reference/config/networking/virtual-service/#HTTPMatchRequest) fields to match requests against are: `authority`, `method`, `headers` and `uri`. 123 | - Not supported ones: `scheme`, `port`, `queryParams`, etc. 124 | 125 | - Supported assert against [HTTPRouteDestination](https://istio.io/docs/reference/config/networking/virtual-service/#HTTPRouteDestination), [HTTPRewrite](https://istio.io/docs/reference/config/networking/virtual-service/#HTTPRewrite), [HTTPFaultInjection](https://istio.io/latest/docs/reference/config/networking/virtual-service/#HTTPFaultInjection), [Headers](https://istio.io/latest/docs/reference/config/networking/virtual-service/#Headers), [Delegate](https://istio.io/latest/docs/reference/config/networking/virtual-service/#Delegate) and [HTTPRedirect](https://istio.io/docs/reference/config/networking/virtual-service/#HTTPRedirect). 126 | 127 | ## Security 128 | 129 | For sensitive security matters please contact [security@getyourguide.com](mailto:security@getyourguide.com). 130 | 131 | ## Legal 132 | 133 | Copyright 2020 GetYourGuide GmbH. 134 | 135 | istio-config-validator is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full text. 136 | -------------------------------------------------------------------------------- /cmd/istio-config-validator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/getyourguide/istio-config-validator/internal/pkg/unit" 12 | ) 13 | 14 | type multiValueFlag []string 15 | 16 | func (m *multiValueFlag) String() string { 17 | return strings.Join(*m, ",") 18 | } 19 | 20 | func (m *multiValueFlag) Set(value string) error { 21 | *m = append(*m, value) 22 | return nil 23 | } 24 | 25 | func main() { 26 | flag.Usage = func() { 27 | fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [-s] -t [-t ...] [ ...]\n\n", os.Args[0]) 28 | flag.PrintDefaults() 29 | } 30 | var testCaseParams multiValueFlag 31 | flag.Var(&testCaseParams, "t", "Testcase files/folders") 32 | summaryOnly := flag.Bool("s", false, "show only summary of tests (in case of failures full details are shown)") 33 | strict := flag.Bool("strict", false, "fail on unknown fields") 34 | 35 | flag.Parse() 36 | istioConfigFiles := getFiles(flag.Args()) 37 | testCaseFiles := getFiles(testCaseParams) 38 | 39 | if len(testCaseFiles) < 1 { 40 | fmt.Fprintf(os.Stderr, "Missing testcases file/folder, please provide at least one testcases file or folder\n") 41 | flag.Usage() 42 | os.Exit(1) 43 | } 44 | if len(istioConfigFiles) < 1 { 45 | fmt.Fprintf(os.Stderr, "Missing istio config file/folder, please provide at least one istio config file or folder\n") 46 | flag.Usage() 47 | os.Exit(1) 48 | } 49 | 50 | summary, details, err := unit.Run(testCaseFiles, istioConfigFiles, *strict) 51 | if err != nil { 52 | fmt.Println(strings.Join(details, "\n")) 53 | log.Fatal(err.Error()) 54 | } 55 | if !*summaryOnly { 56 | fmt.Println(strings.Join(details, "\n")) 57 | fmt.Println("") 58 | } 59 | fmt.Println(strings.Join(summary, "\n")) 60 | } 61 | 62 | func getFiles(names []string) []string { 63 | var files []string 64 | for _, name := range names { 65 | err := filepath.Walk(name, func(path string, info os.FileInfo, err error) error { 66 | if err != nil { 67 | log.Fatal(err.Error()) 68 | } 69 | if !info.IsDir() && isYaml(info) { 70 | files = append(files, path) 71 | } 72 | return nil 73 | }) 74 | if err != nil { 75 | log.Fatal(err.Error()) 76 | } 77 | } 78 | return files 79 | } 80 | 81 | func isYaml(info os.FileInfo) bool { 82 | extension := filepath.Ext(info.Name()) 83 | return extension == ".yaml" || extension == ".yml" 84 | } 85 | -------------------------------------------------------------------------------- /docs/test-cases.md: -------------------------------------------------------------------------------- 1 | # TestCases Reference 2 | 3 | This document describes how one can define test cases to be used by the testing framework. Refer to the [examples](https://github.com/getyourguide/istio-config-validator/tree/main/examples) to understand how to test different use cases. 4 | 5 | ## TestCases 6 | 7 | Test cases objects are interpreted by the framework to build up the mock and run the tests agaisnt the configuration. 8 | 9 | | Field | Type | Description | 10 | |-----------|-------------------------|----------------------------| 11 | | testCases | [testCase[]](#TestCase) | List of test cases to run. | 12 | 13 | ## TestCase 14 | 15 | [TestCase](https://github.com/getyourguide/istio-config-validator/blob/195017ef364f89b773492f2108e4478188a754ff/internal/pkg/parser/testcase.go#L32-L42) defines each test that will be run sequentially. 16 | 17 | | Field | Type | Description | 18 | |-------------|-----------------------------------------------------------------------------------------------------------------|------------------------------------------------------------| 19 | | description | string | Short description of what the testing is about. | 20 | | wantMatch | bool | If the test case should assert `true` or `false` | 21 | | request | [request](#Request) | Crafted requests that will mocked against VirtualServices | 22 | | route | [HTTPRouteDestination[]](https://istio.io/docs/reference/config/networking/virtual-service/#HTTPRouteDestination) | Route destinations that will be asserted for each request. | 23 | | redirect | [HTTPRedirect](https://istio.io/latest/docs/reference/config/networking/virtual-service/#HTTPRedirect) | Any redirect logic to test 24 | | rewrite | [HTTPRewrite](https://istio.io/latest/docs/reference/config/networking/virtual-service/#HTTPRewrite) | Any rewrite logic to test 25 | | fault | [HTTPFaultInjection](https://istio.io/latest/docs/reference/config/networking/virtual-service/#HTTPFaultInjection) | Add fault injection (delay, abort) to test to test for timeouts, auth, etc. 26 | | headers | [Headers](https://istio.io/latest/docs/reference/config/networking/virtual-service/#Headers) | Test header manipulation rules. These are different from `request.headers`, i.e. headers present in the test request. 27 | | delegate | [Delegate](https://istio.io/latest/docs/reference/config/networking/virtual-service/#Delegate) | Any delegation logic to test 28 | 29 | 30 | ## Request 31 | 32 | [Request](https://github.com/getyourguide/istio-config-validator/blob/195017ef364f89b773492f2108e4478188a754ff/internal/pkg/parser/testcase.go#L45) can contain more than one host (authority), method, uri, etc. The framework will mock requests in all possible combination defined here. 33 | 34 | 35 | | Field | Type | Description | 36 | |-----------|-------------------|--------------------------------------------------------------------| 37 | | authority | string[] | List of authority (host) that will be used to craft HTTP requests. | 38 | | method | string[] | List of methods to craft requests. | 39 | | uri | string[] | List of URIs to craft requests. | 40 | | headers | map[string]string | Headers present in all crafted requests. | 41 | -------------------------------------------------------------------------------- /examples/delegate_virtualservice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: networking.istio.io/v1 3 | kind: VirtualService 4 | metadata: 5 | name: merchants 6 | namespace: example 7 | spec: 8 | hosts: 9 | - www.example.org 10 | - example.org 11 | http: 12 | - match: 13 | - uri: 14 | regex: /merchants(/.*)? 15 | delegate: 16 | name: merchants-delegate 17 | - match: 18 | - uri: 19 | regex: /seller(/.*)? 20 | delegate: 21 | name: seller-delegate 22 | - match: 23 | - uri: 24 | regex: /product(/.*)? 25 | delegate: 26 | name: product-delegate 27 | --- 28 | apiVersion: networking.istio.io/v1alpha3 29 | kind: VirtualService 30 | metadata: 31 | name: merchants-delegate 32 | namespace: example 33 | spec: 34 | http: 35 | - match: 36 | - uri: 37 | regex: /merchants(/.*)? 38 | route: 39 | - destination: 40 | host: merchants.merchants.svc.cluster.local 41 | port: 42 | number: 80 43 | headers: 44 | request: 45 | set: 46 | x-custom-header: ok 47 | --- 48 | apiVersion: networking.istio.io/v1alpha3 49 | kind: VirtualService 50 | metadata: 51 | name: product-delegate 52 | namespace: example 53 | spec: 54 | http: 55 | - match: 56 | - uri: 57 | prefix: / 58 | route: 59 | - destination: 60 | host: product.product.svc.cluster.local 61 | port: 62 | number: 80 63 | rewrite: 64 | uri: "/product" 65 | headers: 66 | request: 67 | set: 68 | x-custom-header: ok 69 | -------------------------------------------------------------------------------- /examples/destination.yml: -------------------------------------------------------------------------------- 1 | # This file is to test skiping non virtual services files 2 | apiVersion: networking.istio.io/v1alpha3 3 | kind: DestinationRule 4 | metadata: 5 | name: bookinfo-ratings 6 | spec: 7 | host: ratings.prod.svc.cluster.local 8 | trafficPolicy: 9 | loadBalancer: 10 | simple: LEAST_CONN 11 | -------------------------------------------------------------------------------- /examples/multidocument_virtualservice.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: VirtualService 3 | metadata: 4 | name: example-2 5 | namespace: example-2 6 | spec: 7 | gateways: 8 | - mesh 9 | hosts: 10 | - www.example2.com 11 | - example2.com 12 | http: 13 | - match: 14 | - uri: 15 | regex: /users(/.*)? 16 | route: 17 | - destination: 18 | host: users.users.svc.cluster.local 19 | port: 20 | number: 80 21 | headers: 22 | request: 23 | set: 24 | x-custom-header: ok 25 | --- 26 | apiVersion: networking.istio.io/v1beta1 27 | kind: VirtualService 28 | metadata: 29 | name: example-3 30 | namespace: example-3 31 | spec: 32 | gateways: 33 | - mesh 34 | hosts: 35 | - www.example3.com 36 | - example3.com 37 | http: 38 | - match: 39 | - uri: 40 | prefix: /partners 41 | method: 42 | regex: (GET|OPTIONS) 43 | route: 44 | - destination: 45 | host: partner.partner.svc.cluster.local 46 | port: 47 | number: 8000 48 | - match: 49 | - uri: 50 | prefix: /reseller 51 | headers: 52 | x-request-class: 53 | exact: bot 54 | route: 55 | - destination: 56 | host: partner.partner.svc.cluster.local 57 | fault: 58 | abort: 59 | percentage: 60 | value: 100 61 | httpStatus: 403 62 | - match: 63 | - uri: 64 | prefix: /reseller 65 | route: 66 | - destination: 67 | host: partner.partner.svc.cluster.local 68 | rewrite: 69 | uri: "/partner" 70 | - route: 71 | - destination: 72 | host: monolith.monolith.svc.cluster.local 73 | -------------------------------------------------------------------------------- /examples/virtualservice.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1 2 | kind: VirtualService 3 | metadata: 4 | name: example 5 | namespace: example 6 | spec: 7 | gateways: 8 | - mesh 9 | hosts: 10 | - www.example.com 11 | - example.com 12 | http: 13 | - match: 14 | - uri: 15 | prefix: /home 16 | redirect: 17 | uri: / 18 | authority: www.example.com 19 | - match: 20 | - uri: 21 | regex: /users(/.*)? 22 | route: 23 | - destination: 24 | host: users.users.svc.cluster.local 25 | port: 26 | number: 80 27 | headers: 28 | request: 29 | set: 30 | x-custom-header: ok 31 | - match: 32 | - uri: 33 | prefix: /partners 34 | method: 35 | regex: (GET|OPTIONS) 36 | route: 37 | - destination: 38 | host: partner.partner.svc.cluster.local 39 | port: 40 | number: 8000 41 | - match: 42 | - uri: 43 | prefix: /reseller 44 | headers: 45 | x-request-class: 46 | exact: bot 47 | route: 48 | - destination: 49 | host: partner.partner.svc.cluster.local 50 | fault: 51 | abort: 52 | percentage: 53 | value: 100 54 | httpStatus: 403 55 | - match: 56 | - uri: 57 | prefix: /reseller 58 | route: 59 | - destination: 60 | host: partner.partner.svc.cluster.local 61 | rewrite: 62 | uri: "/partner" 63 | - route: 64 | - destination: 65 | host: monolith.monolith.svc.cluster.local 66 | -------------------------------------------------------------------------------- /examples/virtualservice_delegate_test.yml: -------------------------------------------------------------------------------- 1 | # Delegate test 2 | testCases: 3 | - description: Delegate to merchants 4 | wantMatch: true 5 | request: 6 | authority: ['example.org'] 7 | method: ['GET', 'POST'] 8 | uri: ['/merchants', '/merchants/v2/'] 9 | route: 10 | - destination: 11 | host: merchants.merchants.svc.cluster.local 12 | port: 13 | number: 80 14 | headers: 15 | request: 16 | set: 17 | x-custom-header: ok 18 | - description: Delegate to seller 19 | wantMatch: true 20 | request: 21 | authority: ['example.org'] 22 | method: ['GET', 'POST'] 23 | uri: ['/seller', '/seller/1234-abcd-5678-efgh'] 24 | delegate: 25 | name: seller-delegate 26 | - description: Delegate to product 27 | wantMatch: true 28 | request: 29 | authority: ['example.org'] 30 | method: ['GET', 'POST'] 31 | uri: ['/product', '/product/v2/', '/product/v2/1234-abcd-5678-efgh'] 32 | route: 33 | - destination: 34 | host: product.product.svc.cluster.local 35 | port: 36 | number: 80 37 | headers: 38 | request: 39 | set: 40 | x-custom-header: ok 41 | delegate: 42 | name: product-delegate 43 | -------------------------------------------------------------------------------- /examples/virtualservice_test.yml: -------------------------------------------------------------------------------- 1 | testCases: 2 | - description: happy path users 3 | wantMatch: true 4 | request: 5 | authority: ["www.example.com", "example.com"] 6 | method: ["GET", "POST"] 7 | uri: ["/users", "/users/"] 8 | headers: 9 | x-user-id: abc123 10 | route: 11 | - destination: 12 | host: users.users.svc.cluster.local 13 | port: 14 | number: 80 15 | headers: 16 | request: 17 | set: 18 | x-custom-header: ok 19 | - description: Partner service only accepts GET or OPTIONS 20 | wantMatch: false 21 | request: 22 | authority: ["example.com"] 23 | method: ["PUT", "POST", "PATCH"] 24 | uri: ["/partners", "/partners/1"] 25 | route: 26 | - destination: 27 | host: partner.partner.svc.cluster.local 28 | --- 29 | # Multidoc test 30 | testCases: 31 | - description: Redirect /home to / 32 | wantMatch: true 33 | request: 34 | authority: ["www.example.com"] 35 | method: ["GET"] 36 | uri: ["/home"] 37 | redirect: 38 | uri: "/" 39 | authority: "www.example.com" 40 | - description: Reseller is rewritten as partner 41 | wantMatch: true 42 | request: 43 | authority: ["example.com"] 44 | method: ["PUT", "POST", "PATCH"] 45 | uri: ["/reseller"] 46 | route: 47 | - destination: 48 | host: partner.partner.svc.cluster.local 49 | rewrite: 50 | uri: "/partner" 51 | - description: Reseller doesn't match rewritten as catalog 52 | wantMatch: false 53 | request: 54 | authority: ["example.com"] 55 | method: ["PUT", "POST", "PATCH"] 56 | uri: ["/reseller"] 57 | route: 58 | - destination: 59 | host: catalog.catalog.svc.cluster.local 60 | rewrite: 61 | uri: "/catalog" 62 | - description: Return 403 for bot traffic 63 | wantMatch: true 64 | request: 65 | authority: ["example.com"] 66 | method: ["PUT", "POST", "PATCH"] 67 | uri: ["/reseller"] 68 | headers: 69 | x-request-class: bot 70 | route: 71 | - destination: 72 | host: partner.partner.svc.cluster.local 73 | fault: 74 | abort: 75 | percentage: 76 | value: 100 77 | httpStatus: 403 78 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/getyourguide/istio-config-validator 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/envoyproxy/go-control-plane/envoy v1.32.5-0.20250606132447-d936c5d669b5 7 | github.com/go-logr/logr v1.4.3 8 | github.com/spf13/cobra v1.9.1 9 | github.com/stretchr/testify v1.10.0 10 | gopkg.in/yaml.v3 v3.0.1 11 | istio.io/api v1.26.1 12 | istio.io/client-go v1.26.1 13 | istio.io/istio v0.0.0-20250421215352-748c90d41d4b 14 | k8s.io/apimachinery v0.33.1 15 | ) 16 | 17 | require ( 18 | cel.dev/expr v0.20.0 // indirect 19 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 20 | dario.cat/mergo v1.0.1 // indirect 21 | github.com/Masterminds/goutils v1.1.1 // indirect 22 | github.com/Masterminds/semver/v3 v3.3.1 // indirect 23 | github.com/Masterminds/sprig/v3 v3.3.0 // indirect 24 | github.com/alecholmes/xfccparser v0.4.0 // indirect 25 | github.com/alecthomas/participle/v2 v2.1.1 // indirect 26 | github.com/antlr4-go/antlr/v4 v4.13.1 // indirect 27 | github.com/beorn7/perks v1.0.1 // indirect 28 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 29 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 30 | github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect 31 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect 32 | github.com/coreos/go-oidc/v3 v3.13.0 // indirect 33 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 34 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect 35 | github.com/docker/cli v28.0.1+incompatible // indirect 36 | github.com/docker/distribution v2.8.3+incompatible // indirect 37 | github.com/docker/docker-credential-helpers v0.8.2 // indirect 38 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 39 | github.com/envoyproxy/go-control-plane/contrib v1.32.5-0.20250417060501-8a0456ee578e // indirect 40 | github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect 41 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 42 | github.com/fsnotify/fsnotify v1.8.0 // indirect 43 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 44 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 45 | github.com/go-logr/stdr v1.2.2 // indirect 46 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 47 | github.com/go-openapi/jsonreference v0.21.0 // indirect 48 | github.com/go-openapi/swag v0.23.0 // indirect 49 | github.com/goccy/go-json v0.10.4 // indirect 50 | github.com/gogo/protobuf v1.3.2 // indirect 51 | github.com/golang/protobuf v1.5.4 // indirect 52 | github.com/google/btree v1.1.3 // indirect 53 | github.com/google/cel-go v0.22.1 // indirect 54 | github.com/google/gnostic-models v0.6.9 // indirect 55 | github.com/google/go-cmp v0.7.0 // indirect 56 | github.com/google/go-containerregistry v0.20.3 // indirect 57 | github.com/google/uuid v1.6.0 // indirect 58 | github.com/gorilla/mux v1.8.1 // indirect 59 | github.com/gorilla/websocket v1.5.3 // indirect 60 | github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect 61 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 62 | github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect 63 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 64 | github.com/hashicorp/errwrap v1.1.0 // indirect 65 | github.com/hashicorp/go-multierror v1.1.1 // indirect 66 | github.com/hashicorp/go-version v1.7.0 // indirect 67 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 68 | github.com/huandu/xstrings v1.5.0 // indirect 69 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 70 | github.com/josharian/intern v1.0.0 // indirect 71 | github.com/json-iterator/go v1.1.12 // indirect 72 | github.com/klauspost/compress v1.17.11 // indirect 73 | github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect 74 | github.com/lestrrat-go/blackmagic v1.0.2 // indirect 75 | github.com/lestrrat-go/httpcc v1.0.1 // indirect 76 | github.com/lestrrat-go/iter v1.0.2 // indirect 77 | github.com/lestrrat-go/jwx v1.2.30 // indirect 78 | github.com/lestrrat-go/option v1.0.1 // indirect 79 | github.com/mailru/easyjson v0.9.0 // indirect 80 | github.com/miekg/dns v1.1.64 // indirect 81 | github.com/mitchellh/copystructure v1.2.0 // indirect 82 | github.com/mitchellh/go-homedir v1.1.0 // indirect 83 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 84 | github.com/moby/spdystream v0.5.0 // indirect 85 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 86 | github.com/modern-go/reflect2 v1.0.2 // indirect 87 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 88 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 89 | github.com/opencontainers/go-digest v1.0.0 // indirect 90 | github.com/opencontainers/image-spec v1.1.0 // indirect 91 | github.com/openshift/api v0.0.0-20250313134101-8a7efbfb5316 // indirect 92 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 93 | github.com/pkg/errors v0.9.1 // indirect 94 | github.com/planetscale/vtprotobuf v0.6.1-0.20240409071808-615f978279ca // indirect 95 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 96 | github.com/prometheus/client_golang v1.21.1 // indirect 97 | github.com/prometheus/client_model v0.6.2 // indirect 98 | github.com/prometheus/common v0.62.0 // indirect 99 | github.com/prometheus/procfs v0.15.1 // indirect 100 | github.com/prometheus/prometheus v0.302.1 // indirect 101 | github.com/ryanuber/go-glob v1.0.0 // indirect 102 | github.com/shopspring/decimal v1.4.0 // indirect 103 | github.com/sirupsen/logrus v1.9.3 // indirect 104 | github.com/spf13/cast v1.7.0 // indirect 105 | github.com/spf13/pflag v1.0.6 // indirect 106 | github.com/stoewer/go-strcase v1.3.0 // indirect 107 | github.com/vbatts/tar-split v0.11.6 // indirect 108 | github.com/x448/float16 v0.8.4 // indirect 109 | github.com/yl2chen/cidranger v1.0.2 // indirect 110 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 111 | go.opentelemetry.io/otel v1.35.0 // indirect 112 | go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect 113 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 114 | go.opentelemetry.io/otel/sdk v1.35.0 // indirect 115 | go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect 116 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 117 | go.opentelemetry.io/proto/otlp v1.7.0 // indirect 118 | go.uber.org/atomic v1.11.0 // indirect 119 | go.uber.org/multierr v1.11.0 // indirect 120 | go.uber.org/zap v1.27.0 // indirect 121 | golang.org/x/crypto v0.38.0 // indirect 122 | golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect 123 | golang.org/x/mod v0.23.0 // indirect 124 | golang.org/x/net v0.40.0 // indirect 125 | golang.org/x/oauth2 v0.28.0 // indirect 126 | golang.org/x/sync v0.14.0 // indirect 127 | golang.org/x/sys v0.33.0 // indirect 128 | golang.org/x/term v0.32.0 // indirect 129 | golang.org/x/text v0.25.0 // indirect 130 | golang.org/x/time v0.11.0 // indirect 131 | golang.org/x/tools v0.30.0 // indirect 132 | gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect 133 | google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect 134 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect 135 | google.golang.org/grpc v1.72.2 // indirect 136 | google.golang.org/protobuf v1.36.6 // indirect 137 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 138 | gopkg.in/inf.v0 v0.9.1 // indirect 139 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 140 | k8s.io/api v0.32.3 // indirect 141 | k8s.io/apiextensions-apiserver v0.32.3 // indirect 142 | k8s.io/apiserver v0.32.3 // indirect 143 | k8s.io/client-go v0.32.3 // indirect 144 | k8s.io/klog/v2 v2.130.1 // indirect 145 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 146 | k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect 147 | sigs.k8s.io/gateway-api v1.3.0-rc.1.0.20250404104637-92efbedcc2b4 // indirect 148 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 149 | sigs.k8s.io/mcs-api v0.1.1-0.20240624222831-d7001fe1d21c // indirect 150 | sigs.k8s.io/randfill v1.0.0 // indirect 151 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 152 | sigs.k8s.io/yaml v1.4.0 // indirect 153 | ) 154 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= 2 | cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= 3 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 5 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 6 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 7 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 8 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= 9 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 10 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 11 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 12 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 13 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 14 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 15 | github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= 16 | github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 17 | github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= 18 | github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= 19 | github.com/alecholmes/xfccparser v0.4.0 h1:IFB4bP34oorjcV3n8utZtBhEwlAw9rZ43pb4LgT23Vo= 20 | github.com/alecholmes/xfccparser v0.4.0/go.mod h1:J9fzzUOtjw74IwNdGVbjnOVj1UDlwGQj1zZzgQRlRDY= 21 | github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= 22 | github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= 23 | github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= 24 | github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= 25 | github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= 26 | github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 27 | github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= 28 | github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= 29 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 30 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 31 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= 32 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 33 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 34 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 35 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 36 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 37 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 38 | github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3 h1:LRxW8pdmWmyhoNh+TxUjxsAinGtCsVGjsl3xg6zoRSs= 39 | github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3/go.mod h1:6jR2SzckGv8hIIS9zWJ160mzGVVOYp4AXZMDtacL6LE= 40 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 41 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 42 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 43 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 44 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 45 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 46 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 47 | github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= 48 | github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= 49 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= 50 | github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= 51 | github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8= 52 | github.com/coreos/go-oidc/v3 v3.13.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= 53 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 54 | github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= 55 | github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 56 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 58 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 59 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 60 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= 61 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 62 | github.com/docker/cli v28.0.1+incompatible h1:g0h5NQNda3/CxIsaZfH4Tyf6vpxFth7PYl3hgCPOKzs= 63 | github.com/docker/cli v28.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 64 | github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= 65 | github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 66 | github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= 67 | github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= 68 | github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= 69 | github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 70 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 71 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 72 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 73 | github.com/envoyproxy/go-control-plane/contrib v1.32.5-0.20250417060501-8a0456ee578e h1:TE+9a7aG8Nb6oiKxxRTJdbKO+V+rJ1uLPR7oRSWsyRE= 74 | github.com/envoyproxy/go-control-plane/contrib v1.32.5-0.20250417060501-8a0456ee578e/go.mod h1:Sv+Gtm58b4U4QIu1S5Gl51uqfIBD16xi4ETIX0gCTdM= 75 | github.com/envoyproxy/go-control-plane/envoy v1.32.5-0.20250606132447-d936c5d669b5 h1:j0JXo2O5WPzMfvWwWkY6TjKBQN7okgZBsfl6Mjdgnrw= 76 | github.com/envoyproxy/go-control-plane/envoy v1.32.5-0.20250606132447-d936c5d669b5/go.mod h1:hs5LzGArSb7WUWZP8G8K0/wuVCbyhqJ/V17HT7HY0f0= 77 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 78 | github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= 79 | github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= 80 | github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= 81 | github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 82 | github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= 83 | github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 84 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 85 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 86 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 87 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 88 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 89 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 90 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 91 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 92 | github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= 93 | github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= 94 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 95 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 96 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 97 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 98 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 99 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 100 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 101 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 102 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 103 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 104 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 105 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 106 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 107 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 108 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 109 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 110 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 111 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 112 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 113 | github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= 114 | github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 115 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 116 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 117 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 118 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 119 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 120 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 121 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 122 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 123 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 124 | github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 125 | github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 126 | github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40= 127 | github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= 128 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 129 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 130 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 131 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 132 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 133 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 134 | github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= 135 | github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= 136 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 137 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 138 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 139 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= 140 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 141 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 142 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 143 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 144 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 145 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 146 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 147 | github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= 148 | github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= 149 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= 150 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 151 | github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= 152 | github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= 153 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= 154 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 155 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= 156 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= 157 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= 158 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 159 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 160 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 161 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 162 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 163 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 164 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 165 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 166 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 167 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 168 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 169 | github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= 170 | github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 171 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 172 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 173 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 174 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 175 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 176 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 177 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 178 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 179 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 180 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 181 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 182 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 183 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 184 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 185 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 186 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 187 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 188 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 189 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 190 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 191 | github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= 192 | github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= 193 | github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 194 | github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 195 | github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 196 | github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 197 | github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 198 | github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 199 | github.com/lestrrat-go/jwx v1.2.30 h1:VKIFrmjYn0z2J51iLPadqoHIVLzvWNa1kCsTqNDHYPA= 200 | github.com/lestrrat-go/jwx v1.2.30/go.mod h1:vMxrwFhunGZ3qddmfmEm2+uced8MSI6QFWGTKygjSzQ= 201 | github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 202 | github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 203 | github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 204 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 205 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 206 | github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ= 207 | github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= 208 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 209 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 210 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 211 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 212 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 213 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 214 | github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= 215 | github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= 216 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 217 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 218 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 219 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 220 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 221 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 222 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 223 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= 224 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 225 | github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM= 226 | github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= 227 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 228 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 229 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 230 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 231 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 232 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 233 | github.com/openshift/api v0.0.0-20250313134101-8a7efbfb5316 h1:iJ1OkAUvFbQPB6qWRDxrH1jj8iA9GA/Jx2vYz7o+i1E= 234 | github.com/openshift/api v0.0.0-20250313134101-8a7efbfb5316/go.mod h1:yk60tHAmHhtVpJQo3TwVYq2zpuP70iJIFDCmeKMIzPw= 235 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 236 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 237 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 238 | github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0= 239 | github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY= 240 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 241 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 242 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 243 | github.com/planetscale/vtprotobuf v0.6.1-0.20240409071808-615f978279ca h1:ujRGEVWJEoaxQ+8+HMl8YEpGaDAgohgZxJ5S+d2TTFQ= 244 | github.com/planetscale/vtprotobuf v0.6.1-0.20240409071808-615f978279ca/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 245 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 246 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 247 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 248 | github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= 249 | github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= 250 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 251 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 252 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 253 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 254 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 255 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 256 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 257 | github.com/prometheus/prometheus v0.302.1 h1:xqVdrwrB4WNpdgJqxsz5loqFWNUZitsK8myqLuSZ6Ag= 258 | github.com/prometheus/prometheus v0.302.1/go.mod h1:YcyCoTbUR/TM8rY3Aoeqr0AWTu/pu1Ehh+trpX3eRzg= 259 | github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 260 | github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 261 | github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= 262 | github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= 263 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 264 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 265 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 266 | github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 267 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 268 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 269 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 270 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 271 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 272 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 273 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= 274 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 275 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 276 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 277 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 278 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 279 | github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= 280 | github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= 281 | github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= 282 | github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= 283 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 284 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 285 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 286 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 287 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 288 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 289 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 290 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 291 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 292 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 293 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 294 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 295 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 296 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 297 | github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= 298 | github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= 299 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 300 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 301 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 302 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 303 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 304 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 305 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 306 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 307 | github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= 308 | github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= 309 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 310 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 311 | github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= 312 | github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= 313 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 314 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 315 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= 316 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= 317 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 318 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 319 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= 320 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= 321 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= 322 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= 323 | go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk= 324 | go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c= 325 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 326 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 327 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 328 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 329 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 330 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 331 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 332 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 333 | go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= 334 | go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= 335 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 336 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 337 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 338 | go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 339 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 340 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 341 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 342 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 343 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 344 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 345 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 346 | go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= 347 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 348 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 349 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 350 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 351 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 352 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 353 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 354 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 355 | golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4= 356 | golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 357 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 358 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 359 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 360 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 361 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 362 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 363 | golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= 364 | golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 365 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 366 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 367 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 368 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 369 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 370 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 371 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 372 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 373 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 374 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 375 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 376 | golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= 377 | golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 378 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 379 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 380 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 381 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 382 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 383 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 384 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 385 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 386 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 387 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 388 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 389 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 391 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 392 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 393 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 394 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 395 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 396 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 397 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 398 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 399 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 400 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 401 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 402 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 403 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 404 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 405 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 406 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 407 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 408 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 409 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 410 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 411 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 412 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 413 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 414 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 415 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 416 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 417 | gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= 418 | gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 419 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 420 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 421 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 422 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 423 | google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 424 | google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= 425 | google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= 426 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= 427 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 428 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 429 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 430 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 431 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 432 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 433 | google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= 434 | google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 435 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 436 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 437 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 438 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 439 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 440 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 441 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 442 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 443 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 444 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 445 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 446 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 447 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 448 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 449 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 450 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 451 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 452 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 453 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 454 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 455 | gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= 456 | gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 457 | helm.sh/helm/v3 v3.17.1 h1:gzVoAD+qVuoJU6KDMSAeo0xRJ6N1znRxz3wyuXRmJDk= 458 | helm.sh/helm/v3 v3.17.1/go.mod h1:nvreuhuR+j78NkQcLC3TYoprCKStLyw5P4T7E5itv2w= 459 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 460 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 461 | istio.io/api v1.26.1 h1:1OM0JtHgA6VLkmWna02W0RFqHsOQqFtowAf+u9jVOR0= 462 | istio.io/api v1.26.1/go.mod h1:DTVGH6CLXj5W8FF9JUD3Tis78iRgT1WeuAnxfTz21Wg= 463 | istio.io/client-go v1.26.1 h1:Y16LEA1iOhBgebd2D8UPWnBJZz/dmz9czt7JdZy2E74= 464 | istio.io/client-go v1.26.1/go.mod h1:1nRkn8XmI8AZVKElaLsC79jrVqS+PPIu1EczNvmGst8= 465 | istio.io/istio v0.0.0-20250421215352-748c90d41d4b h1:EQ70H+oINSMtLx0Mo6zT8yVf2OAlWbCuqT8oFjGTen0= 466 | istio.io/istio v0.0.0-20250421215352-748c90d41d4b/go.mod h1:isEgatnYxbrQavjjhN2kkDT2MrLYKHbzAi7xygSz5bc= 467 | k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= 468 | k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= 469 | k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= 470 | k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= 471 | k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= 472 | k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 473 | k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8= 474 | k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc= 475 | k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= 476 | k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= 477 | k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k= 478 | k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI= 479 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 480 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 481 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 482 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 483 | k8s.io/kubectl v0.32.3 h1:VMi584rbboso+yjfv0d8uBHwwxbC438LKq+dXd5tOAI= 484 | k8s.io/kubectl v0.32.3/go.mod h1:6Euv2aso5GKzo/UVMacV6C7miuyevpfI91SvBvV9Zdg= 485 | k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= 486 | k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 487 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.1 h1:uOuSLOMBWkJH0TWa9X6l+mj5nZdm6Ay6Bli8HL8rNfk= 488 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.1/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= 489 | sigs.k8s.io/gateway-api v1.3.0-rc.1.0.20250404104637-92efbedcc2b4 h1:B5WxrbbwAJQpC5UatORrm0MArdaQgj2NhAlMRQwAqho= 490 | sigs.k8s.io/gateway-api v1.3.0-rc.1.0.20250404104637-92efbedcc2b4/go.mod h1:uM5idPTEQZVyd0bRSu00mbtF4VEgraPyU1OFNbY6lqk= 491 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 492 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 493 | sigs.k8s.io/mcs-api v0.1.1-0.20240624222831-d7001fe1d21c h1:F7hIEutAxtXDOQX9NXFdvhWmWETu2zmUPHuPPcAez7g= 494 | sigs.k8s.io/mcs-api v0.1.1-0.20240624222831-d7001fe1d21c/go.mod h1:DPFniRsBzCeLB4ANjlPEvQQt9QGIX489d1faK+GPvI4= 495 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 496 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 497 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 498 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 499 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 500 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 501 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 502 | -------------------------------------------------------------------------------- /hack/istio-router-check/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-bullseye as builder 2 | 3 | WORKDIR /work 4 | 5 | COPY go.mod go.sum ./ 6 | 7 | RUN go mod download 8 | 9 | COPY . . 10 | 11 | RUN go build -o bin/istio-router-check hack/istio-router-check/main.go 12 | 13 | FROM getyourguide/router-check-tool:release-1.22 14 | 15 | COPY --from=builder /work/bin/istio-router-check /usr/local/bin/ 16 | 17 | ENTRYPOINT ["/usr/local/bin/istio-router-check"] 18 | -------------------------------------------------------------------------------- /hack/istio-router-check/README.md: -------------------------------------------------------------------------------- 1 | # Istio Router Check 2 | 3 | An _experimental_ command that generates configs and tests to Envoy [Route Table Check Tool](https://www.envoyproxy.io/docs/envoy/latest/operations/tools/route_table_check_tool#install-tools-route-table-check-tool). 4 | 5 | 1. It parses Istio configuration such as VirtualServices and outputs Envoy [HTTP Route](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#http-route-components-proto) format required by router check tool. 6 | 2. It parses mutitple Envoy Tests and consolidate them into a single file to be used by router check tool. 7 | 3. It parses istio-config-validator test format and converts to the router check tool format. Note that this is highly experimental and does not cover all tests. 8 | 9 | ## Running 10 | 11 | Generate routes and consolidate tests: 12 | 13 | ```shell 14 | $ docker run \ 15 | -v $(pwd)/examples:/examples \ 16 | --rm docker.io/getyourguide/istio-router-check:release-1.22 --config-dir /examples/virtualservices --test-dir examples/envoy-tests/ -o examples/build/ 17 | 18 | time=2024-07-09T13:41:43.137Z level=INFO msg="reading tests" dir=examples/envoy-tests/ 19 | time=2024-07-09T13:41:43.145Z level=INFO msg="writing tests" file=examples/build/tests.json 20 | time=2024-07-09T13:41:43.146Z level=INFO msg="reading virtualservices" 21 | time=2024-07-09T13:41:43.244Z level=INFO msg="writing route" route=80 file=examples/build/route_sidecar_80.json 22 | ``` 23 | 24 | Run envoy route table check tool using the generated files. 25 | 26 | ```shell 27 | docker run \ 28 | -v $(pwd)/examples:/examples \ 29 | --entrypoint=/usr/local/bin/router_check_tool \ 30 | --rm docker.io/getyourguide/istio-router-check:release-1.22 \ 31 | -c /examples/build/route_sidecar_80.json -t /examples/build/tests.json --only-show-failures --disable-deprecation-check 32 | 33 | Current route coverage: 50% 34 | ``` 35 | 36 | ## Usage 37 | 38 | ```shell 39 | docker run \ 40 | --rm docker.io/getyourguide/istio-router-check:release-1.22 -h 41 | 42 | Usage: 43 | istio-router-check [flags] 44 | 45 | Flags: 46 | -v, -- int Log verbosity level 47 | -c, --config-dir string Directory with Istio VirtualService and Gateway files 48 | --gateway string Only consider VirtualServices bound to this gateway (i.e: istio-system/istio-ingressgateway) 49 | -h, --help Help for istio-router-check 50 | -o, --output-dir string Directory to output Envoy routes and tests 51 | -t, --test-dir string Directory with Envoy test files 52 | ``` 53 | 54 | Envoy Router Check 55 | 56 | ```shell 57 | docker run \ 58 | --entrypoint=/usr/local/bin/router_check_tool 59 | --rm docker.io/getyourguide/istio-router-check:release-1.22 -h 60 | 61 | 62 | USAGE: 63 | 64 | /usr/local/bin/router_check_tool [--detailed-coverage] [-o ] 65 | [-t ] [-c ] [--covall] 66 | [-f ] 67 | [--disable-deprecation-check] 68 | [--only-show-failures] [-d] [--] 69 | [--version] [-h] 70 | ... 71 | 72 | 73 | Where: 74 | 75 | --detailed-coverage 76 | Show detailed coverage with routes without tests 77 | 78 | -o , --output-path 79 | Path to output file to write test results 80 | 81 | -t , --test-path 82 | Path to test file. 83 | 84 | -c , --config-path 85 | Path to configuration file. 86 | 87 | --covall 88 | Measure coverage by checking all route fields 89 | 90 | -f , --fail-under 91 | Fail if test coverage is under a specified amount 92 | 93 | --disable-deprecation-check 94 | Disable deprecated fields check 95 | 96 | --only-show-failures 97 | Only display failing tests 98 | 99 | -d, --details 100 | Show detailed test execution results 101 | 102 | --, --ignore_rest 103 | Ignores the rest of the labeled arguments following this flag. 104 | 105 | --version 106 | Displays version information and exits. 107 | 108 | -h, --help 109 | Displays usage information and exits. 110 | 111 | (accepted multiple times) 112 | unlabelled configs 113 | 114 | 115 | router_check_tool 116 | ``` 117 | -------------------------------------------------------------------------------- /hack/istio-router-check/examples/envoy-tests/test.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | - test_name: test details.prod.svc.cluster.local/api/v2/products 3 | input: 4 | authority: details.prod.svc.cluster.local 5 | path: /api/v2/products 6 | method: GET 7 | validate: 8 | cluster_name: outbound|80|v2|details.prod.svc.cluster.local 9 | path_rewrite: /api/newdetails 10 | - test_name: test details.prod.svc.cluster.local/api/v2/items 11 | input: 12 | authority: details.prod.svc.cluster.local 13 | path: /api/v2/items 14 | method: GET 15 | validate: 16 | cluster_name: outbound|80|v2|details.prod.svc.cluster.local 17 | path_rewrite: /api/newdetails 18 | -------------------------------------------------------------------------------- /hack/istio-router-check/examples/virtualservices/virtualservice.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1 2 | kind: VirtualService 3 | metadata: 4 | name: details 5 | spec: 6 | hosts: 7 | - details.prod.svc.cluster.local 8 | http: 9 | - name: "details-v2-routes" 10 | match: 11 | - uri: 12 | prefix: "/api/v2/products" 13 | - uri: 14 | prefix: "/api/v2/items" 15 | rewrite: 16 | uri: "/api/newdetails" 17 | route: 18 | - destination: 19 | host: details.prod.svc.cluster.local 20 | subset: v2 21 | -------------------------------------------------------------------------------- /hack/istio-router-check/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/getyourguide/istio-config-validator/internal/pkg/istio-router-check/cmd" 8 | ) 9 | 10 | func main() { 11 | cmdRoot, err := cmd.NewCmdRoot() 12 | if err != nil { 13 | fmt.Printf("failed to create command: %v", err) 14 | os.Exit(1) 15 | } 16 | if err := cmdRoot.Execute(); err != nil { 17 | fmt.Println(err) 18 | os.Exit(1) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "log/slog" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/getyourguide/istio-config-validator/internal/pkg/istio-router-check/envoy" 15 | "github.com/getyourguide/istio-config-validator/internal/pkg/istio-router-check/helpers" 16 | "github.com/getyourguide/istio-config-validator/internal/pkg/parser" 17 | "github.com/go-logr/logr" 18 | "github.com/spf13/cobra" 19 | "istio.io/api/networking/v1alpha3" 20 | "istio.io/istio/pkg/util/protomarshal" 21 | ) 22 | 23 | type RootCommand struct { 24 | ConfigDir string 25 | TestDir string 26 | ConvertTests bool 27 | OutputDir string 28 | Gateway string 29 | Verbosity int 30 | } 31 | 32 | const ( 33 | LevelInfo = 0 34 | LevelDebug = 9 35 | ) 36 | 37 | func NewCmdRoot() (*cobra.Command, error) { 38 | ctx := context.Background() 39 | rootCmd := &RootCommand{} 40 | cmd := &cobra.Command{ 41 | Use: "istio-router-check", 42 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 43 | logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ 44 | Level: slog.Level(rootCmd.Verbosity), 45 | })) 46 | ctx = logr.NewContextWithSlogLogger(ctx, logger) 47 | cmd.SetContext(ctx) 48 | return nil 49 | }, 50 | RunE: func(cmd *cobra.Command, _ []string) error { 51 | return rootCmd.Run(cmd.Context()) 52 | }, 53 | SilenceUsage: true, 54 | } 55 | 56 | cmd.Flags().IntVarP(&rootCmd.Verbosity, "", "v", LevelInfo, "Log verbosity level") 57 | cmd.Flags().StringVarP(&rootCmd.Gateway, "gateway", "", "", "Only consider VirtualServices bound to this gateway (i.e: istio-system/istio-ingressgateway)") 58 | cmd.Flags().StringVarP(&rootCmd.ConfigDir, "config-dir", "c", "", "Directory with Istio VirtualService and Gateway files") 59 | cmd.Flags().StringVarP(&rootCmd.TestDir, "test-dir", "t", "", "Directory with Envoy test files") 60 | cmd.Flags().BoolVarP(&rootCmd.ConvertTests, "convert-tests", "", false, "Convert istio-config-validator tests into Envoy tests") 61 | cmd.Flags().StringVarP(&rootCmd.OutputDir, "output-dir", "o", "", "Directory to output Envoy routes and tests") 62 | 63 | for _, flag := range []string{"output-dir", "config-dir", "test-dir"} { 64 | if err := cmd.MarkFlagRequired(flag); err != nil { 65 | return nil, fmt.Errorf("failed to mark flag %q required: %w", flag, err) 66 | } 67 | if err := cmd.MarkFlagDirname(flag); err != nil { 68 | return nil, fmt.Errorf("failed to mark flag %q as dirname: %w", flag, err) 69 | } 70 | } 71 | 72 | if err := cmd.Flags().MarkHidden("convert-tests"); err != nil { 73 | return nil, fmt.Errorf("failed to mark flag hidden: %w", err) 74 | } 75 | 76 | return cmd, nil 77 | } 78 | 79 | func (c *RootCommand) Run(ctx context.Context) error { 80 | if err := os.MkdirAll(c.OutputDir, os.ModePerm); err != nil && !os.IsNotExist(err) { 81 | return fmt.Errorf("failed to create output directory: %w", err) 82 | } 83 | 84 | if err := c.prepareRoutes(ctx); err != nil { 85 | return fmt.Errorf("failed to prepare routes: %w", err) 86 | } 87 | 88 | if c.ConvertTests { 89 | if err := c.prepareTests(ctx); err != nil { 90 | return fmt.Errorf("failed to prepare istio-config-validator tests: %w", err) 91 | } 92 | return nil 93 | } 94 | 95 | if err := c.prepareEnvoyTests(ctx); err != nil { 96 | return fmt.Errorf("failed to prepare envoy tests: %w", err) 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func (c *RootCommand) prepareEnvoyTests(ctx context.Context) error { 103 | log := logr.FromContextOrDiscard(ctx) 104 | if c.TestDir == "" { 105 | log.V(LevelDebug).Info("no envoy test directory provided") 106 | return nil 107 | } 108 | 109 | log.Info("reading tests", "dir", c.TestDir) 110 | tests, err := envoy.ReadTests(c.TestDir) 111 | if err != nil { 112 | return fmt.Errorf("failed to read envoy test files: %w", err) 113 | } 114 | 115 | rawTests, err := json.Marshal(tests) 116 | if err != nil { 117 | return fmt.Errorf("failed to marshal tests: %w", err) 118 | } 119 | outputFile := filepath.Join(c.OutputDir, "tests.json") 120 | log.Info("writing tests", "file", outputFile) 121 | err = os.WriteFile(outputFile, rawTests, os.ModePerm) 122 | if err != nil { 123 | return fmt.Errorf("failed to write tests: %w", err) 124 | } 125 | return nil 126 | } 127 | 128 | func (c *RootCommand) prepareRoutes(ctx context.Context) error { 129 | log := logr.FromContextOrDiscard(ctx) 130 | 131 | log.Info("reading virtualservices") 132 | cfg, err := envoy.ReadCRDs(c.ConfigDir) 133 | if err != nil { 134 | return fmt.Errorf("failed to read config files: %w", err) 135 | } 136 | routeGen := envoy.NewRouteGenerator( 137 | envoy.WithConfigs(cfg), 138 | envoy.WithGateway(c.Gateway), 139 | ) 140 | 141 | routes, err := routeGen.Routes() 142 | if err != nil { 143 | return fmt.Errorf("failed to generate routes: %w", err) 144 | } 145 | if len(routes) <= 0 { 146 | return fmt.Errorf("expected at least one route, got %d. Parsed %d configs", len(routes), len(cfg)) 147 | } 148 | for _, route := range routes { 149 | raw, err := protomarshal.ToJSON(route) 150 | if err != nil { 151 | return fmt.Errorf("failed to marshal route: %w", err) 152 | } 153 | routeName := fmt.Sprintf("route_%s_%s.json", cmp.Or(strings.ReplaceAll(c.Gateway, "/", "_"), "sidecar"), route.Name) 154 | routeFile := filepath.Join(c.OutputDir, routeName) 155 | log.Info("writing route", "route", route.Name, "file", routeFile) 156 | err = os.WriteFile(routeFile, []byte(raw), os.ModePerm) 157 | if err != nil { 158 | return fmt.Errorf("failed to write route: %w", err) 159 | } 160 | } 161 | return nil 162 | } 163 | 164 | func (c *RootCommand) prepareTests(ctx context.Context) error { 165 | if c.TestDir == "" { 166 | return nil 167 | } 168 | log := logr.FromContextOrDiscard(ctx) 169 | log.Info("reading tests", "dir", c.TestDir) 170 | 171 | oldFiles, err := helpers.WalkYAML(c.TestDir) 172 | if err != nil { 173 | return fmt.Errorf("could not read directory %s: %w", c.TestDir, err) 174 | } 175 | strict := true 176 | testCases, err := parser.ParseTestCases(oldFiles, strict) 177 | if err != nil { 178 | return fmt.Errorf("parsing testcases failed: %w", err) 179 | } 180 | var newTests envoy.Tests 181 | for _, tc := range testCases { 182 | inputs, err := tc.Request.Unfold() 183 | if err != nil { 184 | return fmt.Errorf("could not unfold request: %w", err) 185 | } 186 | if !tc.WantMatch { 187 | log.V(LevelDebug).Info("skipping negative test", "test", tc.Description, "reason", "router_check_tool does not support negative tests") 188 | continue 189 | } 190 | if tc.Rewrite != nil { 191 | log.V(LevelDebug).Info("skipping rewrite test", "test", tc.Description, "reason", "format assertion is different in envoy tests") 192 | continue 193 | } 194 | if tc.Redirect != nil { 195 | log.V(LevelDebug).Info("skipping redirect test", "test", tc.Description, "reason", "format assertion is different in envoy tests") 196 | continue 197 | } 198 | for _, req := range inputs { 199 | var reqHeaders []envoy.Header 200 | for key, value := range req.Headers { 201 | reqHeaders = append(reqHeaders, envoy.Header{Key: key, Value: value}) 202 | } 203 | input := envoy.Input{ 204 | SSL: true, 205 | Authority: req.Authority, 206 | Method: cmp.Or(req.Method, http.MethodGet), 207 | Path: cmp.Or(req.URI, "/"), 208 | AdditionalRequestHeaders: reqHeaders, 209 | } 210 | validate, err := convertValidate(input, tc) 211 | if err != nil { 212 | return fmt.Errorf("could not convert test %q: %w", tc.Description, err) 213 | } 214 | newTests.Tests = append(newTests.Tests, envoy.Test{ 215 | TestName: fmt.Sprintf("%s: method=%q authority=%q path=%q headers=%+v", tc.Description, input.Method, input.Authority, input.Path, input.AdditionalRequestHeaders), 216 | Input: input, 217 | Validate: validate, 218 | }) 219 | } 220 | } 221 | outputFile := filepath.Join(c.OutputDir, "tests.json") 222 | log.Info("writing tests", "file", outputFile) 223 | raw, err := json.Marshal(newTests) 224 | if err != nil { 225 | return fmt.Errorf("failed to marshal tests: %w", err) 226 | } 227 | err = os.WriteFile(outputFile, raw, os.ModePerm) 228 | if err != nil { 229 | return fmt.Errorf("failed to write tests: %w", err) 230 | } 231 | 232 | return nil 233 | } 234 | 235 | func convertValidate(input envoy.Input, tc *parser.TestCase) (envoy.Validate, error) { 236 | output := envoy.Validate{} 237 | if tc.Route != nil { 238 | var route *v1alpha3.HTTPRouteDestination 239 | for _, r := range tc.Route { 240 | if r.GetWeight() >= route.GetWeight() { 241 | route = r 242 | } 243 | } 244 | output.ClusterName = fmt.Sprintf("outbound|%d|%s|%s", 245 | cmp.Or(route.GetDestination().GetPort().GetNumber(), 80), 246 | route.GetDestination().GetSubset(), 247 | route.GetDestination().GetHost(), 248 | ) 249 | } 250 | return output, nil 251 | } 252 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/cmd/root_test.go: -------------------------------------------------------------------------------- 1 | package cmd_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/getyourguide/istio-config-validator/internal/pkg/istio-router-check/cmd" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestRootCommand(t *testing.T) { 12 | cmdRoot, err := cmd.NewCmdRoot() 13 | require.NoError(t, err) 14 | require.NotNil(t, cmdRoot) 15 | 16 | t.Run("it should fail with missing required flags", func(t *testing.T) { 17 | // TODO(cainelli): add router_check_tool to CI 18 | if os.Getenv("CI") == "true" { 19 | t.Skip("skip as it requires router_check_tool binary not yet in CI") 20 | } 21 | err = cmdRoot.Execute() 22 | require.ErrorContains(t, err, "required flag(s)") 23 | require.ErrorContains(t, err, "config-dir") 24 | require.ErrorContains(t, err, "test-dir") 25 | }) 26 | 27 | t.Run("it should run the test", func(t *testing.T) { 28 | // TODO(cainelli): add router_check_tool to CI 29 | if os.Getenv("CI") == "true" { 30 | t.Skip("skip as it requires router_check_tool binary not yet in CI") 31 | } 32 | cmdRoot.SetArgs([]string{ 33 | "--config-dir", "testdata/virtualservice.yml", 34 | "--test-dir", "testdata/test.yml", 35 | "--output-dir", "/tmp/output", 36 | }) 37 | err = cmdRoot.Execute() 38 | require.NoError(t, err) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/cmd/testdata/test.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | - test_name: test details.prod.svc.cluster.local/api/v2/products 3 | input: 4 | authority: details.prod.svc.cluster.local 5 | path: /api/v2/products 6 | method: GET 7 | validate: 8 | cluster_name: outbound|80|v2|details.prod.svc.cluster.local 9 | path_rewrite: /api/newdetails 10 | - test_name: test details.prod.svc.cluster.local/api/v2/items 11 | input: 12 | authority: details.prod.svc.cluster.local 13 | path: /api/v2/items 14 | method: GET 15 | validate: 16 | cluster_name: outbound|80|v2|details.prod.svc.cluster.local 17 | path_rewrite: /api/newdetails 18 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/cmd/testdata/virtualservice.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1 2 | kind: VirtualService 3 | metadata: 4 | name: details 5 | spec: 6 | hosts: 7 | - details.prod.svc.cluster.local 8 | http: 9 | - name: "details-v2-routes" 10 | match: 11 | - uri: 12 | prefix: "/api/v2/products" 13 | - uri: 14 | prefix: "/api/v2/items" 15 | rewrite: 16 | uri: "/api/newdetails" 17 | route: 18 | - destination: 19 | host: details.prod.svc.cluster.local 20 | subset: v2 21 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/envoy/options.go: -------------------------------------------------------------------------------- 1 | package envoy 2 | 3 | import ( 4 | "istio.io/istio/pkg/config" 5 | ) 6 | 7 | type optionFunc func(*routeGenerator) 8 | 9 | // WithConfigs sets the configs to be used by the route generator. 10 | func WithConfigs(cfgs []config.Config) optionFunc { 11 | return func(rg *routeGenerator) { 12 | rg.configs = cfgs 13 | } 14 | } 15 | 16 | // WithGateway generate routes with the provided gateway view. If the gateway does not exist in the provided configs, 17 | // it will be created with the default values accepting "*" as hosts. 18 | func WithGateway(name string) optionFunc { 19 | return func(rg *routeGenerator) { 20 | rg.gatewayName = name 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/envoy/routes.go: -------------------------------------------------------------------------------- 1 | package envoy 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 8 | "github.com/getyourguide/istio-config-validator/internal/pkg/istio-router-check/helpers" 9 | v1 "istio.io/api/networking/v1" 10 | "istio.io/api/networking/v1alpha3" 11 | "istio.io/istio/pilot/pkg/config/kube/crd" 12 | "istio.io/istio/pilot/pkg/model" 13 | "istio.io/istio/pilot/test/xds" 14 | "istio.io/istio/pkg/config" 15 | "istio.io/istio/pkg/config/schema/gvk" 16 | 17 | istiolog "istio.io/istio/pkg/log" 18 | istiotest "istio.io/istio/pkg/test" 19 | ) 20 | 21 | type routeGenerator struct { 22 | configs []config.Config 23 | proxy *model.Proxy 24 | gatewayName string 25 | routes []*route.RouteConfiguration 26 | } 27 | 28 | func NewRouteGenerator(opts ...optionFunc) *routeGenerator { 29 | rg := &routeGenerator{} 30 | for _, opt := range opts { 31 | opt(rg) 32 | } 33 | return rg 34 | } 35 | 36 | // Routes returns a list of routes in Envoy format. It uses istio's fake discovery server to generate the routes. 37 | // The routes are generated from the Configs loaded in the RouteGenerator. 38 | // TODO(cainelli): The RouterGenerator only takes VirtualServices into account when they are bound to `mesh`, VirtualServices bound exclusively to `gateway` are not considered. 39 | func (rg *routeGenerator) Routes() ([]*route.RouteConfiguration, error) { 40 | logOpts := istiolog.DefaultOptions() 41 | logOpts.SetDefaultOutputLevel("all", istiolog.ErrorLevel) 42 | err := istiolog.Configure(logOpts) 43 | if err != nil { 44 | return nil, fmt.Errorf("failed to configure logging: %w", err) 45 | } 46 | 47 | // Add a random endpoint, otherwise there will be no routes to check 48 | rg.configs = append(rg.configs, config.Config{ 49 | Meta: config.Meta{ 50 | GroupVersionKind: gvk.ServiceEntry, 51 | Namespace: "a", 52 | Name: "wg-a", 53 | Labels: map[string]string{ 54 | "grouplabel": "notonentry", 55 | }, 56 | }, 57 | Spec: &v1alpha3.ServiceEntry{ 58 | Hosts: []string{"pod.pod.svc.cluster.local"}, 59 | Ports: []*v1alpha3.ServicePort{{ 60 | Number: 80, 61 | Protocol: "HTTP", 62 | Name: "http", 63 | }}, 64 | Location: v1alpha3.ServiceEntry_MESH_INTERNAL, 65 | Resolution: v1alpha3.ServiceEntry_STATIC, 66 | Endpoints: []*v1alpha3.WorkloadEntry{{ 67 | Address: "10.10.10.20", 68 | }}, 69 | }, 70 | }) 71 | 72 | err = istiotest.Wrap(func(t istiotest.Failer) { 73 | if err := rg.prepareProxy(); err != nil { 74 | t.FailNow() 75 | } 76 | srv := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ 77 | Configs: rg.configs, 78 | }) 79 | proxy := srv.SetupProxy(rg.proxy) 80 | rg.routes = srv.Routes(proxy) 81 | }) 82 | if err != nil { 83 | return nil, fmt.Errorf("failed to generate routes: %w", err) 84 | } 85 | return rg.routes, nil 86 | } 87 | 88 | // prepareProxy creates a proxy with the provided metadata. If the gateway is set, it will create a proxy with the 89 | // metadata of the gateway. If the gateway does not exist in the provided configs, it will be created with the default 90 | // values accepting "*" as hosts. 91 | func (rg *routeGenerator) prepareProxy() error { 92 | if rg.gatewayName == "" { 93 | return nil 94 | } 95 | 96 | namespacedName := helpers.NewNamespacedNameFromString(rg.gatewayName) 97 | metadata := &model.NodeMetadata{ 98 | Namespace: namespacedName.Namespace, 99 | Labels: map[string]string{ 100 | "istio": "ingressgateway", 101 | }, 102 | } 103 | var gatewayFound bool 104 | for _, cfg := range rg.configs { 105 | if cfg.Meta.GroupVersionKind != gvk.Gateway { 106 | continue 107 | } 108 | if cfg.Meta.Name == namespacedName.Name && cfg.Meta.Namespace == namespacedName.Namespace { 109 | gatewayFound = true 110 | var selector map[string]string 111 | switch v := cfg.Spec.(type) { 112 | case *v1.Gateway: 113 | selector = v.Selector 114 | default: 115 | return fmt.Errorf("could not cast Gateway spec (%T) for %s/%s", v, cfg.Meta.Namespace, cfg.Meta.Name) 116 | } 117 | 118 | metadata = &model.NodeMetadata{ 119 | Namespace: cfg.Meta.Namespace, 120 | Labels: selector, 121 | } 122 | break 123 | } 124 | } 125 | 126 | if !gatewayFound { 127 | rg.configs = append(rg.configs, config.Config{ 128 | Meta: config.Meta{ 129 | GroupVersionKind: gvk.Gateway, 130 | Name: namespacedName.Name, 131 | Namespace: namespacedName.Namespace, 132 | Labels: metadata.Labels, 133 | }, 134 | Spec: &v1.Gateway{ 135 | Selector: metadata.Labels, 136 | Servers: []*v1.Server{{ 137 | Hosts: []string{"*"}, 138 | Port: &v1.Port{ 139 | Number: 80, 140 | Protocol: "HTTP", 141 | }, 142 | }}, 143 | }, 144 | }) 145 | } 146 | 147 | rg.proxy = &model.Proxy{ 148 | Type: model.Router, 149 | Labels: metadata.Labels, 150 | Metadata: metadata, 151 | } 152 | 153 | return nil 154 | } 155 | 156 | func ReadCRDs(baseDir string) ([]config.Config, error) { 157 | var configs []config.Config 158 | yamlFiles, err := helpers.WalkYAML(baseDir) 159 | if err != nil { 160 | return nil, fmt.Errorf("could not read directory %s: %w", baseDir, err) 161 | } 162 | for _, path := range yamlFiles { 163 | data, err := os.ReadFile(path) 164 | if err != nil { 165 | return nil, fmt.Errorf("could not read file %s: %w", path, err) 166 | } 167 | c, _, err := crd.ParseInputs(string(data)) 168 | if err != nil { 169 | return nil, fmt.Errorf("failed to parse CRD: %w\n%s", err, string(data)) 170 | } 171 | configs = append(configs, c...) 172 | } 173 | return configs, nil 174 | } 175 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/envoy/routes_test.go: -------------------------------------------------------------------------------- 1 | package envoy_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/getyourguide/istio-config-validator/internal/pkg/istio-router-check/envoy" 7 | "github.com/stretchr/testify/require" 8 | "istio.io/istio/pkg/config" 9 | ) 10 | 11 | func TestRoutesGenerator(t *testing.T) { 12 | t.Run("it should generate one route", func(t *testing.T) { 13 | cfg, err := envoy.ReadCRDs("testdata/virtualservice.yml") 14 | require.NoError(t, err) 15 | rg := envoy.NewRouteGenerator( 16 | envoy.WithConfigs(cfg), 17 | envoy.WithGateway("istio-system/istio-ingressgateway"), 18 | ) 19 | routes, err := rg.Routes() 20 | require.NoError(t, err) 21 | require.Len(t, routes, 1) 22 | }) 23 | } 24 | 25 | func TestReadCRDs(t *testing.T) { 26 | for _, tt := range []struct { 27 | name string 28 | path string 29 | assert func(t *testing.T, got []config.Config) 30 | }{{ 31 | name: "it should return single virtualservice", 32 | path: "testdata/bookinfo/reviews", 33 | assert: func(t *testing.T, got []config.Config) { 34 | require.Len(t, got, 1) 35 | require.Equal(t, "reviews-route", got[0].GetName()) 36 | }, 37 | }, { 38 | name: "it should return multiple virtualservices across directories", 39 | path: "testdata/bookinfo", 40 | assert: func(t *testing.T, got []config.Config) { 41 | require.Len(t, got, 3) 42 | wantVS := []string{"reviews-route", "product-details-route", "details-fallback"} 43 | for _, c := range got { 44 | require.Contains(t, wantVS, c.GetName()) 45 | } 46 | }, 47 | }} { 48 | t.Run(tt.name, func(t *testing.T) { 49 | got, err := envoy.ReadCRDs(tt.path) 50 | require.NoError(t, err) 51 | tt.assert(t, got) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/envoy/testdata/bookinfo/details/virtualservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1 2 | kind: VirtualService 3 | metadata: 4 | name: product-details-route 5 | spec: 6 | hosts: 7 | - details.prod.svc.cluster.local 8 | http: 9 | - name: "details-v2-routes" 10 | match: 11 | - uri: 12 | prefix: "/api/v2/products" 13 | - uri: 14 | prefix: "/api/v2/items" 15 | rewrite: 16 | uri: "/api/newdetails" 17 | route: 18 | - destination: 19 | host: details.prod.svc.cluster.local 20 | subset: v2 21 | - name: "details-v1-route" 22 | delegate: 23 | name: details-fallback 24 | --- 25 | apiVersion: networking.istio.io/v1 26 | kind: VirtualService 27 | metadata: 28 | name: details-fallback 29 | spec: 30 | http: 31 | - route: 32 | - destination: 33 | host: details.prod.svc.cluster.local 34 | subset: v1 35 | retries: 36 | attempts: 3 37 | perTryTimeout: 25s 38 | retryOn: retriable-status-codes,connect-failure,refused-stream 39 | timeout: 25s 40 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/envoy/testdata/bookinfo/reviews/file.txt: -------------------------------------------------------------------------------- 1 | file.txt 2 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/envoy/testdata/bookinfo/reviews/virtualservice.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1 2 | kind: VirtualService 3 | metadata: 4 | name: reviews-route 5 | spec: 6 | hosts: 7 | - reviews.prod.svc.cluster.local 8 | http: 9 | - name: "reviews-v2-routes" 10 | match: 11 | - uri: 12 | prefix: "/wpcatalog" 13 | - uri: 14 | prefix: "/consumercatalog" 15 | rewrite: 16 | uri: "/newcatalog" 17 | route: 18 | - destination: 19 | host: reviews.prod.svc.cluster.local 20 | subset: v2 21 | - name: "reviews-v1-route" 22 | route: 23 | - destination: 24 | host: reviews.prod.svc.cluster.local 25 | subset: v1 26 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/envoy/testdata/tests/details/details_test.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | - test_name: test details.prod.svc.cluster.local/api/v2/products 3 | input: 4 | authority: details.prod.svc.cluster.local 5 | path: /api/v2/products 6 | method: GET 7 | validate: 8 | cluster_name: outbound|80|v2|details.prod.svc.cluster.local 9 | path_rewrite: /api/newdetails 10 | - test_name: test details.prod.svc.cluster.local/api/v2/items 11 | input: 12 | authority: details.prod.svc.cluster.local 13 | path: /api/v2/items 14 | method: GET 15 | validate: 16 | cluster_name: outbound|80|v2|details.prod.svc.cluster.local 17 | path_rewrite: /api/newdetails 18 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/envoy/testdata/tests/details/file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getyourguide/istio-config-validator/c92f6b7d10fde6847d2f9e0bf95768fb3f512015/internal/pkg/istio-router-check/envoy/testdata/tests/details/file.txt -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/envoy/testdata/tests/reviews/reviews_test.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - test_name: test reviews.prod.svc.cluster.local/wpcatalog 3 | input: 4 | authority: reviews.prod.svc.cluster.local 5 | path: /wpcatalog 6 | method: GET 7 | validate: 8 | cluster_name: outbound|80|v2|reviews.prod.svc.cluster.local 9 | path_rewrite: /newcatalog 10 | - test_name: test reviews.prod.svc.cluster.local/consumercatalog 11 | input: 12 | authority: reviews.prod.svc.cluster.local 13 | path: /consumercatalog 14 | method: GET 15 | validate: 16 | cluster_name: outbound|80|v2|reviews.prod.svc.cluster.local 17 | path_rewrite: /newcatalog 18 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/envoy/testdata/virtualservice.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1 2 | kind: VirtualService 3 | metadata: 4 | name: reviews-route 5 | spec: 6 | gateways: 7 | - istio-system/istio-ingressgateway 8 | hosts: 9 | - reviews.prod.svc.cluster.local 10 | http: 11 | - name: "reviews-v2-routes" 12 | match: 13 | - uri: 14 | prefix: "/wpcatalog" 15 | - uri: 16 | prefix: "/consumercatalog" 17 | rewrite: 18 | uri: "/newcatalog" 19 | route: 20 | - destination: 21 | host: reviews.prod.svc.cluster.local 22 | subset: v2 23 | - name: "reviews-v1-route" 24 | route: 25 | - destination: 26 | host: reviews.prod.svc.cluster.local 27 | subset: v1 28 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/envoy/testfile.go: -------------------------------------------------------------------------------- 1 | package envoy 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/getyourguide/istio-config-validator/internal/pkg/istio-router-check/helpers" 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | type Tests struct { 12 | Tests []Test `yaml:"tests,omitempty" json:"tests,omitempty"` 13 | } 14 | type Test struct { 15 | TestName string `yaml:"test_name,omitempty" json:"test_name,omitempty"` 16 | Input Input `yaml:"input,omitempty" json:"input,omitempty"` 17 | Validate Validate `yaml:"validate" json:"validate"` 18 | } 19 | 20 | type Validate struct { 21 | ClusterName string `yaml:"cluster_name" json:"cluster_name"` 22 | VirtualClusterName string `yaml:"virtual_cluster_name,omitempty" json:"virtual_cluster_name,omitempty"` 23 | VirtualHostName string `yaml:"virtual_host_name,omitempty" json:"virtual_host_name,omitempty"` 24 | HostRewrite string `yaml:"host_rewrite,omitempty" json:"host_rewrite,omitempty"` 25 | PathRewrite string `yaml:"path_rewrite,omitempty" json:"path_rewrite,omitempty"` 26 | PathRedirect string `yaml:"path_redirect,omitempty" json:"path_redirect,omitempty"` 27 | RequestHeaderMatches []RequestHeaderMatch `yaml:"request_header_matches,omitempty" json:"request_header_matches,omitempty"` 28 | ResponseHeaderMatches []ResponseHeaderMatch `yaml:"response_header_matches,omitempty" json:"response_header_matches,omitempty"` 29 | } 30 | 31 | type RequestHeaderMatch struct { 32 | Name string `yaml:"name,omitempty" json:"name,omitempty"` 33 | StringMatch StringMatch `yaml:"string_match,omitempty" json:"string_match,omitempty"` 34 | } 35 | 36 | type ResponseHeaderMatch struct { 37 | Name string `yaml:"name,omitempty" json:"name,omitempty"` 38 | StringMatch map[string]any `yaml:"string_match,omitempty" json:"string_match,omitempty"` 39 | PresenceMatch PresenceMatch `yaml:"presence_match,omitempty" json:"presence_match,omitempty"` 40 | } 41 | 42 | type StringMatch struct { 43 | Exact string `yaml:"exact,omitempty" json:"exact,omitempty"` 44 | } 45 | 46 | type PresenceMatch struct{} 47 | 48 | type Input struct { 49 | Authority string `yaml:"authority,omitempty" json:"authority,omitempty"` 50 | Path string `yaml:"path,omitempty" json:"path,omitempty"` 51 | Method string `yaml:"method,omitempty" json:"method,omitempty"` 52 | Internal bool `yaml:"internal,omitempty" json:"internal,omitempty"` 53 | RandomValue string `yaml:"random_value,omitempty" json:"random_value,omitempty"` 54 | SSL bool `yaml:"ssl,omitempty" json:"ssl,omitempty"` 55 | Runtime string `yaml:"runtime,omitempty" json:"runtime,omitempty"` 56 | AdditionalRequestHeaders []Header `yaml:"additional_request_headers,omitempty" json:"additional_request_headers,omitempty"` 57 | AdditionalResponseHeaders []Header `yaml:"additional_response_headers,omitempty" json:"additional_response_headers,omitempty"` 58 | } 59 | 60 | type Header struct { 61 | Key string `yaml:"key,omitempty" json:"key,omitempty"` 62 | Value string `yaml:"value,omitempty" json:"value,omitempty"` 63 | } 64 | 65 | func ReadTests(baseDir string) (Tests, error) { 66 | var tests Tests 67 | yamlFiles, err := helpers.WalkYAML(baseDir) 68 | if err != nil { 69 | return Tests{}, fmt.Errorf("could not read directory %s: %w", baseDir, err) 70 | } 71 | for _, path := range yamlFiles { 72 | data, err := os.ReadFile(path) 73 | if err != nil { 74 | return Tests{}, fmt.Errorf("could not read file %s: %w", path, err) 75 | } 76 | var t Tests 77 | err = yaml.Unmarshal(data, &t) 78 | if err != nil { 79 | return Tests{}, fmt.Errorf("could not unmarshalling file %s: %w", path, err) 80 | } 81 | tests.Tests = append(tests.Tests, t.Tests...) 82 | } 83 | return tests, nil 84 | } 85 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/envoy/testfile_test.go: -------------------------------------------------------------------------------- 1 | package envoy_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/getyourguide/istio-config-validator/internal/pkg/istio-router-check/envoy" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestReadTests(t *testing.T) { 11 | for _, tt := range []struct { 12 | name string 13 | path string 14 | want []string 15 | }{{ 16 | name: "it should return single folder test", 17 | path: "testdata/tests/reviews", 18 | want: []string{ 19 | "test reviews.prod.svc.cluster.local/wpcatalog", 20 | "test reviews.prod.svc.cluster.local/consumercatalog", 21 | }, 22 | }, { 23 | name: "it should return tests from multiple folders", 24 | path: "testdata/tests/", 25 | want: []string{ 26 | "test details.prod.svc.cluster.local/api/v2/products", 27 | "test details.prod.svc.cluster.local/api/v2/items", 28 | "test reviews.prod.svc.cluster.local/wpcatalog", 29 | "test reviews.prod.svc.cluster.local/consumercatalog", 30 | }, 31 | }} { 32 | t.Run(tt.name, func(t *testing.T) { 33 | parsed, err := envoy.ReadTests(tt.path) 34 | require.NoError(t, err) 35 | var gotTests []string 36 | for _, t := range parsed.Tests { 37 | gotTests = append(gotTests, t.TestName) 38 | } 39 | require.ElementsMatch(t, tt.want, gotTests) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/helpers/namespacedname.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package helpers 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | ) 23 | 24 | // NamespacedName comprises a resource name, with a mandatory namespace, 25 | // rendered as "/". Being a type captures intent and 26 | // helps make sure that UIDs, namespaced names and non-namespaced names 27 | // do not get conflated in code. For most use cases, namespace and name 28 | // will already have been format validated at the API entry point, so we 29 | // don't do that here. Where that's not the case (e.g. in testing), 30 | // consider using NamespacedNameOrDie() in testing.go in this package. 31 | 32 | type NamespacedName struct { 33 | Namespace string 34 | Name string 35 | } 36 | 37 | const ( 38 | Separator = '/' 39 | ) 40 | 41 | // String returns the general purpose string representation 42 | func (n NamespacedName) String() string { 43 | return fmt.Sprintf("%s%c%s", n.Namespace, Separator, n.Name) 44 | } 45 | 46 | // NewNamespacedNameFromString parses the provided string and returns a NamespacedName. 47 | // The expected format is as per String() above. 48 | // If the input string is invalid, the returned NamespacedName has all empty string field values. 49 | // This allows a single-value return from this function, while still allowing error checks in the caller. 50 | // Note that an input string which does not include exactly one Separator is not a valid input (as it could never 51 | // have neem returned by String() ) 52 | func NewNamespacedNameFromString(s string) NamespacedName { 53 | nn := NamespacedName{} 54 | result := strings.Split(s, string(Separator)) 55 | if len(result) == 2 { 56 | nn.Namespace = result[0] 57 | nn.Name = result[1] 58 | } 59 | return nn 60 | } 61 | -------------------------------------------------------------------------------- /internal/pkg/istio-router-check/helpers/walkdir.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // WalkYAML walks the baseDirs and returns a list of all files found with yaml extension. 10 | func WalkYAML(baseDirs ...string) ([]string, error) { 11 | return WalkFilter(func(path string, info os.FileInfo) bool { 12 | if filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml" { 13 | return true 14 | } 15 | return false 16 | }, baseDirs...) 17 | } 18 | 19 | // WalkFilter walks the baseDirs and returns a list of all files found that return true in the filterFunc. 20 | func WalkFilter(filterFunc func(path string, info os.FileInfo) bool, baseDirs ...string) ([]string, error) { 21 | var files []string 22 | for _, baseDir := range baseDirs { 23 | err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error { 24 | if err != nil { 25 | return fmt.Errorf("could not read directory %q: %w", baseDir, err) 26 | } 27 | if info.IsDir() { 28 | return nil 29 | } 30 | if !filterFunc(path, info) { 31 | return nil 32 | } 33 | files = append(files, path) 34 | return nil 35 | }) 36 | if err != nil { 37 | return nil, fmt.Errorf("could not read directory %q: %w", baseDir, err) 38 | } 39 | } 40 | return files, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/pkg/parser/testcase.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/url" 10 | "os" 11 | "strings" 12 | 13 | yamlV3 "gopkg.in/yaml.v3" 14 | networkingv1alpha3 "istio.io/api/networking/v1alpha3" 15 | ) 16 | 17 | var ( 18 | // ErrEmptyAuthorityList indicates an empty Authority list 19 | ErrEmptyAuthorityList = errors.New("authority list is empty") 20 | // ErrEmptyMethodList indicates an empty Method list 21 | ErrEmptyMethodList = errors.New("method list is empty") 22 | // ErrEmptyURIList indicates an empty URI list 23 | ErrEmptyURIList = errors.New("URI list is empty") 24 | ) 25 | 26 | // TestCaseYAML define the list of TestCase 27 | type TestCaseYAML struct { 28 | TestCases []*TestCase `yaml:"testCases"` 29 | } 30 | 31 | // TestCase defines the API for declaring unit tests 32 | type TestCase struct { 33 | Description string `yaml:"description"` 34 | Request *Request `yaml:"request"` 35 | Route []*networkingv1alpha3.HTTPRouteDestination `yaml:"route"` 36 | Redirect *networkingv1alpha3.HTTPRedirect `yaml:"redirect"` 37 | Rewrite *networkingv1alpha3.HTTPRewrite `yaml:"rewrite"` 38 | Fault *networkingv1alpha3.HTTPFaultInjection `yaml:"fault"` 39 | Headers *networkingv1alpha3.Headers `yaml:"headers"` 40 | Delegate *networkingv1alpha3.Delegate `yaml:"delegate"` 41 | WantMatch bool `yaml:"wantMatch"` 42 | } 43 | 44 | // Request define the crafted http request present in the test case file. 45 | type Request struct { 46 | Authority []string `yaml:"authority"` 47 | Method []string `yaml:"method"` 48 | URI []string `yaml:"uri"` 49 | Headers map[string]string `yaml:"headers"` 50 | } 51 | 52 | // Input contains the data structure which will be used to assert 53 | type Input struct { 54 | Authority string 55 | Method string 56 | URI string 57 | Headers map[string]string 58 | } 59 | 60 | // Destination define the destination we should assert 61 | type Destination struct { 62 | Host string `yaml:"host"` 63 | Port Port `yaml:"port"` 64 | } 65 | 66 | // Port define the port of a given Destination 67 | type Port struct { 68 | Number int16 `yaml:"number"` 69 | } 70 | 71 | // Unfold returns a list of Input objects constructed by all possibilities defined in the Request object. Ex: 72 | // Request{Authority: {"www.example.com", "example.com"}, Method: {"GET", "OPTIONS"}} 73 | // 74 | // returns []Input{ 75 | // {Authority:"www.example.com", Method: "GET"}, 76 | // {Authority:"www.example.com", Method: "OPTIONS"} 77 | // {Authority:"example.com", Method: "GET"}, 78 | // {Authority:"example.com", Method: "OPTIONS"}, 79 | // } 80 | func (r *Request) Unfold() ([]Input, error) { 81 | out := []Input{} 82 | 83 | if len(r.Authority) == 0 { 84 | return out, ErrEmptyAuthorityList 85 | } 86 | if len(r.Method) == 0 { 87 | return out, ErrEmptyMethodList 88 | } 89 | if len(r.URI) == 0 { 90 | return out, ErrEmptyURIList 91 | } 92 | 93 | for _, uri := range r.URI { 94 | u, err := url.Parse(uri) 95 | if err != nil { 96 | return out, err 97 | } 98 | 99 | for _, auth := range r.Authority { 100 | for _, method := range r.Method { 101 | out = append(out, Input{Authority: auth, Method: method, URI: u.Path, Headers: r.Headers}) 102 | } 103 | } 104 | } 105 | 106 | return out, nil 107 | } 108 | 109 | func ParseTestCases(files []string, strict bool) ([]*TestCase, error) { 110 | out := []*TestCase{} 111 | 112 | for _, file := range files { 113 | fileContent, err := os.ReadFile(file) 114 | if err != nil { 115 | return nil, fmt.Errorf("reading file %q failed: %w", file, err) 116 | } 117 | 118 | decoder := yamlV3.NewDecoder(strings.NewReader(string(fileContent))) 119 | for { 120 | var testcaseInterface interface{} 121 | if err = decoder.Decode(&testcaseInterface); err != nil { 122 | if errors.Is(err, io.EOF) { 123 | break 124 | } 125 | return out, fmt.Errorf("error while trying to unmarshal into interface (%s): %w", file, err) 126 | } 127 | 128 | jsonBytes, err := json.Marshal(testcaseInterface) 129 | if err != nil { 130 | return nil, fmt.Errorf("yamltojson conversion failed for file %q: %w", file, err) 131 | } 132 | jsonDecoder := json.NewDecoder(bytes.NewReader(jsonBytes)) 133 | if strict { 134 | jsonDecoder.DisallowUnknownFields() 135 | } 136 | 137 | var yamlFile TestCaseYAML 138 | if err := jsonDecoder.Decode(&yamlFile); err != nil { 139 | return nil, fmt.Errorf("unmarshaling failed for file %q: %w", file, err) 140 | } 141 | 142 | out = append(out, yamlFile.TestCases...) 143 | } 144 | } 145 | return out, nil 146 | } 147 | -------------------------------------------------------------------------------- /internal/pkg/parser/testcase_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestUnknownField(t *testing.T) { 11 | testsFiles := []string{"testdata/invalid_test.yml"} 12 | _, err := ParseTestCases(testsFiles, true) 13 | require.ErrorContains(t, err, "json: unknown field") 14 | 15 | _, err = ParseTestCases(testsFiles, false) 16 | require.NoError(t, err) 17 | } 18 | 19 | func TestParseTestCases(t *testing.T) { 20 | expectedTestCases := []*TestCase{ 21 | {Description: "happy path users"}, 22 | {Description: "Partner service only accepts GET or OPTIONS"}, 23 | } 24 | testcasefiles := []string{"../../../examples/virtualservice_test.yml"} 25 | testCases, err := ParseTestCases(testcasefiles, false) 26 | require.NoError(t, err) 27 | require.NotEmpty(t, testCases) 28 | 29 | for _, expected := range expectedTestCases { 30 | testPass := false 31 | for _, out := range testCases { 32 | if expected.Description == out.Description { 33 | testPass = true 34 | } 35 | } 36 | if !testPass { 37 | t.Errorf("could not find expected description:'%v'", expected.Description) 38 | } 39 | } 40 | } 41 | 42 | func TestUnfoldRequest(t *testing.T) { 43 | testCases := []struct { 44 | Name string 45 | In Request 46 | Out []Input 47 | Error error 48 | }{ 49 | { 50 | "single authority, method and URI", 51 | Request{ 52 | Authority: []string{"www.example.com"}, 53 | Method: []string{"GET"}, 54 | URI: []string{"/"}, 55 | }, 56 | []Input{ 57 | { 58 | Authority: "www.example.com", 59 | Method: "GET", 60 | URI: "/", 61 | }, 62 | }, 63 | nil, 64 | }, 65 | { 66 | "single authority, method and URI with headers", 67 | Request{ 68 | Authority: []string{"www.example.com"}, 69 | Method: []string{"GET"}, 70 | URI: []string{"/"}, 71 | Headers: map[string]string{ 72 | "Cookie": "namnamnamnamnamnam", 73 | "x-y-z": "X-Y-Z", 74 | }, 75 | }, 76 | []Input{ 77 | { 78 | Authority: "www.example.com", 79 | Method: "GET", 80 | URI: "/", 81 | Headers: map[string]string{ 82 | "Cookie": "namnamnamnamnamnam", 83 | "x-y-z": "X-Y-Z", 84 | }, 85 | }, 86 | }, 87 | nil, 88 | }, 89 | { 90 | "single authority, method and multiple URIs", 91 | Request{ 92 | Authority: []string{"www.example.com"}, 93 | Method: []string{"GET"}, 94 | URI: []string{"/", "/healthz", "/.well-known/foo"}, 95 | }, 96 | []Input{ 97 | { 98 | Authority: "www.example.com", 99 | Method: "GET", 100 | URI: "/", 101 | }, 102 | { 103 | Authority: "www.example.com", 104 | Method: "GET", 105 | URI: "/healthz", 106 | }, 107 | { 108 | Authority: "www.example.com", 109 | Method: "GET", 110 | URI: "/.well-known/foo", 111 | }, 112 | }, 113 | nil, 114 | }, 115 | { 116 | "multiple authorites and single method and URI", 117 | Request{ 118 | Authority: []string{"www.example.com", "example.com", "foo.bar"}, 119 | Method: []string{"GET"}, 120 | URI: []string{"/"}, 121 | }, 122 | []Input{ 123 | { 124 | Authority: "www.example.com", 125 | Method: "GET", 126 | URI: "/", 127 | }, 128 | { 129 | Authority: "example.com", 130 | Method: "GET", 131 | URI: "/", 132 | }, 133 | { 134 | Authority: "foo.bar", 135 | Method: "GET", 136 | URI: "/", 137 | }, 138 | }, 139 | nil, 140 | }, 141 | { 142 | "single authority and multiple methods and single URI", 143 | Request{ 144 | Authority: []string{"www.example.com"}, 145 | Method: []string{"GET", "POST", "PUT"}, 146 | URI: []string{"/"}, 147 | }, 148 | []Input{ 149 | { 150 | Authority: "www.example.com", 151 | Method: "GET", 152 | URI: "/", 153 | }, 154 | { 155 | Authority: "www.example.com", 156 | Method: "POST", 157 | URI: "/", 158 | }, 159 | { 160 | Authority: "www.example.com", 161 | Method: "PUT", 162 | URI: "/", 163 | }, 164 | }, 165 | nil, 166 | }, 167 | { 168 | "multiple authorities and multiple methods and multiple URIs with headers", 169 | Request{ 170 | Authority: []string{"www.example.com", "example.com"}, 171 | Method: []string{"GET", "POST", "PUT"}, 172 | URI: []string{"/", "/healthz"}, 173 | Headers: map[string]string{ 174 | "Cookie": "namnamnamnamnamnam", 175 | "x-y-z": "X-Y-Z", 176 | }, 177 | }, 178 | []Input{ 179 | { 180 | Authority: "www.example.com", 181 | Method: "GET", 182 | URI: "/", 183 | Headers: map[string]string{ 184 | "Cookie": "namnamnamnamnamnam", 185 | "x-y-z": "X-Y-Z", 186 | }, 187 | }, 188 | { 189 | Authority: "www.example.com", 190 | Method: "POST", 191 | URI: "/", 192 | Headers: map[string]string{ 193 | "Cookie": "namnamnamnamnamnam", 194 | "x-y-z": "X-Y-Z", 195 | }, 196 | }, 197 | { 198 | Authority: "www.example.com", 199 | Method: "PUT", 200 | URI: "/", 201 | Headers: map[string]string{ 202 | "Cookie": "namnamnamnamnamnam", 203 | "x-y-z": "X-Y-Z", 204 | }, 205 | }, 206 | { 207 | Authority: "example.com", 208 | Method: "GET", 209 | URI: "/", 210 | Headers: map[string]string{ 211 | "Cookie": "namnamnamnamnamnam", 212 | "x-y-z": "X-Y-Z", 213 | }, 214 | }, 215 | { 216 | Authority: "example.com", 217 | Method: "POST", 218 | URI: "/", 219 | Headers: map[string]string{ 220 | "Cookie": "namnamnamnamnamnam", 221 | "x-y-z": "X-Y-Z", 222 | }, 223 | }, 224 | { 225 | Authority: "example.com", 226 | Method: "PUT", 227 | URI: "/", 228 | Headers: map[string]string{ 229 | "Cookie": "namnamnamnamnamnam", 230 | "x-y-z": "X-Y-Z", 231 | }, 232 | }, 233 | { 234 | Authority: "www.example.com", 235 | Method: "GET", 236 | URI: "/healthz", 237 | Headers: map[string]string{ 238 | "Cookie": "namnamnamnamnamnam", 239 | "x-y-z": "X-Y-Z", 240 | }, 241 | }, 242 | { 243 | Authority: "www.example.com", 244 | Method: "POST", 245 | URI: "/healthz", 246 | Headers: map[string]string{ 247 | "Cookie": "namnamnamnamnamnam", 248 | "x-y-z": "X-Y-Z", 249 | }, 250 | }, 251 | { 252 | Authority: "www.example.com", 253 | Method: "PUT", 254 | URI: "/healthz", 255 | Headers: map[string]string{ 256 | "Cookie": "namnamnamnamnamnam", 257 | "x-y-z": "X-Y-Z", 258 | }, 259 | }, 260 | { 261 | Authority: "example.com", 262 | Method: "GET", 263 | URI: "/healthz", 264 | Headers: map[string]string{ 265 | "Cookie": "namnamnamnamnamnam", 266 | "x-y-z": "X-Y-Z", 267 | }, 268 | }, 269 | { 270 | Authority: "example.com", 271 | Method: "POST", 272 | URI: "/healthz", 273 | Headers: map[string]string{ 274 | "Cookie": "namnamnamnamnamnam", 275 | "x-y-z": "X-Y-Z", 276 | }, 277 | }, 278 | { 279 | Authority: "example.com", 280 | Method: "PUT", 281 | URI: "/healthz", 282 | Headers: map[string]string{ 283 | "Cookie": "namnamnamnamnamnam", 284 | "x-y-z": "X-Y-Z", 285 | }, 286 | }, 287 | }, 288 | nil, 289 | }, 290 | { 291 | "multiple authorities and multiple methods and multiple URIs", 292 | Request{ 293 | Authority: []string{"www.example.com", "example.com"}, 294 | Method: []string{"GET", "POST", "PUT"}, 295 | URI: []string{"/", "/healthz"}, 296 | }, 297 | []Input{ 298 | { 299 | Authority: "www.example.com", 300 | Method: "GET", 301 | URI: "/", 302 | }, 303 | { 304 | Authority: "www.example.com", 305 | Method: "POST", 306 | URI: "/", 307 | }, 308 | { 309 | Authority: "www.example.com", 310 | Method: "PUT", 311 | URI: "/", 312 | }, 313 | { 314 | Authority: "example.com", 315 | Method: "GET", 316 | URI: "/", 317 | }, 318 | { 319 | Authority: "example.com", 320 | Method: "POST", 321 | URI: "/", 322 | }, 323 | { 324 | Authority: "example.com", 325 | Method: "PUT", 326 | URI: "/", 327 | }, 328 | { 329 | Authority: "www.example.com", 330 | Method: "GET", 331 | URI: "/healthz", 332 | }, 333 | { 334 | Authority: "www.example.com", 335 | Method: "POST", 336 | URI: "/healthz", 337 | }, 338 | { 339 | Authority: "www.example.com", 340 | Method: "PUT", 341 | URI: "/healthz", 342 | }, 343 | { 344 | Authority: "example.com", 345 | Method: "GET", 346 | URI: "/healthz", 347 | }, 348 | { 349 | Authority: "example.com", 350 | Method: "POST", 351 | URI: "/healthz", 352 | }, 353 | { 354 | Authority: "example.com", 355 | Method: "PUT", 356 | URI: "/healthz", 357 | }, 358 | }, 359 | nil, 360 | }, 361 | { 362 | "multiple authorities and multiple methods and single URI", 363 | Request{ 364 | Authority: []string{"www.example.com", "example.com"}, 365 | Method: []string{"GET", "POST", "PUT"}, 366 | URI: []string{"/"}, 367 | }, 368 | []Input{ 369 | { 370 | Authority: "www.example.com", 371 | Method: "GET", 372 | URI: "/", 373 | }, 374 | { 375 | Authority: "www.example.com", 376 | Method: "POST", 377 | URI: "/", 378 | }, 379 | { 380 | Authority: "www.example.com", 381 | Method: "PUT", 382 | URI: "/", 383 | }, 384 | { 385 | Authority: "example.com", 386 | Method: "GET", 387 | URI: "/", 388 | }, 389 | { 390 | Authority: "example.com", 391 | Method: "POST", 392 | URI: "/", 393 | }, 394 | { 395 | Authority: "example.com", 396 | Method: "PUT", 397 | URI: "/", 398 | }, 399 | }, 400 | nil, 401 | }, 402 | { 403 | "empty authority list", 404 | Request{ 405 | Authority: []string{}, 406 | Method: []string{"GET", "OPTIONS"}, 407 | URI: []string{"/"}, 408 | }, 409 | []Input{}, 410 | ErrEmptyAuthorityList, 411 | }, 412 | { 413 | "empty method list", 414 | Request{ 415 | Authority: []string{"www.example.com", "www.example.net"}, 416 | Method: []string{}, 417 | URI: []string{"/"}, 418 | }, 419 | []Input{}, 420 | ErrEmptyMethodList, 421 | }, 422 | { 423 | "empty URI list", 424 | Request{ 425 | Authority: []string{"www.example.com"}, 426 | Method: []string{"GET"}, 427 | URI: []string{}, 428 | }, 429 | []Input{}, 430 | ErrEmptyURIList, 431 | }, 432 | { 433 | "empty authority list and method list", 434 | Request{ 435 | Authority: []string{}, 436 | Method: []string{}, 437 | URI: []string{"/"}, 438 | }, 439 | []Input{}, 440 | ErrEmptyAuthorityList, 441 | }, 442 | { 443 | "empty authority list and URI list", 444 | Request{ 445 | Authority: []string{}, 446 | Method: []string{"GET"}, 447 | URI: []string{}, 448 | }, 449 | []Input{}, 450 | ErrEmptyAuthorityList, 451 | }, 452 | { 453 | "empty authority list and method list and URI list", 454 | Request{ 455 | Authority: []string{}, 456 | Method: []string{}, 457 | URI: []string{}, 458 | }, 459 | []Input{}, 460 | ErrEmptyAuthorityList, 461 | }, 462 | { 463 | "empty method list and URI list", 464 | Request{ 465 | Authority: []string{"www.example.com"}, 466 | Method: []string{}, 467 | URI: []string{}, 468 | }, 469 | []Input{}, 470 | ErrEmptyMethodList, 471 | }, 472 | { 473 | "query parameters should be removed", 474 | Request{ 475 | Authority: []string{"www.example.com"}, 476 | Method: []string{"POST"}, 477 | URI: []string{"/reseller?partner_id=12344"}, 478 | }, 479 | []Input{ 480 | { 481 | Authority: "www.example.com", 482 | Method: "POST", 483 | URI: "/reseller", 484 | }, 485 | }, 486 | nil, 487 | }, 488 | } 489 | 490 | for _, testCase := range testCases { 491 | t.Run(testCase.Name, func(t *testing.T) { 492 | got, gotErr := testCase.In.Unfold() 493 | if gotErr != testCase.Error { 494 | t.Errorf("expected err=%v, got err=%v", testCase.Error, gotErr) 495 | } 496 | assert.ElementsMatch(t, testCase.Out, got) 497 | }) 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /internal/pkg/parser/testdata/invalid_test.yml: -------------------------------------------------------------------------------- 1 | testCases: 2 | - description: a test 3 | unknownField: true # not a valid field 4 | wantMatch: true 5 | request: 6 | authority: 7 | - www.example.com 8 | method: 9 | - GET 10 | uri: 11 | - /users 12 | route: 13 | - destination: 14 | host: users.users.svc.cluster.local 15 | port: 16 | number: 80 17 | -------------------------------------------------------------------------------- /internal/pkg/parser/testdata/invalid_vs.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1 2 | kind: VirtualService 3 | metadata: 4 | name: example 5 | namespace: example 6 | spec: 7 | hosts: www.example.com # invalid type 8 | http: 9 | - match: 10 | - uri: 11 | regex: /users(/.*)? 12 | route: 13 | - destination: 14 | host: users.users.svc.cluster.local 15 | port: 16 | number: 80 17 | -------------------------------------------------------------------------------- /internal/pkg/parser/virtualservice.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | networking "istio.io/api/networking/v1" 8 | v1 "istio.io/client-go/pkg/apis/networking/v1" 9 | "istio.io/istio/pilot/pkg/config/kube/crd" 10 | "istio.io/istio/pkg/config/schema/gvk" 11 | ) 12 | 13 | func ParseVirtualServices(files []string) ([]*v1.VirtualService, error) { 14 | out := []*v1.VirtualService{} 15 | for _, file := range files { 16 | fileContent, err := os.ReadFile(file) 17 | if err != nil { 18 | return nil, fmt.Errorf("reading file %q failed: %w", file, err) 19 | } 20 | configs, _, err := crd.ParseInputs(string(fileContent)) 21 | if err != nil { 22 | return nil, fmt.Errorf("failed to parse CRD %q: %w", file, err) 23 | } 24 | for _, c := range configs { 25 | if c.Meta.GroupVersionKind != gvk.VirtualService { 26 | continue 27 | } 28 | spec, ok := c.Spec.(*networking.VirtualService) 29 | if !ok { 30 | return nil, fmt.Errorf("failed to convert spec in %q to VirtualService: %w", file, err) 31 | } 32 | vs := &v1.VirtualService{ 33 | ObjectMeta: c.ToObjectMeta(), 34 | Spec: *spec, //nolint as deep copying mess up with reflect.DeepEqual comparison. 35 | } 36 | out = append(out, vs) 37 | } 38 | } 39 | return out, nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/pkg/parser/virtualservice_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | networkingv1alpha3 "istio.io/api/networking/v1alpha3" 9 | v1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" 10 | ) 11 | 12 | func TestParseVirtualServices(t *testing.T) { 13 | expectedTestCases := []*v1alpha3.VirtualService{{Spec: networkingv1alpha3.VirtualService{ 14 | Hosts: []string{"www.example.com", "example.com"}, 15 | }}} 16 | configfiles := []string{"../../../examples/virtualservice.yml"} 17 | virtualServices, err := ParseVirtualServices(configfiles) 18 | require.NoError(t, err) 19 | require.NotEmpty(t, virtualServices) 20 | 21 | for _, expected := range expectedTestCases { 22 | for _, out := range virtualServices { 23 | assert.ElementsMatch(t, expected.Spec.Hosts, out.Spec.Hosts) 24 | } 25 | } 26 | } 27 | 28 | func TestParseMultipleVirtualServices(t *testing.T) { 29 | wantHosts := []string{"www.example2.com", "example2.com", "www.example3.com", "example3.com"} 30 | 31 | configfiles := []string{"../../../examples/multidocument_virtualservice.yml"} 32 | virtualServices, err := ParseVirtualServices(configfiles) 33 | require.NoError(t, err) 34 | require.NotEmpty(t, virtualServices) 35 | require.GreaterOrEqual(t, len(virtualServices), 2) 36 | 37 | var gotHosts []string 38 | for _, vs := range virtualServices { 39 | gotHosts = append(gotHosts, vs.Spec.Hosts...) 40 | } 41 | require.ElementsMatch(t, wantHosts, gotHosts) 42 | } 43 | 44 | func TestVirtualServiceUnknownFields(t *testing.T) { 45 | vsFiles := []string{"testdata/invalid_vs.yml"} 46 | _, err := ParseVirtualServices(vsFiles) 47 | require.ErrorContains(t, err, "cannot parse proto message") 48 | } 49 | -------------------------------------------------------------------------------- /internal/pkg/unit/match_request.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/getyourguide/istio-config-validator/internal/pkg/parser" 10 | "istio.io/api/networking/v1alpha3" 11 | ) 12 | 13 | // ExtendedStringMatch copies istio ExtendedStringMatch definition and extends it to add helper methods. 14 | type ExtendedStringMatch struct { 15 | *v1alpha3.StringMatch 16 | } 17 | 18 | // IsEmpty returns true if struct is empty 19 | func (sm ExtendedStringMatch) IsEmpty() bool { 20 | return reflect.DeepEqual(sm, ExtendedStringMatch{}) 21 | } 22 | 23 | // Match will return true if a given string matches a StringMatch object. 24 | func (sm *ExtendedStringMatch) Match(s string) (bool, error) { 25 | if sm.IsEmpty() { 26 | return true, nil 27 | } 28 | 29 | switch { 30 | case sm.GetExact() != "": 31 | return sm.GetExact() == s, nil 32 | case sm.GetPrefix() != "": 33 | return strings.HasPrefix(s, sm.GetPrefix()), nil 34 | case sm.GetRegex() != "": 35 | // The rule will not match if only a subsequence of the string matches the regex. 36 | // https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routematch-safe-regex 37 | r, err := regexp.Compile("^" + sm.GetRegex() + "$") 38 | if err != nil { 39 | return false, fmt.Errorf("could not compile regex %s: %v", sm.GetRegex(), err) 40 | } 41 | return r.MatchString(s), nil 42 | } 43 | return false, nil 44 | } 45 | 46 | // matchRequest takes an Input and evaluates against a HTTPMatchRequest block. It replicates 47 | // Istio VirtualService semantic returning true when ALL conditions within the block are true. 48 | // TODO: Add support for all fields within a match block. The ones supported today are: 49 | // Authority, Uri, Method and Headers. 50 | func matchRequest(input parser.Input, httpMatchRequest *v1alpha3.HTTPMatchRequest) (bool, error) { 51 | authority := &ExtendedStringMatch{httpMatchRequest.Authority} 52 | uri := &ExtendedStringMatch{httpMatchRequest.Uri} 53 | method := &ExtendedStringMatch{httpMatchRequest.Method} 54 | 55 | for headerName, sm := range httpMatchRequest.Headers { 56 | if _, ok := input.Headers[headerName]; !ok { 57 | return false, nil 58 | } 59 | header := &ExtendedStringMatch{sm} 60 | match, err := header.Match(input.Headers[headerName]) 61 | if err != nil { 62 | return false, err 63 | } 64 | if !match { 65 | return false, nil 66 | } 67 | } 68 | 69 | uriMatch, err := uri.Match(input.URI) 70 | if err != nil { 71 | return false, err 72 | } 73 | authorityMatch, err := authority.Match(input.Authority) 74 | if err != nil { 75 | return false, err 76 | } 77 | methodMatch, err := method.Match(input.Method) 78 | if err != nil { 79 | return false, err 80 | } 81 | return authorityMatch && uriMatch && methodMatch, nil 82 | } 83 | -------------------------------------------------------------------------------- /internal/pkg/unit/match_request_test.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/getyourguide/istio-config-validator/internal/pkg/parser" 7 | networkingv1alpha3 "istio.io/api/networking/v1alpha3" 8 | ) 9 | 10 | func Test_matchRequest(t *testing.T) { 11 | type args struct { 12 | input parser.Input 13 | httpMatchRequest *networkingv1alpha3.HTTPMatchRequest 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want bool 19 | wantErr bool 20 | }{{ 21 | name: "no match conditions should always match", 22 | args: args{ 23 | input: parser.Input{Authority: "www.example.com", URI: "/", Method: "GET"}, 24 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{}, 25 | }, 26 | want: true, 27 | wantErr: false, 28 | }, { 29 | name: "single match exact (true)", 30 | args: args{ 31 | input: parser.Input{Authority: "www.example.com", URI: "/exac", Method: "GET"}, 32 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 33 | Uri: &networkingv1alpha3.StringMatch{ 34 | MatchType: &networkingv1alpha3.StringMatch_Exact{ 35 | Exact: "/exac", 36 | }, 37 | }, 38 | }, 39 | }, 40 | want: true, 41 | wantErr: false, 42 | }, { 43 | name: "single match exact (false)", 44 | args: args{ 45 | input: parser.Input{Authority: "www.example.com", URI: "/exac", Method: "GET"}, 46 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 47 | Uri: &networkingv1alpha3.StringMatch{ 48 | MatchType: &networkingv1alpha3.StringMatch_Exact{ 49 | Exact: "/exac/", 50 | }, 51 | }, 52 | }, 53 | }, 54 | want: false, 55 | wantErr: false, 56 | }, { 57 | name: "single match prefix (true)", 58 | args: args{ 59 | input: parser.Input{Authority: "www.example.com", URI: "/prefix/anotherpath", Method: "GET"}, 60 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 61 | Uri: &networkingv1alpha3.StringMatch{ 62 | MatchType: &networkingv1alpha3.StringMatch_Prefix{ 63 | Prefix: "/prefix", 64 | }, 65 | }, 66 | }, 67 | }, 68 | want: true, 69 | wantErr: false, 70 | }, { 71 | name: "single match prefix (false)", 72 | args: args{ 73 | input: parser.Input{Authority: "www.example.com", URI: "/not-prefix/anotherpath", Method: "GET"}, 74 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 75 | Uri: &networkingv1alpha3.StringMatch{ 76 | MatchType: &networkingv1alpha3.StringMatch_Prefix{ 77 | Prefix: "/prefix", 78 | }, 79 | }, 80 | }, 81 | }, 82 | want: false, 83 | wantErr: false, 84 | }, { 85 | name: "single match regex (true)", 86 | args: args{ 87 | input: parser.Input{Authority: "www.example.com", URI: "/regex/", Method: "POST"}, 88 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 89 | Uri: &networkingv1alpha3.StringMatch{ 90 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 91 | Regex: "^/reg.+?(/)$", 92 | }, 93 | }, 94 | }, 95 | }, 96 | want: true, 97 | wantErr: false, 98 | }, { 99 | name: "single match invalid Authority regex", 100 | args: args{ 101 | input: parser.Input{Authority: "www.example.com", URI: "/regex/test", Method: "POST"}, 102 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 103 | Authority: &networkingv1alpha3.StringMatch{ 104 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 105 | Regex: "example(", 106 | }, 107 | }, 108 | }, 109 | }, 110 | want: false, 111 | wantErr: true, 112 | }, { 113 | name: "single match invalid Method regex", 114 | args: args{ 115 | input: parser.Input{Authority: "www.example.com", URI: "/regex/test", Method: "POST"}, 116 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 117 | Method: &networkingv1alpha3.StringMatch{ 118 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 119 | Regex: "P(OS|U]T", 120 | }, 121 | }, 122 | }, 123 | }, 124 | want: false, 125 | wantErr: true, 126 | }, { 127 | name: "single match invalid URI regex", 128 | args: args{ 129 | input: parser.Input{Authority: "www.example.com", URI: "/regex/test", Method: "POST"}, 130 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 131 | Uri: &networkingv1alpha3.StringMatch{ 132 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 133 | Regex: "/reg.+?(/", 134 | }, 135 | }, 136 | }, 137 | }, 138 | want: false, 139 | wantErr: true, 140 | }, { 141 | name: "single match non-existing Header regex", 142 | args: args{ 143 | input: parser.Input{Authority: "www.example.com", URI: "/regex/test", Method: "POST", Headers: map[string]string{"x-header-test": "foo"}}, 144 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 145 | Headers: map[string]*networkingv1alpha3.StringMatch{ 146 | "x-header-not-found": { 147 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 148 | Regex: ".*", 149 | }, 150 | }, 151 | }, 152 | }, 153 | }, 154 | want: false, 155 | wantErr: false, 156 | }, { 157 | name: "single match invalid Header regex", 158 | args: args{ 159 | input: parser.Input{Authority: "www.example.com", URI: "/regex/test", Method: "POST", Headers: map[string]string{"x-header-test": "foo"}}, 160 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 161 | Headers: map[string]*networkingv1alpha3.StringMatch{ 162 | "x-header-test": { 163 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 164 | Regex: "(", 165 | }, 166 | }, 167 | }, 168 | }, 169 | }, 170 | want: false, 171 | wantErr: true, 172 | }, { 173 | name: "single match regex (false)", 174 | args: args{ 175 | input: parser.Input{Authority: "www.example.com", URI: "/not-regex/test", Method: "PATCH"}, 176 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 177 | Uri: &networkingv1alpha3.StringMatch{ 178 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 179 | Regex: "/reg(/)", 180 | }, 181 | }, 182 | }, 183 | }, 184 | want: false, 185 | wantErr: false, 186 | }, { 187 | name: "single match partial regex (false)", 188 | args: args{ 189 | input: parser.Input{Authority: "www.example.com", URI: "/regex", Method: "PATCH"}, 190 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 191 | Uri: &networkingv1alpha3.StringMatch{ 192 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 193 | Regex: "/reg", 194 | }, 195 | }, 196 | }, 197 | }, 198 | want: false, 199 | wantErr: false, 200 | }, { 201 | name: "multiple match exact, prefix and regex (true)", 202 | args: args{ 203 | input: parser.Input{Authority: "www.example.com", URI: "/prefix/anotherpath", Method: "GET"}, 204 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 205 | Authority: &networkingv1alpha3.StringMatch{ 206 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 207 | Regex: "(www.)example.com", 208 | }, 209 | }, 210 | Uri: &networkingv1alpha3.StringMatch{ 211 | MatchType: &networkingv1alpha3.StringMatch_Prefix{ 212 | Prefix: "/prefix", 213 | }, 214 | }, 215 | Method: &networkingv1alpha3.StringMatch{ 216 | MatchType: &networkingv1alpha3.StringMatch_Exact{ 217 | Exact: "GET", 218 | }, 219 | }, 220 | }, 221 | }, 222 | want: true, 223 | wantErr: false, 224 | }, { 225 | name: "multiple match exact, prefix and regex (false)", 226 | args: args{ 227 | input: parser.Input{Authority: "www.example.co", URI: "/prefix/anotherpath", Method: "GET"}, 228 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 229 | Authority: &networkingv1alpha3.StringMatch{ 230 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 231 | Regex: "(www.)example.com", 232 | }, 233 | }, 234 | Uri: &networkingv1alpha3.StringMatch{ 235 | MatchType: &networkingv1alpha3.StringMatch_Prefix{ 236 | Prefix: "/prefix", 237 | }, 238 | }, 239 | Method: &networkingv1alpha3.StringMatch{ 240 | MatchType: &networkingv1alpha3.StringMatch_Exact{ 241 | Exact: "GET", 242 | }, 243 | }, 244 | }, 245 | }, 246 | want: false, 247 | wantErr: false, 248 | }, { 249 | name: "multiple match in headers, regex, prefix, exact (true)", 250 | args: args{ 251 | input: parser.Input{Authority: "www.example.com", URI: "/", Method: "GET", Headers: map[string]string{ 252 | "x-header-exact": "exact", 253 | "x-header-prefix": "prefix-something", 254 | "x-header-regex": "capture-this-regex", 255 | }}, 256 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 257 | Authority: &networkingv1alpha3.StringMatch{ 258 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 259 | Regex: "(www.)example.com", 260 | }, 261 | }, 262 | Headers: map[string]*networkingv1alpha3.StringMatch{ 263 | "x-header-prefix": { 264 | MatchType: &networkingv1alpha3.StringMatch_Prefix{ 265 | Prefix: "prefix-", 266 | }, 267 | }, 268 | "x-header-exact": { 269 | MatchType: &networkingv1alpha3.StringMatch_Exact{ 270 | Exact: "exact", 271 | }, 272 | }, 273 | "x-header-regex": { 274 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 275 | Regex: ".+?-this-.+?", 276 | }, 277 | }, 278 | }, 279 | }, 280 | }, 281 | want: true, 282 | wantErr: false, 283 | }, { 284 | name: "multiple match in headers, regex, prefix, exact (false)", 285 | args: args{ 286 | input: parser.Input{Authority: "www.example.com", URI: "/", Method: "GET", Headers: map[string]string{ 287 | "x-header-exact": "exact", 288 | "x-header-prefix": "prefix-something", 289 | "x-header-regex": "capture-this-regex", 290 | }}, 291 | httpMatchRequest: &networkingv1alpha3.HTTPMatchRequest{ 292 | Authority: &networkingv1alpha3.StringMatch{ 293 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 294 | Regex: "(www.)example.com", 295 | }, 296 | }, 297 | Headers: map[string]*networkingv1alpha3.StringMatch{ 298 | "x-header-prefix": { 299 | MatchType: &networkingv1alpha3.StringMatch_Prefix{ 300 | Prefix: "not-prefix-", 301 | }, 302 | }, 303 | "x-header-exact": { 304 | MatchType: &networkingv1alpha3.StringMatch_Exact{ 305 | Exact: "exact", 306 | }, 307 | }, 308 | "x-header-regex": { 309 | MatchType: &networkingv1alpha3.StringMatch_Regex{ 310 | Regex: ".+?-this-.+?", 311 | }, 312 | }, 313 | }, 314 | }, 315 | }, 316 | want: false, 317 | wantErr: false, 318 | }} 319 | 320 | for _, tt := range tests { 321 | t.Run(tt.name, func(t *testing.T) { 322 | got, err := matchRequest(tt.args.input, tt.args.httpMatchRequest) 323 | if (err != nil) != tt.wantErr { 324 | t.Errorf("matchRequest() error = %v, wantErr %v", err, tt.wantErr) 325 | return 326 | } 327 | if got != tt.want { 328 | t.Errorf("matchRequest() = %v, want %v", got, tt.want) 329 | } 330 | }) 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /internal/pkg/unit/unit.go: -------------------------------------------------------------------------------- 1 | // Package unit contains logic to run the unit tests against istio configuration. 2 | // It intends to replicates istio logic when it comes to matching requests and defining its destinations. 3 | // Once the destinations are found for a given test case, it will try to assert with the expected results. 4 | package unit 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "slices" 10 | 11 | "github.com/getyourguide/istio-config-validator/internal/pkg/parser" 12 | networking "istio.io/api/networking/v1" 13 | v1 "istio.io/client-go/pkg/apis/networking/v1" 14 | ) 15 | 16 | // Run is the entrypoint to run all unit tests defined in test cases 17 | func Run(testfiles, configfiles []string, strict bool) ([]string, []string, error) { 18 | var summary, details []string 19 | 20 | testCases, err := parser.ParseTestCases(testfiles, strict) 21 | if err != nil { 22 | return nil, nil, fmt.Errorf("parsing testcases failed: %w", err) 23 | } 24 | 25 | virtualServices, err := parser.ParseVirtualServices(configfiles) 26 | if err != nil { 27 | return nil, nil, fmt.Errorf("parsing virtualservices failed: %w", err) 28 | } 29 | 30 | inputCount := 0 31 | for _, testCase := range testCases { 32 | details = append(details, "running test: "+testCase.Description) 33 | inputs, err := testCase.Request.Unfold() 34 | if err != nil { 35 | return summary, details, err 36 | } 37 | for _, input := range inputs { 38 | checkHosts := true 39 | route, err := GetRoute(input, virtualServices, checkHosts) 40 | if err != nil { 41 | details = append(details, fmt.Sprintf("FAIL input:[%v]", input)) 42 | return summary, details, fmt.Errorf("error getting destinations: %v", err) 43 | } 44 | if route.Delegate != nil { 45 | if testCase.Delegate != nil { 46 | if reflect.DeepEqual(route.Delegate, testCase.Delegate) != testCase.WantMatch { 47 | details = append(details, fmt.Sprintf("FAIL input:[%v]", input)) 48 | return summary, details, fmt.Errorf("delegate missmatch=%v, want %v, rule matched: %v", route.Delegate, testCase.Delegate, route.Match) 49 | } 50 | details = append(details, fmt.Sprintf("PASS input:[%v]", input)) 51 | } 52 | if testCase.Route != nil { 53 | vs, err := GetDelegatedVirtualService(route.Delegate, virtualServices) 54 | if err != nil { 55 | details = append(details, fmt.Sprintf("FAIL input:[%v]", input)) 56 | return summary, details, fmt.Errorf("error getting delegate virtual service: %v", err) 57 | } 58 | checkHosts = false 59 | route, err = GetRoute(input, []*v1.VirtualService{vs}, checkHosts) 60 | if err != nil { 61 | details = append(details, fmt.Sprintf("FAIL input:[%v]", input)) 62 | return summary, details, fmt.Errorf("error getting destinations for delegate %v: %v", route.Delegate, err) 63 | } 64 | } 65 | } 66 | if testCase.Route != nil { 67 | if reflect.DeepEqual(route.Route, testCase.Route) != testCase.WantMatch { 68 | details = append(details, fmt.Sprintf("FAIL input:[%v]", input)) 69 | return summary, details, fmt.Errorf("destination missmatch=%v, want %v, rule matched: %v", route.Route, testCase.Route, route.Match) 70 | } 71 | } 72 | if testCase.Rewrite != nil { 73 | if reflect.DeepEqual(route.Rewrite, testCase.Rewrite) != testCase.WantMatch { 74 | details = append(details, fmt.Sprintf("FAIL input:[%v]", input)) 75 | return summary, details, fmt.Errorf("rewrite missmatch=%v, want %v, rule matched: %v", route.Rewrite, testCase.Rewrite, route.Match) 76 | } 77 | } 78 | if testCase.Fault != nil { 79 | if reflect.DeepEqual(route.Fault, testCase.Fault) != testCase.WantMatch { 80 | details = append(details, fmt.Sprintf("FAIL input:[%v]", input)) 81 | return summary, details, fmt.Errorf("fault missmatch=%v, want %v, rule matched: %v", route.Fault, testCase.Fault, route.Match) 82 | } 83 | } 84 | if testCase.Headers != nil { 85 | if reflect.DeepEqual(route.Headers, testCase.Headers) != testCase.WantMatch { 86 | details = append(details, fmt.Sprintf("FAIL input:[%v]", input)) 87 | return summary, details, fmt.Errorf("headers missmatch=%v, want %v, rule matched: %v", route.Headers, testCase.Headers, route.Match) 88 | } 89 | } 90 | if testCase.Redirect != nil { 91 | if reflect.DeepEqual(route.Redirect, testCase.Redirect) != testCase.WantMatch { 92 | details = append(details, fmt.Sprintf("FAIL input:[%v]", input)) 93 | return summary, details, fmt.Errorf("redirect missmatch=%v, want %v, rule matched: %v", route.Redirect, testCase.Redirect, route.Match) 94 | } 95 | } 96 | details = append(details, fmt.Sprintf("PASS input:[%v]", input)) 97 | } 98 | inputCount += len(inputs) 99 | details = append(details, "===========================") 100 | } 101 | summary = append(summary, "Test summary:") 102 | summary = append(summary, fmt.Sprintf(" - %d testfiles, %d configfiles", len(testfiles), len(configfiles))) 103 | summary = append(summary, fmt.Sprintf(" - %d testcases with %d inputs passed", len(testCases), inputCount)) 104 | return summary, details, nil 105 | } 106 | 107 | // GetRoute returns the route that matched a given input. 108 | func GetRoute(input parser.Input, virtualServices []*v1.VirtualService, checkHosts bool) (*networking.HTTPRoute, error) { 109 | for _, vs := range virtualServices { 110 | spec := &vs.Spec 111 | if checkHosts && !slices.Contains(spec.Hosts, input.Authority) { 112 | continue 113 | } 114 | 115 | for _, httpRoute := range spec.Http { 116 | if len(httpRoute.Match) == 0 { 117 | return httpRoute, nil 118 | } 119 | for _, matchBlock := range httpRoute.Match { 120 | if match, err := matchRequest(input, matchBlock); err != nil { 121 | return &networking.HTTPRoute{}, err 122 | } else if match { 123 | return httpRoute, nil 124 | } 125 | } 126 | } 127 | } 128 | 129 | return &networking.HTTPRoute{}, nil 130 | } 131 | 132 | // GetDelegatedVirtualService returns the virtualservice matching namespace/name matching the delegate argument. 133 | func GetDelegatedVirtualService(delegate *networking.Delegate, virtualServices []*v1.VirtualService) (*v1.VirtualService, error) { 134 | for _, vs := range virtualServices { 135 | if vs.Name == delegate.Name { 136 | if delegate.Namespace != "" && vs.Namespace != delegate.Namespace { 137 | continue 138 | } 139 | return vs, nil 140 | } 141 | } 142 | return nil, fmt.Errorf("virtualservice %s not found", delegate.Name) 143 | } 144 | -------------------------------------------------------------------------------- /internal/pkg/unit/unit_test.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/getyourguide/istio-config-validator/internal/pkg/parser" 8 | "github.com/stretchr/testify/require" 9 | networking "istio.io/api/networking/v1" 10 | v1 "istio.io/client-go/pkg/apis/networking/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | func TestRun(t *testing.T) { 15 | testcasefiles := []string{"../../../examples/virtualservice_test.yml"} 16 | configfiles := []string{"../../../examples/virtualservice.yml"} 17 | var strict bool 18 | _, _, err := Run(testcasefiles, configfiles, strict) 19 | require.NoError(t, err) 20 | } 21 | 22 | func TestRunDelegate(t *testing.T) { 23 | testcasefiles := []string{"../../../examples/virtualservice_delegate_test.yml"} 24 | configfiles := []string{"../../../examples/delegate_virtualservice.yml"} 25 | var strict bool 26 | _, _, err := Run(testcasefiles, configfiles, strict) 27 | require.NoError(t, err) 28 | } 29 | 30 | func TestGetRoute(t *testing.T) { 31 | type args struct { 32 | input parser.Input 33 | virtualServices []*v1.VirtualService 34 | checkHosts bool 35 | } 36 | tests := []struct { 37 | name string 38 | args args 39 | want *networking.HTTPRoute 40 | wantErr bool 41 | }{ 42 | { 43 | name: "no host match, empty destination", 44 | args: args{ 45 | input: parser.Input{Authority: "www.exemple.com", URI: "/"}, 46 | virtualServices: []*v1.VirtualService{{ 47 | Spec: networking.VirtualService{ 48 | Hosts: []string{"www.another-example.com"}, 49 | Http: []*networking.HTTPRoute{{ 50 | Match: []*networking.HTTPMatchRequest{{ 51 | Uri: &networking.StringMatch{ 52 | MatchType: &networking.StringMatch_Exact{ 53 | Exact: "/", 54 | }, 55 | }, 56 | }}, 57 | }}, 58 | }, 59 | }}, 60 | checkHosts: true, 61 | }, 62 | want: &networking.HTTPRoute{}, 63 | wantErr: false, 64 | }, 65 | { 66 | name: "match a fallback destination", 67 | args: args{ 68 | input: parser.Input{Authority: "www.example.com", URI: "/path-to-fallback"}, 69 | virtualServices: []*v1.VirtualService{{ 70 | Spec: networking.VirtualService{ 71 | Hosts: []string{"www.example.com"}, 72 | Http: []*networking.HTTPRoute{{ 73 | Match: []*networking.HTTPMatchRequest{{ 74 | Uri: &networking.StringMatch{ 75 | MatchType: &networking.StringMatch_Exact{ 76 | Exact: "/", 77 | }, 78 | }, 79 | }}, 80 | }, { 81 | Route: []*networking.HTTPRouteDestination{{ 82 | Destination: &networking.Destination{ 83 | Host: "fallback.fallback.svc.cluster.local", 84 | }, 85 | }}, 86 | }}, 87 | }, 88 | }}, 89 | checkHosts: true, 90 | }, 91 | want: &networking.HTTPRoute{ 92 | Route: []*networking.HTTPRouteDestination{{ 93 | Destination: &networking.Destination{ 94 | Host: "fallback.fallback.svc.cluster.local", 95 | }, 96 | }}, 97 | }, 98 | wantErr: false, 99 | }, 100 | { 101 | name: "match single destination, multiple virtualservices", 102 | args: args{ 103 | input: parser.Input{Authority: "www.match.com", URI: "/"}, 104 | virtualServices: []*v1.VirtualService{{ 105 | Spec: networking.VirtualService{ 106 | Hosts: []string{"www.notmatch.com"}, 107 | Http: []*networking.HTTPRoute{{ 108 | Route: []*networking.HTTPRouteDestination{{ 109 | Destination: &networking.Destination{ 110 | Host: "notmatch.notmatch.svc.cluster.local", 111 | }, 112 | }}, 113 | Match: []*networking.HTTPMatchRequest{{ 114 | Uri: &networking.StringMatch{ 115 | MatchType: &networking.StringMatch_Exact{ 116 | Exact: "/", 117 | }, 118 | }, 119 | }}, 120 | }}, 121 | }, 122 | }, { 123 | Spec: networking.VirtualService{ 124 | Hosts: []string{"www.match.com"}, 125 | Http: []*networking.HTTPRoute{{ 126 | Route: []*networking.HTTPRouteDestination{{ 127 | Destination: &networking.Destination{ 128 | Host: "match.match.svc.cluster.local", 129 | }, 130 | }}, 131 | Match: []*networking.HTTPMatchRequest{{ 132 | Uri: &networking.StringMatch{ 133 | MatchType: &networking.StringMatch_Exact{ 134 | Exact: "/", 135 | }, 136 | }, 137 | }}, 138 | }}, 139 | }, 140 | }}, 141 | checkHosts: true, 142 | }, 143 | want: &networking.HTTPRoute{ 144 | Route: []*networking.HTTPRouteDestination{{ 145 | Destination: &networking.Destination{ 146 | Host: "match.match.svc.cluster.local", 147 | }, 148 | }}, Match: []*networking.HTTPMatchRequest{{ 149 | Uri: &networking.StringMatch{ 150 | MatchType: &networking.StringMatch_Exact{ 151 | Exact: "/", 152 | }, 153 | }, 154 | }}, 155 | }, 156 | wantErr: false, 157 | }, 158 | { 159 | name: "match and assert rewrite and destination", 160 | args: args{ 161 | input: parser.Input{Authority: "www.match.com", URI: "/"}, 162 | virtualServices: []*v1.VirtualService{{ 163 | Spec: networking.VirtualService{ 164 | Hosts: []string{"www.match.com"}, 165 | Http: []*networking.HTTPRoute{{ 166 | Route: []*networking.HTTPRouteDestination{{ 167 | Destination: &networking.Destination{ 168 | Host: "match.match.svc.cluster.local", 169 | }, 170 | }}, 171 | Match: []*networking.HTTPMatchRequest{{ 172 | Uri: &networking.StringMatch{ 173 | MatchType: &networking.StringMatch_Exact{ 174 | Exact: "/", 175 | }, 176 | }, 177 | }}, 178 | }}, 179 | }, 180 | }}, 181 | checkHosts: true, 182 | }, 183 | want: &networking.HTTPRoute{ 184 | Route: []*networking.HTTPRouteDestination{{ 185 | Destination: &networking.Destination{ 186 | Host: "match.match.svc.cluster.local", 187 | }, 188 | }}, 189 | Match: []*networking.HTTPMatchRequest{{ 190 | Uri: &networking.StringMatch{ 191 | MatchType: &networking.StringMatch_Exact{ 192 | Exact: "/", 193 | }, 194 | }, 195 | }}, 196 | }, 197 | wantErr: false, 198 | }, 199 | { 200 | name: "match virtualservice with no hosts", 201 | args: args{ 202 | input: parser.Input{Authority: "www.match.com", URI: "/"}, 203 | virtualServices: []*v1.VirtualService{{ 204 | Spec: networking.VirtualService{ 205 | Http: []*networking.HTTPRoute{{ 206 | Route: []*networking.HTTPRouteDestination{{ 207 | Destination: &networking.Destination{ 208 | Host: "match.match.svc.cluster.local", 209 | }, 210 | }}, 211 | Match: []*networking.HTTPMatchRequest{{ 212 | Uri: &networking.StringMatch{ 213 | MatchType: &networking.StringMatch_Exact{ 214 | Exact: "/", 215 | }, 216 | }, 217 | }}, 218 | }}, 219 | }, 220 | }}, 221 | checkHosts: false, 222 | }, 223 | want: &networking.HTTPRoute{ 224 | Route: []*networking.HTTPRouteDestination{{ 225 | Destination: &networking.Destination{ 226 | Host: "match.match.svc.cluster.local", 227 | }, 228 | }}, Match: []*networking.HTTPMatchRequest{{ 229 | Uri: &networking.StringMatch{ 230 | MatchType: &networking.StringMatch_Exact{ 231 | Exact: "/", 232 | }, 233 | }, 234 | }}, 235 | }, 236 | wantErr: false, 237 | }, 238 | } 239 | for _, tt := range tests { 240 | t.Run(tt.name, func(t *testing.T) { 241 | got, err := GetRoute(tt.args.input, tt.args.virtualServices, tt.args.checkHosts) 242 | if (err != nil) != tt.wantErr { 243 | t.Errorf("GetRoute() error = %v, wantErr %v", err, tt.wantErr) 244 | return 245 | } 246 | if !reflect.DeepEqual(got, tt.want) { 247 | t.Errorf("GetRoute() = %v, want %v", got, tt.want) 248 | } 249 | }) 250 | } 251 | } 252 | 253 | func TestGetDelegatedVirtualService(t *testing.T) { 254 | type args struct { 255 | delegate *networking.Delegate 256 | virtualServices []*v1.VirtualService 257 | } 258 | tests := []struct { 259 | name string 260 | args args 261 | want *v1.VirtualService 262 | wantErr bool 263 | }{ 264 | { 265 | name: "match", 266 | args: args{ 267 | delegate: &networking.Delegate{ 268 | Name: "delegate", 269 | }, 270 | virtualServices: []*v1.VirtualService{{ 271 | ObjectMeta: metav1.ObjectMeta{ 272 | Name: "delegate", 273 | Namespace: "default", 274 | }, 275 | }}, 276 | }, 277 | want: &v1.VirtualService{ 278 | ObjectMeta: metav1.ObjectMeta{ 279 | Name: "delegate", 280 | Namespace: "default", 281 | }, 282 | }, 283 | wantErr: false, 284 | }, { 285 | name: "match with namespace", 286 | args: args{ 287 | delegate: &networking.Delegate{ 288 | Name: "delegate", 289 | Namespace: "test", 290 | }, 291 | virtualServices: []*v1.VirtualService{{ 292 | ObjectMeta: metav1.ObjectMeta{ 293 | Name: "delegate", 294 | Namespace: "default", 295 | }, 296 | }, { 297 | ObjectMeta: metav1.ObjectMeta{ 298 | Name: "delegate", 299 | Namespace: "test", 300 | }, 301 | }}, 302 | }, 303 | want: &v1.VirtualService{ 304 | ObjectMeta: metav1.ObjectMeta{ 305 | Name: "delegate", 306 | Namespace: "test", 307 | }, 308 | }, 309 | wantErr: false, 310 | }, { 311 | name: "no match", 312 | args: args{ 313 | delegate: &networking.Delegate{ 314 | Name: "delegate", 315 | }, 316 | virtualServices: []*v1.VirtualService{{ 317 | ObjectMeta: metav1.ObjectMeta{ 318 | Name: "delegate-abc", 319 | Namespace: "default", 320 | }, 321 | }}, 322 | }, 323 | want: nil, 324 | wantErr: true, 325 | }, { 326 | name: "no match with namespace", 327 | args: args{ 328 | delegate: &networking.Delegate{ 329 | Name: "delegate", 330 | Namespace: "production", 331 | }, 332 | virtualServices: []*v1.VirtualService{{ 333 | ObjectMeta: metav1.ObjectMeta{ 334 | Name: "delegate", 335 | Namespace: "default", 336 | }, 337 | }, { 338 | ObjectMeta: metav1.ObjectMeta{ 339 | Name: "delegate", 340 | Namespace: "test", 341 | }, 342 | }}, 343 | }, 344 | want: nil, 345 | wantErr: true, 346 | }, 347 | } 348 | for _, tt := range tests { 349 | t.Run(tt.name, func(t *testing.T) { 350 | got, err := GetDelegatedVirtualService(tt.args.delegate, tt.args.virtualServices) 351 | if (err != nil) != tt.wantErr { 352 | t.Errorf("GetDelegatedVirtualService() error = %v, wantErr %v", err, tt.wantErr) 353 | return 354 | } 355 | if !reflect.DeepEqual(got, tt.want) { 356 | t.Errorf("GetDelegatedVirtualService() = %v, want %v", got, tt.want) 357 | } 358 | }) 359 | } 360 | } 361 | --------------------------------------------------------------------------------