├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ └── ci.yaml ├── .gitignore ├── .golangci.yml ├── CODE_OF_CONDUCT.md ├── EXPECTED_GO_VERSION ├── LICENSE ├── Makefile ├── README.md ├── cmd └── helmtest │ └── main.go ├── docs ├── functions.md └── world-model.md ├── go.mod ├── go.sum ├── integration_test ├── integration_test.go └── testdata │ ├── helmtest │ └── suite.yaml │ └── nginx-example │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ ├── tests │ │ └── test-connection.yaml │ └── upgrade.test.yaml │ └── values.yaml ├── internal ├── compiler │ ├── builtins.go │ └── compile.go ├── logic │ ├── errors.go │ └── truthiness.go ├── parser │ ├── parser.go │ ├── post_process.go │ ├── query.go │ └── source_context.go ├── rox-imported │ ├── README.md │ ├── gziputil │ │ └── compress.go │ ├── helmutil │ │ └── values.go │ ├── pointers │ │ └── pointer.go │ ├── set │ │ └── gen-string-generic.go │ ├── sliceutils │ │ └── unique.go │ └── stringutils │ │ └── consume.go └── schemas │ ├── builtin.go │ ├── fs_registry.go │ ├── openapi-schemas │ ├── README.md │ ├── com.coreos.json.gz │ ├── kubernetes-1.20.2.json.gz │ ├── openshift-3.11.0.json.gz │ ├── openshift-4.1.0.json.gz │ └── openshift-4.18.json.gz │ ├── registry.go │ ├── schema.go │ ├── schemas.go │ └── schemas_test.go ├── pkg └── framework │ ├── capabilities_spec.go │ ├── loader.go │ ├── loader_test.go │ ├── normalize_string.go │ ├── release_spec.go │ ├── runner.go │ ├── spec.go │ ├── spec_unmarshal.go │ ├── target.go │ ├── test.go │ ├── test_test.go │ ├── testdata │ ├── additional_dir │ │ └── additional.test.yaml │ └── suite │ │ ├── helm.test.yaml │ │ └── suite.yaml │ ├── util.go │ └── util_test.go └── tool-imports ├── empty.go ├── go.mod ├── go.sum └── tools.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # For further documentation on CODEOWNERS, visit 2 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#about-code-owners 3 | # This will automatically assign a team / people as reviewers for PRs based on the files changed within the PR. 4 | * @stackrox/install 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'gomod' 9 | directory: '/' 10 | schedule: 11 | interval: 'daily' 12 | open-pull-requests-limit: 1 13 | reviewers: 14 | - "stackrox/backend-dep-updaters" 15 | - package-ecosystem: "github-actions" 16 | directory: "/" 17 | schedule: 18 | interval: 'weekly' 19 | day: 'wednesday' 20 | open-pull-requests-limit: 1 21 | reviewers: 22 | - "stackrox/backend-dep-updaters" 23 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request 2 | name: Dependabot auto-merge 3 | on: pull_request 4 | 5 | permissions: 6 | contents: write 7 | pull-requests: write 8 | 9 | jobs: 10 | dependabot: 11 | runs-on: ubuntu-latest 12 | if: github.actor == 'dependabot[bot]' 13 | steps: 14 | - name: Dependabot metadata 15 | id: metadata 16 | uses: dependabot/fetch-metadata@v2 17 | with: 18 | github-token: "${{ secrets.GITHUB_TOKEN }}" 19 | - name: Enable auto-merge for Dependabot PRs 20 | if: steps.metadata.outputs.update-type == 'version-update:semver-minor' 21 | run: gh pr merge --auto --squash "$PR_URL" 22 | env: 23 | PR_URL: ${{github.event.pull_request.html_url}} 24 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: helmtest CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout VCS 14 | uses: actions/checkout@v4 15 | 16 | - name: Prepare environment 17 | run: | 18 | go_version=$(cat EXPECTED_GO_VERSION) 19 | echo "GO_VERSION=$go_version" >> $GITHUB_ENV 20 | echo "CI=true" >> $GITHUB_ENV 21 | 22 | - name: Setup GO 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: ${{ env.GO_VERSION }} 26 | 27 | - name: Lint 28 | run: make golangci-lint 29 | 30 | - name: Test 31 | run: make test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /.gobin/ 3 | /deps 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | go: "1.23" 4 | 5 | issues: 6 | exclude-use-default: false 7 | 8 | exclude-rules: 9 | - linters: 10 | - revive 11 | text: "should have a package comment" 12 | 13 | linters-settings: 14 | govet: 15 | enable-all: true 16 | disable: 17 | - shadow 18 | - fieldalignment 19 | settings: 20 | printf: # analyzer name, run `go tool vet help` to see all analyzers 21 | funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer 22 | - Print 23 | - Printf 24 | - Println 25 | - Debug 26 | - Debugf 27 | - Info 28 | - Infof 29 | - Warn 30 | - Warnf 31 | - Error 32 | - Errorf 33 | gocritic: 34 | enabled-tags: 35 | - diagnostic 36 | - experimental 37 | - opinionated 38 | - performance 39 | - style 40 | disabled-checks: 41 | - dupImport # https://github.com/go-critic/go-critic/issues/845 42 | - commentFormatting 43 | - octalLiteral 44 | - unnamedResult 45 | - unnecessaryDefer 46 | - importShadow 47 | - emptyStringTest 48 | - hugeParam 49 | - rangeValCopy 50 | gosimple: 51 | checks: ["all"] 52 | nolintlint: 53 | allow-leading-space: false # require machine-readable nolint directives (i.e. with no leading space) 54 | allow-unused: false # report any unused nolint directives 55 | require-explanation: false # don't require an explanation for nolint directives 56 | require-specific: true # require nolint directives to be specific about which linter is being skipped 57 | revive: 58 | min-confidence: 0 59 | staticcheck: 60 | checks: ["all"] 61 | stylecheck: 62 | checks: ["all", "-ST1000"] 63 | 64 | linters: 65 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 66 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 67 | disable-all: true 68 | enable: 69 | - asciicheck 70 | # - bodyclose 71 | - copyloopvar 72 | # - deadcode 73 | # - depguard 74 | # - dogsled 75 | # - dupl 76 | # - errcheck 77 | # - funlen 78 | # - gochecknoglobals 79 | # - gochecknoinits 80 | # - gocognit 81 | # - goconst 82 | - gocritic 83 | # - gocyclo 84 | # - godot 85 | # - godox 86 | # - goerr113 87 | - gofmt 88 | - goimports 89 | # - gomnd 90 | # - goprintffuncname 91 | # - gosec 92 | - gosimple 93 | - govet 94 | - ineffassign 95 | # - interfacer 96 | # - lll 97 | # - maligned 98 | # - misspell 99 | - nakedret 100 | # - nestif 101 | - nolintlint 102 | # - prealloc 103 | - revive 104 | - rowserrcheck 105 | # - scopelint 106 | - staticcheck 107 | # - structcheck 108 | - stylecheck 109 | # - testpackage 110 | # - typecheck 111 | - unconvert 112 | - unparam 113 | - unused 114 | # - varcheck 115 | # - whitespace 116 | # - wsl 117 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The Stackrox code of conduct can be found [here](https://stackrox.io/code-conduct). 4 | -------------------------------------------------------------------------------- /EXPECTED_GO_VERSION: -------------------------------------------------------------------------------- 1 | 1.23.5 2 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | .PHONY: none 2 | none: 3 | 4 | 5 | deps: go.mod 6 | @echo "+ $@" 7 | @go mod tidy 8 | ifdef CI 9 | @git diff --exit-code -- go.mod go.sum || { echo "go.mod/go.sum files were updated after running 'go mod tidy', run this command on your local machine and commit the results." ; exit 1 ; } 10 | endif 11 | go mod verify 12 | @touch deps 13 | 14 | GOBIN := $(CURDIR)/.gobin 15 | PATH := $(GOBIN):$(PATH) 16 | 17 | # Makefile on Mac doesn't pass the updated PATH and GOBIN to the shell 18 | # and so, without the following line, the shell does not end up 19 | # trying commands in $(GOBIN) first. 20 | # See https://stackoverflow.com/a/36226784/3690207 21 | SHELL := env GOBIN=$(GOBIN) PATH=$(PATH) /bin/bash 22 | 23 | ######################################## 24 | ###### Binaries we depend on ########### 25 | ######################################## 26 | 27 | GOLANGCILINT_BIN := $(GOBIN)/golangci-lint 28 | $(GOLANGCILINT_BIN): deps 29 | @echo "+ $@" 30 | cd tool-imports; \ 31 | GOBIN=$(GOBIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint 32 | 33 | ########### 34 | ## Lint ## 35 | ########### 36 | 37 | .PHONY: golangci-lint 38 | golangci-lint: $(GOLANGCILINT_BIN) 39 | ifdef CI 40 | @echo '+ $@' 41 | @echo 'The environment indicates we are in CI; running linters in check mode.' 42 | @echo 'If this fails, run `make lint`.' 43 | golangci-lint run 44 | else 45 | golangci-lint run --fix 46 | endif 47 | 48 | .PHONY: lint 49 | lint: golangci-lint 50 | 51 | #################### 52 | ## Code generation # 53 | #################### 54 | 55 | .PHONY: go-generated-srcs 56 | go-generated-srcs: deps 57 | go generate ./... 58 | 59 | .PHONY: generated-srcs 60 | generated-srcs: go-generated-srcs 61 | 62 | ############# 63 | ## Compile ## 64 | ############# 65 | 66 | 67 | .PHONY: build 68 | build: 69 | 70 | ########## 71 | ## Test ## 72 | ########## 73 | 74 | .PHONY: test 75 | test: 76 | go test ./... 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Package `helmtest` 2 | ====== 3 | 4 | The `helmtest` package allows you to declaratively specify test suites for Helm charts. It specifically 5 | seeks to address two inconveniences of the "normal" Go unit test-based approach: 6 | - it allows testing a multitude of different configurations via a hierarchical, YAML-based specification 7 | of test cases. 8 | - it makes writing assertions about the generated Kubernets objects easy, by using `jq` filters as the 9 | assertion language. 10 | 11 | Format of a test 12 | ========= 13 | A `helmtest` test is generally defined in a YAML file according to the format specified in `spec.go`. 14 | Tests are organized in a hierarchical fashion, in the sense that a test may contain one or more 15 | sub-tests. Tests with no sub-tests are called "leaf tests", and other tests are called "non-leaf tests". 16 | A Helm chart is only rendered and checked against expectations in leaf tests; in such a setting, 17 | the leaf test inherits certain properties from its non-leaf ancestors. 18 | 19 | The general schema of a test is as follows: 20 | ```yaml 21 | name: "string" # the name of the test (optional but strongly recommended). Auto-generated if left empty. 22 | release: # Overrides for the Helm release properties. These are applied in root-to-leaf order. 23 | name: "string" # override for the Helm release name 24 | namespace: "string" # override for the Helm release namespace 25 | revision: int # override for the Helm release revision 26 | isInstall: bool # override for the "IsInstall" property of the release options 27 | isUpgrade: bool # override for the "IsUpgrade" property of the release options 28 | server: 29 | visibleSchemas: # openAPI schema which is visible to helm, i.e. to check API resource availability 30 | # all valid schemas are: 31 | - kubernetes-1.20.2 32 | - openshift-3.11.0 33 | - openshift-4.1.0 34 | - openshift-4.18 35 | - com.coreos 36 | availableSchemas: [] # openAPI schema to validate against, i.e. to validate if rendered objects could be applied 37 | objects: # objects visible to Helm's k8s client, for example via the `lookup` function 38 | # example object specification: 39 | - apiVersion: string 40 | kind: string 41 | metadata: 42 | name: string 43 | namespace: string # optional for cluster-scoped objects 44 | noInherit: bool # indicates that server-side settings should *not* be inherited from the enclosing scope 45 | capabilities: # represents the .Capabilities in Helm 46 | kubeVersion: string # the kubernetes version which is discoverable via `.Capabilities.KubeVersion` 47 | values: # values as consumed by Helm via the `-f` CLI flag. 48 | key: value 49 | set: # alternative format for Helm values, as consumed via the `--set` CLI flag. 50 | nes.ted.key: value 51 | defs: | 52 | # Sequence of jq "def" statements. Each def statement must be terminated with a semicolon (;). Defined functions 53 | # are only visible in this and descendant scopes, but not in ancestor scopes. 54 | def name: .metadata.name; 55 | 56 | expectError: bool # indicates whether we can tolerate an error. If unset, inherit from the parent test, or 57 | # default to `false` at the root level. 58 | expect: | 59 | # Sequence of jq filters, one per line (or spanning multiple lines, where each continuation line must begin with a 60 | # space). 61 | # See the below section on the world model and special functions. 62 | .objects[] | select(.metadata?.name? // "" == "") 63 | | assertNotExist # continuation line 64 | tests: [] # a list of sub-tests. Determines whether the test is a leaf test or non-leaf test. 65 | ``` 66 | 67 | A comprehensive set of hierarchically organized tests to be run against a Helm chart is called a "suite". Each suite 68 | is defined in a set of YAML files located in a single directory on the filesystem (a directory may hold at most one 69 | suite). The properties of the top-level test in the suite (such as a common set of expectations or Helm values to be 70 | inherited by all tests) can be specified in a `suite.yaml` file within this directory. The `suite.yaml` file may be 71 | absent, in which case there are no values, definitions, expectations etc. shared among all the tests in the suite. In 72 | addition to the tests specified in the `tests:` stanza of the `suite.yaml` file (if any), the tests of the suite are 73 | additionally read from files with the extension `.test.yaml` in the suite directory. Note that any combination of 74 | defining tests in the `suite.yaml` and in individual files may be used, these tests will then be combined. In 75 | particular, it is possible to define arbitrary test suites either with only `.test.yaml` files, with only a `suite.yaml` 76 | file, or with combinations thereof. 77 | 78 | Inheritance 79 | ---------------- 80 | For most fields in the test specification, a test will inherit the value from its parent test (which might use an 81 | inherited value as well, etc.). If an explicit value is given, this value 82 | - overrides the values from the parent for the following fields: `expectError` and the individual sub-fields of 83 | `release`. 84 | - is merged with the values from the parent for the following fields: `values`, `set` (in such a way that the values 85 | from the child take precedence). 86 | - is added to the values from the parents for the following fields: `expect`, `defs`. 87 | 88 | World model 89 | ============ 90 | 91 | As stated above, expectations are written as `jq` filters (using `gojq` as an evaluation engine). Generally, a filter 92 | that evaluates to a "falsy" value is treated as a violation. In contrast to normal JS/`jq` semantics, an empty list, 93 | object, or string will also be treated as "falsy". The input to those filters (i.e., the `.` value at the start of 94 | each `jq` pipeline) is a JSON object containing anything that is relevant to the test execution, referred to as the 95 | "world". See [the world model documentation](./docs/world-model.md) for an explanation of what it contains. 96 | 97 | Special functions 98 | =============== 99 | 100 | See the [documentation on functions](./docs/functions.md) for an overview of what functions are available in filters, 101 | beyond the ones known from `jq`. 102 | 103 | Debugging 104 | =============== 105 | 106 | **Run a single test:** 107 | 108 | To get the name of a single test: 109 | 1. Run the whole test suite 110 | 2. Look up the test to execute 111 | 3. Copy the complete name from the CLI (or logs) 112 | 113 | ``` 114 | $ go test -run "TestWithHelmtest/testdata/helmtest/some_values.test.yaml/some_example_test" 115 | ``` 116 | 117 | **Print rendered manifests and values:** 118 | 119 | ``` 120 | - name: "some example test" 121 | expect: | 122 | .helm[]| toyaml | print ## Print all Helm values 123 | .secrets["secret-name"] | toyaml | print ## Print a specific object 124 | .objects[]| toyaml | print ## Print the complete rendered Helm chart 125 | ``` 126 | -------------------------------------------------------------------------------- /cmd/helmtest/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stackrox/helmtest/pkg/framework" 9 | 10 | "github.com/pkg/errors" 11 | "helm.sh/helm/v3/pkg/chart" 12 | "helm.sh/helm/v3/pkg/chart/loader" 13 | "helm.sh/helm/v3/pkg/chartutil" 14 | ) 15 | 16 | func mainCmd() error { 17 | var chartPath string 18 | var help bool 19 | 20 | var releaseName string 21 | var namespace string 22 | var upgrade bool 23 | 24 | flag.BoolVar(&help, "help", false, "print help information and exit") 25 | 26 | flag.StringVar(&chartPath, "chart", "", "archive file or directory containing the chart to test") 27 | 28 | flag.BoolVar(&upgrade, "upgrade", false, "if set, render chart as if performing an upgrade instead of an installation") 29 | flag.StringVar(&releaseName, "release-name", "helmtest-release", "the name of the Helm release") 30 | flag.StringVar(&namespace, "namespace", "default", "the namespace into which to simulate installing") 31 | 32 | testing.Init() 33 | 34 | flag.Parse() 35 | 36 | if help { 37 | flag.Usage() 38 | return nil 39 | } 40 | 41 | if chartPath == "" { 42 | return errors.New("no chart specified") 43 | } 44 | 45 | suiteDirs := flag.Args() 46 | if len(suiteDirs) == 0 { 47 | return errors.New("no test suites specified") 48 | } 49 | 50 | st, err := os.Stat(chartPath) 51 | if err != nil { 52 | return errors.Wrap(err, "loading chart") 53 | } 54 | var chartToTest *chart.Chart 55 | switch { 56 | case st.IsDir(): 57 | chartToTest, err = loader.LoadDir(chartPath) 58 | case st.Mode().IsRegular(): 59 | chartToTest, err = loader.LoadFile(chartPath) 60 | default: 61 | return errors.Errorf("invalid chart %q: neither a directory nor a regular file", chartPath) 62 | } 63 | 64 | if err != nil { 65 | return errors.Wrapf(err, "loading chart %q", chartPath) 66 | } 67 | 68 | target := &framework.Target{ 69 | ReleaseOptions: chartutil.ReleaseOptions{ 70 | Name: releaseName, 71 | Namespace: namespace, 72 | Revision: 1, 73 | IsUpgrade: upgrade, 74 | IsInstall: !upgrade, 75 | }, 76 | Chart: chartToTest, 77 | } 78 | 79 | var suites []*framework.Test 80 | for _, suiteDir := range suiteDirs { 81 | suite, err := framework.NewLoader(suiteDir).LoadSuite() 82 | if err != nil { 83 | return errors.Wrapf(err, "loading suite %q", suiteDir) 84 | } 85 | suites = append(suites, suite) 86 | } 87 | 88 | tests := make([]testing.InternalTest, 0, len(suites)) 89 | for _, suite := range suites { 90 | s := suite 91 | tests = append(tests, testing.InternalTest{ 92 | Name: suite.Name, 93 | F: func(t *testing.T) { 94 | t.Parallel() 95 | s.DoRun(t, target) 96 | }, 97 | }) 98 | } 99 | 100 | testing.Main(func(string, string) (bool, error) { return true, nil }, tests, nil, nil) 101 | return nil 102 | } 103 | 104 | func main() { 105 | if err := mainCmd(); err != nil { 106 | panic(err) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /docs/functions.md: -------------------------------------------------------------------------------- 1 | Functions 2 | =============== 3 | 4 | As `helmtest` predicates are `jq` filters, 5 | [all the functions available in `jq`](https://stedolan.github.io/jq/manual/#Builtinoperatorsandfunctions) are available 6 | in `helmtest` as well. 7 | 8 | Additionally, `helmtest` adds a number of additional functions to facilitate writing test cases for Kubernetes 9 | manifests: 10 | 11 | | Function name(s) | Description | 12 | | -------------------- | ----------- | 13 | | `fromyaml`, `toyaml` | The equivalent of `fromjson` and `tojson` for YAML. | 14 | | `assertNotExist` | This function will fail if ever executed. Semantically equivalent to just writing `false`, but prints the offending object. | 15 | | `assertThat(f)` | Asserts that a filter `f` holds for the input object. If `. \| f` evaluates to `false`, this will print the value of `.` as well as the original string representation of `f`. Hence, while `... \| .name == "foo"` and `\| assertThat(.name == "foo")` are semantically equivalent, the latter is preferable as it is much easier to debug. | 16 | | `assumeThat(f)` | Assumes that a filter `f` holds for the input object. If it doesn't, the evaluation is aborted for the given input object, and no failure is triggered. | 17 | | `print` | Prints input directly with `fmt.Println` and returns it. To print all objects in a test as `yaml`, write `.objects[] \| toyaml \| print`. | 18 | -------------------------------------------------------------------------------- /docs/world-model.md: -------------------------------------------------------------------------------- 1 | # World Model 2 | 3 | The world model describes everything that is available for you to reason about in your test expectations. It contains, 4 | among other things, the parsed rendering output, the message generated from `NOTES.txt`, error messages (if any) 5 | as well as the input values. The "world" is given as a JSON object, and is used as the input (`.` at the beginning of 6 | the pipeline) for every `jq` filter that is evaluated as an expectation. To allow referencing properties of the world 7 | model in custom functions, where `.` might not have its original value, the world object can also be referenced via 8 | the `$_` variable. 9 | 10 | This JSON object contains the following properties: 11 | - `helm`: a JSON object representing the values passed to the Helm rendering engine, such as `Values`, `Release`, 12 | `Chart` etc. 13 | - `notesRaw`: a string containing the message that would be displayed after running `helm install` or `helm upgrade`, 14 | rendered from the `NOTES.txt` template. 15 | - `notes`: same as `notesRaw`, but in normalized form - all sequences of one or more whitespaces (spaces, tabs, line 16 | breaks) having been replaced by a single space (`' '`) character. This allows checking for the occurrence of phrases 17 | without paying attention to line wrapping, and is almost always to preferable over `notesRaw`. 18 | - `errorRaw`: a string containing the Helm-generated error message occurred from rendering templates (if any). Note that 19 | schema validation errors do not end up here. 20 | - `error`: same as `errorRaw`, but in normalized form (see `notes` above). 21 | - `objects`: an array of all the objects in the rendering output, parsed directly from YAML. 22 | 23 | Additionally, for every object kind that occurred in the rendering output, the world object contains a field named after 24 | the plural form in lowercase. This field references a name-indexed object of all Kubernetes object of the given 25 | resource, provided the name is unique across all namespaces. That is, to locate the deployment "server", you can thus 26 | either write 27 | ``` 28 | .objects[] | select(.metadata.kind == "Deployment" and .metadata.name == "server") 29 | ``` 30 | or simply `.deployments.server`. Note that since Helm releases should be confined to a single namespace, two or more 31 | resources of the same kind and with the same name will be treated as an error. 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stackrox/helmtest 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/google/gnostic-models v0.6.9 9 | github.com/itchyny/gojq v0.12.15 10 | github.com/pkg/errors v0.9.1 11 | github.com/stretchr/testify v1.10.0 12 | gopkg.in/yaml.v3 v3.0.1 13 | helm.sh/helm/v3 v3.17.3 14 | k8s.io/apimachinery v0.32.3 15 | k8s.io/client-go v0.32.3 16 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f 17 | k8s.io/kubectl v0.32.3 18 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 19 | sigs.k8s.io/yaml v1.4.0 20 | ) 21 | 22 | require ( 23 | dario.cat/mergo v1.0.1 // indirect 24 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 25 | github.com/Masterminds/goutils v1.1.1 // indirect 26 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 27 | github.com/Masterminds/sprig/v3 v3.3.0 // indirect 28 | github.com/blang/semver/v4 v4.0.0 // indirect 29 | github.com/cyphar/filepath-securejoin v0.3.6 // indirect 30 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 31 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 32 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect 33 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 34 | github.com/go-errors/errors v1.4.2 // indirect 35 | github.com/go-logr/logr v1.4.2 // indirect 36 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 37 | github.com/go-openapi/jsonreference v0.20.2 // indirect 38 | github.com/go-openapi/swag v0.23.0 // indirect 39 | github.com/gobwas/glob v0.2.3 // indirect 40 | github.com/gogo/protobuf v1.3.2 // indirect 41 | github.com/golang/protobuf v1.5.4 // indirect 42 | github.com/google/go-cmp v0.6.0 // indirect 43 | github.com/google/gofuzz v1.2.0 // indirect 44 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 45 | github.com/google/uuid v1.6.0 // indirect 46 | github.com/huandu/xstrings v1.5.0 // indirect 47 | github.com/itchyny/timefmt-go v0.1.6 // indirect 48 | github.com/josharian/intern v1.0.0 // indirect 49 | github.com/json-iterator/go v1.1.12 // indirect 50 | github.com/mailru/easyjson v0.7.7 // indirect 51 | github.com/mitchellh/copystructure v1.2.0 // indirect 52 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 54 | github.com/modern-go/reflect2 v1.0.2 // indirect 55 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 56 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 57 | github.com/onsi/ginkgo/v2 v2.22.2 // indirect 58 | github.com/onsi/gomega v1.36.2 // indirect 59 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 60 | github.com/rogpeppe/go-internal v1.13.1 // indirect 61 | github.com/shopspring/decimal v1.4.0 // indirect 62 | github.com/spf13/cast v1.7.0 // indirect 63 | github.com/spf13/pflag v1.0.6 // indirect 64 | github.com/x448/float16 v0.8.4 // indirect 65 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 66 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 67 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 68 | github.com/xlab/treeprint v1.2.0 // indirect 69 | golang.org/x/crypto v0.36.0 // indirect 70 | golang.org/x/net v0.38.0 // indirect 71 | golang.org/x/oauth2 v0.25.0 // indirect 72 | golang.org/x/sync v0.12.0 // indirect 73 | golang.org/x/sys v0.31.0 // indirect 74 | golang.org/x/term v0.30.0 // indirect 75 | golang.org/x/text v0.23.0 // indirect 76 | golang.org/x/time v0.9.0 // indirect 77 | golang.org/x/tools v0.30.0 // indirect 78 | google.golang.org/protobuf v1.36.4 // indirect 79 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 80 | gopkg.in/inf.v0 v0.9.1 // indirect 81 | k8s.io/api v0.32.3 // indirect 82 | k8s.io/apiextensions-apiserver v0.32.2 // indirect 83 | k8s.io/cli-runtime v0.32.3 // indirect 84 | k8s.io/klog/v2 v2.130.1 // indirect 85 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 86 | sigs.k8s.io/kustomize/api v0.18.0 // indirect 87 | sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect 88 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 89 | ) 90 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 4 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 5 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= 6 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 7 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 8 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 9 | github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= 10 | github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 11 | github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= 12 | github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= 13 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 14 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 15 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 16 | github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= 17 | github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 23 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 24 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= 25 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= 26 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 27 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 28 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 29 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 30 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 31 | github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 32 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 33 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 34 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 35 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 36 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 37 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 38 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 39 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 40 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 41 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 42 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 43 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 44 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 45 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 46 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 47 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 48 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 49 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 50 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 51 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 52 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 53 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 54 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 55 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 56 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 57 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 58 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= 59 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 60 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 61 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 62 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 63 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 64 | github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= 65 | github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 66 | github.com/itchyny/gojq v0.12.15 h1:WC1Nxbx4Ifw5U2oQWACYz32JK8G9qxNtHzrvW4KEcqI= 67 | github.com/itchyny/gojq v0.12.15/go.mod h1:uWAHCbCIla1jiNxmeT5/B5mOjSdfkCq6p8vxWg+BM10= 68 | github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= 69 | github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= 70 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 71 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 72 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 73 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 74 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 75 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 76 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 77 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 78 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 79 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 80 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 81 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 82 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 83 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 84 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 85 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 86 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 87 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 88 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 89 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 90 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 91 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 92 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 93 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 94 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= 95 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 96 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 97 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 98 | github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= 99 | github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= 100 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 101 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 102 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 103 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 104 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 105 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 106 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 107 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 108 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 109 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 110 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 111 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 112 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 113 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= 114 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 115 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 116 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 117 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 118 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 119 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 120 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 121 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 122 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 123 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 124 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 125 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 126 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 127 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 128 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 129 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 130 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 131 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 132 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 133 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 134 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 135 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 136 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 137 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 138 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 139 | github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= 140 | github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= 141 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 142 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 143 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 144 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 145 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 146 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 147 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 148 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 149 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 150 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 151 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 152 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 153 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 154 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 155 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 156 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 157 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 158 | golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= 159 | golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 160 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 161 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 162 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 163 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 164 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 165 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 166 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 169 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 170 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 171 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 172 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 173 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 174 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 175 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 176 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 177 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 178 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 179 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 180 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 181 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 182 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 183 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 184 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 185 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 186 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 187 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 188 | google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= 189 | google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 190 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 191 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 192 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 193 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 194 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 195 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 196 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 197 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 198 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 199 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 200 | helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg= 201 | helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= 202 | k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= 203 | k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= 204 | k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= 205 | k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= 206 | k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= 207 | k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 208 | k8s.io/cli-runtime v0.32.3 h1:khLF2ivU2T6Q77H97atx3REY9tXiA3OLOjWJxUrdvss= 209 | k8s.io/cli-runtime v0.32.3/go.mod h1:vZT6dZq7mZAca53rwUfdFSZjdtLyfF61mkf/8q+Xjak= 210 | k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= 211 | k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= 212 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 213 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 214 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= 215 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 216 | k8s.io/kubectl v0.32.3 h1:VMi584rbboso+yjfv0d8uBHwwxbC438LKq+dXd5tOAI= 217 | k8s.io/kubectl v0.32.3/go.mod h1:6Euv2aso5GKzo/UVMacV6C7miuyevpfI91SvBvV9Zdg= 218 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 219 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 220 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 221 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 222 | sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= 223 | sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= 224 | sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= 225 | sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= 226 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= 227 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 228 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 229 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 230 | -------------------------------------------------------------------------------- /integration_test/integration_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stackrox/helmtest/pkg/framework" 7 | "github.com/stretchr/testify/require" 8 | "helm.sh/helm/v3/pkg/chart/loader" 9 | "helm.sh/helm/v3/pkg/chartutil" 10 | ) 11 | 12 | func TestHelmTestShouldSucceed(t *testing.T) { 13 | l := framework.NewLoader("testdata/helmtest") 14 | s, err := l.LoadSuite() 15 | require.NoError(t, err) 16 | 17 | chart, err := loader.Load("testdata/nginx-example") 18 | require.NoError(t, err) 19 | 20 | target := &framework.Target{ 21 | Chart: chart, 22 | ReleaseOptions: chartutil.ReleaseOptions{ 23 | Name: "nginx-lb", 24 | Namespace: "loadbalancer", 25 | IsInstall: true, 26 | IsUpgrade: false, 27 | }, 28 | } 29 | 30 | s.Run(t, target) 31 | } 32 | -------------------------------------------------------------------------------- /integration_test/testdata/helmtest/suite.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | visibleSchemas: 3 | - kubernetes-1.20.2 4 | 5 | tests: 6 | - name: "Expect secret to be rendered on upgrades" 7 | server: 8 | objects: 9 | - apiVersion: test.stackrox.io 10 | kind: FakeResource 11 | metadata: 12 | name: example-fr 13 | namespace: loadbalancer 14 | release: 15 | isUpgrade: true 16 | expect: | 17 | .secrets["some-secret-on-upgrade"] | assertThat(. != null) 18 | .notesRaw | assertThat(. | contains("BTW, lookup saw 1 FakeResource")) 19 | -------------------------------------------------------------------------------- /integration_test/testdata/nginx-example/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /integration_test/testdata/nginx-example/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: nginx-example 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /integration_test/testdata/nginx-example/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "nginx-example.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "nginx-example.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "nginx-example.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "nginx-example.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | 24 | BTW, lookup saw {{ (lookup "test.stackrox.io" "FakeResource" .Release.Namespace "").items | len }} FakeResource 25 | -------------------------------------------------------------------------------- /integration_test/testdata/nginx-example/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "nginx-example.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "nginx-example.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "nginx-example.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "nginx-example.labels" -}} 37 | helm.sh/chart: {{ include "nginx-example.chart" . }} 38 | {{ include "nginx-example.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "nginx-example.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "nginx-example.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "nginx-example.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "nginx-example.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /integration_test/testdata/nginx-example/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "nginx-example.fullname" . }} 5 | labels: 6 | {{- include "nginx-example.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "nginx-example.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "nginx-example.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "nginx-example.serviceAccountName" . }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | securityContext: 33 | {{- toYaml .Values.securityContext | nindent 12 }} 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | ports: 37 | - name: http 38 | containerPort: 80 39 | protocol: TCP 40 | livenessProbe: 41 | httpGet: 42 | path: / 43 | port: http 44 | readinessProbe: 45 | httpGet: 46 | path: / 47 | port: http 48 | resources: 49 | {{- toYaml .Values.resources | nindent 12 }} 50 | {{- with .Values.nodeSelector }} 51 | nodeSelector: 52 | {{- toYaml . | nindent 8 }} 53 | {{- end }} 54 | {{- with .Values.affinity }} 55 | affinity: 56 | {{- toYaml . | nindent 8 }} 57 | {{- end }} 58 | {{- with .Values.tolerations }} 59 | tolerations: 60 | {{- toYaml . | nindent 8 }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /integration_test/testdata/nginx-example/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "nginx-example.fullname" . }} 6 | labels: 7 | {{- include "nginx-example.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "nginx-example.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /integration_test/testdata/nginx-example/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "nginx-example.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "nginx-example.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /integration_test/testdata/nginx-example/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "nginx-example.fullname" . }} 5 | labels: 6 | {{- include "nginx-example.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "nginx-example.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /integration_test/testdata/nginx-example/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "nginx-example.serviceAccountName" . }} 6 | labels: 7 | {{- include "nginx-example.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /integration_test/testdata/nginx-example/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "nginx-example.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "nginx-example.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "nginx-example.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /integration_test/testdata/nginx-example/templates/upgrade.test.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Release.IsUpgrade }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: some-secret-on-upgrade 6 | data: 7 | .secret-file: some-data 8 | {{ end }} 9 | -------------------------------------------------------------------------------- /integration_test/testdata/nginx-example/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for nginx-example. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: nginx 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | 26 | podAnnotations: {} 27 | 28 | podSecurityContext: {} 29 | # fsGroup: 2000 30 | 31 | securityContext: {} 32 | # capabilities: 33 | # drop: 34 | # - ALL 35 | # readOnlyRootFilesystem: true 36 | # runAsNonRoot: true 37 | # runAsUser: 1000 38 | 39 | service: 40 | type: ClusterIP 41 | port: 80 42 | 43 | ingress: 44 | enabled: false 45 | className: "" 46 | annotations: {} 47 | # kubernetes.io/ingress.class: nginx 48 | # kubernetes.io/tls-acme: "true" 49 | hosts: 50 | - host: chart-example.local 51 | paths: 52 | - path: / 53 | pathType: ImplementationSpecific 54 | tls: [] 55 | # - secretName: chart-example-tls 56 | # hosts: 57 | # - chart-example.local 58 | 59 | resources: {} 60 | # We usually recommend not to specify default resources and to leave this as a conscious 61 | # choice for the user. This also increases chances charts run on environments with little 62 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 63 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 64 | # limits: 65 | # cpu: 100m 66 | # memory: 128Mi 67 | # requests: 68 | # cpu: 100m 69 | # memory: 128Mi 70 | 71 | autoscaling: 72 | enabled: false 73 | minReplicas: 1 74 | maxReplicas: 100 75 | targetCPUUtilizationPercentage: 80 76 | # targetMemoryUtilizationPercentage: 80 77 | 78 | nodeSelector: {} 79 | 80 | tolerations: [] 81 | 82 | affinity: {} 83 | -------------------------------------------------------------------------------- /internal/compiler/builtins.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/itchyny/gojq" 7 | "github.com/pkg/errors" 8 | "github.com/stackrox/helmtest/internal/logic" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | // Additional built-ins for gojq 13 | 14 | func fromYaml(obj interface{}, _ []interface{}) interface{} { 15 | var bytes []byte 16 | switch o := obj.(type) { 17 | case []byte: 18 | bytes = o 19 | case string: 20 | bytes = []byte(o) 21 | default: 22 | return errors.Errorf("expected string or bytes as input to fromyaml, got %T", obj) 23 | } 24 | 25 | var out map[string]interface{} 26 | if err := yaml.Unmarshal(bytes, &out); err != nil { 27 | return errors.Wrap(err, "fromyaml") 28 | } 29 | return out 30 | } 31 | 32 | func toYaml(obj interface{}, _ []interface{}) interface{} { 33 | bytes, err := yaml.Marshal(obj) 34 | if err != nil { 35 | return errors.Wrap(err, "toyaml") 36 | } 37 | return string(bytes) 38 | } 39 | 40 | func printjq(obj interface{}, _ []interface{}) interface{} { 41 | fmt.Println("--------DEBUG--------") 42 | fmt.Println(obj) 43 | return obj 44 | } 45 | 46 | func assertThat(obj interface{}, args []interface{}) interface{} { 47 | evalResult := args[0] 48 | if !logic.Truthy(evalResult) { 49 | return errors.Errorf("%+v failed predicate '%s'", obj, args[1]) 50 | } 51 | return true 52 | } 53 | 54 | func assumeThat(obj interface{}, args []interface{}) interface{} { 55 | evalResult := args[0] 56 | if !logic.Truthy(evalResult) { 57 | return logic.ErrAssumptionViolation 58 | } 59 | return obj 60 | } 61 | 62 | func assertNotExist(obj interface{}, _ []interface{}) interface{} { 63 | return errors.Errorf("object %+v should not exist", obj) 64 | } 65 | 66 | var builtinOpts = []gojq.CompilerOption{ 67 | gojq.WithFunction("fromyaml", 0, 0, fromYaml), 68 | gojq.WithFunction("toyaml", 0, 0, toYaml), 69 | gojq.WithFunction("assertThat", 2, 2, assertThat), 70 | gojq.WithFunction("assertNotExist", 0, 0, assertNotExist), 71 | gojq.WithFunction("assumeThat", 1, 1, assumeThat), 72 | gojq.WithFunction("print", 0, 0, printjq), 73 | } 74 | -------------------------------------------------------------------------------- /internal/compiler/compile.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import "github.com/itchyny/gojq" 4 | 5 | // Compile compiles a given query, applying the given options. This function mainly differs from `gojq.Compile` 6 | // in that it injects builtin functions. 7 | func Compile(query *gojq.Query, compilerOpts ...gojq.CompilerOption) (*gojq.Code, error) { 8 | var allOpts []gojq.CompilerOption 9 | if len(compilerOpts) == 0 { 10 | allOpts = builtinOpts 11 | } else { 12 | allOpts = make([]gojq.CompilerOption, 0, len(compilerOpts)+len(builtinOpts)) 13 | allOpts = append(allOpts, builtinOpts...) 14 | allOpts = append(allOpts, compilerOpts...) 15 | } 16 | return gojq.Compile(query, allOpts...) 17 | } 18 | -------------------------------------------------------------------------------- /internal/logic/errors.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import "github.com/pkg/errors" 4 | 5 | var ( 6 | // ErrAssumptionViolation is a marker error that signals an unsatisfied assumption. 7 | ErrAssumptionViolation = errors.New("assumption violation") 8 | ) 9 | -------------------------------------------------------------------------------- /internal/logic/truthiness.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import "reflect" 4 | 5 | // Truthy returns the truthiness value of an arbitary value. The nil interface and zero values are always falsy. 6 | // Empty slices and maps are falsy as well, even if they are non-nil. All other values are truthy. 7 | func Truthy(val interface{}) bool { 8 | if val == nil { 9 | return false 10 | } 11 | rval := reflect.ValueOf(val) 12 | if rval.IsZero() { 13 | return false 14 | } 15 | if rval.Kind() == reflect.Slice || rval.Kind() == reflect.Map { 16 | return rval.Len() > 0 17 | } 18 | return true 19 | } 20 | -------------------------------------------------------------------------------- /internal/parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bufio" 5 | "strings" 6 | "unicode" 7 | 8 | "github.com/itchyny/gojq" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // ParseExpectations parses an "expect" section. The expect section consists of several jq filters, one per line. 13 | // In order to allow longer filter expressions, a filter expression may be continued on the next line. This is indicated 14 | // by having the continuation line start with any whitespace character. 15 | func ParseExpectations(spec string, sctx SourceContext) ([]*ParsedQuery, error) { 16 | var queries []*ParsedQuery 17 | scanner := bufio.NewScanner(strings.NewReader(spec)) 18 | current := "" 19 | scanned := true 20 | if sctx.IsZero() { 21 | sctx = SourceContext{ 22 | Filename: "", 23 | Line: 0, 24 | } 25 | } 26 | currentSCtx := sctx 27 | for ; scanned; sctx.Line++ { 28 | scanned = scanner.Scan() 29 | var next string 30 | if scanned { 31 | line := strings.TrimRightFunc(scanner.Text(), unicode.IsSpace) 32 | trimmed := strings.TrimLeftFunc(line, unicode.IsSpace) 33 | if len(trimmed) < len(line) { 34 | // Continuation line. 35 | if current == "" { 36 | return nil, errors.Errorf("unexpected continuation at %s", sctx) 37 | } 38 | current += " " + trimmed 39 | continue 40 | } 41 | next = line 42 | } 43 | 44 | if current != "" { 45 | query, err := ParseQuery(current, currentSCtx) 46 | if err != nil { 47 | return nil, errors.Wrapf(err, "parsing query ending at %s", sctx) 48 | } 49 | queries = append(queries, query) 50 | } 51 | current = next 52 | currentSCtx = sctx 53 | } 54 | if err := scanner.Err(); err != nil { 55 | return nil, errors.Wrap(err, "parsing expectations") 56 | } 57 | 58 | if current != "" { 59 | query, err := ParseQuery(current, currentSCtx) 60 | if err != nil { 61 | return nil, errors.Wrapf(err, "parsing query ending at %s", sctx) 62 | } 63 | queries = append(queries, query) 64 | } 65 | 66 | return queries, nil 67 | } 68 | 69 | // ParseQuery parses a single query. 70 | func ParseQuery(src string, sctx SourceContext) (*ParsedQuery, error) { 71 | query, err := gojq.Parse(src) 72 | if err != nil { 73 | return nil, err 74 | } 75 | if err := postProcessQuery(query); err != nil { 76 | return nil, err 77 | } 78 | return &ParsedQuery{ 79 | Query: query, 80 | Source: src, 81 | SourceCtx: sctx, 82 | }, nil 83 | } 84 | -------------------------------------------------------------------------------- /internal/parser/post_process.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/itchyny/gojq" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // postProcessQuery processes query to allow the realization of the special `assertThat` function. 9 | // Generally, functions defined via `def` may receive filters as arguments, but builtin functions will only ever see 10 | // the concrete values. We therefore patch the AST to change every invocation of `assertThat` such that it passes 11 | // the string representation of the first argument as the second argument. This allows us to give a more specific 12 | // error message, stating what the predicate was that caused the assertion to be violated. 13 | func postProcessQuery(query *gojq.Query) error { 14 | if query == nil { 15 | return nil 16 | } 17 | 18 | if err := postProcessQuery(query.Left); err != nil { 19 | return err 20 | } 21 | if err := postProcessQuery(query.Right); err != nil { 22 | return err 23 | } 24 | if query.Term != nil { 25 | if fn := query.Term.Func; fn != nil && fn.Name == "assertThat" && len(fn.Args) != 2 { 26 | if len(fn.Args) != 1 { 27 | return errors.Errorf("incorrect number of arguments for assertThat: %d, expected 1 or 2", len(fn.Args)) 28 | } 29 | filterStr := &gojq.Query{ 30 | Term: &gojq.Term{ 31 | Type: gojq.TermTypeString, 32 | Str: &gojq.String{ 33 | Str: query.Term.Func.Args[0].String(), 34 | }, 35 | }, 36 | } 37 | fn.Args = append(fn.Args, filterStr) 38 | } 39 | 40 | if arr := query.Term.Array; arr != nil { 41 | if err := postProcessQuery(arr.Query); err != nil { 42 | return err 43 | } 44 | } 45 | if un := query.Term.Unary; un != nil { 46 | if err := postProcessQuery(un.Term.Query); err != nil { 47 | return err 48 | } 49 | } 50 | 51 | if err := postProcessQuery(query.Term.Query); err != nil { 52 | return err 53 | } 54 | if lbl := query.Term.Label; lbl != nil { 55 | if err := postProcessQuery(lbl.Body); err != nil { 56 | return err 57 | } 58 | } 59 | for _, suff := range query.Term.SuffixList { 60 | if bind := suff.Bind; bind != nil { 61 | if err := postProcessQuery(bind.Body); err != nil { 62 | return err 63 | } 64 | } 65 | } 66 | } 67 | 68 | for _, fd := range query.FuncDefs { 69 | if err := postProcessQuery(fd.Body); err != nil { 70 | return err 71 | } 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/parser/query.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "github.com/itchyny/gojq" 4 | 5 | // ParsedQuery is a parsed query, with some extra metadata to aid diagnosing test failures. 6 | type ParsedQuery struct { 7 | *gojq.Query 8 | Source string 9 | SourceCtx SourceContext 10 | } 11 | 12 | // Copy returns deep copy of ParsedQuery 13 | func (q *ParsedQuery) Copy() *ParsedQuery { 14 | query, _ := gojq.Parse(q.String()) 15 | return &ParsedQuery{ 16 | Query: query, 17 | Source: q.Source, 18 | SourceCtx: q.SourceCtx, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/parser/source_context.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "fmt" 4 | 5 | // SourceContext stores information about location in a source file. 6 | type SourceContext struct { 7 | Filename string 8 | Line int 9 | } 10 | 11 | // String returns a human-readable string representation of this source context. 12 | func (c SourceContext) String() string { 13 | filename := c.Filename 14 | if filename == "" { 15 | filename = "" 16 | } 17 | return fmt.Sprintf("%s:%d", filename, c.Line+1) // c.Line is zero-based, add 1 for human-readable 18 | } 19 | 20 | // IsZero checks whether this SourceContext is equal to the zero SourceContext. 21 | func (c *SourceContext) IsZero() bool { 22 | return c.Filename == "" && c.Line == 0 23 | } 24 | -------------------------------------------------------------------------------- /internal/rox-imported/README.md: -------------------------------------------------------------------------------- 1 | # rox-imported directory 2 | 3 | The Go packages in subdirectories are stripped-down versions imported from another repository. 4 | 5 | They are in the `internal/` directory as the intention is to open-source them as a standalone 6 | library at a later point in time. Once that happens, they can be removed and said library should 7 | be added as a dependency to this repository. In the meantime, however, we do not want anyone to 8 | start pulling them from this repo. 9 | -------------------------------------------------------------------------------- /internal/rox-imported/gziputil/compress.go: -------------------------------------------------------------------------------- 1 | package gziputil 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "io" 7 | ) 8 | 9 | // Decompress decompresses the given bytes of compressed data using gzip. 10 | func Decompress(compressedData []byte) ([]byte, error) { 11 | r, err := gzip.NewReader(bytes.NewReader(compressedData)) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | data, err := io.ReadAll(r) 17 | if err != nil { 18 | return nil, err 19 | } 20 | if err := r.Close(); err != nil { 21 | return nil, err 22 | } 23 | 24 | return data, nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/rox-imported/helmutil/values.go: -------------------------------------------------------------------------------- 1 | package helmutil 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/pkg/errors" 7 | "helm.sh/helm/v3/pkg/chartutil" 8 | ) 9 | 10 | // ValuesForKVPair returns a chartutil.Values that has exactly the single key identified by `configPath` set to 11 | // `val`. The `chartutil.CoalesceTables` function can be used to merge this into an existing values 12 | // dictionary. 13 | func ValuesForKVPair(configPath string, val interface{}) (chartutil.Values, error) { 14 | if configPath == "" { 15 | return nil, errors.New("empty config path") 16 | } 17 | keyPath := strings.Split(configPath, ".") 18 | 19 | mapForSet := map[string]interface{}{keyPath[len(keyPath)-1]: val} 20 | for i := len(keyPath) - 2; i >= 0; i-- { 21 | mapForSet = map[string]interface{}{keyPath[i]: mapForSet} 22 | } 23 | return chartutil.Values(mapForSet), nil 24 | } 25 | -------------------------------------------------------------------------------- /internal/rox-imported/pointers/pointer.go: -------------------------------------------------------------------------------- 1 | package pointers 2 | 3 | // Bool returns a pointer of the passed bool 4 | func Bool(b bool) *bool { 5 | return &b 6 | } 7 | -------------------------------------------------------------------------------- /internal/rox-imported/set/gen-string-generic.go: -------------------------------------------------------------------------------- 1 | // Code generated by genny. DO NOT EDIT. 2 | // This file was automatically generated by genny. 3 | // Any changes will be lost if this file is regenerated. 4 | // see https://github.com/mauricelam/genny 5 | 6 | package set 7 | 8 | import ( 9 | "fmt" 10 | "sort" 11 | "strings" 12 | ) 13 | 14 | // If you want to add a set for your custom type, simply add another go generate line along with the 15 | // existing ones. If you're creating a set for a primitive type, you can follow the example of "string" 16 | // and create the generated file in this package. 17 | // For non-primitive sets, please make the generated code files go outside this package. 18 | // Sometimes, you might need to create it in the same package where it is defined to avoid import cycles. 19 | // The permission set is an example of how to do that. 20 | // You can also specify the -imp command to specify additional imports in your generated file, if required. 21 | 22 | // string represents a generic type that we want to have a set of. 23 | 24 | // StringSet will get translated to generic sets. 25 | type StringSet map[string]struct{} 26 | 27 | // Add adds an element of type string. 28 | func (k *StringSet) Add(i string) bool { 29 | if *k == nil { 30 | *k = make(map[string]struct{}) 31 | } 32 | 33 | oldLen := len(*k) 34 | (*k)[i] = struct{}{} 35 | return len(*k) > oldLen 36 | } 37 | 38 | // AddMatching is a utility function that adds all the elements that match the given function to the set. 39 | func (k *StringSet) AddMatching(matchFunc func(string) bool, elems ...string) bool { 40 | oldLen := len(*k) 41 | for _, elem := range elems { 42 | if !matchFunc(elem) { 43 | continue 44 | } 45 | if *k == nil { 46 | *k = make(map[string]struct{}) 47 | } 48 | (*k)[elem] = struct{}{} 49 | } 50 | return len(*k) > oldLen 51 | } 52 | 53 | // AddAll adds all elements of type string. The return value is true if any new element 54 | // was added. 55 | func (k *StringSet) AddAll(is ...string) bool { 56 | if len(is) == 0 { 57 | return false 58 | } 59 | if *k == nil { 60 | *k = make(map[string]struct{}) 61 | } 62 | 63 | oldLen := len(*k) 64 | for _, i := range is { 65 | (*k)[i] = struct{}{} 66 | } 67 | return len(*k) > oldLen 68 | } 69 | 70 | // Remove removes an element of type string. 71 | func (k *StringSet) Remove(i string) bool { 72 | if len(*k) == 0 { 73 | return false 74 | } 75 | 76 | oldLen := len(*k) 77 | delete(*k, i) 78 | return len(*k) < oldLen 79 | } 80 | 81 | // RemoveAll removes the given elements. 82 | func (k *StringSet) RemoveAll(is ...string) bool { 83 | if len(*k) == 0 { 84 | return false 85 | } 86 | 87 | oldLen := len(*k) 88 | for _, i := range is { 89 | delete(*k, i) 90 | } 91 | return len(*k) < oldLen 92 | } 93 | 94 | // RemoveMatching removes all elements that match a given predicate. 95 | func (k *StringSet) RemoveMatching(pred func(string) bool) bool { 96 | if len(*k) == 0 { 97 | return false 98 | } 99 | 100 | oldLen := len(*k) 101 | for elem := range *k { 102 | if pred(elem) { 103 | delete(*k, elem) 104 | } 105 | } 106 | return len(*k) < oldLen 107 | } 108 | 109 | // Contains returns whether the set contains an element of type string. 110 | func (k StringSet) Contains(i string) bool { 111 | _, ok := k[i] 112 | return ok 113 | } 114 | 115 | // Cardinality returns the number of elements in the set. 116 | func (k StringSet) Cardinality() int { 117 | return len(k) 118 | } 119 | 120 | // IsEmpty returns whether the underlying set is empty (includes uninitialized). 121 | // 122 | // Deprecated: use Cardinality() == 0 instead 123 | func (k StringSet) IsEmpty() bool { 124 | return len(k) == 0 125 | } 126 | 127 | // Clone returns a copy of this set. 128 | func (k StringSet) Clone() StringSet { 129 | if k == nil { 130 | return nil 131 | } 132 | cloned := make(map[string]struct{}, len(k)) 133 | for elem := range k { 134 | cloned[elem] = struct{}{} 135 | } 136 | return cloned 137 | } 138 | 139 | // Difference returns a new set with all elements of k not in other. 140 | func (k StringSet) Difference(other StringSet) StringSet { 141 | if len(k) == 0 || len(other) == 0 { 142 | return k.Clone() 143 | } 144 | 145 | retained := make(map[string]struct{}, len(k)) 146 | for elem := range k { 147 | if !other.Contains(elem) { 148 | retained[elem] = struct{}{} 149 | } 150 | } 151 | return retained 152 | } 153 | 154 | // Helper function for intersections. 155 | func (k StringSet) getSmallerLargerAndMaxIntLen(other StringSet) (smaller StringSet, larger StringSet, maxIntLen int) { 156 | maxIntLen = len(k) 157 | smaller, larger = k, other 158 | if l := len(other); l < maxIntLen { 159 | maxIntLen = l 160 | smaller, larger = larger, smaller 161 | } 162 | return smaller, larger, maxIntLen 163 | } 164 | 165 | // Intersects returns whether the set has a non-empty intersection with the other set. 166 | func (k StringSet) Intersects(other StringSet) bool { 167 | smaller, larger, maxIntLen := k.getSmallerLargerAndMaxIntLen(other) 168 | if maxIntLen == 0 { 169 | return false 170 | } 171 | for elem := range smaller { 172 | if _, ok := larger[elem]; ok { 173 | return true 174 | } 175 | } 176 | return false 177 | } 178 | 179 | // Intersect returns a new set with the intersection of the members of both sets. 180 | func (k StringSet) Intersect(other StringSet) StringSet { 181 | smaller, larger, maxIntLen := k.getSmallerLargerAndMaxIntLen(other) 182 | if maxIntLen == 0 { 183 | return nil 184 | } 185 | 186 | retained := make(map[string]struct{}, maxIntLen) 187 | for elem := range smaller { 188 | if _, ok := larger[elem]; ok { 189 | retained[elem] = struct{}{} 190 | } 191 | } 192 | return retained 193 | } 194 | 195 | // Union returns a new set with the union of the members of both sets. 196 | func (k StringSet) Union(other StringSet) StringSet { 197 | if len(k) == 0 { 198 | return other.Clone() 199 | } else if len(other) == 0 { 200 | return k.Clone() 201 | } 202 | 203 | underlying := make(map[string]struct{}, len(k)+len(other)) 204 | for elem := range k { 205 | underlying[elem] = struct{}{} 206 | } 207 | for elem := range other { 208 | underlying[elem] = struct{}{} 209 | } 210 | return underlying 211 | } 212 | 213 | // Equal returns a bool if the sets are equal 214 | func (k StringSet) Equal(other StringSet) bool { 215 | thisL, otherL := len(k), len(other) 216 | if thisL == 0 && otherL == 0 { 217 | return true 218 | } 219 | if thisL != otherL { 220 | return false 221 | } 222 | for elem := range k { 223 | if _, ok := other[elem]; !ok { 224 | return false 225 | } 226 | } 227 | return true 228 | } 229 | 230 | // AsSlice returns a slice of the elements in the set. The order is unspecified. 231 | func (k StringSet) AsSlice() []string { 232 | if len(k) == 0 { 233 | return nil 234 | } 235 | elems := make([]string, 0, len(k)) 236 | for elem := range k { 237 | elems = append(elems, elem) 238 | } 239 | return elems 240 | } 241 | 242 | // GetArbitraryElem returns an arbitrary element from the set. 243 | // This can be useful if, for example, you know the set has exactly one 244 | // element, and you want to pull it out. 245 | // If the set is empty, the zero value is returned. 246 | func (k StringSet) GetArbitraryElem() (arbitraryElem string) { 247 | for elem := range k { 248 | arbitraryElem = elem 249 | break 250 | } 251 | return arbitraryElem 252 | } 253 | 254 | // AsSortedSlice returns a slice of the elements in the set, sorted using the passed less function. 255 | func (k StringSet) AsSortedSlice(less func(i, j string) bool) []string { 256 | slice := k.AsSlice() 257 | if len(slice) < 2 { 258 | return slice 259 | } 260 | // Since we're generating the code, we might as well use sort.Sort 261 | // and avoid paying the reflection penalty of sort.Slice. 262 | sortable := &sortableStringSlice{slice: slice, less: less} 263 | sort.Sort(sortable) 264 | return sortable.slice 265 | } 266 | 267 | // Clear empties the set 268 | func (k *StringSet) Clear() { 269 | *k = nil 270 | } 271 | 272 | // Freeze returns a new, frozen version of the set. 273 | func (k StringSet) Freeze() FrozenStringSet { 274 | return NewFrozenStringSetFromMap(k) 275 | } 276 | 277 | // ElementsString returns a string representation of all elements, with individual element strings separated by `sep`. 278 | // The string representation of an individual element is obtained via `fmt.Fprint`. 279 | func (k StringSet) ElementsString(sep string) string { 280 | if len(k) == 0 { 281 | return "" 282 | } 283 | var sb strings.Builder 284 | first := true 285 | for elem := range k { 286 | if !first { 287 | sb.WriteString(sep) 288 | } 289 | fmt.Fprint(&sb, elem) 290 | first = false 291 | } 292 | return sb.String() 293 | } 294 | 295 | // NewStringSet returns a new thread unsafe set with the given key type. 296 | func NewStringSet(initial ...string) StringSet { 297 | underlying := make(map[string]struct{}, len(initial)) 298 | for _, elem := range initial { 299 | underlying[elem] = struct{}{} 300 | } 301 | return underlying 302 | } 303 | 304 | type sortableStringSlice struct { 305 | slice []string 306 | less func(i, j string) bool 307 | } 308 | 309 | func (s *sortableStringSlice) Len() int { 310 | return len(s.slice) 311 | } 312 | 313 | func (s *sortableStringSlice) Less(i, j int) bool { 314 | return s.less(s.slice[i], s.slice[j]) 315 | } 316 | 317 | func (s *sortableStringSlice) Swap(i, j int) { 318 | s.slice[j], s.slice[i] = s.slice[i], s.slice[j] 319 | } 320 | 321 | // A FrozenStringSet is a frozen set of string elements, which 322 | // cannot be modified after creation. This allows users to use it as if it were 323 | // a "const" data structure, and also makes it slightly more optimal since 324 | // we don't have to lock accesses to it. 325 | type FrozenStringSet struct { 326 | underlying map[string]struct{} 327 | } 328 | 329 | // NewFrozenStringSetFromMap returns a new frozen set from the set-style map. 330 | func NewFrozenStringSetFromMap(m map[string]struct{}) FrozenStringSet { 331 | if len(m) == 0 { 332 | return FrozenStringSet{} 333 | } 334 | underlying := make(map[string]struct{}, len(m)) 335 | for elem := range m { 336 | underlying[elem] = struct{}{} 337 | } 338 | return FrozenStringSet{ 339 | underlying: underlying, 340 | } 341 | } 342 | 343 | // NewFrozenStringSet returns a new frozen set with the provided elements. 344 | func NewFrozenStringSet(elements ...string) FrozenStringSet { 345 | underlying := make(map[string]struct{}, len(elements)) 346 | for _, elem := range elements { 347 | underlying[elem] = struct{}{} 348 | } 349 | return FrozenStringSet{ 350 | underlying: underlying, 351 | } 352 | } 353 | 354 | // Contains returns whether the set contains the element. 355 | func (k FrozenStringSet) Contains(elem string) bool { 356 | _, ok := k.underlying[elem] 357 | return ok 358 | } 359 | 360 | // Cardinality returns the cardinality of the set. 361 | func (k FrozenStringSet) Cardinality() int { 362 | return len(k.underlying) 363 | } 364 | 365 | // IsEmpty returns whether the underlying set is empty (includes uninitialized). 366 | func (k FrozenStringSet) IsEmpty() bool { 367 | return len(k.underlying) == 0 368 | } 369 | 370 | // AsSlice returns the elements of the set. The order is unspecified. 371 | func (k FrozenStringSet) AsSlice() []string { 372 | if len(k.underlying) == 0 { 373 | return nil 374 | } 375 | slice := make([]string, 0, len(k.underlying)) 376 | for elem := range k.underlying { 377 | slice = append(slice, elem) 378 | } 379 | return slice 380 | } 381 | 382 | // AsSortedSlice returns the elements of the set as a sorted slice. 383 | func (k FrozenStringSet) AsSortedSlice(less func(i, j string) bool) []string { 384 | slice := k.AsSlice() 385 | if len(slice) < 2 { 386 | return slice 387 | } 388 | // Since we're generating the code, we might as well use sort.Sort 389 | // and avoid paying the reflection penalty of sort.Slice. 390 | sortable := &sortableStringSlice{slice: slice, less: less} 391 | sort.Sort(sortable) 392 | return sortable.slice 393 | } 394 | 395 | // ElementsString returns a string representation of all elements, with individual element strings separated by `sep`. 396 | // The string representation of an individual element is obtained via `fmt.Fprint`. 397 | func (k FrozenStringSet) ElementsString(sep string) string { 398 | if len(k.underlying) == 0 { 399 | return "" 400 | } 401 | var sb strings.Builder 402 | first := true 403 | for elem := range k.underlying { 404 | if !first { 405 | sb.WriteString(sep) 406 | } 407 | fmt.Fprint(&sb, elem) 408 | first = false 409 | } 410 | return sb.String() 411 | } 412 | 413 | // The following functions make use of casting `k.underlying` into a mutable Set. This is safe, since we never leak 414 | // references to these objects, and only invoke mutable set methods that are guaranteed to return a new copy. 415 | 416 | // Union returns a frozen set that represents the union between this and other. 417 | func (k FrozenStringSet) Union(other FrozenStringSet) FrozenStringSet { 418 | if len(k.underlying) == 0 { 419 | return other 420 | } 421 | if len(other.underlying) == 0 { 422 | return k 423 | } 424 | return FrozenStringSet{ 425 | underlying: StringSet(k.underlying).Union(other.underlying), 426 | } 427 | } 428 | 429 | // Intersect returns a frozen set that represents the intersection between this and other. 430 | func (k FrozenStringSet) Intersect(other FrozenStringSet) FrozenStringSet { 431 | return FrozenStringSet{ 432 | underlying: StringSet(k.underlying).Intersect(other.underlying), 433 | } 434 | } 435 | 436 | // Difference returns a frozen set that represents the set difference between this and other. 437 | func (k FrozenStringSet) Difference(other FrozenStringSet) FrozenStringSet { 438 | return FrozenStringSet{ 439 | underlying: StringSet(k.underlying).Difference(other.underlying), 440 | } 441 | } 442 | 443 | // Unfreeze returns a mutable set with the same contents as this frozen set. This set will not be affected by any 444 | // subsequent modifications to the returned set. 445 | func (k FrozenStringSet) Unfreeze() StringSet { 446 | return StringSet(k.underlying).Clone() 447 | } 448 | -------------------------------------------------------------------------------- /internal/rox-imported/sliceutils/unique.go: -------------------------------------------------------------------------------- 1 | package sliceutils 2 | 3 | // StringUnique returns a new slice that contains only the first occurrence of each element in slice. 4 | func StringUnique(slice []string) []string { 5 | result := make([]string, 0, len(slice)) 6 | seen := make(map[string]struct{}, len(slice)) 7 | for _, elem := range slice { 8 | if _, ok := seen[elem]; !ok { 9 | result = append(result, elem) 10 | seen[elem] = struct{}{} 11 | } 12 | } 13 | return result 14 | } 15 | -------------------------------------------------------------------------------- /internal/rox-imported/stringutils/consume.go: -------------------------------------------------------------------------------- 1 | package stringutils 2 | 3 | import "strings" 4 | 5 | // ConsumePrefix checks if *s has the given prefix, and if yes, modifies it 6 | // to remove the prefix. The return value indicates whether the original string 7 | // had the given prefix. 8 | func ConsumePrefix(s *string, prefix string) bool { 9 | orig := *s 10 | if !strings.HasPrefix(orig, prefix) { 11 | return false 12 | } 13 | *s = orig[len(prefix):] 14 | return true 15 | } 16 | -------------------------------------------------------------------------------- /internal/schemas/builtin.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "sync" 7 | ) 8 | 9 | //go:embed openapi-schemas/* 10 | var builtinSchemasFS embed.FS 11 | 12 | var ( 13 | builtinSchemasRegistry Registry 14 | builtinSchmeasRegistryInit sync.Once 15 | ) 16 | 17 | // BuiltinSchemas returns a registry with built-in schemas. 18 | func BuiltinSchemas() Registry { 19 | builtinSchmeasRegistryInit.Do(func() { 20 | subFS, err := fs.Sub(builtinSchemasFS, "openapi-schemas") 21 | if err != nil { 22 | panic(err) 23 | } 24 | builtinSchemasRegistry = newFSBasedRegistry(subFS) 25 | }) 26 | return builtinSchemasRegistry 27 | } 28 | -------------------------------------------------------------------------------- /internal/schemas/fs_registry.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import ( 4 | "io/fs" 5 | "sync" 6 | 7 | openapi_v2 "github.com/google/gnostic-models/openapiv2" 8 | "github.com/pkg/errors" 9 | "github.com/stackrox/helmtest/internal/rox-imported/gziputil" 10 | ) 11 | 12 | type fsBasedRegistry struct { 13 | fs fs.FS 14 | 15 | mutex sync.RWMutex 16 | schemas map[string]*schemaEntry 17 | } 18 | 19 | func newFSBasedRegistry(fs fs.FS) *fsBasedRegistry { 20 | return &fsBasedRegistry{ 21 | fs: fs, 22 | schemas: make(map[string]*schemaEntry), 23 | } 24 | } 25 | 26 | type schemaEntry struct { 27 | schema Schema 28 | loadErr error 29 | } 30 | 31 | func (r *fsBasedRegistry) GetSchema(name string) (Schema, error) { 32 | if e := r.getCachedSchema(name); e != nil { 33 | return e.schema, e.loadErr 34 | } 35 | 36 | r.mutex.Lock() 37 | defer r.mutex.Unlock() 38 | if e := r.schemas[name]; e != nil { 39 | return e.schema, e.loadErr 40 | } 41 | s, err := r.loadSchema(name) 42 | r.schemas[name] = &schemaEntry{ 43 | schema: s, 44 | loadErr: err, 45 | } 46 | return s, err 47 | } 48 | 49 | func (r *fsBasedRegistry) getCachedSchema(name string) *schemaEntry { 50 | r.mutex.RLock() 51 | defer r.mutex.RUnlock() 52 | 53 | return r.schemas[name] 54 | } 55 | 56 | func (r *fsBasedRegistry) loadSchema(name string) (Schema, error) { 57 | schemaBytes, err := fs.ReadFile(r.fs, name+".json.gz") 58 | if err != nil { 59 | return nil, errors.Wrapf(err, "no schema found for name %q", name) 60 | } 61 | 62 | openapiDoc, err := gziputil.Decompress(schemaBytes) 63 | if err != nil { 64 | return nil, errors.Wrapf(err, "failed reading openapi docs for schema %s", name) 65 | } 66 | 67 | doc, err := openapi_v2.ParseDocument(openapiDoc) 68 | if err != nil { 69 | return nil, errors.Wrapf(err, "parsing OpenAPI doc for schema %s", name) 70 | } 71 | schema, err := newSchema(doc) 72 | if err != nil { 73 | return nil, errors.Wrapf(err, "creating OpenAPI schema from document for schema %s", name) 74 | } 75 | 76 | return schema, nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/schemas/openapi-schemas/README.md: -------------------------------------------------------------------------------- 1 | ## Schema sources 2 | 3 | - OpenShift 3.11 schema source: https://github.com/garethr/openshift-json-schema/blob/master/v3.11.0/_definitions.json 4 | - OpenShift 4.1 schema source: https://github.com/garethr/openshift-json-schema/blob/master/v4.1.0/_definitions.json 5 | - OpenShift 4.18 schema source: 6 | - Install openshift 7 | - `oc get --raw /openapi/v2 | jq --sort-keys 'del(.parameters, .paths[], .security, .securityDefinitions)' > openshift-${version}.json` 8 | - Update the `.info` in the resulting file 9 | - `gzip -9 openshift-${version}.json` 10 | 11 | ## Adhoc generation 12 | 13 | If there are no JSON schemas available the CRD can be used to fill the 14 | gap. e.g. see com.coreos.json.gz, the definition can be largely derived from: 15 | 16 | ``` 17 | oc get crd servicemonitors.monitoring.coreos.com -o json | jq '.spec.versions[0].schema.openAPIV3Schema' 18 | ``` 19 | 20 | with an addition of a "x-kubernetes-group-version-kind" to map to the API ref: 21 | 22 | ``` 23 | "x-kubernetes-group-version-kind": [ 24 | { 25 | "group": "monitoring.coreos.com", 26 | "kind": "ServiceMonitor", 27 | "version": "v1" 28 | } 29 | ] 30 | ``` 31 | -------------------------------------------------------------------------------- /internal/schemas/openapi-schemas/com.coreos.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackrox/helmtest/bb329bee239de09cf7956764b77c3e1426a91c04/internal/schemas/openapi-schemas/com.coreos.json.gz -------------------------------------------------------------------------------- /internal/schemas/openapi-schemas/kubernetes-1.20.2.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackrox/helmtest/bb329bee239de09cf7956764b77c3e1426a91c04/internal/schemas/openapi-schemas/kubernetes-1.20.2.json.gz -------------------------------------------------------------------------------- /internal/schemas/openapi-schemas/openshift-3.11.0.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackrox/helmtest/bb329bee239de09cf7956764b77c3e1426a91c04/internal/schemas/openapi-schemas/openshift-3.11.0.json.gz -------------------------------------------------------------------------------- /internal/schemas/openapi-schemas/openshift-4.1.0.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackrox/helmtest/bb329bee239de09cf7956764b77c3e1426a91c04/internal/schemas/openapi-schemas/openshift-4.1.0.json.gz -------------------------------------------------------------------------------- /internal/schemas/openapi-schemas/openshift-4.18.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackrox/helmtest/bb329bee239de09cf7956764b77c3e1426a91c04/internal/schemas/openapi-schemas/openshift-4.18.json.gz -------------------------------------------------------------------------------- /internal/schemas/registry.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | // Registry allows retrieving schemas by name. 4 | type Registry interface { 5 | GetSchema(name string) (Schema, error) 6 | } 7 | -------------------------------------------------------------------------------- /internal/schemas/schema.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import ( 4 | "strings" 5 | 6 | openapi_v2 "github.com/google/gnostic-models/openapiv2" 7 | "github.com/pkg/errors" 8 | "github.com/stackrox/helmtest/internal/rox-imported/set" 9 | "gopkg.in/yaml.v3" 10 | "helm.sh/helm/v3/pkg/chartutil" 11 | k8sSchema "k8s.io/apimachinery/pkg/runtime/schema" 12 | "k8s.io/kubectl/pkg/util/openapi" 13 | ) 14 | 15 | // Schema is a schema that can be used for validation. 16 | type Schema interface { 17 | openapi.Resources 18 | VersionSet() chartutil.VersionSet 19 | } 20 | 21 | type schema struct { 22 | openapi.Resources 23 | allGVKs map[k8sSchema.GroupVersionKind]struct{} 24 | } 25 | 26 | func newSchema(doc *openapi_v2.Document) (*schema, error) { 27 | resources, err := openapi.NewOpenAPIData(doc) 28 | if err != nil { 29 | return nil, errors.Wrap(err, "parsing OpenAPI document") 30 | } 31 | allGVKs := make(map[k8sSchema.GroupVersionKind]struct{}) 32 | for _, def := range doc.GetDefinitions().GetAdditionalProperties() { 33 | for _, vendorExt := range def.GetValue().GetVendorExtension() { 34 | if vendorExt.GetName() != "x-kubernetes-group-version-kind" { 35 | continue 36 | } 37 | var gvks []k8sSchema.GroupVersionKind 38 | yamlDec := yaml.NewDecoder(strings.NewReader(vendorExt.GetValue().GetYaml())) 39 | yamlDec.KnownFields(true) 40 | if err := yamlDec.Decode(&gvks); err != nil { 41 | return nil, errors.Wrap(err, "decoding x-kubernetes-group-version-kind vendor extension") 42 | } 43 | for _, gvk := range gvks { 44 | allGVKs[gvk] = struct{}{} 45 | } 46 | } 47 | } 48 | return &schema{ 49 | Resources: resources, 50 | allGVKs: allGVKs, 51 | }, nil 52 | } 53 | 54 | func (s *schema) VersionSet() chartutil.VersionSet { 55 | allVersions := set.NewStringSet() 56 | for gvk := range s.allGVKs { 57 | prefix := "" 58 | if gvk.Group != "" { 59 | allVersions.Add(gvk.Group) 60 | prefix = gvk.Group + "/" 61 | } 62 | allVersions.Add(prefix + gvk.Version) 63 | allVersions.Add(prefix + gvk.Version + "/" + gvk.Kind) 64 | } 65 | return allVersions.AsSortedSlice(alphabetically) 66 | } 67 | -------------------------------------------------------------------------------- /internal/schemas/schemas.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import ( 4 | "github.com/stackrox/helmtest/internal/rox-imported/set" 5 | 6 | "helm.sh/helm/v3/pkg/chartutil" 7 | k8sSchema "k8s.io/apimachinery/pkg/runtime/schema" 8 | "k8s.io/kube-openapi/pkg/util/proto" 9 | ) 10 | 11 | // Schemas is a list of schemas to be combined. 12 | type Schemas []Schema 13 | 14 | // LookupResource locates a given GVK in the schema. 15 | func (s Schemas) LookupResource(gvk k8sSchema.GroupVersionKind) proto.Schema { 16 | for _, subSchema := range s { 17 | if protoSchema := subSchema.LookupResource(gvk); protoSchema != nil { 18 | return protoSchema 19 | } 20 | } 21 | return nil 22 | } 23 | 24 | // VersionSet returns the set of all API versions (Group, Group/Version, Group/Version/Kind) supported by the schemas. 25 | func (s Schemas) VersionSet() chartutil.VersionSet { 26 | allVersions := set.NewStringSet() 27 | for _, subSchema := range s { 28 | allVersions.AddAll(subSchema.VersionSet()...) 29 | } 30 | return allVersions.AsSortedSlice(alphabetically) 31 | } 32 | 33 | // GetConsumes returns the set of all consumes supported by the schemas. 34 | func (s Schemas) GetConsumes(gvk k8sSchema.GroupVersionKind, operation string) []string { 35 | consumes := set.NewStringSet() 36 | for _, subSchema := range s { 37 | consumes.AddAll(subSchema.GetConsumes(gvk, operation)...) 38 | } 39 | return consumes.AsSortedSlice(alphabetically) 40 | } 41 | 42 | func alphabetically(a, b string) bool { return a < b } 43 | -------------------------------------------------------------------------------- /internal/schemas/schemas_test.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSchemas(t *testing.T) { 10 | requiredSchemas := []string{ 11 | "kubernetes-1.20.2", 12 | "openshift-3.11.0", 13 | "openshift-4.1.0", 14 | "openshift-4.18", 15 | "com.coreos", 16 | } 17 | 18 | for _, schemaName := range requiredSchemas { 19 | _, err := BuiltinSchemas().GetSchema(schemaName) 20 | assert.NoErrorf(t, err, "failed to load required schema %s", schemaName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/framework/capabilities_spec.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import "helm.sh/helm/v3/pkg/chartutil" 4 | 5 | func (c *CapabilitiesSpec) toHelmKubeVersion() chartutil.KubeVersion { 6 | kubeVersion := chartutil.KubeVersion{} 7 | 8 | ver := c.KubeVersion 9 | if ver.Major != "" { 10 | kubeVersion.Major = ver.Major 11 | } 12 | if ver.Minor != "" { 13 | kubeVersion.Minor = ver.Minor 14 | } 15 | if ver.Version != "" { 16 | kubeVersion.Version = ver.Version 17 | } 18 | 19 | return kubeVersion 20 | } 21 | -------------------------------------------------------------------------------- /pkg/framework/loader.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | "k8s.io/utils/strings/slices" 10 | ) 11 | 12 | const ( 13 | testFileGlobPattern = "*.test.yaml" 14 | ) 15 | 16 | // Loader loads a test suite. 17 | type Loader struct { 18 | rootDir string 19 | additionalTestDirs []string 20 | } 21 | 22 | // NewLoader returns a loader and applies options to it. 23 | func NewLoader(rootDir string, opts ...LoaderOpt) *Loader { 24 | loader := Loader{ 25 | rootDir: rootDir, 26 | } 27 | 28 | for _, opt := range opts { 29 | opt(&loader) 30 | } 31 | return &loader 32 | } 33 | 34 | // LoaderOpt allows setting custom options. 35 | type LoaderOpt func(loader *Loader) 36 | 37 | // WithAdditionalTestDirs adds additional test source directories which are scanned for tests. 38 | func WithAdditionalTestDirs(path ...string) LoaderOpt { 39 | return func(loader *Loader) { 40 | loader.additionalTestDirs = append(loader.additionalTestDirs, path...) 41 | } 42 | } 43 | 44 | // LoadSuite loads a helmtest suite from the given directory. 45 | func (loader *Loader) LoadSuite() (*Test, error) { 46 | var suite Test 47 | if err := unmarshalYamlFromFileStrict(filepath.Join(loader.rootDir, "suite.yaml"), &suite); err != nil && !os.IsNotExist(err) { 48 | return nil, errors.Wrap(err, "loading suite specification") 49 | } 50 | 51 | if suite.Name == "" { 52 | suite.Name = strings.TrimRight(loader.rootDir, "/") 53 | } 54 | 55 | testYAMLFiles, err := loader.readTestYAMLFiles() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | for _, file := range testYAMLFiles { 61 | test := Test{ 62 | parent: &suite, 63 | } 64 | if err := unmarshalYamlFromFileStrict(file, &test); err != nil { 65 | return nil, errors.Wrapf(err, "loading test specification from file %s", file) 66 | } 67 | if test.Name == "" { 68 | test.Name = filepath.Base(file) 69 | } 70 | suite.Tests = append(suite.Tests, &test) 71 | } 72 | 73 | if err := suite.initialize(); err != nil { 74 | return nil, err 75 | } 76 | 77 | return &suite, nil 78 | } 79 | 80 | // readTestYAMLFiles locates test files, if any. 81 | func (loader *Loader) readTestYAMLFiles() ([]string, error) { 82 | var testYAMLFiles []string 83 | var scannedDirs []string 84 | 85 | dirs := []string{loader.rootDir} 86 | dirs = append(dirs, loader.additionalTestDirs...) 87 | for _, dir := range dirs { 88 | if slices.Contains(scannedDirs, dir) { 89 | continue 90 | } 91 | 92 | dirGlob := filepath.Join(dir, testFileGlobPattern) 93 | 94 | yamlFiles, err := filepath.Glob(dirGlob) 95 | if err != nil { 96 | return nil, errors.Wrapf(err, "resolving files using wildcard pattern %s", dirGlob) 97 | } 98 | testYAMLFiles = append(testYAMLFiles, yamlFiles...) 99 | scannedDirs = append(scannedDirs, dir) 100 | } 101 | 102 | return testYAMLFiles, nil 103 | } 104 | -------------------------------------------------------------------------------- /pkg/framework/loader_test.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestLoader(t *testing.T) { 11 | const testdataPath = "testdata/suite" 12 | 13 | tests := map[string]struct { 14 | opts []LoaderOpt 15 | expectedFunc func(*testing.T, *Test) 16 | additionalDir string 17 | }{ 18 | "With root dir": { 19 | expectedFunc: func(t *testing.T, helmTest *Test) { 20 | assert.Len(t, helmTest.Tests, 2) 21 | }, 22 | }, 23 | "Loader loads test hierarchy": { 24 | expectedFunc: func(t *testing.T, test *Test) { 25 | require.Len(t, test.Tests[1].Tests, 1) 26 | childTest := test.findFirst([]string{testdataPath, "helm.test.yaml", "test in helm.test.yaml", "with overwrites"}) 27 | assert.Equal(t, "with overwrites", childTest.Name) 28 | assert.EqualValues(t, map[string]interface{}{"testValue": "value overwrite"}, childTest.Values) 29 | }, 30 | }, 31 | "Loader loads additional dir": { 32 | additionalDir: "testdata/additional_dir", 33 | expectedFunc: func(t *testing.T, test *Test) { 34 | childTest := test.findFirst([]string{testdataPath, "additional.test.yaml"}) 35 | require.NotNil(t, test) 36 | assert.Equal(t, "additional.test.yaml", childTest.Name) 37 | }, 38 | }, 39 | } 40 | 41 | for name, tt := range tests { 42 | t.Run(name, func(t *testing.T) { 43 | var opts []LoaderOpt 44 | if tt.additionalDir != "" { 45 | opts = append(opts, WithAdditionalTestDirs(tt.additionalDir)) 46 | } 47 | 48 | loader := NewLoader(testdataPath, opts...) 49 | helmTests, err := loader.LoadSuite() 50 | require.NoError(t, err) 51 | 52 | tt.expectedFunc(t, helmTests) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/framework/normalize_string.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import "regexp" 4 | 5 | var ( 6 | whitespaceRegex = regexp.MustCompile(`\s+`) 7 | ) 8 | 9 | // normalizeString normalizes a given string by replacing all whitespace character segments 10 | // with a single ' ' space. 11 | func normalizeString(s string) string { 12 | return whitespaceRegex.ReplaceAllString(s, " ") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/framework/release_spec.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import "helm.sh/helm/v3/pkg/chartutil" 4 | 5 | // apply modifies a `ReleaseOptions` object according to the overrides specified in the `ReleaseSpec` section (while 6 | // leaving everything else untouched). 7 | func (s *ReleaseSpec) apply(opts *chartutil.ReleaseOptions) { 8 | if s.Name != "" { 9 | opts.Name = s.Name 10 | } 11 | if s.Namespace != "" { 12 | opts.Namespace = s.Namespace 13 | } 14 | if s.Revision != nil { 15 | opts.Revision = *s.Revision 16 | } 17 | if s.IsInstall != nil { 18 | opts.IsInstall = *s.IsInstall 19 | } 20 | if s.IsUpgrade != nil { 21 | opts.IsUpgrade = *s.IsUpgrade 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/framework/runner.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "path" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/stackrox/helmtest/internal/compiler" 14 | "github.com/stackrox/helmtest/internal/logic" 15 | "github.com/stackrox/helmtest/internal/parser" 16 | "github.com/stackrox/helmtest/internal/rox-imported/sliceutils" 17 | "github.com/stackrox/helmtest/internal/rox-imported/stringutils" 18 | "github.com/stackrox/helmtest/internal/schemas" 19 | 20 | "github.com/itchyny/gojq" 21 | "github.com/pkg/errors" 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | "helm.sh/helm/v3/pkg/chartutil" 25 | "helm.sh/helm/v3/pkg/engine" 26 | "k8s.io/apimachinery/pkg/api/meta" 27 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | "k8s.io/apimachinery/pkg/runtime/schema" 30 | "k8s.io/apimachinery/pkg/util/yaml" 31 | "k8s.io/client-go/dynamic" 32 | "k8s.io/client-go/dynamic/fake" 33 | "k8s.io/kubectl/pkg/util/openapi" 34 | "k8s.io/kubectl/pkg/validation" 35 | k8sYaml "sigs.k8s.io/yaml" 36 | ) 37 | 38 | type runner struct { 39 | t *testing.T 40 | test *Test 41 | tgt *Target 42 | } 43 | 44 | func (r *runner) Assert() *assert.Assertions { 45 | return assert.New(r.t) 46 | } 47 | 48 | func (r *runner) Require() *require.Assertions { 49 | return require.New(r.t) 50 | } 51 | 52 | func (r *runner) readAndValidateYAML(fileName, fileContents string, resources openapi.Resources) []unstructured.Unstructured { 53 | validator := validation.NewSchemaValidation(openAPIResourcesGetter{resources: resources}) 54 | 55 | yamlReader := yaml.NewYAMLReader(bufio.NewReader(strings.NewReader(fileContents))) 56 | 57 | var objs []unstructured.Unstructured 58 | 59 | docCounter := 0 60 | var yamlDoc []byte 61 | var err error 62 | var emptyDocs []int 63 | for yamlDoc, err = yamlReader.Read(); err == nil; yamlDoc, err = yamlReader.Read() { 64 | docCounter++ 65 | 66 | // We can tolerate empty documents in some circumstances (such as when the entire file is empty), but having 67 | // empty documents in an overall non-empty file will at least cause lint errors. 68 | if len(bytes.TrimSpace(yamlDoc)) == 0 { 69 | emptyDocs = append(emptyDocs, docCounter) 70 | continue 71 | } 72 | 73 | // Do the validation before converting to JSON such that we get accurate line numbers. 74 | validationErr := validator.ValidateBytes(yamlDoc) 75 | r.Assert().NoErrorf(validationErr, "YAML document #%d in file %s failed validation", docCounter, fileName) 76 | 77 | // YAMLToJSONStrict will not only convert to YAML, but also validate that there are no duplicate keys. 78 | jsonBytes, err := k8sYaml.YAMLToJSONStrict(yamlDoc) 79 | if !r.Assert().NoErrorf(err, "could not convert YAML document #%d in file %s to JSON", docCounter, fileName) { 80 | continue 81 | } 82 | 83 | obj, _, err := unstructured.UnstructuredJSONScheme.Decode(jsonBytes, nil, nil) 84 | if !r.Assert().NoErrorf(err, "could not decode Kubernetes object in YAML document #%d in file %s", docCounter, fileName) { 85 | continue 86 | } 87 | 88 | unstructuredObj, _ := obj.(*unstructured.Unstructured) 89 | if !r.Assert().NotNilf(unstructuredObj, "YAML document #%d in file %s is not a Kubernetes object", docCounter, fileName) { 90 | continue 91 | } 92 | 93 | r.Assert().NotNilf(resources.LookupResource(unstructuredObj.GroupVersionKind()), "YAML document #%d in file %s defines object of kind %s not known in schema", docCounter, fileName, unstructuredObj.GroupVersionKind()) 94 | objs = append(objs, *unstructuredObj) 95 | } 96 | 97 | // The only acceptable error is EOF. 98 | if !errors.Is(err, io.EOF) { 99 | r.Assert().NoErrorf(err, "reading multi-document YAML file %s", fileName) 100 | } 101 | 102 | // Validate that there is at most a single empty document, and only if the file is otherwise empty. 103 | if len(objs) > 0 { 104 | // We can tolerate an empty document at the beginning and at the end 105 | if len(emptyDocs) > 0 && emptyDocs[0] == 1 { 106 | emptyDocs = emptyDocs[1:] 107 | } 108 | if len(emptyDocs) > 0 && emptyDocs[len(emptyDocs)-1] == docCounter { 109 | emptyDocs = emptyDocs[:len(emptyDocs)-1] 110 | } 111 | r.Assert().Empty(emptyDocs, "multi-document YAML file %s is non-empty but has empty documents", fileName) 112 | } else { 113 | r.Assert().LessOrEqualf(len(emptyDocs), 1, "multi-document YAML file %s has multiple empty documents", fileName) 114 | } 115 | 116 | return objs 117 | } 118 | 119 | type clientProviderFromDynamicClient struct { 120 | dynIface dynamic.Interface 121 | } 122 | 123 | func (c clientProviderFromDynamicClient) GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error) { 124 | // This is suboptimal, but the best kind->resource translation we can do without a discovery client. 125 | gvr, _ := meta.UnsafeGuessKindToResource(schema.FromAPIVersionAndKind(apiVersion, kind)) 126 | namespaced := true // we infer this from the namespace argument to the lookup function 127 | return c.dynIface.Resource(gvr), namespaced, nil 128 | } 129 | 130 | func (r *runner) instantiateWorld(renderVals chartutil.Values, resources openapi.Resources, objects []runtime.Object) map[string]interface{} { 131 | world := make(map[string]interface{}) 132 | 133 | renderValsBytes, err := json.Marshal(renderVals) 134 | if err != nil { 135 | panic(errors.Wrap(err, "marshaling Helm render values to JSON")) 136 | } 137 | var helmRenderVals map[string]interface{} 138 | if err := json.Unmarshal(renderValsBytes, &helmRenderVals); err != nil { 139 | panic(errors.Wrap(err, "unmarshaling Helm render values")) 140 | } 141 | 142 | client := fake.NewSimpleDynamicClient(runtime.NewScheme(), objects...) 143 | clientProvider := clientProviderFromDynamicClient{dynIface: client} 144 | world["helm"] = helmRenderVals 145 | 146 | renderedTemplates, err := engine.RenderWithClientProvider(r.tgt.Chart, renderVals, clientProvider) 147 | 148 | if *r.test.ExpectError { 149 | r.Require().Error(err, "expected rendering to fail") 150 | errStr := err.Error() 151 | // Store the error string normalized, to avoid impact of formatting. 152 | world["error"] = normalizeString(errStr) 153 | world["errorRaw"] = errStr 154 | return world 155 | } 156 | 157 | r.Require().NoError(err, "expected rendering to succeed") 158 | 159 | var allObjects []interface{} 160 | 161 | for fileName, renderedContents := range renderedTemplates { 162 | // TODO: Support subcharts (even though we don't use them) 163 | if !r.Assert().Truef(stringutils.ConsumePrefix(&fileName, r.tgt.Chart.Name()+"/"), "unexpected file %s", fileName) { 164 | continue 165 | } 166 | 167 | if fileName == "templates/NOTES.txt" { 168 | world["notes"] = normalizeString(renderedContents) 169 | world["notesRaw"] = renderedContents 170 | continue 171 | } 172 | 173 | if !r.Assert().Equalf(".yaml", path.Ext(fileName), "unexpected file type for file %s", fileName) { 174 | continue 175 | } 176 | 177 | objs := r.readAndValidateYAML(fileName, renderedContents, resources) 178 | 179 | for _, obj := range objs { 180 | kindPlural := strings.TrimSuffix(strings.ToLower(obj.GetKind()), "s") + "s" 181 | objsByKind, _ := world[kindPlural].(map[string]interface{}) 182 | if objsByKind == nil { 183 | objsByKind = make(map[string]interface{}) 184 | world[kindPlural] = objsByKind 185 | } 186 | if !r.Assert().NotContainsf(objsByKind, obj.GetName(), "duplicate object %s/%s", kindPlural, obj.GetName()) { 187 | continue 188 | } 189 | objsByKind[obj.GetName()] = obj.Object 190 | allObjects = append(allObjects, obj.Object) 191 | } 192 | } 193 | 194 | world["objects"] = allObjects 195 | return world 196 | } 197 | 198 | func (r *runner) loadServerSettings() (visible, available schemas.Schemas, objects []runtime.Object) { 199 | var visibleSchemaNames, availableSchemaNames []string 200 | r.test.forEachScopeTopDown(func(t *Test) { 201 | server := t.Server 202 | if server == nil { 203 | return 204 | } 205 | if server.NoInherit { 206 | visibleSchemaNames = nil 207 | availableSchemaNames = nil 208 | } 209 | for _, schemaName := range server.AvailableSchemas { 210 | schemaName = strings.ToLower(schemaName) 211 | availableSchemaNames = append(availableSchemaNames, schemaName) 212 | } 213 | for _, schemaName := range server.VisibleSchemas { 214 | schemaName = strings.ToLower(schemaName) 215 | visibleSchemaNames = append(visibleSchemaNames, schemaName) 216 | // Every visible schema is also available (but not vice versa) 217 | availableSchemaNames = append(availableSchemaNames, schemaName) 218 | } 219 | for _, o := range server.Objects { 220 | obj := &unstructured.Unstructured{Object: o} 221 | objects = append(objects, obj.DeepCopyObject()) 222 | } 223 | }) 224 | 225 | availableSchemaNames = sliceutils.StringUnique(availableSchemaNames) 226 | visibleSchemaNames = sliceutils.StringUnique(visibleSchemaNames) 227 | 228 | schemaRegistry := r.tgt.SchemaRegistry 229 | if schemaRegistry == nil { 230 | schemaRegistry = schemas.BuiltinSchemas() 231 | } 232 | 233 | for _, schemaName := range availableSchemaNames { 234 | schema, err := schemaRegistry.GetSchema(schemaName) 235 | r.Require().NoErrorf(err, "failed to load schema %q", schemaName) 236 | available = append(available, schema) 237 | } 238 | for _, schemaName := range visibleSchemaNames { 239 | schema, err := schemaRegistry.GetSchema(schemaName) 240 | r.Require().NoErrorf(err, "failed to load schema %q", schemaName) 241 | visible = append(visible, schema) 242 | } 243 | 244 | return visible, available, objects 245 | } 246 | 247 | func (r *runner) Run() { 248 | var values map[string]interface{} 249 | releaseOpts := r.tgt.ReleaseOptions 250 | 251 | r.test.forEachScopeBottomUp(func(t *Test) { 252 | scopeVals := t.Values 253 | if len(scopeVals) == 0 { 254 | return 255 | } 256 | values = chartutil.CoalesceTables(values, runtime.DeepCopyJSON(scopeVals)) 257 | }) 258 | r.test.forEachScopeTopDown(func(t *Test) { 259 | rel := t.Release 260 | if rel == nil { 261 | return 262 | } 263 | rel.apply(&releaseOpts) 264 | }) 265 | 266 | visibleSchemas, availableSchemas, availableObjects := r.loadServerSettings() 267 | 268 | caps := r.tgt.Capabilities 269 | if caps == nil { 270 | caps = chartutil.DefaultCapabilities 271 | } 272 | if len(visibleSchemas) > 0 { 273 | newCaps := *caps 274 | newCaps.APIVersions = visibleSchemas.VersionSet() 275 | caps = &newCaps 276 | } 277 | 278 | r.test.forEachScopeTopDown(func(t *Test) { 279 | if t.Capabilities == nil { 280 | return 281 | } 282 | 283 | newCaps := *caps 284 | newCaps.KubeVersion = t.Capabilities.toHelmKubeVersion() 285 | caps = &newCaps 286 | }) 287 | 288 | renderVals, err := chartutil.ToRenderValues(r.tgt.Chart, values, releaseOpts, caps) 289 | r.Require().NoError(err, "failed to obtain render values") 290 | world := r.instantiateWorld(renderVals, availableSchemas, availableObjects) 291 | r.evaluatePredicates(world) 292 | } 293 | 294 | func (r *runner) evaluatePredicates(world map[string]interface{}) { 295 | var allFuncDefs []*gojq.FuncDef 296 | var allPreds []*parser.ParsedQuery 297 | 298 | r.test.forEachScopeTopDown(func(t *Test) { 299 | allFuncDefs = append(allFuncDefs, t.funcDefs...) 300 | for _, pred := range t.predicates { 301 | predWithFuncs := pred.Copy() 302 | predWithFuncs.FuncDefs = make([]*gojq.FuncDef, 0, len(allFuncDefs)+len(pred.FuncDefs)) 303 | predWithFuncs.FuncDefs = append(predWithFuncs.FuncDefs, allFuncDefs...) 304 | predWithFuncs.FuncDefs = append(predWithFuncs.FuncDefs, pred.FuncDefs...) 305 | allPreds = append(allPreds, predWithFuncs) 306 | } 307 | }) 308 | 309 | for _, pred := range allPreds { 310 | code, err := compiler.Compile(pred.Query, gojq.WithVariables([]string{"$_"})) 311 | 312 | if !r.Assert().NoErrorf(err, "failed to compile predicate %q", pred) { 313 | continue 314 | } 315 | 316 | worldCopy := runtime.DeepCopyJSON(world) 317 | iter := code.Run(worldCopy, worldCopy) 318 | hadElem := false 319 | var errMsgs []string 320 | for result, ok := iter.Next(); ok; result, ok = iter.Next() { 321 | hadElem = true 322 | err, _ := result.(error) 323 | if errors.Is(err, logic.ErrAssumptionViolation) { 324 | continue 325 | } 326 | if err != nil { 327 | errMsgs = append(errMsgs, err.Error()) 328 | } else if !logic.Truthy(result) { 329 | errMsgs = append(errMsgs, fmt.Sprintf("evaluated to falsy result %v", result)) 330 | } 331 | } 332 | if !hadElem { 333 | errMsgs = append(errMsgs, "evaluated to empty sequence") 334 | } 335 | if len(errMsgs) == 0 { 336 | continue 337 | } 338 | fullMsg := "predicate" 339 | if sctx := pred.SourceCtx; !sctx.IsZero() { 340 | fullMsg += fmt.Sprintf(" at %s", sctx) 341 | } 342 | fullMsg += fmt.Sprintf(":\n %s\nerrors:\n", pred.Source) 343 | for _, errMsg := range errMsgs { 344 | fullMsg += fmt.Sprintf(" %s\n", errMsg) 345 | } 346 | r.Assert().Failf(fullMsg, "error(s) evaluating predicate %q", pred) 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /pkg/framework/spec.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "github.com/itchyny/gojq" 5 | "github.com/stackrox/helmtest/internal/parser" 6 | ) 7 | 8 | // RawDict is an alias for map[string]interface{}, that is needed because `yaml.Unmarshal` and `json.Unmarshal` differ 9 | // in that the latter will never produce int values, while the former may. 10 | type RawDict map[string]interface{} 11 | 12 | // Test defines a helmtest test. A Test can be regarded as the equivalent of the *testing.T scope of a Go unit test. 13 | // Tests are scoped, and a test may either define concrete expectations, or contain an arbitrary number of nested tests. 14 | // See README.md in this directory for a more detailed explanation. 15 | type Test struct { 16 | // Public section - fields settable via YAML 17 | 18 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 19 | 20 | Values RawDict `json:"values,omitempty" yaml:"values,omitempty"` 21 | Set RawDict `json:"set,omitempty" yaml:"set,omitempty"` 22 | 23 | Defs string `json:"defs,omitempty" yaml:"defs,omitempty"` 24 | Release *ReleaseSpec `json:"release,omitempty" yaml:"release,omitempty"` 25 | Server *ServerSpec `json:"server,omitempty" yaml:"server,omitempty"` 26 | Capabilities *CapabilitiesSpec `json:"capabilities,omitempty" yaml:"capabilities,omitempty"` 27 | 28 | Expect string `json:"expect,omitempty" yaml:"expect,omitempty"` 29 | ExpectError *bool `json:"expectError,omitempty" yaml:"expectError,omitempty"` 30 | 31 | Tests []*Test `json:"tests,omitempty" yaml:"tests,omitempty"` 32 | 33 | // Private section - the following fields are never set in the YAML, they are always populated by initialize 34 | // or during YAML parsing. 35 | parent *Test 36 | 37 | funcDefs []*gojq.FuncDef 38 | predicates []*parser.ParsedQuery 39 | 40 | defsSrcCtx parser.SourceContext 41 | expectSrcCtx parser.SourceContext 42 | } 43 | 44 | // ReleaseSpec specifies how the release options for Helm will be constructed. 45 | type ReleaseSpec struct { 46 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 47 | Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` 48 | Revision *int `json:"revision,omitempty" yaml:"revision,omitempty"` 49 | IsInstall *bool `json:"isInstall,omitempty" yaml:"isInstall,omitempty"` 50 | IsUpgrade *bool `json:"isUpgrade,omitempty" yaml:"isUpgrade,omitempty"` 51 | } 52 | 53 | // ServerSpec specifies how the model of the server will be constructed. 54 | type ServerSpec struct { 55 | // AvailableSchemas are the names of schemas that are available on the server (i.e., that rendered objects must 56 | // pass validation against, but not necessarily discoverable via `.Capabilities.APIVersions`). 57 | AvailableSchemas []string `json:"availableSchemas,omitempty" yaml:"availableSchemas,omitempty"` 58 | // VisibleSchemas are the names of schemas that are available on the server AND discoverable via 59 | // `.Capabilities.APIVersions`. 60 | VisibleSchemas []string `json:"visibleSchemas,omitempty" yaml:"visibleSchemas,omitempty"` 61 | // Objects are definitions of objects visible to Helm's k8s client, for example via the `lookup` function. 62 | Objects []map[string]interface{} `json:"objects,omitempty" yaml:"objects,omitempty"` 63 | 64 | // NoInherit indicates that server-side settings should *not* be inherited from the enclosing scope. 65 | NoInherit bool `json:"noInherit,omitempty" yaml:"noInherit,omitempty"` 66 | } 67 | 68 | // CapabilitiesSpec represents the `Capabilities` in Helm. 69 | type CapabilitiesSpec struct { 70 | // KubeVersion represents the kubernetes version which is discoverable via `.Capabilities.KubeVersion`. 71 | KubeVersion *KubeVersion `json:"kubeVersion,omitempty" yaml:"kubeVersion,omitempty"` 72 | } 73 | 74 | // KubeVersion is the Kubernetes version. 75 | type KubeVersion struct { 76 | Version string `json:"version,omitempty" yaml:"version,omitempty"` // i.e. v1.18 77 | Major string `json:"major,omitempty" yaml:"major,omitempty"` // i.e. 1 78 | Minor string `json:"minor,omitempty" yaml:"minor,omitempty"` // i.e. 18 79 | } 80 | -------------------------------------------------------------------------------- /pkg/framework/spec_unmarshal.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/stackrox/helmtest/internal/parser" 9 | yamlv3 "gopkg.in/yaml.v3" 10 | "sigs.k8s.io/yaml" 11 | ) 12 | 13 | const ( 14 | filenamePragma = `#!helmtest-filename:` 15 | ) 16 | 17 | // injectFilename stores the filename into a "fake" foot comment of the respective node. This allows us to reconstruct 18 | // the filename in UnmarshalYAML, even though it isn't stored in the node itself (and there is no context that would 19 | // allow us to pass other values). 20 | func injectFilename(node *yamlv3.Node, filename string) { 21 | node.FootComment += fmt.Sprintf("\n%s%s", filenamePragma, filename) 22 | for _, child := range node.Content { 23 | injectFilename(child, filename) 24 | } 25 | } 26 | 27 | // UnmarshalYAML unmarshals a test spec from YAML, preserving line information that we're interested in. 28 | func (t *Test) UnmarshalYAML(node *yamlv3.Node) error { 29 | // Create an alias of this type without a custom UnmarshalYAML method. 30 | type testNoMethods Test 31 | if err := node.Decode((*testNoMethods)(t)); err != nil { 32 | return err 33 | } 34 | 35 | if node.Kind != yamlv3.MappingNode { 36 | return nil // weird but ok 37 | } 38 | 39 | // We've moved the filename into a fake footer comment, to allow extracting it here. 40 | var filename string 41 | for _, line := range strings.Split(node.FootComment, "\n") { 42 | line = strings.TrimSpace(line) 43 | if strings.HasPrefix(line, filenamePragma) { 44 | filename = line[len(filenamePragma):] 45 | break 46 | } 47 | } 48 | 49 | // The Content slice of a Mapping node stores keys and values in an alternating fashion. That is, keys 50 | // are stored at even indices, and the values mapped to those keys are stored at the odd indices following 51 | // the respective key index. 52 | // Here, we are only interested in certain mapping values for fields of interest, like 'expect'. 53 | for i := 0; i < len(node.Content); i += 2 { 54 | valueNode := node.Content[i+1] 55 | valueSrcContext := parser.SourceContext{ 56 | Filename: filename, 57 | Line: valueNode.Line - 1, // Node.Line is one-based, but SourceContext.Line is zero-based 58 | } 59 | // In literal and folded style, the value only begins on the preceding line. 60 | if valueNode.Style == yamlv3.LiteralStyle || valueNode.Style == yamlv3.FoldedStyle { 61 | valueSrcContext.Line++ 62 | } 63 | 64 | keyNode := node.Content[i] 65 | if keyNode.Value == "defs" { 66 | t.defsSrcCtx = valueSrcContext 67 | } else if keyNode.Value == "expect" { 68 | t.expectSrcCtx = valueSrcContext 69 | } 70 | } 71 | return nil 72 | } 73 | 74 | // UnmarshalYAML unmarshals a RawDict from YAML, making sure that the resulting type matches the result of 75 | // json.Unmarshal on the equivalent JSON. 76 | func (d *RawDict) UnmarshalYAML(node *yamlv3.Node) error { 77 | yamlBytes, err := yamlv3.Marshal(node) 78 | if err != nil { 79 | return err 80 | } 81 | jsonBytes, err := yaml.YAMLToJSON(yamlBytes) 82 | if err != nil { 83 | return err 84 | } 85 | return json.Unmarshal(jsonBytes, d) 86 | } 87 | -------------------------------------------------------------------------------- /pkg/framework/target.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "github.com/stackrox/helmtest/internal/schemas" 5 | "helm.sh/helm/v3/pkg/chart" 6 | "helm.sh/helm/v3/pkg/chartutil" 7 | ) 8 | 9 | // Target is the target to run a test against. This must at the minimum include a chart and default release options 10 | // (such as the release name or the namespace). Capabilities is optional and will default to the standard capabilities 11 | // used by Helm in client-only mode. 12 | type Target struct { 13 | Chart *chart.Chart 14 | ReleaseOptions chartutil.ReleaseOptions 15 | Capabilities *chartutil.Capabilities 16 | 17 | SchemaRegistry schemas.Registry 18 | } 19 | -------------------------------------------------------------------------------- /pkg/framework/test.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stackrox/helmtest/internal/parser" 9 | helmUtil "github.com/stackrox/helmtest/internal/rox-imported/helmutil" 10 | "github.com/stackrox/helmtest/internal/rox-imported/pointers" 11 | 12 | "github.com/pkg/errors" 13 | "helm.sh/helm/v3/pkg/chartutil" 14 | ) 15 | 16 | // applySetOptions takes the values specified in the `set` stanza and merges them into the otherwise defined values. 17 | func (t *Test) applySetOptions() error { 18 | for keyPathStr, val := range t.Set { 19 | vals, err := helmUtil.ValuesForKVPair(keyPathStr, val) 20 | if err != nil { 21 | return errors.Wrap(err, "in 'set'") 22 | } 23 | t.Values = chartutil.CoalesceTables(vals, t.Values) 24 | } 25 | t.Set = nil // no longer used, but make sure this is idempotent. 26 | 27 | return nil 28 | } 29 | 30 | // parseDefs parses the `Defs` section into a slice of `*gojq.FuncDef`s, and populates the `funcDefs` field. 31 | func (t *Test) parseDefs() error { 32 | defsStr := strings.TrimSpace(t.Defs) 33 | if defsStr == "" { 34 | return nil 35 | } 36 | if !strings.HasSuffix(defsStr, ";") { 37 | return errors.New("definitions block must end with a semicolon") 38 | } 39 | parsedDefs, err := parser.ParseQuery(defsStr, t.defsSrcCtx) 40 | if err != nil { 41 | return errors.Wrap(err, "parsing definitions") 42 | } 43 | t.funcDefs = parsedDefs.FuncDefs 44 | 45 | return nil 46 | } 47 | 48 | // parsePredicates parses the `Expect` section into a slice of `*gojq.Query` objects, and populates the `predicates` 49 | // field. 50 | func (t *Test) parsePredicates() error { 51 | expectStr := strings.TrimSpace(t.Expect) 52 | if expectStr == "" { 53 | return nil 54 | } 55 | 56 | predicates, err := parser.ParseExpectations(expectStr, t.expectSrcCtx) 57 | if err != nil { 58 | return errors.Wrap(err, "parsing expectations") 59 | } 60 | 61 | t.predicates = predicates 62 | 63 | return nil 64 | } 65 | 66 | // initialize initializes the test, parsing some string-based values into their semantic counterparts. It also 67 | // recursively initializes the sub-tests. initialize assumes that a name as well as the parent pointer has been set, and 68 | // that the parent is fully initialized. 69 | func (t *Test) initialize() error { 70 | if err := t.applySetOptions(); err != nil { 71 | return err 72 | } 73 | if err := t.parseDefs(); err != nil { 74 | return err 75 | } 76 | 77 | if t.ExpectError == nil { 78 | if t.parent != nil { 79 | t.ExpectError = t.parent.ExpectError 80 | } else { 81 | t.ExpectError = pointers.Bool(false) 82 | } 83 | } 84 | 85 | if err := t.parsePredicates(); err != nil { 86 | return errors.Wrap(err, "parsing predicates") 87 | } 88 | 89 | for i, subTest := range t.Tests { 90 | subTest.parent = t 91 | if subTest.Name == "" { 92 | subTest.Name = fmt.Sprintf("#%d", i) 93 | } 94 | if err := subTest.initialize(); err != nil { 95 | return errors.Wrapf(err, "initializing %q", subTest.Name) 96 | } 97 | } 98 | 99 | return nil 100 | } 101 | 102 | // Run runs a test against the given target. 103 | func (t *Test) Run(testingT *testing.T, tgt *Target) { 104 | testingT.Run(t.Name, func(testingT *testing.T) { 105 | testingT.Parallel() 106 | t.DoRun(testingT, tgt) 107 | }) 108 | } 109 | 110 | // DoRun runs a test directly, without an intermediate `testingT.Run` invocation. 111 | func (t *Test) DoRun(testingT *testing.T, tgt *Target) { 112 | if len(t.Tests) > 0 { 113 | // non-leaf case 114 | for _, subTest := range t.Tests { 115 | subTest.Run(testingT, tgt) 116 | } 117 | return 118 | } 119 | 120 | // leaf case 121 | runner := &runner{ 122 | t: testingT, 123 | test: t, 124 | tgt: tgt, 125 | } 126 | runner.Run() 127 | } 128 | 129 | // forEachScopeBottomUp runs the given doFn function for each test in the hierarchy, starting with the current 130 | // test and ending at the root (suite). 131 | func (t *Test) forEachScopeBottomUp(doFn func(t *Test)) { 132 | doFn(t) 133 | if t.parent == nil { 134 | return 135 | } 136 | t.parent.forEachScopeBottomUp(doFn) 137 | } 138 | 139 | // forEachScopeTopDown runs the given doFn function for each test in the hierarchy, starting with the root (suite) 140 | // and ending at the current test. 141 | func (t *Test) forEachScopeTopDown(doFn func(t *Test)) { 142 | if t.parent != nil { 143 | t.parent.forEachScopeTopDown(doFn) 144 | } 145 | doFn(t) 146 | } 147 | 148 | // findFirst tries to find the first test under a given path where the path's index represents the level of the test. 149 | func (t *Test) findFirst(path []string) *Test { 150 | r := t.find(path) 151 | if len(r) > 0 { 152 | return r[0] 153 | } 154 | 155 | return nil 156 | } 157 | 158 | // find tries to find all tests under a given path where the path's index represents the level of the test. 159 | func (t *Test) find(path []string) []*Test { 160 | var result []*Test 161 | if t == nil || len(path) == 0 { 162 | return nil 163 | } 164 | 165 | if path[0] != t.Name { 166 | return nil 167 | } 168 | if len(path) == 1 { 169 | return append(result, t) 170 | } 171 | 172 | for _, child := range t.Tests { 173 | found := child.find(path[1:]) 174 | if found != nil { 175 | return append(result, found...) 176 | } 177 | } 178 | 179 | return nil 180 | } 181 | -------------------------------------------------------------------------------- /pkg/framework/test_test.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestFind(t *testing.T) { 11 | childTest := &Test{ 12 | Name: "child test", 13 | Tests: []*Test{{ 14 | Name: "child child test", 15 | }}, 16 | } 17 | anotherChildTest := &Test{ 18 | Name: "another child test", 19 | Tests: []*Test{{ 20 | Name: "another child child test", 21 | }}, 22 | } 23 | 24 | suite := &Test{ 25 | Name: "root test", 26 | Tests: []*Test{ 27 | childTest, 28 | anotherChildTest, 29 | {Name: "same name"}, 30 | {Name: "same name"}, 31 | }, 32 | } 33 | 34 | testCases := map[string]struct { 35 | query []string 36 | expectNotFound bool 37 | }{ 38 | "with only root node": {query: []string{"root test"}}, 39 | "with child test": {query: []string{"root test", "child test"}}, 40 | "with nested child": {query: []string{"root test", "child test", "child child test"}}, 41 | "with not existing nested": {query: []string{"root test", "child test", "child child test", "does not exist"}, expectNotFound: true}, 42 | "with not existing root": {query: []string{"root does not exist"}, expectNotFound: true}, 43 | "with another child": {query: []string{"root test", "another child test"}}, 44 | "with another nested child": {query: []string{"root test", "another child test", "another child child test"}}, 45 | "with same name finds both": {query: []string{"root test", "same name"}}, 46 | "with not existent root should not be found": {query: []string{"non-existent root test", "child test"}, expectNotFound: true}, 47 | } 48 | 49 | for _, tt := range testCases { 50 | results := suite.find(tt.query) 51 | if tt.expectNotFound { 52 | assert.Empty(t, results) 53 | } else { 54 | require.NotEmpty(t, results) 55 | for _, result := range results { 56 | assert.Equal(t, tt.query[len(tt.query)-1], result.Name) 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/framework/testdata/additional_dir/additional.test.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - name: test in additional.test.yaml 3 | values: 4 | additional: additionalValue 5 | -------------------------------------------------------------------------------- /pkg/framework/testdata/suite/helm.test.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - name: test in helm.test.yaml 3 | values: 4 | testValue: value 5 | 6 | tests: 7 | - name: with overwrites 8 | values: 9 | testValue: value overwrite 10 | -------------------------------------------------------------------------------- /pkg/framework/testdata/suite/suite.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - name: test in suite.yaml 3 | -------------------------------------------------------------------------------- /pkg/framework/util.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "os" 5 | 6 | "k8s.io/kubectl/pkg/util/openapi" 7 | 8 | "github.com/pkg/errors" 9 | yamlv3 "gopkg.in/yaml.v3" 10 | ) 11 | 12 | // unmarshalYamlFromFileStrict unmarshals the contents of filename into out, relying on gopkg.in/yaml.v3 semantics. 13 | // Any field that is not present in the output data type, as well as any duplicate keys within the same YAML object, 14 | // will result in an error. 15 | func unmarshalYamlFromFileStrict(filename string, out interface{}) error { 16 | contents, err := os.ReadFile(filename) 17 | if err != nil { 18 | return err 19 | } 20 | var root yamlv3.Node 21 | if err := yamlv3.Unmarshal(contents, &root); err != nil { 22 | return errors.Wrapf(err, "parsing YAML in file %s", filename) 23 | } 24 | injectFilename(&root, filename) 25 | if err := root.Decode(out); err != nil { 26 | return errors.Wrapf(err, "decoding YAML in file %s", filename) 27 | } 28 | return nil 29 | } 30 | 31 | type openAPIResourcesGetter struct { 32 | resources openapi.Resources 33 | } 34 | 35 | func (o openAPIResourcesGetter) OpenAPISchema() (openapi.Resources, error) { 36 | return o.resources, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/framework/util_test.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stackrox/helmtest/internal/logic" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestTruthiness(t *testing.T) { 12 | truthyValues := []interface{}{ 13 | "foo", 14 | 1, 15 | map[string]interface{}{"foo": ""}, 16 | []string{"bar"}, 17 | 1.0, 18 | } 19 | 20 | for _, v := range truthyValues { 21 | assert.Truef(t, logic.Truthy(v), "expected value %v to be truthy", v) 22 | } 23 | 24 | falsyValues := []interface{}{ 25 | "", 26 | 0, 27 | map[string]interface{}(nil), 28 | map[string]interface{}{}, 29 | []string(nil), 30 | []string{}, 31 | 0.0, 32 | } 33 | 34 | for _, v := range falsyValues { 35 | assert.Falsef(t, logic.Truthy(v), "expected value %v to be falsy") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tool-imports/empty.go: -------------------------------------------------------------------------------- 1 | package toolimports 2 | 3 | // Empty file to ensure that this directory has at least one Go file even without build tags. 4 | -------------------------------------------------------------------------------- /tool-imports/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stackrox/kube-linter/tool-imports 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.2 6 | 7 | require github.com/golangci/golangci-lint v1.64.5 8 | 9 | require ( 10 | 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 11 | 4d63.com/gochecknoglobals v0.2.2 // indirect 12 | github.com/4meepo/tagalign v1.4.1 // indirect 13 | github.com/Abirdcfly/dupword v0.1.3 // indirect 14 | github.com/Antonboom/errname v1.0.0 // indirect 15 | github.com/Antonboom/nilnil v1.0.1 // indirect 16 | github.com/Antonboom/testifylint v1.5.2 // indirect 17 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 18 | github.com/Crocmagnon/fatcontext v0.7.1 // indirect 19 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect 20 | github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect 21 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 22 | github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect 23 | github.com/alecthomas/go-check-sumtype v0.3.1 // indirect 24 | github.com/alexkohler/nakedret/v2 v2.0.5 // indirect 25 | github.com/alexkohler/prealloc v1.0.0 // indirect 26 | github.com/alingse/asasalint v0.0.11 // indirect 27 | github.com/alingse/nilnesserr v0.1.2 // indirect 28 | github.com/ashanbrown/forbidigo v1.6.0 // indirect 29 | github.com/ashanbrown/makezero v1.2.0 // indirect 30 | github.com/beorn7/perks v1.0.1 // indirect 31 | github.com/bkielbasa/cyclop v1.2.3 // indirect 32 | github.com/blizzy78/varnamelen v0.8.0 // indirect 33 | github.com/bombsimon/wsl/v4 v4.5.0 // indirect 34 | github.com/breml/bidichk v0.3.2 // indirect 35 | github.com/breml/errchkjson v0.4.0 // indirect 36 | github.com/butuzov/ireturn v0.3.1 // indirect 37 | github.com/butuzov/mirror v1.3.0 // indirect 38 | github.com/catenacyber/perfsprint v0.8.1 // indirect 39 | github.com/ccojocar/zxcvbn-go v1.0.2 // indirect 40 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 41 | github.com/charithe/durationcheck v0.0.10 // indirect 42 | github.com/chavacava/garif v0.1.0 // indirect 43 | github.com/ckaznocha/intrange v0.3.0 // indirect 44 | github.com/curioswitch/go-reassign v0.3.0 // indirect 45 | github.com/daixiang0/gci v0.13.5 // indirect 46 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 47 | github.com/denis-tingaikin/go-header v0.5.0 // indirect 48 | github.com/ettle/strcase v0.2.0 // indirect 49 | github.com/fatih/color v1.18.0 // indirect 50 | github.com/fatih/structtag v1.2.0 // indirect 51 | github.com/firefart/nonamedreturns v1.0.5 // indirect 52 | github.com/fsnotify/fsnotify v1.7.0 // indirect 53 | github.com/fzipp/gocyclo v0.6.0 // indirect 54 | github.com/ghostiam/protogetter v0.3.9 // indirect 55 | github.com/go-critic/go-critic v0.12.0 // indirect 56 | github.com/go-toolsmith/astcast v1.1.0 // indirect 57 | github.com/go-toolsmith/astcopy v1.1.0 // indirect 58 | github.com/go-toolsmith/astequal v1.2.0 // indirect 59 | github.com/go-toolsmith/astfmt v1.1.0 // indirect 60 | github.com/go-toolsmith/astp v1.1.0 // indirect 61 | github.com/go-toolsmith/strparse v1.1.0 // indirect 62 | github.com/go-toolsmith/typep v1.1.0 // indirect 63 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 64 | github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect 65 | github.com/gobwas/glob v0.2.3 // indirect 66 | github.com/gofrs/flock v0.12.1 // indirect 67 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect 68 | github.com/golangci/go-printf-func-name v0.1.0 // indirect 69 | github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect 70 | github.com/golangci/misspell v0.6.0 // indirect 71 | github.com/golangci/plugin-module-register v0.1.1 // indirect 72 | github.com/golangci/revgrep v0.8.0 // indirect 73 | github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect 74 | github.com/google/go-cmp v0.6.0 // indirect 75 | github.com/gordonklaus/ineffassign v0.1.0 // indirect 76 | github.com/gostaticanalysis/analysisutil v0.7.1 // indirect 77 | github.com/gostaticanalysis/comment v1.4.2 // indirect 78 | github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect 79 | github.com/gostaticanalysis/nilerr v0.1.1 // indirect 80 | github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect 81 | github.com/hashicorp/go-version v1.7.0 // indirect 82 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 83 | github.com/hashicorp/hcl v1.0.1-vault-5 // indirect 84 | github.com/hexops/gotextdiff v1.0.3 // indirect 85 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 86 | github.com/jgautheron/goconst v1.7.1 // indirect 87 | github.com/jingyugao/rowserrcheck v1.1.1 // indirect 88 | github.com/jjti/go-spancheck v0.6.4 // indirect 89 | github.com/julz/importas v0.2.0 // indirect 90 | github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect 91 | github.com/kisielk/errcheck v1.8.0 // indirect 92 | github.com/kkHAIKE/contextcheck v1.1.5 // indirect 93 | github.com/kulti/thelper v0.6.3 // indirect 94 | github.com/kunwardeep/paralleltest v1.0.10 // indirect 95 | github.com/lasiar/canonicalheader v1.1.2 // indirect 96 | github.com/ldez/exptostd v0.4.1 // indirect 97 | github.com/ldez/gomoddirectives v0.6.1 // indirect 98 | github.com/ldez/grignotin v0.9.0 // indirect 99 | github.com/ldez/tagliatelle v0.7.1 // indirect 100 | github.com/ldez/usetesting v0.4.2 // indirect 101 | github.com/leonklingele/grouper v1.1.2 // indirect 102 | github.com/macabu/inamedparam v0.1.3 // indirect 103 | github.com/magiconair/properties v1.8.7 // indirect 104 | github.com/maratori/testableexamples v1.0.0 // indirect 105 | github.com/maratori/testpackage v1.1.1 // indirect 106 | github.com/matoous/godox v1.1.0 // indirect 107 | github.com/mattn/go-colorable v0.1.14 // indirect 108 | github.com/mattn/go-isatty v0.0.20 // indirect 109 | github.com/mattn/go-runewidth v0.0.16 // indirect 110 | github.com/mgechev/revive v1.6.1 // indirect 111 | github.com/mitchellh/go-homedir v1.1.0 // indirect 112 | github.com/mitchellh/mapstructure v1.5.0 // indirect 113 | github.com/moricho/tparallel v0.3.2 // indirect 114 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 115 | github.com/nakabonne/nestif v0.3.1 // indirect 116 | github.com/nishanths/exhaustive v0.12.0 // indirect 117 | github.com/nishanths/predeclared v0.2.2 // indirect 118 | github.com/nunnatsa/ginkgolinter v0.19.0 // indirect 119 | github.com/olekukonko/tablewriter v0.0.5 // indirect 120 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 121 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 122 | github.com/polyfloyd/go-errorlint v1.7.1 // indirect 123 | github.com/prometheus/client_golang v1.20.4 // indirect 124 | github.com/prometheus/client_model v0.6.1 // indirect 125 | github.com/prometheus/common v0.55.0 // indirect 126 | github.com/prometheus/procfs v0.15.1 // indirect 127 | github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect 128 | github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect 129 | github.com/quasilyte/gogrep v0.5.0 // indirect 130 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect 131 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect 132 | github.com/raeperd/recvcheck v0.2.0 // indirect 133 | github.com/rivo/uniseg v0.4.7 // indirect 134 | github.com/rogpeppe/go-internal v1.13.1 // indirect 135 | github.com/ryancurrah/gomodguard v1.3.5 // indirect 136 | github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect 137 | github.com/sagikazarmark/locafero v0.4.0 // indirect 138 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 139 | github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect 140 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect 141 | github.com/sashamelentyev/interfacebloat v1.1.0 // indirect 142 | github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect 143 | github.com/securego/gosec/v2 v2.22.1 // indirect 144 | github.com/sirupsen/logrus v1.9.3 // indirect 145 | github.com/sivchari/containedctx v1.0.3 // indirect 146 | github.com/sivchari/tenv v1.12.1 // indirect 147 | github.com/sonatard/noctx v0.1.0 // indirect 148 | github.com/sourcegraph/conc v0.3.0 // indirect 149 | github.com/sourcegraph/go-diff v0.7.0 // indirect 150 | github.com/spf13/afero v1.12.0 // indirect 151 | github.com/spf13/cast v1.7.0 // indirect 152 | github.com/spf13/cobra v1.8.1 // indirect 153 | github.com/spf13/pflag v1.0.6 // indirect 154 | github.com/spf13/viper v1.19.0 // indirect 155 | github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect 156 | github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect 157 | github.com/stretchr/objx v0.5.2 // indirect 158 | github.com/stretchr/testify v1.10.0 // indirect 159 | github.com/subosito/gotenv v1.6.0 // indirect 160 | github.com/tdakkota/asciicheck v0.4.0 // indirect 161 | github.com/tetafro/godot v1.4.20 // indirect 162 | github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect 163 | github.com/timonwong/loggercheck v0.10.1 // indirect 164 | github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect 165 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect 166 | github.com/ultraware/funlen v0.2.0 // indirect 167 | github.com/ultraware/whitespace v0.2.0 // indirect 168 | github.com/uudashr/gocognit v1.2.0 // indirect 169 | github.com/uudashr/iface v1.3.1 // indirect 170 | github.com/xen0n/gosmopolitan v1.2.2 // indirect 171 | github.com/yagipy/maintidx v1.0.0 // indirect 172 | github.com/yeya24/promlinter v0.3.0 // indirect 173 | github.com/ykadowak/zerologlint v0.1.5 // indirect 174 | gitlab.com/bosi/decorder v0.4.2 // indirect 175 | go-simpler.org/musttag v0.13.0 // indirect 176 | go-simpler.org/sloglint v0.9.0 // indirect 177 | go.uber.org/automaxprocs v1.6.0 // indirect 178 | go.uber.org/multierr v1.11.0 // indirect 179 | go.uber.org/zap v1.27.0 // indirect 180 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect 181 | golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect 182 | golang.org/x/mod v0.23.0 // indirect 183 | golang.org/x/sync v0.11.0 // indirect 184 | golang.org/x/sys v0.30.0 // indirect 185 | golang.org/x/text v0.22.0 // indirect 186 | golang.org/x/tools v0.30.0 // indirect 187 | google.golang.org/protobuf v1.36.4 // indirect 188 | gopkg.in/ini.v1 v1.67.0 // indirect 189 | gopkg.in/yaml.v2 v2.4.0 // indirect 190 | gopkg.in/yaml.v3 v3.0.1 // indirect 191 | honnef.co/go/tools v0.6.0 // indirect 192 | mvdan.cc/gofumpt v0.7.0 // indirect 193 | mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect 194 | ) 195 | -------------------------------------------------------------------------------- /tool-imports/go.sum: -------------------------------------------------------------------------------- 1 | 4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA= 2 | 4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs= 3 | 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= 4 | 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= 5 | github.com/4meepo/tagalign v1.4.1 h1:GYTu2FaPGOGb/xJalcqHeD4il5BiCywyEYZOA55P6J4= 6 | github.com/4meepo/tagalign v1.4.1/go.mod h1:2H9Yu6sZ67hmuraFgfZkNcg5Py9Ch/Om9l2K/2W1qS4= 7 | github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= 8 | github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw= 9 | github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA= 10 | github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI= 11 | github.com/Antonboom/nilnil v1.0.1 h1:C3Tkm0KUxgfO4Duk3PM+ztPncTFlOf0b2qadmS0s4xs= 12 | github.com/Antonboom/nilnil v1.0.1/go.mod h1:CH7pW2JsRNFgEh8B2UaPZTEPhCMuFowP/e8Udp9Nnb0= 13 | github.com/Antonboom/testifylint v1.5.2 h1:4s3Xhuv5AvdIgbd8wOOEeo0uZG7PbDKQyKY5lGoQazk= 14 | github.com/Antonboom/testifylint v1.5.2/go.mod h1:vxy8VJ0bc6NavlYqjZfmp6EfqXMtBgQ4+mhCojwC1P8= 15 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= 16 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 17 | github.com/Crocmagnon/fatcontext v0.7.1 h1:SC/VIbRRZQeQWj/TcQBS6JmrXcfA+BU4OGSVUt54PjM= 18 | github.com/Crocmagnon/fatcontext v0.7.1/go.mod h1:1wMvv3NXEBJucFGfwOJBxSVWcoIO6emV215SMkW9MFU= 19 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= 20 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= 21 | github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 h1:/fTUt5vmbkAcMBt4YQiuC23cV0kEsN1MVMNqeOW43cU= 22 | github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0/go.mod h1:ONJg5sxcbsdQQ4pOW8TGdTidT2TMAUy/2Xhr8mrYaao= 23 | github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= 24 | github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 25 | github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA= 26 | github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ= 27 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 28 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 29 | github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= 30 | github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= 31 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 32 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 33 | github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU= 34 | github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= 35 | github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= 36 | github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= 37 | github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= 38 | github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= 39 | github.com/alingse/nilnesserr v0.1.2 h1:Yf8Iwm3z2hUUrP4muWfW83DF4nE3r1xZ26fGWUKCZlo= 40 | github.com/alingse/nilnesserr v0.1.2/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= 41 | github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= 42 | github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= 43 | github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU= 44 | github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4= 45 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 46 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 47 | github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= 48 | github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= 49 | github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= 50 | github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= 51 | github.com/bombsimon/wsl/v4 v4.5.0 h1:iZRsEvDdyhd2La0FVi5k6tYehpOR/R7qIUjmKk7N74A= 52 | github.com/bombsimon/wsl/v4 v4.5.0/go.mod h1:NOQ3aLF4nD7N5YPXMruR6ZXDOAqLoM0GEpLwTdvmOSc= 53 | github.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs= 54 | github.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos= 55 | github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk= 56 | github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8= 57 | github.com/butuzov/ireturn v0.3.1 h1:mFgbEI6m+9W8oP/oDdfA34dLisRFCj2G6o/yiI1yZrY= 58 | github.com/butuzov/ireturn v0.3.1/go.mod h1:ZfRp+E7eJLC0NQmk1Nrm1LOrn/gQlOykv+cVPdiXH5M= 59 | github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= 60 | github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= 61 | github.com/catenacyber/perfsprint v0.8.1 h1:bGOHuzHe0IkoGeY831RW4aSlt1lPRd3WRAScSWOaV7E= 62 | github.com/catenacyber/perfsprint v0.8.1/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50= 63 | github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= 64 | github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= 65 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 66 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 67 | github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= 68 | github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= 69 | github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= 70 | github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= 71 | github.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd5ZY= 72 | github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo= 73 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 74 | github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= 75 | github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= 76 | github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= 77 | github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= 78 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 79 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 80 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 81 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 82 | github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= 83 | github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= 84 | github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= 85 | github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 86 | github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= 87 | github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= 88 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 89 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 90 | github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= 91 | github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= 92 | github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= 93 | github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= 94 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 95 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 96 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 97 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 98 | github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= 99 | github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= 100 | github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYFOI4lQ= 101 | github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= 102 | github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w= 103 | github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w= 104 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 105 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 106 | github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= 107 | github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 108 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 109 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 110 | github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= 111 | github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= 112 | github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= 113 | github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= 114 | github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= 115 | github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= 116 | github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw= 117 | github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= 118 | github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= 119 | github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= 120 | github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= 121 | github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= 122 | github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk= 123 | github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus= 124 | github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= 125 | github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= 126 | github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= 127 | github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= 128 | github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= 129 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 130 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 131 | github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= 132 | github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= 133 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 134 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 135 | github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= 136 | github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= 137 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= 138 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= 139 | github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU= 140 | github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= 141 | github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= 142 | github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= 143 | github.com/golangci/golangci-lint v1.64.5 h1:5omC86XFBKXZgCrVdUWU+WNHKd+CWCxNx717KXnzKZY= 144 | github.com/golangci/golangci-lint v1.64.5/go.mod h1:WZnwq8TF0z61h3jLQ7Sk5trcP7b3kUFxLD6l1ivtdvU= 145 | github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= 146 | github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= 147 | github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c= 148 | github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= 149 | github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= 150 | github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= 151 | github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs= 152 | github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ= 153 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 154 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 155 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 156 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 157 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 158 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 159 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 160 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= 161 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 162 | github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= 163 | github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= 164 | github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= 165 | github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= 166 | github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= 167 | github.com/gostaticanalysis/comment v1.4.2 h1:hlnx5+S2fY9Zo9ePo4AhgYsYHbM2+eAv8m/s1JiCd6Q= 168 | github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= 169 | github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk= 170 | github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY= 171 | github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= 172 | github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= 173 | github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= 174 | github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= 175 | github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= 176 | github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= 177 | github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= 178 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 179 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 180 | github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 181 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 182 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 183 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 184 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 185 | github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= 186 | github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= 187 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 188 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 189 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 190 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 191 | github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= 192 | github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= 193 | github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= 194 | github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= 195 | github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc= 196 | github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk= 197 | github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= 198 | github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= 199 | github.com/karamaru-alpha/copyloopvar v1.2.1 h1:wmZaZYIjnJ0b5UoKDjUHrikcV0zuPyyxI4SVplLd2CI= 200 | github.com/karamaru-alpha/copyloopvar v1.2.1/go.mod h1:nFmMlFNlClC2BPvNaHMdkirmTJxVCY0lhxBtlfOypMM= 201 | github.com/kisielk/errcheck v1.8.0 h1:ZX/URYa7ilESY19ik/vBmCn6zdGQLxACwjAcWbHlYlg= 202 | github.com/kisielk/errcheck v1.8.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= 203 | github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg= 204 | github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA= 205 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 206 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 207 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 208 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 209 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 210 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 211 | github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= 212 | github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= 213 | github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCTdvWJ/lDDs= 214 | github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= 215 | github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= 216 | github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= 217 | github.com/ldez/exptostd v0.4.1 h1:DIollgQ3LWZMp3HJbSXsdE2giJxMfjyHj3eX4oiD6JU= 218 | github.com/ldez/exptostd v0.4.1/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ= 219 | github.com/ldez/gomoddirectives v0.6.1 h1:Z+PxGAY+217f/bSGjNZr/b2KTXcyYLgiWI6geMBN2Qc= 220 | github.com/ldez/gomoddirectives v0.6.1/go.mod h1:cVBiu3AHR9V31em9u2kwfMKD43ayN5/XDgr+cdaFaKs= 221 | github.com/ldez/grignotin v0.9.0 h1:MgOEmjZIVNn6p5wPaGp/0OKWyvq42KnzAt/DAb8O4Ow= 222 | github.com/ldez/grignotin v0.9.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk= 223 | github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk= 224 | github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I= 225 | github.com/ldez/usetesting v0.4.2 h1:J2WwbrFGk3wx4cZwSMiCQQ00kjGR0+tuuyW0Lqm4lwA= 226 | github.com/ldez/usetesting v0.4.2/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ= 227 | github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= 228 | github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= 229 | github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= 230 | github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= 231 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 232 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 233 | github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= 234 | github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= 235 | github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= 236 | github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= 237 | github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= 238 | github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= 239 | github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= 240 | github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 241 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 242 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 243 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 244 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 245 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 246 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 247 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 248 | github.com/mgechev/revive v1.6.1 h1:ncK0ZCMWtb8GXwVAmk+IeWF2ULIDsvRxSRfg5sTwQ2w= 249 | github.com/mgechev/revive v1.6.1/go.mod h1:/2tfHWVO8UQi/hqJsIYNEKELi+DJy/e+PQpLgTB1v88= 250 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 251 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 252 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 253 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 254 | github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= 255 | github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= 256 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 257 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 258 | github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= 259 | github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= 260 | github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= 261 | github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= 262 | github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= 263 | github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= 264 | github.com/nunnatsa/ginkgolinter v0.19.0 h1:CnHRFAeBS3LdLI9h+Jidbcc5KH71GKOmaBZQk8Srnto= 265 | github.com/nunnatsa/ginkgolinter v0.19.0/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s= 266 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 267 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 268 | github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= 269 | github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= 270 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 271 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 272 | github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= 273 | github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= 274 | github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= 275 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= 276 | github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= 277 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= 278 | github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= 279 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 280 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 281 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 282 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 283 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 284 | github.com/polyfloyd/go-errorlint v1.7.1 h1:RyLVXIbosq1gBdk/pChWA8zWYLsq9UEw7a1L5TVMCnA= 285 | github.com/polyfloyd/go-errorlint v1.7.1/go.mod h1:aXjNb1x2TNhoLsk26iv1yl7a+zTnXPhwEMtEXukiLR8= 286 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 287 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 288 | github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= 289 | github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 290 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 291 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 292 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 293 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 294 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 295 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 296 | github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= 297 | github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= 298 | github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= 299 | github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= 300 | github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= 301 | github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= 302 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= 303 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= 304 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= 305 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= 306 | github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= 307 | github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= 308 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 309 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 310 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 311 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 312 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 313 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 314 | github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU= 315 | github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE= 316 | github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= 317 | github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= 318 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 319 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 320 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 321 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 322 | github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= 323 | github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= 324 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= 325 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= 326 | github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= 327 | github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= 328 | github.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxXUzwsMDBkR21cyQ= 329 | github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= 330 | github.com/securego/gosec/v2 v2.22.1 h1:IcBt3TpI5Y9VN1YlwjSpM2cHu0i3Iw52QM+PQeg7jN8= 331 | github.com/securego/gosec/v2 v2.22.1/go.mod h1:4bb95X4Jz7VSEPdVjC0hD7C/yR6kdeUBvCPOy9gDQ0g= 332 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 333 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 334 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 335 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 336 | github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= 337 | github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= 338 | github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY= 339 | github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw= 340 | github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= 341 | github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= 342 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 343 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 344 | github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= 345 | github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= 346 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= 347 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= 348 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= 349 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 350 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 351 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 352 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 353 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 354 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 355 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 356 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 357 | github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= 358 | github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= 359 | github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= 360 | github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= 361 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 362 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 363 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 364 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 365 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 366 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 367 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 368 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 369 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 370 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 371 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 372 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 373 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 374 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 375 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 376 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 377 | github.com/tdakkota/asciicheck v0.4.0 h1:VZ13Itw4k1i7d+dpDSNS8Op645XgGHpkCEh/WHicgWw= 378 | github.com/tdakkota/asciicheck v0.4.0/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8= 379 | github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= 380 | github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= 381 | github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= 382 | github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= 383 | github.com/tetafro/godot v1.4.20 h1:z/p8Ek55UdNvzt4TFn2zx2KscpW4rWqcnUrdmvWJj7E= 384 | github.com/tetafro/godot v1.4.20/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= 385 | github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 h1:y4mJRFlM6fUyPhoXuFg/Yu02fg/nIPFMOY8tOqppoFg= 386 | github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= 387 | github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg= 388 | github.com/timonwong/loggercheck v0.10.1/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= 389 | github.com/tomarrell/wrapcheck/v2 v2.10.0 h1:SzRCryzy4IrAH7bVGG4cK40tNUhmVmMDuJujy4XwYDg= 390 | github.com/tomarrell/wrapcheck/v2 v2.10.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= 391 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= 392 | github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= 393 | github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= 394 | github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= 395 | github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= 396 | github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= 397 | github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA= 398 | github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= 399 | github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U= 400 | github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= 401 | github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= 402 | github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= 403 | github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= 404 | github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= 405 | github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= 406 | github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= 407 | github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= 408 | github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= 409 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 410 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 411 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 412 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 413 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 414 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 415 | gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= 416 | gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= 417 | go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= 418 | go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= 419 | go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= 420 | go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= 421 | go-simpler.org/sloglint v0.9.0 h1:/40NQtjRx9txvsB/RN022KsUJU+zaaSb/9q9BSefSrE= 422 | go-simpler.org/sloglint v0.9.0/go.mod h1:G/OrAF6uxj48sHahCzrbarVMptL2kjWTaUeC8+fOGww= 423 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 424 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 425 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 426 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 427 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 428 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 429 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 430 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 431 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 432 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 433 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 434 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 435 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 436 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 437 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= 438 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= 439 | golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 440 | golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 441 | golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4= 442 | golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 443 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 444 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 445 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 446 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 447 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 448 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 449 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 450 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 451 | golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 452 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 453 | golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 454 | golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= 455 | golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 456 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 457 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 458 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 459 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 460 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 461 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 462 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 463 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 464 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 465 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 466 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 467 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 468 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 469 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 470 | golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 471 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 472 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 473 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 474 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 475 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 476 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 477 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 478 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 479 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 480 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 481 | golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 482 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 483 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 484 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 485 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 486 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 487 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 488 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 489 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 490 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 491 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 492 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 493 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 494 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 495 | golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 496 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 497 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 498 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 499 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 500 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 501 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 502 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 503 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 504 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 505 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 506 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 507 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 508 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 509 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 510 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 511 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 512 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 513 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 514 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 515 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 516 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 517 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 518 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 519 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 520 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 521 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 522 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 523 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 524 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 525 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 526 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 527 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 528 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 529 | golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 530 | golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 531 | golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 532 | golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 533 | golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 534 | golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= 535 | golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= 536 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 537 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 538 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 539 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 540 | golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= 541 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 542 | golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 543 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 544 | golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 545 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 546 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 547 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 548 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 549 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 550 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 551 | google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= 552 | google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 553 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 554 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 555 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 556 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 557 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 558 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 559 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 560 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 561 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 562 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 563 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 564 | honnef.co/go/tools v0.6.0 h1:TAODvD3knlq75WCp2nyGJtT4LeRV/o7NN9nYPeVJXf8= 565 | honnef.co/go/tools v0.6.0/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= 566 | mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= 567 | mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= 568 | mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= 569 | mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ= 570 | -------------------------------------------------------------------------------- /tool-imports/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package toolimports 5 | 6 | // This file declares dependencies on tool for `go mod` purposes. 7 | // See https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 8 | // for an explanation of the approach. 9 | 10 | import ( 11 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 12 | ) 13 | --------------------------------------------------------------------------------