├── .github └── workflows │ └── go.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── README_CN.md ├── charts.go ├── config-example.yaml ├── deployment ├── deployment.yaml ├── rbac.yaml ├── service.yaml └── serviceaccount.yaml ├── envs.go ├── go.mod ├── go.sum ├── helm-wrapper ├── .helmignore ├── Chart.yaml ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── configmap.yaml │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ │ └── test-connection.yaml └── values.yaml ├── helm.go ├── main.go ├── registries.go ├── releases.go ├── repositories.go ├── router.go └── upload.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Build 33 | run: make build 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # vim swap file 16 | *.swp 17 | 18 | .idea 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | ENV GIN_MODE=release 4 | 5 | #COPY config-example.yaml /config.yaml 6 | COPY bin/helm-wrapper /helm-wrapper 7 | 8 | ENTRYPOINT [ "/helm-wrapper" ] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kumu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY_NAME=helm-wrapper 2 | 3 | GOPATH = $(shell go env GOPATH) 4 | 5 | LDFLAGS="-s -w" 6 | 7 | build: 8 | go build -ldflags ${LDFLAGS} -o bin/${BINARY_NAME} 9 | 10 | # cross compilation 11 | build-linux: 12 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags ${LDFLAGS} -o bin/${BINARY_NAME} 13 | 14 | build-windows: 15 | CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags ${LDFLAGS} -o bin/${BINARY_NAME} 16 | 17 | # build docker image 18 | build-docker: 19 | rm -rf bin/${BINARY_NAME} 20 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags ${LDFLAGS} -o bin/${BINARY_NAME} 21 | docker build -t helm-wrapper:`git rev-parse --short HEAD` . 22 | 23 | .PHONY: golangci-lint 24 | golangci-lint: $(GOLANGCILINT) 25 | @echo 26 | $(GOPATH)/bin/golangci-lint run 27 | 28 | $(GOLANGCILINT): 29 | (cd /; GO111MODULE=on GOPROXY="direct" GOSUMDB=off go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.30.0) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A [Helm3](https://github.com/helm/helm) HTTP Wrapper With [Go SDK](https://helm.sh/docs/topics/advanced/#go-sdk) 2 | 3 | + [中文文档](README_CN.md) 4 | 5 | helm-wrapper is a helm3 HTTP wrapper with [helm Go SDK](https://helm.sh/docs/topics/advanced/#go-sdk). With helm-wrapper, you can use HTTP RESTFul API do something like helm commondline (install/uninstall/upgrade/get/list/rollback...). 6 | 7 | ## Support API 8 | 9 | 10 | * If there are some APIs (`release` related) need to support multiple clusters,you can use the parameters below 11 | 12 | | Params | Description | 13 | |:---| :--- | 14 | | kube_context | Support distinguish multiple clusters by the`kube_context` | 15 | | kube_config | Support distinguish multiple clusters by the`kube_config` | 16 | 17 | 18 | 19 | + helm install 20 | - `POST` 21 | - `/api/namespaces/:namespace/releases/:release?chart=` 22 | 23 | POST Body: 24 | 25 | ``` json 26 | { 27 | "dry_run": false, // `--dry-run` 28 | "disable_hooks": false, // `--no-hooks` 29 | "wait": false, // `--wait` 30 | "timeout": "5m0s", // `--timeout` 31 | "devel": false, // `--false` 32 | "description": "", // `--description` 33 | "atomic": false, // `--atomic` 34 | "skip_crds": false, // `--skip-crds` 35 | "sub_notes": false, // `--render-subchart-notes` 36 | "create_namespace": false, // `--create-namespace` 37 | "dependency_update": false, // `--dependency-update` 38 | "values": "", // `--values` 39 | "set": [], // `--set` 40 | "set_string": [], // `--set-string` 41 | "ca_file": "", // `--ca-file` 42 | "cert_file": "", // `--cert-file` 43 | "key_file": "", // `--key-file` 44 | "insecure_skip_verify": false, // `--insecure-skip-verify` 45 | "keyring": "", // `--keyring` 46 | "password": "", // `--password` 47 | "repo": "", // `--repo` 48 | "username": "", // `--username` 49 | "verify": false, // `--verify` 50 | "version": "" // `--version` 51 | } 52 | ``` 53 | 54 | > `"values"` -> helm install `--values` option 55 | 56 | + helm uninstall 57 | - `DELETE` 58 | - `/api/namespaces/:namespace/releases/:release` 59 | 60 | Delete Body: 61 | ``` json 62 | { 63 | "dry_run": false, // `--dry-run` 64 | "disable_hooks": false, // `--no-hooks` 65 | "wait": false, // `--wait` 66 | "timeout": "5m0s", // `--timeout` 67 | "description": "", // `--description` 68 | "ignore_not_found": false, // `--ignore-not-found` 69 | } 70 | 71 | ``` 72 | + helm upgrade 73 | - `PUT` 74 | - `/api/namespaces/:namespace/releases/:release?chart=` 75 | 76 | PUT Body: 77 | 78 | ``` json 79 | { 80 | "dry_run": false, // `--dry-run` 81 | "disable_hooks": false, // `--no-hooks` 82 | "wait": false, // `--wait` 83 | "timeout": "5m0s", // `--timeout` 84 | "devel": false, // `--false` 85 | "description": "", // `--description` 86 | "atomic": false, // `--atomic` 87 | "skip_crds": false, // `--skip-crds` 88 | "sub_notes": false, // `--render-subchart-notes` 89 | "force": false, // `--force` 90 | "install": false, // `--install` 91 | "recreate": false, // `--recreate` 92 | "reuse_values": false, // `--reuse-values` 93 | "cleanup_on_fail": false, // `--cleanup-on-fail` 94 | "values": "", // `--values` 95 | "set": [], // `--set` 96 | "set_string": [], // `--set-string` 97 | "ca_file": "", // `--ca-file` 98 | "cert_file": "", // `--cert-file` 99 | "key_file": "", // `--key-file` 100 | "insecure_skip_verify": false, // `--insecure-skip-verify` 101 | "keyring": "", // `--keyring` 102 | "password": "", // `--password` 103 | "repo": "", // `--repo` 104 | "username": "", // `--username` 105 | "verify": false, // `--verify` 106 | "version": "" // `--version` 107 | } 108 | ``` 109 | 110 | > `"values"` -> helm install `--values` option 111 | 112 | + helm rollback 113 | - `PUT` 114 | - `/api/namespaces/:namespace/releases/:release/versions/:reversion` 115 | 116 | PUT Body (optional): 117 | 118 | ``` json 119 | { 120 | "dry_run": false, // `--dry-run` 121 | "disable_hooks": false, // `--no-hooks` 122 | "wait": false, // `--wait` 123 | "timeout": "5m0s", // `--timeout` 124 | "force": false, // `--force` 125 | "recreate": false, // `--recreate` 126 | "cleanup_on_fail": false, // `--cleanup-on-fail` 127 | "history_max": // `--history-max` int 128 | } 129 | ``` 130 | 131 | + helm list 132 | - `GET` 133 | - `/api/namespaces/:namespace/releases` 134 | 135 | Body: 136 | 137 | ``` json 138 | { 139 | "all": false, // `--all` 140 | "all_namespaces": false, // `--all-namespaces` 141 | "by_date": false, // `--date` 142 | "sort_reverse": false, // `--reverse` 143 | "limit": , // `--max` 144 | "offset": , // `--offset` 145 | "filter": "", // `--filter` 146 | "uninstalled": false, // `--uninstalled` 147 | "uninstalling": false, // `--uninstalling` 148 | "superseded": false, // `--superseded` 149 | "failed": false, // `--failed` 150 | "deployed": false, // `--deployed` 151 | "pending": false // `--pending` 152 | } 153 | ``` 154 | 155 | + helm get 156 | - `GET` 157 | - `/api/namespaces/:namespace/releases/:release` 158 | 159 | | Params | Description | 160 | | :--- | :--- | 161 | | info | support hooks/manifest/notes/values, default values | 162 | | output | get values output format (only info==values), support json/yaml, default json | 163 | 164 | + helm release history 165 | - `GET` 166 | - `/api/namespaces/:namespace/releases/:release/histories` 167 | 168 | 169 | + helm show 170 | - `GET` 171 | - `/api/charts` 172 | 173 | | Params | Description | 174 | | :--- | :--- | 175 | | chart | chart name, required| 176 | | info | support all/readme/values/chart, default all | 177 | | version | --version | 178 | 179 | + helm search repo 180 | - `GET` 181 | - `/api/repositories/charts` 182 | 183 | | Params | Description | 184 | | :--- | :--- | 185 | | keyword | search keyword,required | 186 | | version | chart version | 187 | | versions | if "true", all versions | 188 | 189 | + helm repo list 190 | - `GET` 191 | - `/api/repositories` 192 | 193 | + helm repo update 194 | - `PUT` 195 | - `/api/repositories` 196 | 197 | + helm env 198 | - `GET` 199 | - `/api/envs` 200 | 201 | + upload chart 202 | - `POST` 203 | - `/api/charts/upload` 204 | 205 | | Params | Description | 206 | | :--- | :--- | 207 | | chart | upload chart file, with suffix .tgz | 208 | 209 | + list local charts 210 | - `GET` 211 | - `/api/charts/upload` 212 | 213 | > __Notes:__ helm-wrapper is Alpha status, no more test 214 | 215 | ### Response 216 | 217 | 218 | ``` go 219 | type respBody struct { 220 | Code int `json:"code"` // 0 or 1, 0 is ok, 1 is error 221 | Data interface{} `json:"data,omitempty"` 222 | Error string `json:"error,omitempty"` 223 | } 224 | ``` 225 | 226 | 227 | ## Build & Run 228 | 229 | ### Build 230 | 231 | ``` 232 | make build 233 | make build-linux // build helm-wrapper Linux binary 234 | make build-docker // build docker image with helm-wrapper 235 | ``` 236 | 237 | #### helm-wrapper help 238 | 239 | ``` 240 | $ helm-wrapper -h 241 | Usage of helm-wrapper: 242 | --addr string server listen addr (default "0.0.0.0") 243 | --alsologtostderr log to standard error as well as files 244 | --config string helm wrapper config (default "config.yaml") 245 | --debug enable verbose output 246 | --kube-context string name of the kubeconfig context to use 247 | --kubeconfig string path to the kubeconfig file 248 | --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) 249 | --log_dir string If non-empty, write log files in this directory 250 | --logtostderr log to standard error instead of files (default true) 251 | -n, --namespace string namespace scope for this request 252 | --port string server listen port (default "8080") 253 | --registry-config string path to the registry config file (default "/root/.config/helm/registry.json") 254 | --repository-cache string path to the file containing cached repository indexes (default "/root/.cache/helm/repository") 255 | --repository-config string path to the file containing repository names and URLs (default "/root/.config/helm/repositories.yaml") 256 | --stderrthreshold severity logs at or above this threshold go to stderr (default 2) 257 | -v, --v Level log level for V logs 258 | --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging 259 | pflag: help requested 260 | ``` 261 | 262 | + `--config` helm-wrapper configuration: 263 | 264 | ``` 265 | $ cat config-example.yaml 266 | uploadPath: /tmp/charts 267 | helmRepos: 268 | - name: bitnami 269 | url: https://charts.bitnami.com/bitnami 270 | ``` 271 | 272 | + `--kubeconfig` default kubeconfig path is `~/.kube/config`.About `kubeconfig`, you can see [Configure Access to Multiple Clusters](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/). 273 | 274 | ### Run 275 | 276 | ``` 277 | $ ./helm-wrapper --config --kubeconfig 278 | ``` 279 | 280 | #### Deploy in Kubernetes Cluster 281 | 282 | replace deployment/deployment.yaml with helm-wrapper image, then: 283 | 284 | ``` 285 | kubectl create -f ./deployment 286 | ``` 287 | 288 | > __Noets:__ with deployment/rbac.yaml, you not need `--kubeconfig` 289 | 290 | ## OCI Registries 291 | For helm install, upgrade, and upgrade-install, replace the `chart` parameter with the OCI registry URL for the chart. 292 | 293 | ### Authentication 294 | There are three ways to authenticate to an OCI registry: request body, config.yaml, and helm settings registry config file. You only need to use one of these methods, but it also shouldn't cause any issues if you provide all three (not that it is advisable to do so). 295 | 296 | #### Request body 297 | You can include a `username` and `password` in the request body. 298 | 299 | #### config.yaml 300 | Similar to how you can add a helm repo under the `helmRepos` key in `config.yaml`, you can add a registry under the `helmRegistries` key. The domain must match the chart registry URL in the upgrade or install request. For example: 301 | ``` 302 | helmRegistries: 303 | - name: registry_name 304 | url: oci://registry.com 305 | username: username 306 | password: password 307 | ``` 308 | 309 | #### Helm settings registry config file 310 | Both of the previous methods will also create/update the registry config file (the same as the helm CLI). However, you can also put this file in the container and helm-wrapper will use that to authenticate. By default, this file is located at `/home/helm/.config/helm/registry/config.json`. Again, the domain must match the chart registry URL in the upgrade or install request. Refer to helm documentation on how to configure this. You can also use one of the other authentication methods and then look at the file that is created in the container. 311 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # A [Helm3](https://github.com/helm/helm) HTTP Wrapper With Go SDK 2 | 3 | Helm3 摒弃了 Helm2 的 Tiller 架构,使用纯命令行的方式执行相关操作。如果想通过 Helm API 来实现相关功能,很遗憾官方并没有提供类似的服务。不过,因为官方提供了相对友好的 [Helm Go SDK](https://helm.sh/docs/topics/advanced/),我们只需在此基础上做封装即可实现。[helm-wrapper](https://github.com/opskumu/helm-wrapper) 就是这样一个通过 Go [Gin](https://github.com/gin-gonic/gin) Web 框架,结合 Helm Go SDK 封装的 HTTP Server,让 Helm 相关的日常命令操作可以通过 Restful API 的方式来实现同样的操作。 4 | 5 | ## Support API 6 | 7 | * 如果某些API(release相关)需要支持多个集群,则可以使用以下参数 8 | 9 | | Params | Description | 10 | | :--- | :--- | 11 | | kube_context | 支持指定kube_context来区分不同集群 | 12 | | kube_config | 支持指定kube_config来区分不同集群 | 13 | 14 | helm 原生命令行和相关 API 对应关系: 15 | 16 | + helm install 17 | - `POST` 18 | - `/api/namespaces/:namespace/releases/:release?chart=` 19 | 20 | POST Body: 21 | 22 | ``` json 23 | { 24 | "dry_run": false, // `--dry-run` 25 | "disable_hooks": false, // `--no-hooks` 26 | "wait": false, // `--wait` 27 | "timeout": "5m0s", // `--timeout` 28 | "devel": false, // `--false` 29 | "description": "", // `--description` 30 | "atomic": false, // `--atomic` 31 | "skip_crds": false, // `--skip-crds` 32 | "sub_notes": false, // `--render-subchart-notes` 33 | "create_namespace": false, // `--create-namespace` 34 | "dependency_update": false, // `--dependency-update` 35 | "values": "", // `--values` 36 | "set": [], // `--set` 37 | "set_string": [], // `--set-string` 38 | "ca_file": "", // `--ca-file` 39 | "cert_file": "", // `--cert-file` 40 | "key_file": "", // `--key-file` 41 | "insecure_skip_verify": "", // `--insecure-skip-verify` 42 | "keyring": "", // `--keyring` 43 | "password": "", // `--password` 44 | "repo": "", // `--repo` 45 | "username": "", // `--username` 46 | "verify": false, // `--verify` 47 | "version": "" // `--version` 48 | } 49 | ``` 50 | 51 | > 此处 values 内容同 helm install `--values` 选项 52 | 53 | + helm uninstall 54 | - `DELETE` 55 | - `/api/namespaces/:namespace/releases/:release` 56 | 57 | 58 | + helm upgrade 59 | - `PUT` 60 | - `/api/namespaces/:namespace/releases/:release?chart=` 61 | 62 | PUT Body: 63 | 64 | ``` json 65 | { 66 | "dry_run": false, // `--dry-run` 67 | "disable_hooks": false, // `--no-hooks` 68 | "wait": false, // `--wait` 69 | "timeout": "5m0s", // `--timeout` 70 | "devel": false, // `--false` 71 | "description": "", // `--description` 72 | "atomic": false, // `--atomic` 73 | "skip_crds": false, // `--skip-crds` 74 | "sub_notes": false, // `--render-subchart-notes` 75 | "force": false, // `--force` 76 | "install": false, // `--install` 77 | "recreate": false, // `--recreate` 78 | "reuse_values": false, // `--reuse-values` 79 | "cleanup_on_fail": false, // `--cleanup-on-fail` 80 | "values": "", // `--values` 81 | "set": [], // `--set` 82 | "set_string": [], // `--set-string` 83 | "ca_file": "", // `--ca-file` 84 | "cert_file": "", // `--cert-file` 85 | "key_file": "", // `--key-file` 86 | "insecure_skip_verify": "", // `--insecure-skip-verify` 87 | "keyring": "", // `--keyring` 88 | "password": "", // `--password` 89 | "repo": "", // `--repo` 90 | "username": "", // `--username` 91 | "verify": false, // `--verify` 92 | "version": "" // `--version` 93 | } 94 | ``` 95 | 96 | 97 | > 此处 values 内容同 helm upgrade `--values` 选项 98 | 99 | + helm rollback 100 | - `PUT` 101 | - `/api/namespaces/:namespace/releases/:release/versions/:reversion` 102 | 103 | PUT Body 可选: 104 | 105 | ``` json 106 | { 107 | "dry_run": false, // `--dry-run` 108 | "disable_hooks": false, // `--no-hooks` 109 | "wait": false, // `--wait` 110 | "timeout": "5m0s", // `--timeout` 111 | "force": false, // `--force` 112 | "recreate": false, // `--recreate` 113 | "cleanup_on_fail": false, // `--cleanup-on-fail` 114 | "history_max": // `--history-max` int 115 | } 116 | ``` 117 | 118 | + helm list 119 | - `GET` 120 | - `/api/namespaces/:namespace/releases` 121 | 122 | Body: 123 | 124 | ``` json 125 | { 126 | "all": false, // `--all` 127 | "all_namespaces": false, // `--all-namespaces` 128 | "by_date": false, // `--date` 129 | "sort_reverse": false, // `--reverse` 130 | "limit": , // `--max` 131 | "offset": , // `--offset` 132 | "filter": "", // `--filter` 133 | "uninstalled": false, // `--uninstalled` 134 | "uninstalling": false, // `--uninstalling` 135 | "superseded": false, // `--superseded` 136 | "failed": false, // `--failed` 137 | "deployed": false, // `--deployed` 138 | "pending": false // `--pending` 139 | } 140 | ``` 141 | 142 | + helm get 143 | - `GET` 144 | - `/api/namespaces/:namespace/releases/:release` 145 | 146 | | Params | Description | 147 | | :--- | :--- | 148 | | info | 支持 hooks/manifest/notes/values 信息,默认为 values | 149 | | output | values 输出格式(仅当 info=values 时有效),支持 json/yaml,默认为 json | 150 | 151 | + helm release history 152 | - `GET` 153 | - `/api/namespaces/:namespace/releases/:release/histories` 154 | 155 | 156 | + helm show 157 | - `GET` 158 | - `/api/charts` 159 | 160 | | Params | Description | 161 | | :--- | :--- | 162 | | chart | 指定 chart 名,必填 | 163 | | info | 支持 all/readme/values/chart 信息,默认为 all | 164 | | version | 支持版本指定,同命令行 | 165 | 166 | + helm search repo 167 | - `GET` 168 | - `/api/repositories/charts` 169 | 170 | | Params | Description | 171 | | :--- | :--- | 172 | | keyword | 搜索关键字,必填 | 173 | | version | 指定 chart version | 174 | | versions | if "true", all versions | 175 | 176 | + helm repo list 177 | - `GET` 178 | - `/api/repositories` 179 | 180 | 181 | + helm repo update 182 | - `PUT` 183 | - `/api/repositories` 184 | 185 | + helm env 186 | - `GET` 187 | - `/api/envs` 188 | 189 | + upload chart 190 | - `POST` 191 | - `/api/charts/upload` 192 | 193 | | Params | Description | 194 | | :--- | :--- | 195 | | chart | chart 包,必须为 .tgz 文件 | 196 | 197 | + list local charts 198 | - `GET` 199 | - `/api/charts/upload` 200 | 201 | > 当前该版本处于 Alpha 状态,还没有经过大量的测试,只是把相关的功能测试了一遍,你也可以在此基础上自定义适合自身的版本。 202 | 203 | ### 响应 204 | 205 | 为了简化,所有请求统一返回 200 状态码,通过返回 Body 中的 Code 值来判断响应是否正常: 206 | 207 | ``` go 208 | type respBody struct { 209 | Code int `json:"code"` // 0 or 1, 0 is ok, 1 is error 210 | Data interface{} `json:"data,omitempty"` 211 | Error string `json:"error,omitempty"` 212 | } 213 | ``` 214 | 215 | 216 | ## Build & Run 217 | 218 | ### Build 219 | 220 | 源码提供了简单的 `Makefile` 文件,如果要构建二进制,只需要通过以下方式构建即可。 221 | 222 | ``` 223 | make build // 构建当前主机架构的二进制版本 224 | make build-linux // 构建 Linux 版本的二进制 225 | make build-docker // 构建 Docker 镜像 226 | ``` 227 | 228 | 直接构建会生成名为 `helm-wrapper` 的二进制程序,你可以通过如下方式获取帮助: 229 | 230 | ``` 231 | $ helm-wrapper -h 232 | Usage of helm-wrapper: 233 | --addr string server listen addr (default "0.0.0.0") 234 | --alsologtostderr log to standard error as well as files 235 | --config string helm wrapper config (default "config.yaml") 236 | --debug enable verbose output 237 | --kube-context string name of the kubeconfig context to use 238 | --kubeconfig string path to the kubeconfig file 239 | --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) 240 | --log_dir string If non-empty, write log files in this directory 241 | --logtostderr log to standard error instead of files (default true) 242 | -n, --namespace string namespace scope for this request 243 | --port string server listen port (default "8080") 244 | --registry-config string path to the registry config file (default "/root/.config/helm/registry.json") 245 | --repository-cache string path to the file containing cached repository indexes (default "/root/.cache/helm/repository") 246 | --repository-config string path to the file containing repository names and URLs (default "/root/.config/helm/repositories.yaml") 247 | --stderrthreshold severity logs at or above this threshold go to stderr (default 2) 248 | -v, --v Level log level for V logs 249 | --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging 250 | pflag: help requested 251 | ``` 252 | 253 | 关键性的选项说明一下: 254 | 255 | + `--config` helm-wrapper 的配置项,内容如下,主要是指定 Helm Repo 命名和 URL,用于 Repo 初始化。 256 | 257 | ``` 258 | $ cat config-example.yaml 259 | uploadPath: /tmp/charts 260 | helmRepos: 261 | - name: bitnami 262 | url: https://charts.bitnami.com/bitnami 263 | ``` 264 | + `--kubeconfig` 默认如果你不指定的话,使用默认的路径,一般是 `~/.kube/config`。这个配置是必须的,这指明了你要操作的 Kubernetes 集群地址以及访问方式。`kubeconfig` 文件如何生成,这里不过多介绍,具体可以详见 [Configure Access to Multiple Clusters](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/) 265 | 266 | ### Run 267 | 268 | 运行比较简单,如果你本地已经有默认的 `kubeconfig` 文件,只需要把 helm-wrapper 需要的 repo 配置文件配置好即可,然后执行以下命令即可运行,示例如下: 269 | 270 | ``` 271 | $ ./helm-wrapper --config --kubeconfig 272 | ``` 273 | 274 | > 启动时会先初始化 repo,因此根据 repo 本身的大小或者网络因素,会耗费些时间 275 | 276 | #### 运行在 Kubernetes 集群中 277 | 278 | 替换 `deployment/deployment.yaml` 中 image 字段为你正确的 helm-wrapper 镜像地址即可,然后执行命令部署: 279 | 280 | ``` 281 | kubectl create -f ./deployment 282 | ``` 283 | 284 | > __注:__ 以上操作会创建 RBAC 相关,因此不需要在构建镜像的时候额外添加 kubeconfig 文件,默认会拥有相关的权限 285 | -------------------------------------------------------------------------------- /charts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | "helm.sh/helm/v3/pkg/action" 9 | "helm.sh/helm/v3/pkg/chart" 10 | "helm.sh/helm/v3/pkg/chart/loader" 11 | "helm.sh/helm/v3/pkg/chartutil" 12 | ) 13 | 14 | var readmeFileNames = []string{"readme.md", "readme.txt", "readme"} 15 | 16 | type file struct { 17 | Name string `json:"name"` 18 | Data string `json:"data"` 19 | } 20 | 21 | func findReadme(files []*chart.File) (file *chart.File) { 22 | for _, file := range files { 23 | for _, n := range readmeFileNames { 24 | if strings.EqualFold(file.Name, n) { 25 | return file 26 | } 27 | } 28 | } 29 | return nil 30 | } 31 | 32 | func showChartInfo(c *gin.Context) { 33 | name := c.Query("chart") 34 | if name == "" { 35 | respErr(c, fmt.Errorf("chart name can not be empty")) 36 | return 37 | } 38 | 39 | // local charts with abs path *.tgz 40 | splitChart := strings.Split(name, ".") 41 | if splitChart[len(splitChart)-1] == "tgz" && !strings.Contains(name, ":") { 42 | name = helmConfig.UploadPath + "/" + name 43 | } 44 | 45 | info := c.Query("info") // all, readme, values, chart 46 | if info == "" { 47 | info = string(action.ShowAll) 48 | } 49 | version := c.Query("version") 50 | 51 | client := action.NewShow(action.ShowAll) 52 | client.Version = version 53 | if info == string(action.ShowChart) { 54 | client.OutputFormat = action.ShowChart 55 | } else if info == string(action.ShowReadme) { 56 | client.OutputFormat = action.ShowReadme 57 | } else if info == string(action.ShowValues) { 58 | client.OutputFormat = action.ShowValues 59 | } else if info == string(action.ShowAll) { 60 | client.OutputFormat = action.ShowAll 61 | } else { 62 | respErr(c, fmt.Errorf("bad info %s, chart info only support readme/values/chart", info)) 63 | return 64 | } 65 | 66 | cp, err := client.ChartPathOptions.LocateChart(name, settings) 67 | if err != nil { 68 | respErr(c, err) 69 | return 70 | } 71 | 72 | chrt, err := loader.Load(cp) 73 | if err != nil { 74 | respErr(c, err) 75 | return 76 | } 77 | 78 | if client.OutputFormat == action.ShowChart { 79 | respOK(c, chrt.Metadata) 80 | return 81 | } 82 | if client.OutputFormat == action.ShowValues { 83 | var values string 84 | for _, v := range chrt.Raw { 85 | if v.Name == chartutil.ValuesfileName { 86 | values = string(v.Data) 87 | break 88 | } 89 | } 90 | respOK(c, values) 91 | return 92 | } 93 | if client.OutputFormat == action.ShowReadme { 94 | respOK(c, string(findReadme(chrt.Files).Data)) 95 | return 96 | } 97 | if client.OutputFormat == action.ShowAll { 98 | values := make([]*file, 0, len(chrt.Raw)) 99 | for _, v := range chrt.Raw { 100 | values = append(values, &file{ 101 | Name: v.Name, 102 | Data: string(v.Data), 103 | }) 104 | } 105 | respOK(c, values) 106 | return 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /config-example.yaml: -------------------------------------------------------------------------------- 1 | uploadPath: /tmp/charts 2 | helmRepos: 3 | - name: bitnami 4 | url: https://charts.bitnami.com/bitnami 5 | -------------------------------------------------------------------------------- /deployment/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: helm-wrapper 6 | name: helm-wrapper 7 | namespace: kube-system 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: helm-wrapper 13 | strategy: 14 | rollingUpdate: 15 | maxSurge: 25% 16 | maxUnavailable: 25% 17 | type: RollingUpdate 18 | template: 19 | metadata: 20 | labels: 21 | app: helm-wrapper 22 | spec: 23 | serviceAccountName: helm-wrapper 24 | containers: 25 | - image: 26 | imagePullPolicy: IfNotPresent 27 | name: helm-wrapper 28 | readinessProbe: 29 | failureThreshold: 3 30 | initialDelaySeconds: 20 31 | periodSeconds: 5 32 | successThreshold: 1 33 | tcpSocket: 34 | port: 8080 35 | timeoutSeconds: 1 36 | ports: 37 | - name: helm-wrapper 38 | containerPort: 8080 39 | dnsPolicy: ClusterFirst 40 | restartPolicy: Always 41 | -------------------------------------------------------------------------------- /deployment/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: helm-wrapper 5 | namespace: kube-system 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRoleBinding 9 | metadata: 10 | name: helm-wrapper 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: cluster-admin 15 | subjects: 16 | - kind: ServiceAccount 17 | name: helm-wrapper 18 | namespace: kube-system 19 | -------------------------------------------------------------------------------- /deployment/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: helm-wrapper 6 | annotations: {} 7 | name: helm-wrapper 8 | namespace: kube-system 9 | spec: 10 | ports: 11 | - port: 80 12 | protocol: TCP 13 | targetPort: 8080 14 | selector: 15 | app: helm-wrapper 16 | type: ClusterIP -------------------------------------------------------------------------------- /deployment/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: helm-wrapper 5 | namespace: kube-system 6 | -------------------------------------------------------------------------------- /envs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func getHelmEnvs(c *gin.Context) { 6 | respOK(c, settings.EnvVars()) 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/opskumu/helm-wrapper 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/Masterminds/semver v1.5.0 7 | github.com/gin-gonic/gin v1.10.0 8 | github.com/gofrs/flock v0.12.1 9 | github.com/golang/glog v1.2.4 10 | github.com/pkg/errors v0.9.1 11 | github.com/spf13/pflag v1.0.5 12 | helm.sh/helm/v3 v3.17.3 13 | k8s.io/cli-runtime v0.32.2 14 | sigs.k8s.io/yaml v1.4.0 15 | ) 16 | 17 | require ( 18 | dario.cat/mergo v1.0.1 // indirect 19 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect 20 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 21 | github.com/BurntSushi/toml v1.4.0 // indirect 22 | github.com/MakeNowJust/heredoc v1.0.0 // indirect 23 | github.com/Masterminds/goutils v1.1.1 // indirect 24 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 25 | github.com/Masterminds/sprig/v3 v3.3.0 // indirect 26 | github.com/Masterminds/squirrel v1.5.4 // indirect 27 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 28 | github.com/beorn7/perks v1.0.1 // indirect 29 | github.com/blang/semver/v4 v4.0.0 // indirect 30 | github.com/bytedance/sonic v1.11.6 // indirect 31 | github.com/bytedance/sonic/loader v0.1.1 // indirect 32 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 33 | github.com/chai2010/gettext-go v1.0.2 // indirect 34 | github.com/cloudwego/base64x v0.1.4 // indirect 35 | github.com/cloudwego/iasm v0.2.0 // indirect 36 | github.com/containerd/containerd v1.7.27 // indirect 37 | github.com/containerd/errdefs v0.3.0 // indirect 38 | github.com/containerd/log v0.1.0 // indirect 39 | github.com/containerd/platforms v0.2.1 // indirect 40 | github.com/cyphar/filepath-securejoin v0.3.6 // indirect 41 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 42 | github.com/distribution/reference v0.6.0 // indirect 43 | github.com/docker/cli v25.0.1+incompatible // indirect 44 | github.com/docker/distribution v2.8.3+incompatible // indirect 45 | github.com/docker/docker v25.0.6+incompatible // indirect 46 | github.com/docker/docker-credential-helpers v0.8.1 // indirect 47 | github.com/docker/go-connections v0.5.0 // indirect 48 | github.com/docker/go-metrics v0.0.1 // indirect 49 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 50 | github.com/evanphx/json-patch v5.9.0+incompatible // indirect 51 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect 52 | github.com/fatih/color v1.13.0 // indirect 53 | github.com/felixge/httpsnoop v1.0.4 // indirect 54 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 55 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 56 | github.com/gin-contrib/sse v0.1.0 // indirect 57 | github.com/go-errors/errors v1.4.2 // indirect 58 | github.com/go-gorp/gorp/v3 v3.1.0 // indirect 59 | github.com/go-logr/logr v1.4.2 // indirect 60 | github.com/go-logr/stdr v1.2.2 // indirect 61 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 62 | github.com/go-openapi/jsonreference v0.20.2 // indirect 63 | github.com/go-openapi/swag v0.23.0 // indirect 64 | github.com/go-playground/locales v0.14.1 // indirect 65 | github.com/go-playground/universal-translator v0.18.1 // indirect 66 | github.com/go-playground/validator/v10 v10.20.0 // indirect 67 | github.com/gobwas/glob v0.2.3 // indirect 68 | github.com/goccy/go-json v0.10.2 // indirect 69 | github.com/gogo/protobuf v1.3.2 // indirect 70 | github.com/golang/protobuf v1.5.4 // indirect 71 | github.com/google/btree v1.0.1 // indirect 72 | github.com/google/gnostic-models v0.6.8 // indirect 73 | github.com/google/go-cmp v0.6.0 // indirect 74 | github.com/google/gofuzz v1.2.0 // indirect 75 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 76 | github.com/google/uuid v1.6.0 // indirect 77 | github.com/gorilla/mux v1.8.0 // indirect 78 | github.com/gorilla/websocket v1.5.0 // indirect 79 | github.com/gosuri/uitable v0.0.4 // indirect 80 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 81 | github.com/hashicorp/errwrap v1.1.0 // indirect 82 | github.com/hashicorp/go-multierror v1.1.1 // indirect 83 | github.com/huandu/xstrings v1.5.0 // indirect 84 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 85 | github.com/jmoiron/sqlx v1.4.0 // indirect 86 | github.com/josharian/intern v1.0.0 // indirect 87 | github.com/json-iterator/go v1.1.12 // indirect 88 | github.com/klauspost/compress v1.16.7 // indirect 89 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 90 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 91 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 92 | github.com/leodido/go-urn v1.4.0 // indirect 93 | github.com/lib/pq v1.10.9 // indirect 94 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 95 | github.com/mailru/easyjson v0.7.7 // indirect 96 | github.com/mattn/go-colorable v0.1.13 // indirect 97 | github.com/mattn/go-isatty v0.0.20 // indirect 98 | github.com/mattn/go-runewidth v0.0.9 // indirect 99 | github.com/mitchellh/copystructure v1.2.0 // indirect 100 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 101 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 102 | github.com/moby/locker v1.0.1 // indirect 103 | github.com/moby/spdystream v0.5.0 // indirect 104 | github.com/moby/term v0.5.0 // indirect 105 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 106 | github.com/modern-go/reflect2 v1.0.2 // indirect 107 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 108 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 109 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 110 | github.com/opencontainers/go-digest v1.0.0 // indirect 111 | github.com/opencontainers/image-spec v1.1.0 // indirect 112 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 113 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 114 | github.com/prometheus/client_golang v1.19.1 // indirect 115 | github.com/prometheus/client_model v0.6.1 // indirect 116 | github.com/prometheus/common v0.55.0 // indirect 117 | github.com/prometheus/procfs v0.15.1 // indirect 118 | github.com/rubenv/sql-migrate v1.7.1 // indirect 119 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 120 | github.com/shopspring/decimal v1.4.0 // indirect 121 | github.com/sirupsen/logrus v1.9.3 // indirect 122 | github.com/spf13/cast v1.7.0 // indirect 123 | github.com/spf13/cobra v1.8.1 // indirect 124 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 125 | github.com/ugorji/go/codec v1.2.12 // indirect 126 | github.com/x448/float16 v0.8.4 // indirect 127 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 128 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 129 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 130 | github.com/xlab/treeprint v1.2.0 // indirect 131 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect 132 | go.opentelemetry.io/otel v1.28.0 // indirect 133 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 134 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 135 | golang.org/x/arch v0.8.0 // indirect 136 | golang.org/x/crypto v0.36.0 // indirect 137 | golang.org/x/net v0.37.0 // indirect 138 | golang.org/x/oauth2 v0.23.0 // indirect 139 | golang.org/x/sync v0.12.0 // indirect 140 | golang.org/x/sys v0.31.0 // indirect 141 | golang.org/x/term v0.30.0 // indirect 142 | golang.org/x/text v0.23.0 // indirect 143 | golang.org/x/time v0.7.0 // indirect 144 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect 145 | google.golang.org/grpc v1.65.0 // indirect 146 | google.golang.org/protobuf v1.35.2 // indirect 147 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 148 | gopkg.in/inf.v0 v0.9.1 // indirect 149 | gopkg.in/yaml.v3 v3.0.1 // indirect 150 | k8s.io/api v0.32.2 // indirect 151 | k8s.io/apiextensions-apiserver v0.32.2 // indirect 152 | k8s.io/apimachinery v0.32.2 // indirect 153 | k8s.io/apiserver v0.32.2 // indirect 154 | k8s.io/client-go v0.32.2 // indirect 155 | k8s.io/component-base v0.32.2 // indirect 156 | k8s.io/klog/v2 v2.130.1 // indirect 157 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 158 | k8s.io/kubectl v0.32.2 // indirect 159 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 160 | oras.land/oras-go v1.2.5 // indirect 161 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 162 | sigs.k8s.io/kustomize/api v0.18.0 // indirect 163 | sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect 164 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 165 | ) 166 | -------------------------------------------------------------------------------- /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 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 4 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 5 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 6 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 7 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 8 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 9 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 10 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 11 | github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= 12 | github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= 13 | github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= 14 | github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= 15 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 16 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 17 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 18 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 19 | github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= 20 | github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 21 | github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= 22 | github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= 23 | github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= 24 | github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= 25 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 26 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 27 | github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= 28 | github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= 29 | github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= 30 | github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= 31 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 32 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 33 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 34 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 35 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= 36 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 37 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 38 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 39 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 40 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 41 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 42 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 43 | github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= 44 | github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= 45 | github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= 46 | github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= 47 | github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= 48 | github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= 49 | github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= 50 | github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= 51 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 52 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 53 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 54 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 55 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 56 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 57 | github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= 58 | github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= 59 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 60 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 61 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 62 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 63 | github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= 64 | github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= 65 | github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= 66 | github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= 67 | github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= 68 | github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= 69 | github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= 70 | github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 71 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 72 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 73 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 74 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 75 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 76 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 77 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 78 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 79 | github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= 80 | github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 81 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 82 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 83 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 84 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 85 | github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc= 86 | github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI= 87 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 88 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 89 | github.com/docker/cli v25.0.1+incompatible h1:mFpqnrS6Hsm3v1k7Wa/BO23oz0k121MTbTO1lpcGSkU= 90 | github.com/docker/cli v25.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 91 | github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= 92 | github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 93 | github.com/docker/docker v25.0.6+incompatible h1:5cPwbwriIcsua2REJe8HqQV+6WlWc1byg2QSXzBxBGg= 94 | github.com/docker/docker v25.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 95 | github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo= 96 | github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= 97 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 98 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 99 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= 100 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= 101 | github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= 102 | github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= 103 | github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= 104 | github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= 105 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 106 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 107 | github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= 108 | github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 109 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= 110 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= 111 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 112 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 113 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 114 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 115 | github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= 116 | github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= 117 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 118 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 119 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 120 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 121 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 122 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 123 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 124 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 125 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 126 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 127 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 128 | github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 129 | github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= 130 | github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= 131 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 132 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 133 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 134 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 135 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 136 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 137 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 138 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 139 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 140 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 141 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 142 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 143 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 144 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 145 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 146 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 147 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 148 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 149 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 150 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 151 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 152 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 153 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 154 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 155 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 156 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 157 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 158 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 159 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 160 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 161 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 162 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 163 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 164 | github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= 165 | github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= 166 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 167 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 168 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 169 | github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= 170 | github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 171 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 172 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 173 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 174 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 175 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 176 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 177 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 178 | github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= 179 | github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= 180 | github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= 181 | github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 182 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 183 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 184 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 185 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 186 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 187 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 188 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 189 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 190 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 191 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 192 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 193 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 194 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 195 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 196 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 197 | github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= 198 | github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= 199 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 200 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 201 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 202 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 203 | github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= 204 | github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= 205 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= 206 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 207 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 208 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 209 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 210 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 211 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 212 | github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= 213 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 214 | github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= 215 | github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 216 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 217 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 218 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 219 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 220 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 221 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 222 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 223 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 224 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 225 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 226 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 227 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 228 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 229 | github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= 230 | github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 231 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 232 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 233 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 234 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 235 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 236 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 237 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 238 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 239 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 240 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 241 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 242 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 243 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 244 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= 245 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= 246 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= 247 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= 248 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 249 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 250 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 251 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 252 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= 253 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= 254 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 255 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 256 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 257 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 258 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 259 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 260 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 261 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 262 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 263 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 264 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 265 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 266 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 267 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 268 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 269 | github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= 270 | github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= 271 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 272 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 273 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 274 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 275 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 276 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 277 | github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= 278 | github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= 279 | github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= 280 | github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= 281 | github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= 282 | github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= 283 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 284 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 285 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 286 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 287 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 288 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 289 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 290 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 291 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 292 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 293 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 294 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= 295 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 296 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 297 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 298 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 299 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= 300 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 301 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 302 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 303 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 304 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 305 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 306 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 307 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 308 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 309 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 310 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 311 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 312 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 313 | github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= 314 | github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= 315 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 316 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 317 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 318 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 319 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 320 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 321 | github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= 322 | github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= 323 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 324 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 325 | github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= 326 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 327 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 328 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 329 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 330 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 331 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 332 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 333 | github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= 334 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 335 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 336 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 337 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 338 | github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 339 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 340 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 341 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 342 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 343 | github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= 344 | github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= 345 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 346 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 347 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 348 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 349 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 350 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 351 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 352 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 353 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 354 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= 355 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 356 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 357 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 358 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 359 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 360 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 361 | github.com/stretchr/objx v0.1.1/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 367 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 368 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 373 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 374 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 375 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 376 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 377 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 378 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 379 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 380 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 381 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 382 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 383 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 384 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 385 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 386 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 387 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 388 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 389 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 390 | github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= 391 | github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= 392 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 393 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 394 | github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= 395 | github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= 396 | github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= 397 | github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= 398 | github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= 399 | github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= 400 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 401 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 402 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= 403 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= 404 | go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= 405 | go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= 406 | go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= 407 | go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= 408 | go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= 409 | go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= 410 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 411 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 412 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 413 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 414 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 415 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 416 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 417 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 418 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 419 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 420 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 421 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 422 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 423 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 424 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 425 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 426 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 427 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 428 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 429 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 430 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 431 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 432 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 433 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= 434 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 435 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 436 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 437 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 438 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 439 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 440 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 441 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 442 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 443 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 444 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 445 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 446 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 447 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 448 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 449 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 450 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 451 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 452 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 453 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 454 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 455 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 456 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 457 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 458 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 459 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 460 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 461 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 462 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 463 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 464 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 465 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 466 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 467 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 468 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 469 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 470 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 471 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 472 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 473 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 474 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 475 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 476 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs= 477 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 478 | google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= 479 | google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 480 | google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= 481 | google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 482 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 483 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 484 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 485 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 486 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 487 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 488 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 489 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 490 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 491 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 492 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 493 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 494 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 495 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 496 | gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= 497 | gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= 498 | helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg= 499 | helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= 500 | k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= 501 | k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= 502 | k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= 503 | k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= 504 | k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= 505 | k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 506 | k8s.io/apiserver v0.32.2 h1:WzyxAu4mvLkQxwD9hGa4ZfExo3yZZaYzoYvvVDlM6vw= 507 | k8s.io/apiserver v0.32.2/go.mod h1:PEwREHiHNU2oFdte7BjzA1ZyjWjuckORLIK/wLV5goM= 508 | k8s.io/cli-runtime v0.32.2 h1:aKQR4foh9qeyckKRkNXUccP9moxzffyndZAvr+IXMks= 509 | k8s.io/cli-runtime v0.32.2/go.mod h1:a/JpeMztz3xDa7GCyyShcwe55p8pbcCVQxvqZnIwXN8= 510 | k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= 511 | k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= 512 | k8s.io/component-base v0.32.2 h1:1aUL5Vdmu7qNo4ZsE+569PV5zFatM9hl+lb3dEea2zU= 513 | k8s.io/component-base v0.32.2/go.mod h1:PXJ61Vx9Lg+P5mS8TLd7bCIr+eMJRQTyXe8KvkrvJq0= 514 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 515 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 516 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= 517 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 518 | k8s.io/kubectl v0.32.2 h1:TAkag6+XfSBgkqK9I7ZvwtF0WVtUAvK8ZqTt+5zi1Us= 519 | k8s.io/kubectl v0.32.2/go.mod h1:+h/NQFSPxiDZYX/WZaWw9fwYezGLISP0ud8nQKg+3g8= 520 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 521 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 522 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 523 | oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= 524 | oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= 525 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 526 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 527 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 528 | sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= 529 | sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= 530 | sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= 531 | sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= 532 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= 533 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 534 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 535 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 536 | -------------------------------------------------------------------------------- /helm-wrapper/.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 | -------------------------------------------------------------------------------- /helm-wrapper/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: helm-wrapper 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 | -------------------------------------------------------------------------------- /helm-wrapper/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 "helm-wrapper.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 "helm-wrapper.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "helm-wrapper.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 "helm-wrapper.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 | -------------------------------------------------------------------------------- /helm-wrapper/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "helm-wrapper.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 "helm-wrapper.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 "helm-wrapper.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "helm-wrapper.labels" -}} 37 | helm.sh/chart: {{ include "helm-wrapper.chart" . }} 38 | {{ include "helm-wrapper.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 "helm-wrapper.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "helm-wrapper.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 "helm-wrapper.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "helm-wrapper.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /helm-wrapper/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "helm-wrapper.fullname" . }} 5 | data: 6 | config.yaml: | 7 | uploadPath: /tmp/charts 8 | helmRepos: [] 9 | -------------------------------------------------------------------------------- /helm-wrapper/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "helm-wrapper.fullname" . }} 5 | labels: 6 | {{- include "helm-wrapper.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "helm-wrapper.selectorLabels" . | nindent 6 }} 14 | strategy: 15 | rollingUpdate: 16 | maxSurge: 25% 17 | maxUnavailable: 25% 18 | type: RollingUpdate 19 | template: 20 | metadata: 21 | {{- with .Values.podAnnotations }} 22 | annotations: 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | labels: 26 | {{- include "helm-wrapper.selectorLabels" . | nindent 8 }} 27 | spec: 28 | {{- with .Values.imagePullSecrets }} 29 | imagePullSecrets: 30 | {{- toYaml . | nindent 8 }} 31 | {{- end }} 32 | serviceAccountName: {{ include "helm-wrapper.serviceAccountName" . }} 33 | securityContext: 34 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 35 | containers: 36 | - name: {{ .Chart.Name }} 37 | securityContext: 38 | {{- toYaml .Values.securityContext | nindent 12 }} 39 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 40 | imagePullPolicy: {{ .Values.image.pullPolicy }} 41 | ports: 42 | - name: http 43 | containerPort: 8080 44 | protocol: TCP 45 | readinessProbe: 46 | failureThreshold: 3 47 | initialDelaySeconds: 20 48 | periodSeconds: 5 49 | successThreshold: 1 50 | timeoutSeconds: 1 51 | httpGet: 52 | path: / 53 | port: http 54 | resources: 55 | {{- toYaml .Values.resources | nindent 12 }} 56 | volumeMounts: 57 | - mountPath: /config.yaml 58 | subPath: config.yaml 59 | name: config 60 | volumes: 61 | - name: config 62 | configMap: 63 | name: {{ include "helm-wrapper.fullname" . }} 64 | {{- with .Values.nodeSelector }} 65 | nodeSelector: 66 | {{- toYaml . | nindent 8 }} 67 | {{- end }} 68 | {{- with .Values.affinity }} 69 | affinity: 70 | {{- toYaml . | nindent 8 }} 71 | {{- end }} 72 | {{- with .Values.tolerations }} 73 | tolerations: 74 | {{- toYaml . | nindent 8 }} 75 | {{- end }} 76 | -------------------------------------------------------------------------------- /helm-wrapper/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "helm-wrapper.fullname" . }} 6 | labels: 7 | {{- include "helm-wrapper.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "helm-wrapper.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 | -------------------------------------------------------------------------------- /helm-wrapper/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "helm-wrapper.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 "helm-wrapper.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 | -------------------------------------------------------------------------------- /helm-wrapper/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "helm-wrapper.fullname" . }} 5 | labels: 6 | {{- include "helm-wrapper.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 "helm-wrapper.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /helm-wrapper/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "helm-wrapper.serviceAccountName" . }} 6 | labels: 7 | {{- include "helm-wrapper.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | --- 13 | apiVersion: rbac.authorization.k8s.io/v1 14 | kind: ClusterRoleBinding 15 | metadata: 16 | name: {{ include "helm-wrapper.serviceAccountName" . }} 17 | roleRef: 18 | apiGroup: rbac.authorization.k8s.io 19 | kind: ClusterRole 20 | name: cluster-admin 21 | subjects: 22 | - kind: ServiceAccount 23 | name: {{ include "helm-wrapper.serviceAccountName" . }} 24 | namespace: {{ .Release.Namespace }} 25 | {{- end }} 26 | -------------------------------------------------------------------------------- /helm-wrapper/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "helm-wrapper.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "helm-wrapper.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 "helm-wrapper.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /helm-wrapper/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for helm-wrapper. 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: helm-wrapper 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 | -------------------------------------------------------------------------------- /helm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/golang/glog" 7 | "helm.sh/helm/v3/pkg/action" 8 | "helm.sh/helm/v3/pkg/kube" 9 | "k8s.io/cli-runtime/pkg/genericclioptions" 10 | ) 11 | 12 | type KubeInformation struct { 13 | AimNamespace string 14 | AimContext string 15 | AimConfig string 16 | } 17 | 18 | func InitKubeInformation(namespace, context, config string) *KubeInformation { 19 | return &KubeInformation{ 20 | AimNamespace: namespace, 21 | AimContext: context, 22 | AimConfig: config, 23 | } 24 | } 25 | 26 | func actionConfigInit(kubeInfo *KubeInformation) (*action.Configuration, error) { 27 | actionConfig := new(action.Configuration) 28 | if kubeInfo.AimContext == "" { 29 | kubeInfo.AimContext = settings.KubeContext 30 | } 31 | clientConfig := new(genericclioptions.ConfigFlags) 32 | if kubeInfo.AimConfig == "" { 33 | clientConfig = kube.GetConfig(settings.KubeConfig, kubeInfo.AimContext, kubeInfo.AimNamespace) 34 | } else { 35 | clientConfig = kube.GetConfig(kubeInfo.AimConfig, kubeInfo.AimContext, kubeInfo.AimNamespace) 36 | } 37 | if settings.KubeToken != "" { 38 | clientConfig.BearerToken = &settings.KubeToken 39 | } 40 | if settings.KubeAPIServer != "" { 41 | clientConfig.APIServer = &settings.KubeAPIServer 42 | } 43 | err := actionConfig.Init(clientConfig, kubeInfo.AimNamespace, os.Getenv("HELM_DRIVER"), glog.Infof) 44 | if err != nil { 45 | glog.Errorf("%+v", err) 46 | return nil, err 47 | } 48 | 49 | return actionConfig, nil 50 | } 51 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "path/filepath" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/gin-gonic/gin" 15 | "github.com/golang/glog" 16 | "github.com/spf13/pflag" 17 | "helm.sh/helm/v3/pkg/cli" 18 | "helm.sh/helm/v3/pkg/repo" 19 | "sigs.k8s.io/yaml" 20 | ) 21 | 22 | type HelmConfig struct { 23 | UploadPath string `yaml:"uploadPath"` 24 | HelmRepos []*repo.Entry `yaml:"helmRepos"` 25 | HelmRegistries []*repo.Entry `yaml:"helmRegistries"` 26 | } 27 | 28 | var ( 29 | settings = cli.New() 30 | defaultUploadPath = "/tmp/charts" 31 | helmConfig = &HelmConfig{} 32 | ) 33 | 34 | func main() { 35 | var ( 36 | listenHost string 37 | listenPort string 38 | config string 39 | ) 40 | 41 | err := flag.Set("logtostderr", "true") 42 | if err != nil { 43 | glog.Fatalln(err) 44 | } 45 | pflag.CommandLine.StringVar(&listenHost, "addr", "0.0.0.0", "server listen addr") 46 | pflag.CommandLine.StringVar(&listenPort, "port", "8080", "server listen port") 47 | pflag.CommandLine.StringVar(&config, "config", "config.yaml", "helm wrapper config") 48 | pflag.CommandLine.AddGoFlagSet(flag.CommandLine) 49 | settings.AddFlags(pflag.CommandLine) 50 | pflag.Parse() 51 | defer glog.Flush() 52 | 53 | configBody, err := os.ReadFile(config) 54 | if err != nil { 55 | glog.Fatalln(err) 56 | } 57 | err = yaml.Unmarshal(configBody, helmConfig) 58 | if err != nil { 59 | glog.Fatalln(err) 60 | } 61 | 62 | // upload chart path 63 | if helmConfig.UploadPath == "" { 64 | helmConfig.UploadPath = defaultUploadPath 65 | } else { 66 | if !filepath.IsAbs(helmConfig.UploadPath) { 67 | glog.Fatalln("charts upload path is not absolute") 68 | } 69 | } 70 | _, err = os.Stat(helmConfig.UploadPath) 71 | if err != nil { 72 | if os.IsNotExist(err) { 73 | err = os.MkdirAll(helmConfig.UploadPath, 0755) 74 | if err != nil { 75 | glog.Fatalln(err) 76 | } 77 | } else { 78 | glog.Fatalln(err) 79 | } 80 | } 81 | 82 | // init repo 83 | for _, c := range helmConfig.HelmRepos { 84 | err = initRepos(c) 85 | if err != nil { 86 | glog.Fatalln(err) 87 | } 88 | } 89 | 90 | // init registries 91 | for _, c := range helmConfig.HelmRegistries { 92 | err := initRegistry(c) 93 | if err != nil { 94 | glog.Fatalln(err) 95 | } 96 | } 97 | 98 | // router 99 | router := gin.New() 100 | router.Use(gin.Recovery()) 101 | router.GET("/", func(c *gin.Context) { 102 | c.String(http.StatusOK, "Welcome helm wrapper server") 103 | }) 104 | 105 | // register router 106 | RegisterRouter(router) 107 | 108 | srv := &http.Server{ 109 | Addr: fmt.Sprintf("%s:%s", listenHost, listenPort), 110 | Handler: router, 111 | } 112 | 113 | go func() { 114 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 115 | glog.Fatalf("listen: %s\n", err) 116 | } 117 | }() 118 | 119 | quit := make(chan os.Signal, 2) 120 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 121 | <-quit 122 | glog.Infoln("Shutdown Server ...") 123 | 124 | ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) 125 | defer cancel() 126 | _ = srv.Shutdown(ctx) 127 | } 128 | -------------------------------------------------------------------------------- /registries.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "helm.sh/helm/v3/pkg/action" 9 | "helm.sh/helm/v3/pkg/registry" 10 | "helm.sh/helm/v3/pkg/repo" 11 | ) 12 | 13 | type RegistryConfig struct { 14 | Host string 15 | Username string 16 | Password string 17 | CertFile string 18 | KeyFile string 19 | CAFile string 20 | } 21 | 22 | func repoEntryToRegistryConfig(repoEntry *repo.Entry) (*RegistryConfig, error) { 23 | if repoEntry.URL == "" || !strings.HasPrefix(repoEntry.URL, "oci://") { 24 | return nil, fmt.Errorf("Invalid OCI registry URL: %s", repoEntry.URL) 25 | } 26 | 27 | hostParts := strings.Split(repoEntry.URL, "oci://") 28 | if len(hostParts) != 2 { 29 | return nil, fmt.Errorf("Invalid OCI registry URL: %s", repoEntry.URL) 30 | } 31 | 32 | registryConfig := RegistryConfig{ 33 | Host: hostParts[1], 34 | Username: repoEntry.Username, 35 | Password: repoEntry.Password, 36 | CertFile: repoEntry.CertFile, 37 | KeyFile: repoEntry.KeyFile, 38 | CAFile: repoEntry.CAFile, 39 | } 40 | 41 | return ®istryConfig, nil 42 | } 43 | 44 | func chartPathOptionsToRegistryConfig(aimChart *string, chartPathOptions *action.ChartPathOptions) (*RegistryConfig, error) { 45 | if !strings.HasPrefix(*aimChart, "oci://") { 46 | return nil, fmt.Errorf("Invalid OCI chart url: %s", aimChart) 47 | } 48 | 49 | chartUrlParts := strings.Split(*aimChart, "oci://") 50 | if len(chartUrlParts) != 2 { 51 | return nil, fmt.Errorf("Invalid OCI chart url: %s", aimChart) 52 | } 53 | 54 | hostParts := strings.Split(chartUrlParts[1], "/") 55 | if len(hostParts) < 2 { 56 | return nil, fmt.Errorf("Invalid OCI chart url: %s", aimChart) 57 | } 58 | 59 | registryConfig := RegistryConfig{ 60 | Host: hostParts[0], 61 | Username: chartPathOptions.Username, 62 | Password: chartPathOptions.Password, 63 | CertFile: chartPathOptions.CertFile, 64 | KeyFile: chartPathOptions.KeyFile, 65 | CAFile: chartPathOptions.CaFile, 66 | } 67 | 68 | return ®istryConfig, nil 69 | } 70 | 71 | func createOCIRegistryClient(registryConfig *RegistryConfig) (*registry.Client, error) { 72 | opts := []registry.ClientOption{ 73 | registry.ClientOptDebug(true), 74 | registry.ClientOptEnableCache(true), 75 | registry.ClientOptWriter(os.Stdout), 76 | registry.ClientOptCredentialsFile(settings.RegistryConfig), 77 | } 78 | 79 | registryClient, err := registry.NewClient(opts...) 80 | if err != nil { 81 | return nil, fmt.Errorf("Failed to create registry client: %s", err) 82 | } 83 | 84 | if registryConfig.Username != "" && registryConfig.Password != "" { 85 | registryClient.Login( 86 | registryConfig.Host, 87 | registry.LoginOptBasicAuth( 88 | registryConfig.Username, 89 | registryConfig.Password, 90 | ), 91 | registry.LoginOptInsecure(false), 92 | registry.LoginOptTLSClientConfig( 93 | registryConfig.CertFile, 94 | registryConfig.KeyFile, 95 | registryConfig.CAFile, 96 | ), 97 | ) 98 | } 99 | 100 | return registryClient, nil 101 | } 102 | 103 | func initRegistry(c *repo.Entry) (error) { 104 | registryConfig, err := repoEntryToRegistryConfig(c) 105 | if err != nil { 106 | return fmt.Errorf("Failed to convert repo entry to registry config: %s", err) 107 | } 108 | 109 | _, err = createOCIRegistryClient(registryConfig) 110 | return err 111 | } 112 | 113 | func createOCIRegistryClientForChartPathOptions(aimChart *string, chartPathOptions *action.ChartPathOptions) (*registry.Client, error) { 114 | if strings.HasPrefix(*aimChart, "oci://") { 115 | registryConfig, err := chartPathOptionsToRegistryConfig(aimChart, chartPathOptions) 116 | if err != nil { 117 | return nil, fmt.Errorf("Failed to convert chart path options to registry config: %s", err) 118 | } 119 | 120 | return createOCIRegistryClient(registryConfig) 121 | } 122 | 123 | return nil, nil 124 | } 125 | -------------------------------------------------------------------------------- /releases.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/url" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/pkg/errors" 13 | "helm.sh/helm/v3/pkg/action" 14 | "helm.sh/helm/v3/pkg/chart" 15 | "helm.sh/helm/v3/pkg/chart/loader" 16 | "helm.sh/helm/v3/pkg/downloader" 17 | "helm.sh/helm/v3/pkg/getter" 18 | "helm.sh/helm/v3/pkg/release" 19 | "helm.sh/helm/v3/pkg/storage/driver" 20 | "helm.sh/helm/v3/pkg/strvals" 21 | helmtime "helm.sh/helm/v3/pkg/time" 22 | "sigs.k8s.io/yaml" 23 | ) 24 | 25 | var defaultTimeout = "5m0s" 26 | 27 | type releaseInfo struct { 28 | Revision int `json:"revision"` 29 | Updated helmtime.Time `json:"updated"` 30 | Status string `json:"status"` 31 | Chart string `json:"chart"` 32 | AppVersion string `json:"app_version"` 33 | Description string `json:"description"` 34 | } 35 | 36 | type releaseHistory []releaseInfo 37 | 38 | type releaseElement struct { 39 | Name string `json:"name"` 40 | Namespace string `json:"namespace"` 41 | Revision string `json:"revision"` 42 | Updated string `json:"updated"` 43 | Status string `json:"status"` 44 | Chart string `json:"chart"` 45 | ChartVersion string `json:"chart_version"` 46 | AppVersion string `json:"app_version"` 47 | 48 | Notes string `json:"notes,omitempty"` 49 | 50 | // TODO: Test Suite? 51 | } 52 | 53 | type releaseOptions struct { 54 | // common 55 | DryRun bool `json:"dry_run"` 56 | DisableHooks bool `json:"disable_hooks"` 57 | Wait bool `json:"wait"` 58 | Devel bool `json:"devel"` 59 | Description string `json:"description"` 60 | Atomic bool `json:"atomic"` 61 | SkipCRDs bool `json:"skip_crds"` 62 | SubNotes bool `json:"sub_notes"` 63 | Timeout string `json:"timeout"` 64 | WaitForJobs bool `json:"wait_for_jobs"` 65 | DisableOpenAPIValidation bool `json:"disable_open_api_validation"` 66 | Values string `json:"values"` 67 | SetValues []string `json:"set"` 68 | SetStringValues []string `json:"set_string"` 69 | ChartPathOptions 70 | 71 | // only install 72 | CreateNamespace bool `json:"create_namespace"` 73 | DependencyUpdate bool `json:"dependency_update"` 74 | 75 | // only upgrade 76 | Install bool `json:"install"` 77 | 78 | // only rollback 79 | MaxHistory int `json:"history_max"` 80 | 81 | // upgrade or rollback 82 | Force bool `json:"force"` 83 | Recreate bool `json:"recreate"` 84 | ReuseValues bool `json:"reuse_values"` 85 | CleanupOnFail bool `json:"cleanup_on_fail"` 86 | } 87 | 88 | // ChartPathOptions captures common options used for controlling chart paths 89 | type ChartPathOptions struct { 90 | CaFile string `json:"ca_file"` // --ca-file 91 | CertFile string `json:"cert_file"` // --cert-file 92 | KeyFile string `json:"key_file"` // --key-file 93 | InsecureSkipTLSverify bool `json:"insecure_skip_verify"` // --insecure-skip-verify 94 | Keyring string `json:"keyring"` // --keyring 95 | Password string `json:"password"` // --password 96 | RepoURL string `json:"repo"` // --repo 97 | Username string `json:"username"` // --username 98 | Verify bool `json:"verify"` // --verify 99 | Version string `json:"version"` // --version 100 | } 101 | 102 | // helm List struct 103 | type releaseListOptions struct { 104 | // All ignores the limit/offset 105 | All bool `json:"all"` 106 | // AllNamespaces searches across namespaces 107 | AllNamespaces bool `json:"all_namespaces"` 108 | // Overrides the default lexicographic sorting 109 | ByDate bool `json:"by_date"` 110 | SortReverse bool `json:"sort_reverse"` 111 | // Limit is the number of items to return per Run() 112 | Limit int `json:"limit"` 113 | // Offset is the starting index for the Run() call 114 | Offset int `json:"offset"` 115 | // Filter is a filter that is applied to the results 116 | Filter string `json:"filter"` 117 | Uninstalled bool `json:"uninstalled"` 118 | Superseded bool `json:"superseded"` 119 | Uninstalling bool `json:"uninstalling"` 120 | Deployed bool `json:"deployed"` 121 | Failed bool `json:"failed"` 122 | Pending bool `json:"pending"` 123 | } 124 | 125 | // helm Uninstall struct 126 | type releaseUninstallOptions struct { 127 | DisableHooks bool `json:"disable_hooks"` 128 | DryRun bool `json:"dry_run"` 129 | IgnoreNotFound bool `json:"ignore_not_found"` 130 | KeepHistory bool `json:"keep_history"` 131 | Wait bool `json:"wait"` 132 | DeletionPropagation string `json:"delete_propagation"` 133 | Timeout time.Duration `json:"timeout"` 134 | Description string `json:"description"` 135 | } 136 | 137 | func formatChartname(c *chart.Chart) string { 138 | if c == nil || c.Metadata == nil { 139 | // This is an edge case that has happened in prod, though we don't 140 | // know how: https://github.com/helm/helm/issues/1347 141 | return "MISSING" 142 | } 143 | return fmt.Sprintf("%s-%s", c.Name(), c.Metadata.Version) 144 | } 145 | 146 | func formatAppVersion(c *chart.Chart) string { 147 | if c == nil || c.Metadata == nil { 148 | // This is an edge case that has happened in prod, though we don't 149 | // know how: https://github.com/helm/helm/issues/1347 150 | return "MISSING" 151 | } 152 | return c.AppVersion() 153 | } 154 | 155 | func mergeValues(options releaseOptions) (map[string]interface{}, error) { 156 | vals := map[string]interface{}{} 157 | values, err := readValues(options.Values) 158 | if err != nil { 159 | return vals, err 160 | } 161 | err = yaml.Unmarshal(values, &vals) 162 | if err != nil { 163 | return vals, fmt.Errorf("failed parsing values") 164 | } 165 | 166 | for _, value := range options.SetValues { 167 | if err := strvals.ParseInto(value, vals); err != nil { 168 | return vals, fmt.Errorf("failed parsing set data") 169 | } 170 | } 171 | 172 | for _, value := range options.SetStringValues { 173 | if err := strvals.ParseIntoString(value, vals); err != nil { 174 | return vals, fmt.Errorf("failed parsing set_string data") 175 | } 176 | } 177 | 178 | return vals, nil 179 | } 180 | 181 | func getReleaseHistory(rls []*release.Release) (history releaseHistory) { 182 | for i := len(rls) - 1; i >= 0; i-- { 183 | r := rls[i] 184 | c := formatChartname(r.Chart) 185 | s := r.Info.Status.String() 186 | v := r.Version 187 | d := r.Info.Description 188 | a := formatAppVersion(r.Chart) 189 | 190 | rInfo := releaseInfo{ 191 | Revision: v, 192 | Status: s, 193 | Chart: c, 194 | AppVersion: a, 195 | Description: d, 196 | } 197 | if !r.Info.LastDeployed.IsZero() { 198 | rInfo.Updated = r.Info.LastDeployed 199 | 200 | } 201 | history = append(history, rInfo) 202 | } 203 | 204 | return history 205 | } 206 | 207 | func constructReleaseElement(r *release.Release, showStatus bool) releaseElement { 208 | element := releaseElement{ 209 | Name: r.Name, 210 | Namespace: r.Namespace, 211 | Revision: strconv.Itoa(r.Version), 212 | Status: r.Info.Status.String(), 213 | Chart: r.Chart.Metadata.Name, 214 | ChartVersion: r.Chart.Metadata.Version, 215 | AppVersion: r.Chart.Metadata.AppVersion, 216 | } 217 | if showStatus { 218 | element.Notes = r.Info.Notes 219 | } 220 | t := "-" 221 | if tspb := r.Info.LastDeployed; !tspb.IsZero() { 222 | t = tspb.String() 223 | } 224 | element.Updated = t 225 | 226 | return element 227 | } 228 | 229 | func isChartInstallable(ch *chart.Chart) (bool, error) { 230 | switch ch.Metadata.Type { 231 | case "", "application": 232 | return true, nil 233 | } 234 | 235 | return false, errors.Errorf("%s charts are not installable", ch.Metadata.Type) 236 | } 237 | 238 | func showReleaseInfo(c *gin.Context) { 239 | name := c.Param("release") 240 | namespace := c.Param("namespace") 241 | info := c.Query("info") 242 | kubeConfig := c.Query("kube_config") 243 | if info == "" { 244 | info = "values" 245 | } 246 | kubeContext := c.Query("kube_context") 247 | infos := []string{"hooks", "manifest", "notes", "values"} 248 | infoMap := map[string]bool{} 249 | for _, i := range infos { 250 | infoMap[i] = true 251 | } 252 | if _, ok := infoMap[info]; !ok { 253 | respErr(c, fmt.Errorf("bad info %s, release info only support hooks/manifest/notes/values", info)) 254 | return 255 | } 256 | actionConfig, err := actionConfigInit(InitKubeInformation(namespace, kubeContext, kubeConfig)) 257 | if err != nil { 258 | respErr(c, err) 259 | return 260 | } 261 | 262 | if info == "values" { 263 | output := c.Query("output") 264 | // get values output format 265 | if output == "" { 266 | output = "json" 267 | } 268 | if output != "json" && output != "yaml" { 269 | respErr(c, fmt.Errorf("invalid format type %s, output only support json/yaml", output)) 270 | return 271 | } 272 | 273 | client := action.NewGetValues(actionConfig) 274 | results, err := client.Run(name) 275 | if err != nil { 276 | respErr(c, err) 277 | return 278 | } 279 | if output == "yaml" { 280 | obj, err := yaml.Marshal(results) 281 | if err != nil { 282 | respErr(c, err) 283 | return 284 | } 285 | respOK(c, string(obj)) 286 | return 287 | } 288 | respOK(c, results) 289 | return 290 | } 291 | 292 | client := action.NewGet(actionConfig) 293 | results, err := client.Run(name) 294 | if err != nil { 295 | respErr(c, err) 296 | return 297 | } 298 | // TODO: support all 299 | if info == "hooks" { 300 | if len(results.Hooks) < 1 { 301 | respOK(c, []*release.Hook{}) 302 | return 303 | } 304 | respOK(c, results.Hooks) 305 | return 306 | } else if info == "manifest" { 307 | respOK(c, results.Manifest) 308 | return 309 | } else if info == "notes" { 310 | respOK(c, results.Info.Notes) 311 | return 312 | } 313 | } 314 | 315 | func installRelease(c *gin.Context) { 316 | name := c.Param("release") 317 | namespace := c.Param("namespace") 318 | aimChart := c.Query("chart") 319 | kubeContext := c.Query("kube_context") 320 | kubeConfig := c.Query("kube_config") 321 | 322 | if aimChart == "" { 323 | respErr(c, fmt.Errorf("chart name can not be empty")) 324 | return 325 | } 326 | 327 | // install with local uploaded charts, *.tgz 328 | splitChart := strings.Split(aimChart, ".") 329 | if splitChart[len(splitChart)-1] == "tgz" && !strings.Contains(aimChart, ":") { 330 | aimChart = helmConfig.UploadPath + "/" + aimChart 331 | } 332 | 333 | var options releaseOptions 334 | err := c.ShouldBindJSON(&options) 335 | if err != nil && err != io.EOF { 336 | respErr(c, err) 337 | return 338 | } 339 | 340 | if err = runInstall(name, namespace, kubeContext, aimChart, kubeConfig, options); err != nil { 341 | respErr(c, err) 342 | return 343 | } 344 | 345 | respOK(c, err) 346 | return 347 | } 348 | 349 | func runInstall(name, namespace, kubeContext, aimChart, kubeConfig string, options releaseOptions) (err error) { 350 | vals, err := mergeValues(options) 351 | if err != nil { 352 | return 353 | } 354 | 355 | actionConfig, err := actionConfigInit(InitKubeInformation(namespace, kubeContext, kubeConfig)) 356 | if err != nil { 357 | return 358 | } 359 | client := action.NewInstall(actionConfig) 360 | client.ReleaseName = name 361 | client.Namespace = namespace 362 | 363 | // merge install options 364 | client.DryRun = options.DryRun 365 | client.DisableHooks = options.DisableHooks 366 | client.Wait = options.Wait 367 | if options.Timeout == "" { 368 | options.Timeout = defaultTimeout 369 | } 370 | client.Timeout, err = time.ParseDuration(options.Timeout) 371 | if err != nil { 372 | return 373 | } 374 | client.WaitForJobs = options.WaitForJobs 375 | client.Devel = options.Devel 376 | client.Description = options.Description 377 | client.Atomic = options.Atomic 378 | client.SkipCRDs = options.SkipCRDs 379 | client.SubNotes = options.SubNotes 380 | client.DisableOpenAPIValidation = options.DisableOpenAPIValidation 381 | client.CreateNamespace = options.CreateNamespace 382 | client.DependencyUpdate = options.DependencyUpdate 383 | 384 | // merge chart path options 385 | client.ChartPathOptions.CaFile = options.ChartPathOptions.CaFile 386 | client.ChartPathOptions.CertFile = options.ChartPathOptions.CertFile 387 | client.ChartPathOptions.KeyFile = options.ChartPathOptions.KeyFile 388 | client.ChartPathOptions.InsecureSkipTLSverify = options.ChartPathOptions.InsecureSkipTLSverify 389 | client.ChartPathOptions.Keyring = options.ChartPathOptions.Keyring 390 | client.ChartPathOptions.Password = options.ChartPathOptions.Password 391 | client.ChartPathOptions.RepoURL = options.ChartPathOptions.RepoURL 392 | client.ChartPathOptions.Username = options.ChartPathOptions.Username 393 | client.ChartPathOptions.Verify = options.ChartPathOptions.Verify 394 | client.ChartPathOptions.Version = options.ChartPathOptions.Version 395 | 396 | registryClient, err := createOCIRegistryClientForChartPathOptions( 397 | &aimChart, 398 | &client.ChartPathOptions, 399 | ) 400 | if err != nil { 401 | return 402 | } 403 | if registryClient != nil { 404 | client.SetRegistryClient(registryClient) 405 | } 406 | 407 | cp, err := client.ChartPathOptions.LocateChart(aimChart, settings) 408 | if err != nil { 409 | return 410 | } 411 | 412 | chartRequested, err := loader.Load(cp) 413 | if err != nil { 414 | return 415 | } 416 | 417 | validInstallableChart, err := isChartInstallable(chartRequested) 418 | if !validInstallableChart { 419 | return 420 | } 421 | 422 | if req := chartRequested.Metadata.Dependencies; req != nil { 423 | // If CheckDependencies returns an error, we have unfulfilled dependencies. 424 | // As of Helm 2.4.0, this is treated as a stopping condition: 425 | // https://github.com/helm/helm/issues/2209 426 | if err = action.CheckDependencies(chartRequested, req); err != nil { 427 | if client.DependencyUpdate { 428 | man := &downloader.Manager{ 429 | ChartPath: cp, 430 | Keyring: client.ChartPathOptions.Keyring, 431 | SkipUpdate: false, 432 | Getters: getter.All(settings), 433 | RepositoryConfig: settings.RepositoryConfig, 434 | RepositoryCache: settings.RepositoryCache, 435 | } 436 | if err = man.Update(); err != nil { 437 | return 438 | } 439 | } else { 440 | return 441 | } 442 | } 443 | } 444 | 445 | _, err = client.Run(chartRequested, vals) 446 | if err != nil { 447 | return 448 | } 449 | 450 | return nil 451 | } 452 | 453 | func uninstallRelease(c *gin.Context) { 454 | name := c.Param("release") 455 | namespace := c.Param("namespace") 456 | kubeContext := c.Query("kube_context") 457 | kubeConfig := c.Query("kube_config") 458 | 459 | var options releaseUninstallOptions 460 | 461 | err := c.ShouldBindJSON(&options) 462 | if err != nil && err != io.EOF { 463 | respErr(c, err) 464 | return 465 | } 466 | 467 | actionConfig, err := actionConfigInit(InitKubeInformation(namespace, kubeContext, kubeConfig)) 468 | if err != nil { 469 | respErr(c, err) 470 | return 471 | } 472 | client := action.NewUninstall(actionConfig) 473 | client.DisableHooks = options.DisableHooks 474 | client.DryRun = options.DryRun 475 | client.IgnoreNotFound = options.IgnoreNotFound 476 | client.KeepHistory = options.KeepHistory 477 | client.Wait = options.Wait 478 | client.DeletionPropagation = options.DeletionPropagation 479 | client.Timeout = options.Timeout 480 | client.Description = options.Description 481 | 482 | _, err = client.Run(name) 483 | if err != nil { 484 | respErr(c, err) 485 | return 486 | } 487 | 488 | respOK(c, nil) 489 | } 490 | 491 | func rollbackRelease(c *gin.Context) { 492 | name := c.Param("release") 493 | namespace := c.Param("namespace") 494 | reversionStr := c.Param("reversion") 495 | kubeContext := c.Query("kube_context") 496 | kubeConfig := c.Query("kube_config") 497 | 498 | reversion, err := strconv.Atoi(reversionStr) 499 | if err != nil { 500 | respErr(c, err) 501 | return 502 | } 503 | 504 | var options releaseOptions 505 | err = c.ShouldBindJSON(&options) 506 | if err != nil && err != io.EOF { 507 | respErr(c, err) 508 | return 509 | } 510 | 511 | actionConfig, err := actionConfigInit(InitKubeInformation(namespace, kubeContext, kubeConfig)) 512 | if err != nil { 513 | respErr(c, err) 514 | return 515 | } 516 | client := action.NewRollback(actionConfig) 517 | client.Version = reversion 518 | 519 | // merge rollback options 520 | client.CleanupOnFail = options.CleanupOnFail 521 | client.Wait = options.Wait 522 | client.DryRun = options.DryRun 523 | client.DisableHooks = options.DisableHooks 524 | client.Force = options.Force 525 | client.Recreate = options.Recreate 526 | client.MaxHistory = options.MaxHistory 527 | if options.Timeout == "" { 528 | options.Timeout = defaultTimeout 529 | } 530 | client.Timeout, err = time.ParseDuration(options.Timeout) 531 | if err != nil { 532 | return 533 | } 534 | 535 | err = client.Run(name) 536 | if err != nil { 537 | respErr(c, err) 538 | return 539 | } 540 | respOK(c, nil) 541 | } 542 | 543 | func upgradeRelease(c *gin.Context) { 544 | name := c.Param("release") 545 | namespace := c.Param("namespace") 546 | aimChart := c.Query("chart") 547 | kubeContext := c.Query("kube_context") 548 | kubeConfig := c.Query("kube_config") 549 | 550 | if aimChart == "" { 551 | respErr(c, fmt.Errorf("chart name can not be empty")) 552 | return 553 | } 554 | 555 | // upgrade with local uploaded charts *.tgz 556 | splitChart := strings.Split(aimChart, ".") 557 | if splitChart[len(splitChart)-1] == "tgz" { 558 | aimChart = helmConfig.UploadPath + "/" + aimChart 559 | } 560 | 561 | var options releaseOptions 562 | err := c.ShouldBindJSON(&options) 563 | if err != nil && err != io.EOF { 564 | respErr(c, err) 565 | return 566 | } 567 | vals, err := mergeValues(options) 568 | if err != nil { 569 | respErr(c, err) 570 | return 571 | } 572 | actionConfig, err := actionConfigInit(InitKubeInformation(namespace, kubeContext, kubeConfig)) 573 | if err != nil { 574 | respErr(c, err) 575 | return 576 | } 577 | client := action.NewUpgrade(actionConfig) 578 | client.Namespace = namespace 579 | 580 | // merge upgrade options 581 | client.DryRun = options.DryRun 582 | client.DisableHooks = options.DisableHooks 583 | client.Wait = options.Wait 584 | client.Devel = options.Devel 585 | client.Description = options.Description 586 | client.Atomic = options.Atomic 587 | client.SkipCRDs = options.SkipCRDs 588 | client.SubNotes = options.SubNotes 589 | client.Force = options.Force 590 | if options.Timeout == "" { 591 | options.Timeout = defaultTimeout 592 | } 593 | client.Timeout, err = time.ParseDuration(options.Timeout) 594 | if err != nil { 595 | return 596 | } 597 | client.Install = options.Install 598 | client.MaxHistory = options.MaxHistory 599 | client.Recreate = options.Recreate 600 | client.ReuseValues = options.ReuseValues 601 | client.CleanupOnFail = options.CleanupOnFail 602 | 603 | // merge chart path options 604 | client.ChartPathOptions.CaFile = options.ChartPathOptions.CaFile 605 | client.ChartPathOptions.CertFile = options.ChartPathOptions.CertFile 606 | client.ChartPathOptions.KeyFile = options.ChartPathOptions.KeyFile 607 | client.ChartPathOptions.InsecureSkipTLSverify = options.ChartPathOptions.InsecureSkipTLSverify 608 | client.ChartPathOptions.Keyring = options.ChartPathOptions.Keyring 609 | client.ChartPathOptions.Password = options.ChartPathOptions.Password 610 | client.ChartPathOptions.RepoURL = options.ChartPathOptions.RepoURL 611 | client.ChartPathOptions.Username = options.ChartPathOptions.Username 612 | client.ChartPathOptions.Verify = options.ChartPathOptions.Verify 613 | client.ChartPathOptions.Version = options.ChartPathOptions.Version 614 | 615 | registryClient, err := createOCIRegistryClientForChartPathOptions( 616 | &aimChart, 617 | &client.ChartPathOptions, 618 | ) 619 | if err != nil { 620 | respErr(c, err) 621 | return 622 | } 623 | if registryClient != nil { 624 | client.SetRegistryClient(registryClient) 625 | } 626 | 627 | cp, err := client.ChartPathOptions.LocateChart(aimChart, settings) 628 | if err != nil { 629 | respErr(c, err) 630 | return 631 | } 632 | 633 | chartRequested, err := loader.Load(cp) 634 | if err != nil { 635 | respErr(c, err) 636 | return 637 | } 638 | if req := chartRequested.Metadata.Dependencies; req != nil { 639 | if err := action.CheckDependencies(chartRequested, req); err != nil { 640 | respErr(c, err) 641 | return 642 | } 643 | } 644 | 645 | if client.Install { 646 | hisClient := action.NewHistory(actionConfig) 647 | hisClient.Max = 1 648 | if _, err := hisClient.Run(name); err == driver.ErrReleaseNotFound { 649 | err = runInstall(name, namespace, kubeContext, aimChart, kubeConfig, options) 650 | if err != nil { 651 | respErr(c, err) 652 | return 653 | } 654 | 655 | respOK(c, err) 656 | return 657 | } else if err != nil { 658 | respErr(c, err) 659 | return 660 | } 661 | } 662 | 663 | _, err = client.Run(name, chartRequested, vals) 664 | if err != nil { 665 | respErr(c, err) 666 | return 667 | } 668 | 669 | respOK(c, nil) 670 | } 671 | 672 | func listReleases(c *gin.Context) { 673 | namespace := c.Param("namespace") 674 | kubeContext := c.Query("kube_context") 675 | kubeConfig := c.Query("kube_config") 676 | 677 | var options releaseListOptions 678 | err := c.ShouldBindJSON(&options) 679 | if err != nil && err != io.EOF { 680 | respErr(c, err) 681 | return 682 | } 683 | if options.AllNamespaces { 684 | namespace = "" 685 | } 686 | actionConfig, err := actionConfigInit(InitKubeInformation(namespace, kubeContext, kubeConfig)) 687 | if err != nil { 688 | respErr(c, err) 689 | return 690 | } 691 | 692 | client := action.NewList(actionConfig) 693 | 694 | // merge list options 695 | client.All = options.All 696 | client.AllNamespaces = options.AllNamespaces 697 | client.ByDate = options.ByDate 698 | client.SortReverse = options.SortReverse 699 | client.Limit = options.Limit 700 | client.Offset = options.Offset 701 | client.Filter = options.Filter 702 | client.Uninstalled = options.Uninstalled 703 | client.Superseded = options.Superseded 704 | client.Uninstalling = options.Uninstalling 705 | client.Deployed = options.Deployed 706 | client.Failed = options.Failed 707 | client.Pending = options.Pending 708 | client.SetStateMask() 709 | 710 | results, err := client.Run() 711 | if err != nil { 712 | respErr(c, err) 713 | return 714 | } 715 | 716 | // Initialize the array so no results returns an empty array instead of null 717 | elements := make([]releaseElement, 0, len(results)) 718 | for _, r := range results { 719 | elements = append(elements, constructReleaseElement(r, false)) 720 | } 721 | 722 | respOK(c, elements) 723 | } 724 | 725 | func getReleaseStatus(c *gin.Context) { 726 | name := c.Param("release") 727 | namespace := c.Param("namespace") 728 | kubeContext := c.Query("kube_context") 729 | kubeConfig := c.Query("kube_config") 730 | 731 | actionConfig, err := actionConfigInit(InitKubeInformation(namespace, kubeContext, kubeConfig)) 732 | if err != nil { 733 | respErr(c, err) 734 | return 735 | } 736 | 737 | client := action.NewStatus(actionConfig) 738 | results, err := client.Run(name) 739 | if err != nil { 740 | respErr(c, err) 741 | return 742 | } 743 | element := constructReleaseElement(results, true) 744 | 745 | respOK(c, &element) 746 | } 747 | 748 | func listReleaseHistories(c *gin.Context) { 749 | name := c.Param("release") 750 | namespace := c.Param("namespace") 751 | kubeContext := c.Query("kube_context") 752 | kubeConfig := c.Query("kube_config") 753 | 754 | actionConfig, err := actionConfigInit(InitKubeInformation(namespace, kubeContext, kubeConfig)) 755 | if err != nil { 756 | respErr(c, err) 757 | return 758 | } 759 | 760 | client := action.NewHistory(actionConfig) 761 | results, err := client.Run(name) 762 | if err != nil { 763 | respErr(c, err) 764 | return 765 | } 766 | if len(results) == 0 { 767 | respOK(c, &releaseHistory{}) 768 | return 769 | } 770 | 771 | respOK(c, getReleaseHistory(results)) 772 | } 773 | 774 | func readValues(filePath string) ([]byte, error) { 775 | u, _ := url.Parse(filePath) 776 | if u == nil { 777 | return []byte(filePath), nil 778 | } 779 | 780 | p := getter.All(settings) 781 | g, err := p.ByScheme(u.Scheme) 782 | // if scheme not support, return self 783 | if err != nil { 784 | return []byte(filePath), nil 785 | } 786 | 787 | data, err := g.Get(filePath, getter.WithURL(filePath)) 788 | if err != nil { 789 | return nil, err 790 | } 791 | 792 | return data.Bytes(), nil 793 | } 794 | -------------------------------------------------------------------------------- /repositories.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/Masterminds/semver" 13 | "github.com/gin-gonic/gin" 14 | "github.com/gofrs/flock" 15 | "github.com/golang/glog" 16 | "github.com/pkg/errors" 17 | "helm.sh/helm/v3/cmd/helm/search" 18 | "helm.sh/helm/v3/pkg/getter" 19 | "helm.sh/helm/v3/pkg/helmpath" 20 | "helm.sh/helm/v3/pkg/repo" 21 | "sigs.k8s.io/yaml" 22 | ) 23 | 24 | const searchMaxScore = 25 25 | 26 | type repoChartElement struct { 27 | Name string `json:"name"` 28 | Version string `json:"version"` 29 | AppVersion string `json:"app_version"` 30 | Description string `json:"description"` 31 | } 32 | 33 | type repoChartList []repoChartElement 34 | 35 | func applyConstraint(version string, versions bool, res []*search.Result) ([]*search.Result, error) { 36 | if len(version) == 0 { 37 | return res, nil 38 | } 39 | 40 | constraint, err := semver.NewConstraint(version) 41 | if err != nil { 42 | return res, errors.Wrap(err, "an invalid version/constraint format") 43 | } 44 | 45 | data := res[:0] 46 | foundNames := map[string]bool{} 47 | for _, r := range res { 48 | if _, found := foundNames[r.Name]; found { 49 | continue 50 | } 51 | v, err := semver.NewVersion(r.Chart.Version) 52 | if err != nil || constraint.Check(v) { 53 | data = append(data, r) 54 | if !versions { 55 | foundNames[r.Name] = true // If user hasn't requested all versions, only show the latest that matches 56 | } 57 | } 58 | } 59 | 60 | return data, nil 61 | } 62 | 63 | func buildSearchIndex(version string) (*search.Index, error) { 64 | i := search.NewIndex() 65 | for _, re := range helmConfig.HelmRepos { 66 | n := re.Name 67 | f := filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(n)) 68 | ind, err := repo.LoadIndexFile(f) 69 | if err != nil { 70 | glog.Warningf("WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n) 71 | continue 72 | } 73 | 74 | i.AddRepo(n, ind, len(version) > 0) 75 | } 76 | return i, nil 77 | } 78 | 79 | func initRepos(c *repo.Entry) error { 80 | // Ensure the file directory exists as it is required for file locking 81 | err := os.MkdirAll(filepath.Dir(settings.RepositoryConfig), os.ModePerm) 82 | if err != nil && !os.IsExist(err) { 83 | return err 84 | } 85 | 86 | // Acquire a file lock for process synchronization 87 | fileLock := flock.New(strings.Replace(settings.RepositoryConfig, filepath.Ext(settings.RepositoryConfig), ".lock", 1)) 88 | lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 89 | defer cancel() 90 | locked, err := fileLock.TryLockContext(lockCtx, time.Second) 91 | if err == nil && locked { 92 | SafeCloser(fileLock, &err) 93 | } 94 | if err != nil { 95 | return err 96 | } 97 | 98 | b, err := os.ReadFile(settings.RepositoryConfig) 99 | if err != nil && !os.IsNotExist(err) { 100 | return err 101 | } 102 | 103 | var f repo.File 104 | if err := yaml.Unmarshal(b, &f); err != nil { 105 | return err 106 | } 107 | 108 | r, err := repo.NewChartRepository(c, getter.All(settings)) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | if _, err := r.DownloadIndexFile(); err != nil { 114 | return err 115 | } 116 | 117 | f.Update(c) 118 | 119 | if err := f.WriteFile(settings.RepositoryConfig, 0644); err != nil { 120 | return err 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func updateChart(c *repo.Entry) error { 127 | r, err := repo.NewChartRepository(c, getter.All(settings)) 128 | if err != nil { 129 | return err 130 | } 131 | _, err = r.DownloadIndexFile() 132 | if err != nil { 133 | return err 134 | } 135 | 136 | return nil 137 | } 138 | 139 | func updateRepos(c *gin.Context) { 140 | type errRepo struct { 141 | Name string 142 | Err string 143 | } 144 | errRepoList := []errRepo{} 145 | 146 | var wg sync.WaitGroup 147 | for _, c := range helmConfig.HelmRepos { 148 | wg.Add(1) 149 | go func(c *repo.Entry) { 150 | defer wg.Done() 151 | err := updateChart(c) 152 | if err != nil { 153 | errRepoList = append(errRepoList, errRepo{ 154 | Name: c.Name, 155 | Err: err.Error(), 156 | }) 157 | } 158 | }(c) 159 | } 160 | wg.Wait() 161 | 162 | if len(errRepoList) > 0 { 163 | respErr(c, fmt.Errorf("error list: %v", errRepoList)) 164 | return 165 | } 166 | 167 | respOK(c, nil) 168 | } 169 | 170 | func listRepos(c *gin.Context) { 171 | type repo struct { 172 | Name string `json:"name"` 173 | URL string `json:"url"` 174 | } 175 | repos := []repo{} 176 | for _, r := range helmConfig.HelmRepos { 177 | repos = append(repos, repo{ 178 | r.Name, 179 | r.URL, 180 | }) 181 | } 182 | 183 | respOK(c, repos) 184 | } 185 | 186 | func listRepoCharts(c *gin.Context) { 187 | version := c.Query("version") // chart version 188 | versions := c.Query("versions") // if "true", all versions 189 | keyword := c.Query("keyword") // search keyword 190 | 191 | // default stable 192 | if version == "" { 193 | version = ">0.0.0" 194 | } 195 | 196 | index, err := buildSearchIndex(version) 197 | if err != nil { 198 | respErr(c, err) 199 | return 200 | } 201 | 202 | var res []*search.Result 203 | if keyword == "" { 204 | res = index.All() 205 | } else { 206 | res, err = index.Search(keyword, searchMaxScore, false) 207 | if err != nil { 208 | respErr(c, err) 209 | return 210 | } 211 | } 212 | 213 | search.SortScore(res) 214 | var versionsB bool 215 | if versions == "true" { 216 | versionsB = true 217 | } 218 | data, err := applyConstraint(version, versionsB, res) 219 | if err != nil { 220 | respErr(c, err) 221 | return 222 | } 223 | chartList := make(repoChartList, 0, len(data)) 224 | for _, v := range data { 225 | chartList = append(chartList, repoChartElement{ 226 | Name: v.Name, 227 | Version: v.Chart.Version, 228 | AppVersion: v.Chart.AppVersion, 229 | Description: v.Chart.Description, 230 | }) 231 | } 232 | 233 | respOK(c, chartList) 234 | } 235 | 236 | func SafeCloser(fileLock *flock.Flock, err *error) { 237 | if fileErr := fileLock.Unlock(); fileErr != nil && *err == nil { 238 | *err = fileErr 239 | glog.Error(fileErr) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/golang/glog" 8 | ) 9 | 10 | type respBody struct { 11 | Code int `json:"code"` // 0 or 1, 0 is ok, 1 is error 12 | Data interface{} `json:"data,omitempty"` 13 | Error string `json:"error,omitempty"` 14 | } 15 | 16 | func respErr(c *gin.Context, err error) { 17 | glog.Warningln(err) 18 | 19 | c.JSON(http.StatusOK, &respBody{ 20 | Code: 1, 21 | Error: err.Error(), 22 | }) 23 | } 24 | 25 | func respOK(c *gin.Context, data interface{}) { 26 | c.JSON(http.StatusOK, &respBody{ 27 | Code: 0, 28 | Data: data, 29 | }) 30 | } 31 | 32 | func RegisterRouter(router *gin.Engine) { 33 | // helm env 34 | envs := router.Group("/api/envs") 35 | { 36 | envs.GET("", getHelmEnvs) 37 | } 38 | 39 | // helm repo 40 | repositories := router.Group("/api/repositories") 41 | { 42 | // helm repo list 43 | repositories.GET("", listRepos) 44 | // helm search repo 45 | repositories.GET("/charts", listRepoCharts) 46 | // helm repo update 47 | repositories.PUT("", updateRepos) 48 | } 49 | 50 | // helm chart 51 | charts := router.Group("/api/charts") 52 | { 53 | // helm show 54 | charts.GET("", showChartInfo) 55 | // upload chart 56 | charts.POST("/upload", uploadChart) 57 | // list uploaded charts 58 | charts.GET("/upload", listUploadedCharts) 59 | // delete chart 60 | charts.DELETE("/upload/:chart", deleteChart) 61 | } 62 | 63 | // helm release 64 | releases := router.Group("/api/namespaces/:namespace/releases") 65 | { 66 | // helm list releases -> helm list 67 | releases.GET("", listReleases) 68 | // helm get 69 | releases.GET("/:release", showReleaseInfo) 70 | // helm install 71 | releases.POST("/:release", installRelease) 72 | // helm upgrade 73 | releases.PUT("/:release", upgradeRelease) 74 | // helm uninstall 75 | releases.DELETE("/:release", uninstallRelease) 76 | // helm rollback 77 | releases.PUT("/:release/versions/:reversion", rollbackRelease) 78 | // helm status 79 | releases.GET("/:release/status", getReleaseStatus) 80 | // helm release history 81 | releases.GET("/:release/histories", listReleaseHistories) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /upload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func uploadChart(c *gin.Context) { 14 | file, header, err := c.Request.FormFile("chart") 15 | if err != nil { 16 | respErr(c, err) 17 | return 18 | } 19 | 20 | filename := header.Filename 21 | t := strings.Split(filename, ".") 22 | if t[len(t)-1] != "tgz" { 23 | respErr(c, fmt.Errorf("chart file suffix must .tgz")) 24 | return 25 | } 26 | 27 | out, err := os.Create(helmConfig.UploadPath + "/" + filename) 28 | if err != nil { 29 | respErr(c, err) 30 | return 31 | } 32 | defer out.Close() 33 | 34 | _, err = io.Copy(out, file) 35 | if err != nil { 36 | respErr(c, err) 37 | return 38 | } 39 | 40 | respOK(c, nil) 41 | } 42 | 43 | func listUploadedCharts(c *gin.Context) { 44 | charts := []string{} 45 | files, err := os.ReadDir(helmConfig.UploadPath) 46 | if err != nil { 47 | respErr(c, err) 48 | return 49 | } 50 | for _, f := range files { 51 | t := strings.Split(f.Name(), ".") 52 | if t[len(t)-1] == "tgz" { 53 | charts = append(charts, f.Name()) 54 | } 55 | } 56 | 57 | respOK(c, charts) 58 | } 59 | 60 | func deleteChart(c *gin.Context) { 61 | chart := c.Param("chart") 62 | if chart == "" { 63 | err := errors.New("chart must be not empty") 64 | respErr(c, err) 65 | return 66 | } 67 | 68 | filePath := helmConfig.UploadPath + "/" + chart 69 | // not exist,ok 70 | _, err := os.Stat(filePath) 71 | if err != nil || os.IsNotExist(err) { 72 | respOK(c, nil) 73 | return 74 | } 75 | 76 | //delete chart from disk 77 | err = os.Remove(filePath) 78 | if err != nil { 79 | respErr(c, err) 80 | return 81 | } 82 | 83 | respOK(c, nil) 84 | } 85 | --------------------------------------------------------------------------------