Repository: caoyingjunz/gopixiu Branch: master Commit: 5fd5f7d45c3f Files: 158 Total size: 746.1 KB Directory structure: gitextract__nbx67ym/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── build-webshell-image.yml │ ├── ci.yml │ └── docker-image.yml ├── .gitignore ├── Makefile ├── README.md ├── api/ │ ├── docs/ │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ └── server/ │ ├── errors/ │ │ └── errors.go │ ├── httpstatus/ │ │ └── status.go │ ├── httputils/ │ │ ├── docs.go │ │ ├── httputils.go │ │ └── httputils_test.go │ ├── middleware/ │ │ ├── admission.go │ │ ├── audit.go │ │ ├── authentication.go │ │ ├── authorization.go │ │ ├── cors.go │ │ ├── limiter.go │ │ ├── log.go │ │ └── middleware.go │ ├── router/ │ │ ├── audit/ │ │ │ ├── audit.go │ │ │ └── audit_routes.go │ │ ├── auth/ │ │ │ ├── auth.go │ │ │ └── auth_routes.go │ │ ├── cluster/ │ │ │ ├── cluster.go │ │ │ ├── cluster_routes.go │ │ │ ├── helper.go │ │ │ ├── informer.go │ │ │ ├── proxy.go │ │ │ └── ws.go │ │ ├── helm/ │ │ │ ├── helm.go │ │ │ ├── release_routes.go │ │ │ └── respository_routes.go │ │ ├── plan/ │ │ │ ├── config_routes.go │ │ │ ├── node_routes.go │ │ │ ├── plan.go │ │ │ ├── plan_routes.go │ │ │ └── task_routes.go │ │ ├── proxy/ │ │ │ ├── helper.go │ │ │ └── proxy.go │ │ ├── router.go │ │ ├── static/ │ │ │ └── index.html │ │ ├── tenant/ │ │ │ ├── tenant.go │ │ │ └── tenant_routes.go │ │ └── user/ │ │ ├── user.go │ │ └── user_routes.go │ └── validator/ │ ├── helper.go │ ├── password.go │ ├── rbac.go │ └── validator.go ├── cmd/ │ ├── app/ │ │ ├── config/ │ │ │ └── config.go │ │ ├── options/ │ │ │ └── options.go │ │ └── server.go │ └── pixiuserver.go ├── config.yaml ├── deploy/ │ ├── README.md │ └── pixiu/ │ ├── .helmignore │ ├── Chart.yaml │ ├── templates/ │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── hpa.yaml │ │ ├── ingress.yaml │ │ ├── service.yaml │ │ └── serviceaccount.yaml │ └── values.yaml ├── docker/ │ ├── Dockerfile │ ├── Dockerfile-toolbox │ └── start.sh ├── docs/ │ ├── OWNERS │ ├── README.md │ ├── apis.md │ └── sql.md ├── go.mod ├── go.sum ├── hack/ │ ├── tools/ │ │ └── licfmt/ │ │ └── licfmt.go │ ├── update-gofmt.sh │ ├── update-image.sh │ └── verify-gofmt.sh ├── install.md ├── pkg/ │ ├── client/ │ │ ├── cache.go │ │ ├── client.go │ │ ├── helm.go │ │ ├── task.go │ │ ├── token_cache.go │ │ └── user_cache.go │ ├── controller/ │ │ ├── audit/ │ │ │ └── audit.go │ │ ├── auth/ │ │ │ └── auth.go │ │ ├── cluster/ │ │ │ ├── cluster.go │ │ │ ├── informer.go │ │ │ ├── util.go │ │ │ └── ws.go │ │ ├── controller.go │ │ ├── helm/ │ │ │ ├── helm.go │ │ │ ├── releases.go │ │ │ └── repository.go │ │ ├── plan/ │ │ │ ├── bootstrap_servers.go │ │ │ ├── checker.go │ │ │ ├── deploy.go │ │ │ ├── plan.go │ │ │ ├── plan_config.go │ │ │ ├── plan_node.go │ │ │ ├── plan_task.go │ │ │ ├── register.go │ │ │ ├── render.go │ │ │ └── worker.go │ │ ├── tenant/ │ │ │ └── tenant.go │ │ ├── user/ │ │ │ ├── user.go │ │ │ └── user_test.go │ │ └── util/ │ │ └── util.go │ ├── db/ │ │ ├── audit.go │ │ ├── cluster.go │ │ ├── factory.go │ │ ├── logger.go │ │ ├── migrator.go │ │ ├── model/ │ │ │ ├── audit.go │ │ │ ├── cluster.go │ │ │ ├── model.go │ │ │ ├── pixiu/ │ │ │ │ └── model.go │ │ │ ├── plan.go │ │ │ ├── rbac.go │ │ │ ├── rbac_test.go │ │ │ ├── repository.go │ │ │ ├── tenant.go │ │ │ └── user.go │ │ ├── options.go │ │ ├── plan.go │ │ ├── repository.go │ │ ├── tenant.go │ │ └── user.go │ ├── jobmanager/ │ │ ├── audit_cleaner.go │ │ ├── cluster_syncer.go │ │ ├── context.go │ │ └── manager.go │ ├── static/ │ │ ├── localfile.go │ │ └── static.go │ ├── types/ │ │ ├── helm.go │ │ ├── meta.go │ │ ├── request.go │ │ └── types.go │ └── util/ │ ├── container/ │ │ └── container.go │ ├── errors/ │ │ └── errors.go │ ├── log/ │ │ └── log.go │ ├── lru/ │ │ └── lru.go │ ├── ssh/ │ │ └── ssh.go │ ├── token/ │ │ └── token.go │ ├── util.go │ ├── util_test.go │ └── uuid/ │ └── uuid.go └── template/ ├── globals.go ├── hosts.go └── multinode.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report labels: ["kind/bug"] about: Create a report to help us improve --- **Describe the Bug** A clear and concise description of what the bug is. For UI issues please also add a screenshot that shows the issue. **Versions Used** KubeSphere: Kubernetes: (If KubeSphere installer used, you can skip this) **Environment** How many nodes and their hardware configuration: For example: CentOS 7.5 / 3 masters: 8cpu/8g; 3 nodes: 8cpu/16g (and other info are welcomed to help us debugging) **How To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature Request about: Have a good idea? Please don't hesitate to write it down, describe the new feature. --- **What's it about?** **What's the reason why we need it?** I believe this is an important feature for Pixiu. There're a few use cases: * case one * case two * ... Please leave your comments below if there's anyone agrees with me. Or just give me a thumb up. **Area Suggestion** /kind feature-request ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ #### What type of PR is this? #### What this PR does / why we need it: #### Which issue(s) this PR fixes: Fixes # #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note ``` #### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.: ```docs ``` ================================================ FILE: .github/workflows/build-webshell-image.yml ================================================ name: Publish webshell image on: push: branches: - "master" paths: - 'docker/Dockerfile-toolbox' - 'Makefile' env: K8S_VERSION: v1.23.6 # the same as Makefile HELM_VERSION: v3.7.1 # the same as Makefile jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Get short commit hash run: echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login in dockerhub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_NAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push webshell image uses: docker/build-push-action@v5 with: context: . file: ./docker/Dockerfile-toolbox platforms: linux/amd64,linux/arm64 build-args: | K8S_VERSION=${{ env.K8S_VERSION }} HELM_VERSION=${{ env.HELM_VERSION }} push: true tags: | ${{ secrets.DOCKER_NAME }}/pixiu-webshell:latest ${{ secrets.DOCKER_NAME }}/pixiu-webshell:v0.1 ${{ secrets.DOCKER_NAME }}/pixiu-webshell:${{ env.COMMIT_HASH }} ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - master pull_request: branches: - master env: GO_VERSION: '1.17.5' jobs: # markdown-lint: # runs-on: ubuntu-latest # container: node:14 # steps: # - name: Checkout # uses: actions/checkout@v4 # # - name: Install mdl # run: apt update && apt-get install ruby-full -y && gem install chef-utils -v 16.6.14 && gem install mdl -v 0.11.0 # # - name: Lint markdown files # run: find ./ -name "*.md" | grep -v '.github' | xargs mdl -r ~MD001,~MD004,~MD005,~MD006,~MD007,~MD010,~MD013,~MD022,~MD023,~MD024,~MD025,~MD029,~MD031,~MD032,~MD033,~MD034,~MD036 golang-lint: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: submodules: true - name: Set up Golang uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} - name: Run go fmt test run: hack/verify-gofmt.sh env: GO111MODULE: auto - name: Build the pixiu binariy run: go build -v ./... - name: Run pixiu unit test run: go test -v ./... ================================================ FILE: .github/workflows/docker-image.yml ================================================ name: Publish pixiu image on: push: branches: - "master" paths-ignore: - 'docs/**' jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Get short commit hash run: | echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV echo "TIMESTAMP=$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 # - name: Login harbor # uses: docker/login-action@v3 # with: # registry: crpi-0ecikjs9ylb2hqyo.cn-hangzhou.personal.cr.aliyuncs.com # username: ${{ secrets.HARBOR_USERNAME }} # password: ${{ secrets.HARBOR_PASSWORD }} # - name: Build and push the pixiu image # uses: docker/build-push-action@v5 # with: # context: . # file: ./docker/Dockerfile # platforms: linux/amd64,linux/arm64 # build-args: | # VERSION=${{ env.COMMIT_HASH }}-${{ env.TIMESTAMP }} # push: true # tags: | # crpi-0ecikjs9ylb2hqyo.cn-hangzhou.personal.cr.aliyuncs.com/pixiu-public/pixiu:latest # crpi-0ecikjs9ylb2hqyo.cn-hangzhou.personal.cr.aliyuncs.com/pixiu-public/pixiu:v1.0.1 # crpi-0ecikjs9ylb2hqyo.cn-hangzhou.personal.cr.aliyuncs.com/pixiu-public/pixiu:${{ env.COMMIT_HASH }} ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib bin testbin/* # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Kubernetes Generated files - skip generated files, except for vendored files !vendor/**/zz_generated.* # editor and IDE paraphernalia .idea .vscode .gitcommits *.swp *.swo # output dist # licensefmt hack/tools/licfmt/licfmt vendor ================================================ FILE: Makefile ================================================ .PHONY: run build image push clean tag = v0.1 releaseName = pixiu dockerhubUser ?= jacky06 k8sVersion ?= v1.23.6 helmVersion ?= v3.7.1 targetDir ?= dist commitHash = $(shell git rev-parse --short HEAD) # e.g. 1862ce5-20240203180617 version = $(commitHash)-$(shell date +%Y%m%d%H%M%S) ALL: run run: build ./pixiu --configfile ./config.yaml build: go build -o $(targetDir)/$(releaseName) -ldflags "-X 'main.version=$(version)'" ./cmd/ image: docker build -t $(dockerhubUser)/$(releaseName):$(tag) --build-arg VERSION=$(version) . push: image docker push $(dockerhubUser)/$(releaseName):$(tag) webshell-image: docker build --build-arg K8S_VERSION=$(k8sVersion) \ --build-arg HELM_VERSION=$(helmVersion) \ -t $(dockerhubUser)/pixiu-webshell:$(tag) -f docker/Dockerfile . push-webshell-image: webshell-image docker push $(dockerhubUser)/pixiu-webshell:$(tag) licfmt: go run hack/tools/licfmt/licfmt.go -v ./* clean: -rm -f ./$(releaseName) .PHONY: api-docs api-docs: ## generate the api docs swag init --generalInfo ./cmd/pixiuserver.go --output ./api/docs ================================================ FILE: README.md ================================================ # Pixiu Overview Pixiu is an open source container platform for cloud-native application management. ![Build Status][build-url] [![Release][release-image]][release-url] [![License][license-image]][license-url] ## 安装手册 - [安装手册](install.md) ## 页面展示 ### 首页 image ### 部署计划 - 创建部署计划 ```text 通过新建部署计划,可以实现通过页面 `点点点` 的方式创建 `kubernetes` 集群, 如同各大云厂商一样 ``` ![image](https://github.com/pixiu-io/dashboard/blob/master/images/plan_deploy.png?raw=true) - 新建节点 ```text 1. 添加各个节点的信息,节点的角色,用户名,密码等 2. 各大组件支持高度的自定义,例如:calico,fannel 2. kubernetes 版本自主选择 ``` ![image](https://github.com/pixiu-io/dashboard/blob/master/images/plan_node.png?raw=true) - 部署详情 ```text 可以看到部署计划在每个部署的运行状态,以及详细日志 ``` ![image](https://github.com/pixiu-io/dashboard/blob/master/images/plan_detail.png?raw=true) ### 集群管理 - 集群概览 ```text cpu状态,内存状态,集群的基本信息,网络信息,集群服务 ``` ![image](https://github.com/pixiu-io/dashboard/blob/master/images/cluster_detail.png?raw=true) - 集群管理 ![image](https://github.com/pixiu-io/dashboard/blob/master/images/cluster_manager.png?raw=true) - 集群工作负载deployment ![image](https://github.com/pixiu-io/dashboard/blob/master/images/cluster_deploy.png?raw=true) - 集群工作负载pod ![image](https://github.com/pixiu-io/dashboard/blob/master/images/cluster_pod.png?raw=true) ### 审计功能 - 审计管理 ![image](https://github.com/pixiu-io/dashboard/blob/master/images/audit_manager.png?raw=true) ## 学习分享 - [go-learning](https://github.com/caoyingjunz/go-learning) ## 沟通交流 - 搜索微信号 `yingjuncz`, 备注(pixiu), 验证通过会加入群聊 - [bilibili](https://space.bilibili.com/3493104248162809?spm_id_from=333.1007.0.0) 技术分享 Copyright 2019 caoyingjun (cao.yingjunz@gmail.com) Apache License 2.0 [build-url]: https://github.com/caoyingjunz/pixiu/actions/workflows/ci.yml/badge.svg [release-image]: https://img.shields.io/badge/release-download-orange.svg [release-url]: https://www.apache.org/licenses/LICENSE-2.0.html [license-image]: https://img.shields.io/badge/license-Apache%202-4EB1BA.svg [license-url]: https://www.apache.org/licenses/LICENSE-2.0.html ================================================ FILE: api/docs/docs.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package docs GENERATED BY SWAG; DO NOT EDIT // This file was generated by swaggo/swag package docs import "github.com/swaggo/swag" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, "swagger": "2.0", "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", "termsOfService": "https://github.com/caoyingjunz/pixiu", "contact": { "name": "API Support", "url": "https://github.com/caoyingjunz/pixiu", "email": "support@pixiu.io" }, "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, "version": "{{.Version}}" }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { "/pixiu/clusters": { "get": { "security": [ { "Bearer": [] } ], "description": "List clusters", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Clusters" ], "summary": "List clusters", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "allOf": [ { "$ref": "#/definitions/httputils.Response" }, { "type": "object", "properties": { "result": { "type": "array", "items": { "$ref": "#/definitions/types.Cluster" } } } } ] } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/clusters/": { "post": { "security": [ { "Bearer": [] } ], "description": "Create by a json cluster", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Clusters" ], "summary": "Create a cluster", "parameters": [ { "description": "Create cluster", "name": "cluster", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.Cluster" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/clusters/{clusterId}": { "get": { "security": [ { "Bearer": [] } ], "description": "Get by cloud cluster ID", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Clusters" ], "summary": "Get Cluster by clusterId", "parameters": [ { "type": "integer", "description": "Cluster ID", "name": "clusterId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/httputils.Response" }, { "type": "object", "properties": { "result": { "$ref": "#/definitions/types.Cluster" } } } ] } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } }, "put": { "security": [ { "Bearer": [] } ], "description": "Update by json cluster", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Clusters" ], "summary": "Update an cluster", "parameters": [ { "type": "integer", "description": "Cluster ID", "name": "clusterId", "in": "path", "required": true }, { "description": "Update cluster", "name": "cluster", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.Cluster" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } }, "delete": { "security": [ { "Bearer": [] } ], "description": "Delete by cloud cluster ID", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Clusters" ], "summary": "Delete cluster by clusterId", "parameters": [ { "type": "integer", "description": "Cluster ID", "name": "clusterId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/clusters/{clusterId}/ping": { "get": { "security": [ { "Bearer": [] } ], "description": "Do ping", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Clusters" ], "summary": "Ping cluster", "parameters": [ { "type": "integer", "description": "Cluster ID", "name": "clusterId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/httputils.Response" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/users": { "get": { "security": [ { "Bearer": [] } ], "description": "List users", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "List users", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "allOf": [ { "$ref": "#/definitions/httputils.Response" }, { "type": "object", "properties": { "result": { "type": "array", "items": { "$ref": "#/definitions/types.User" } } } } ] } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/users/": { "post": { "security": [ { "Bearer": [] } ], "description": "Create by a json user", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Create a user", "parameters": [ { "description": "Create user", "name": "user", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.User" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/users/login": { "post": { "description": "Login by a json user", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Login" ], "summary": "User login", "parameters": [ { "description": "User login", "name": "user", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.User" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/users/{userId}": { "get": { "security": [ { "Bearer": [] } ], "description": "Get by user ID", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Get user by userId", "parameters": [ { "type": "integer", "description": "User ID", "name": "userId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/httputils.Response" }, { "type": "object", "properties": { "result": { "$ref": "#/definitions/types.User" } } } ] } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } }, "put": { "security": [ { "Bearer": [] } ], "description": "Update by json user", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Update an user", "parameters": [ { "type": "integer", "description": "User ID", "name": "userId", "in": "path", "required": true }, { "description": "Update user", "name": "user", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.User" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } }, "delete": { "security": [ { "Bearer": [] } ], "description": "Delete by userID", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Delete user by userId", "parameters": [ { "type": "integer", "description": "User ID", "name": "userId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } } }, "definitions": { "httputils.Response": { "type": "object", "properties": { "code": { "description": "返回的状态码", "type": "integer" }, "message": { "description": "异常返回时的错误信息", "type": "string" }, "result": { "description": "正常返回时的数据,可以为任意数据结构" } } }, "types.Cluster": { "type": "object", "properties": { "alias_name": { "type": "string" }, "cluster_type": { "description": "0:标准集群 1: 自建集群", "type": "integer" }, "description": { "description": "集群用途描述,可以为空", "type": "string" }, "gmt_create": { "description": "pixiu 对象创建时间", "type": "string" }, "gmt_modified": { "description": "pixiu 对象修改时间", "type": "string" }, "id": { "description": "pixiu 对象 ID", "type": "integer" }, "kube_config": { "description": "k8s kubeConfig base64 字段", "type": "string" }, "kubernetes_version": { "description": "集群的版本", "type": "string" }, "name": { "type": "string" }, "nodes": { "description": "节点数量", "type": "integer" }, "resource_version": { "description": "Pixiu 对象版本号", "type": "integer" }, "resources": { "description": "The memory and cpu usage", "$ref": "#/definitions/types.Resources" } } }, "types.Resources": { "type": "object", "properties": { "cpu": { "type": "string" }, "memory": { "type": "string" } } }, "types.User": { "type": "object", "properties": { "description": { "description": "用户描述信息", "type": "string" }, "email": { "description": "用户注册邮件", "type": "string" }, "gmt_create": { "description": "pixiu 对象创建时间", "type": "string" }, "gmt_modified": { "description": "pixiu 对象修改时间", "type": "string" }, "id": { "description": "pixiu 对象 ID", "type": "integer" }, "name": { "description": "用户名称", "type": "string" }, "password": { "description": "用户密码", "type": "string" }, "resource_version": { "description": "Pixiu 对象版本号", "type": "integer" }, "role": { "description": "用户角色,目前只实现管理员,0: 普通用户 1: 管理员 2: 超级管理员", "type": "string" }, "status": { "description": "用户状态标识", "type": "integer" } } } }, "securityDefinitions": { "Bearer": { "description": "Type \"Bearer\" followed by a space and JWT token", "type": "apiKey", "name": "Authorization", "in": "header" } } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "1.0", Host: "localhost:8090", BasePath: "", Schemes: []string{"http", "https"}, Title: "Pixiu API Documentation", Description: "", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, } func init() { swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) } ================================================ FILE: api/docs/swagger.json ================================================ { "schemes": [ "http", "https" ], "swagger": "2.0", "info": { "title": "Pixiu API Documentation", "termsOfService": "https://github.com/caoyingjunz/pixiu", "contact": { "name": "API Support", "url": "https://github.com/caoyingjunz/pixiu", "email": "support@pixiu.io" }, "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, "version": "1.0" }, "host": "localhost:8090", "paths": { "/pixiu/clusters": { "get": { "security": [ { "Bearer": [] } ], "description": "List clusters", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Clusters" ], "summary": "List clusters", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "allOf": [ { "$ref": "#/definitions/httputils.Response" }, { "type": "object", "properties": { "result": { "type": "array", "items": { "$ref": "#/definitions/types.Cluster" } } } } ] } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/clusters/": { "post": { "security": [ { "Bearer": [] } ], "description": "Create by a json cluster", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Clusters" ], "summary": "Create a cluster", "parameters": [ { "description": "Create cluster", "name": "cluster", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.Cluster" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/clusters/{clusterId}": { "get": { "security": [ { "Bearer": [] } ], "description": "Get by cloud cluster ID", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Clusters" ], "summary": "Get Cluster by clusterId", "parameters": [ { "type": "integer", "description": "Cluster ID", "name": "clusterId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/httputils.Response" }, { "type": "object", "properties": { "result": { "$ref": "#/definitions/types.Cluster" } } } ] } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } }, "put": { "security": [ { "Bearer": [] } ], "description": "Update by json cluster", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Clusters" ], "summary": "Update an cluster", "parameters": [ { "type": "integer", "description": "Cluster ID", "name": "clusterId", "in": "path", "required": true }, { "description": "Update cluster", "name": "cluster", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.Cluster" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } }, "delete": { "security": [ { "Bearer": [] } ], "description": "Delete by cloud cluster ID", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Clusters" ], "summary": "Delete cluster by clusterId", "parameters": [ { "type": "integer", "description": "Cluster ID", "name": "clusterId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/clusters/{clusterId}/ping": { "get": { "security": [ { "Bearer": [] } ], "description": "Do ping", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Clusters" ], "summary": "Ping cluster", "parameters": [ { "type": "integer", "description": "Cluster ID", "name": "clusterId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/httputils.Response" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/users": { "get": { "security": [ { "Bearer": [] } ], "description": "List users", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "List users", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "allOf": [ { "$ref": "#/definitions/httputils.Response" }, { "type": "object", "properties": { "result": { "type": "array", "items": { "$ref": "#/definitions/types.User" } } } } ] } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/users/": { "post": { "security": [ { "Bearer": [] } ], "description": "Create by a json user", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Create a user", "parameters": [ { "description": "Create user", "name": "user", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.User" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/users/login": { "post": { "description": "Login by a json user", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Login" ], "summary": "User login", "parameters": [ { "description": "User login", "name": "user", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.User" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } }, "/pixiu/users/{userId}": { "get": { "security": [ { "Bearer": [] } ], "description": "Get by user ID", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Get user by userId", "parameters": [ { "type": "integer", "description": "User ID", "name": "userId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "allOf": [ { "$ref": "#/definitions/httputils.Response" }, { "type": "object", "properties": { "result": { "$ref": "#/definitions/types.User" } } } ] } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } }, "put": { "security": [ { "Bearer": [] } ], "description": "Update by json user", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Update an user", "parameters": [ { "type": "integer", "description": "User ID", "name": "userId", "in": "path", "required": true }, { "description": "Update user", "name": "user", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.User" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } }, "delete": { "security": [ { "Bearer": [] } ], "description": "Delete by userID", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Delete user by userId", "parameters": [ { "type": "integer", "description": "User ID", "name": "userId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/httputils.Response" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/httputils.Response" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/httputils.Response" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/httputils.Response" } } } } } }, "definitions": { "httputils.Response": { "type": "object", "properties": { "code": { "description": "返回的状态码", "type": "integer" }, "message": { "description": "异常返回时的错误信息", "type": "string" }, "result": { "description": "正常返回时的数据,可以为任意数据结构" } } }, "types.Cluster": { "type": "object", "properties": { "alias_name": { "type": "string" }, "cluster_type": { "description": "0:标准集群 1: 自建集群", "type": "integer" }, "description": { "description": "集群用途描述,可以为空", "type": "string" }, "gmt_create": { "description": "pixiu 对象创建时间", "type": "string" }, "gmt_modified": { "description": "pixiu 对象修改时间", "type": "string" }, "id": { "description": "pixiu 对象 ID", "type": "integer" }, "kube_config": { "description": "k8s kubeConfig base64 字段", "type": "string" }, "kubernetes_version": { "description": "集群的版本", "type": "string" }, "name": { "type": "string" }, "nodes": { "description": "节点数量", "type": "integer" }, "resource_version": { "description": "Pixiu 对象版本号", "type": "integer" }, "resources": { "description": "The memory and cpu usage", "$ref": "#/definitions/types.Resources" } } }, "types.Resources": { "type": "object", "properties": { "cpu": { "type": "string" }, "memory": { "type": "string" } } }, "types.User": { "type": "object", "properties": { "description": { "description": "用户描述信息", "type": "string" }, "email": { "description": "用户注册邮件", "type": "string" }, "gmt_create": { "description": "pixiu 对象创建时间", "type": "string" }, "gmt_modified": { "description": "pixiu 对象修改时间", "type": "string" }, "id": { "description": "pixiu 对象 ID", "type": "integer" }, "name": { "description": "用户名称", "type": "string" }, "password": { "description": "用户密码", "type": "string" }, "resource_version": { "description": "Pixiu 对象版本号", "type": "integer" }, "role": { "description": "用户角色,目前只实现管理员,0: 普通用户 1: 管理员 2: 超级管理员", "type": "string" }, "status": { "description": "用户状态标识", "type": "integer" } } } }, "securityDefinitions": { "Bearer": { "description": "Type \"Bearer\" followed by a space and JWT token", "type": "apiKey", "name": "Authorization", "in": "header" } } } ================================================ FILE: api/docs/swagger.yaml ================================================ definitions: httputils.Response: properties: code: description: 返回的状态码 type: integer message: description: 异常返回时的错误信息 type: string result: description: 正常返回时的数据,可以为任意数据结构 type: object types.Cluster: properties: alias_name: type: string cluster_type: description: '0:标准集群 1: 自建集群' type: integer description: description: 集群用途描述,可以为空 type: string gmt_create: description: pixiu 对象创建时间 type: string gmt_modified: description: pixiu 对象修改时间 type: string id: description: pixiu 对象 ID type: integer kube_config: description: k8s kubeConfig base64 字段 type: string kubernetes_version: description: 集群的版本 type: string name: type: string nodes: description: 节点数量 type: integer resource_version: description: Pixiu 对象版本号 type: integer resources: $ref: '#/definitions/types.Resources' description: The memory and cpu usage type: object types.Resources: properties: cpu: type: string memory: type: string type: object types.User: properties: description: description: 用户描述信息 type: string email: description: 用户注册邮件 type: string gmt_create: description: pixiu 对象创建时间 type: string gmt_modified: description: pixiu 对象修改时间 type: string id: description: pixiu 对象 ID type: integer name: description: 用户名称 type: string password: description: 用户密码 type: string resource_version: description: Pixiu 对象版本号 type: integer role: description: '用户角色,目前只实现管理员,0: 普通用户 1: 管理员 2: 超级管理员' type: string status: description: 用户状态标识 type: integer type: object host: localhost:8090 info: contact: email: support@pixiu.io name: API Support url: https://github.com/caoyingjunz/pixiu license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html termsOfService: https://github.com/caoyingjunz/pixiu title: Pixiu API Documentation version: "1.0" paths: /pixiu/clusters: get: consumes: - application/json description: List clusters produces: - application/json responses: "200": description: OK schema: items: allOf: - $ref: '#/definitions/httputils.Response' - properties: result: items: $ref: '#/definitions/types.Cluster' type: array type: object type: array "400": description: Bad Request schema: $ref: '#/definitions/httputils.Response' "404": description: Not Found schema: $ref: '#/definitions/httputils.Response' "500": description: Internal Server Error schema: $ref: '#/definitions/httputils.Response' security: - Bearer: [] summary: List clusters tags: - Clusters /pixiu/clusters/: post: consumes: - application/json description: Create by a json cluster parameters: - description: Create cluster in: body name: cluster required: true schema: $ref: '#/definitions/types.Cluster' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/httputils.Response' "400": description: Bad Request schema: $ref: '#/definitions/httputils.Response' "404": description: Not Found schema: $ref: '#/definitions/httputils.Response' "500": description: Internal Server Error schema: $ref: '#/definitions/httputils.Response' security: - Bearer: [] summary: Create a cluster tags: - Clusters /pixiu/clusters/{clusterId}: delete: consumes: - application/json description: Delete by cloud cluster ID parameters: - description: Cluster ID in: path name: clusterId required: true type: integer produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/httputils.Response' "400": description: Bad Request schema: $ref: '#/definitions/httputils.Response' "404": description: Not Found schema: $ref: '#/definitions/httputils.Response' "500": description: Internal Server Error schema: $ref: '#/definitions/httputils.Response' security: - Bearer: [] summary: Delete cluster by clusterId tags: - Clusters get: consumes: - application/json description: Get by cloud cluster ID parameters: - description: Cluster ID in: path name: clusterId required: true type: integer produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/httputils.Response' - properties: result: $ref: '#/definitions/types.Cluster' type: object "400": description: Bad Request schema: $ref: '#/definitions/httputils.Response' "404": description: Not Found schema: $ref: '#/definitions/httputils.Response' "500": description: Internal Server Error schema: $ref: '#/definitions/httputils.Response' security: - Bearer: [] summary: Get Cluster by clusterId tags: - Clusters put: consumes: - application/json description: Update by json cluster parameters: - description: Cluster ID in: path name: clusterId required: true type: integer - description: Update cluster in: body name: cluster required: true schema: $ref: '#/definitions/types.Cluster' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/httputils.Response' "400": description: Bad Request schema: $ref: '#/definitions/httputils.Response' "404": description: Not Found schema: $ref: '#/definitions/httputils.Response' "500": description: Internal Server Error schema: $ref: '#/definitions/httputils.Response' security: - Bearer: [] summary: Update an cluster tags: - Clusters /pixiu/clusters/{clusterId}/ping: get: consumes: - application/json description: Do ping parameters: - description: Cluster ID in: path name: clusterId required: true type: integer produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/httputils.Response' type: array "400": description: Bad Request schema: $ref: '#/definitions/httputils.Response' "404": description: Not Found schema: $ref: '#/definitions/httputils.Response' "500": description: Internal Server Error schema: $ref: '#/definitions/httputils.Response' security: - Bearer: [] summary: Ping cluster tags: - Clusters /pixiu/users: get: consumes: - application/json description: List users produces: - application/json responses: "200": description: OK schema: items: allOf: - $ref: '#/definitions/httputils.Response' - properties: result: items: $ref: '#/definitions/types.User' type: array type: object type: array "400": description: Bad Request schema: $ref: '#/definitions/httputils.Response' "404": description: Not Found schema: $ref: '#/definitions/httputils.Response' "500": description: Internal Server Error schema: $ref: '#/definitions/httputils.Response' security: - Bearer: [] summary: List users tags: - Users /pixiu/users/: post: consumes: - application/json description: Create by a json user parameters: - description: Create user in: body name: user required: true schema: $ref: '#/definitions/types.User' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/httputils.Response' "400": description: Bad Request schema: $ref: '#/definitions/httputils.Response' "404": description: Not Found schema: $ref: '#/definitions/httputils.Response' "500": description: Internal Server Error schema: $ref: '#/definitions/httputils.Response' security: - Bearer: [] summary: Create a user tags: - Users /pixiu/users/{userId}: delete: consumes: - application/json description: Delete by userID parameters: - description: User ID in: path name: userId required: true type: integer produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/httputils.Response' "400": description: Bad Request schema: $ref: '#/definitions/httputils.Response' "404": description: Not Found schema: $ref: '#/definitions/httputils.Response' "500": description: Internal Server Error schema: $ref: '#/definitions/httputils.Response' security: - Bearer: [] summary: Delete user by userId tags: - Users get: consumes: - application/json description: Get by user ID parameters: - description: User ID in: path name: userId required: true type: integer produces: - application/json responses: "200": description: OK schema: allOf: - $ref: '#/definitions/httputils.Response' - properties: result: $ref: '#/definitions/types.User' type: object "400": description: Bad Request schema: $ref: '#/definitions/httputils.Response' "404": description: Not Found schema: $ref: '#/definitions/httputils.Response' "500": description: Internal Server Error schema: $ref: '#/definitions/httputils.Response' security: - Bearer: [] summary: Get user by userId tags: - Users put: consumes: - application/json description: Update by json user parameters: - description: User ID in: path name: userId required: true type: integer - description: Update user in: body name: user required: true schema: $ref: '#/definitions/types.User' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/httputils.Response' "400": description: Bad Request schema: $ref: '#/definitions/httputils.Response' "404": description: Not Found schema: $ref: '#/definitions/httputils.Response' "500": description: Internal Server Error schema: $ref: '#/definitions/httputils.Response' security: - Bearer: [] summary: Update an user tags: - Users /pixiu/users/login: post: consumes: - application/json description: Login by a json user parameters: - description: User login in: body name: user required: true schema: $ref: '#/definitions/types.User' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/httputils.Response' "400": description: Bad Request schema: $ref: '#/definitions/httputils.Response' "404": description: Not Found schema: $ref: '#/definitions/httputils.Response' "500": description: Internal Server Error schema: $ref: '#/definitions/httputils.Response' summary: User login tags: - Login schemes: - http - https securityDefinitions: Bearer: description: Type "Bearer" followed by a space and JWT token in: header name: Authorization type: apiKey swagger: "2.0" ================================================ FILE: api/server/errors/errors.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "net/http" "github.com/caoyingjunz/pixiu/pkg/util/errors" ) type Error struct { Code int Err error } func (e Error) Error() string { return e.Err.Error() } func NewError(err error, code int) Error { return Error{ Code: code, Err: err, } } var ( ErrUnauthorized = Error{ Code: http.StatusUnauthorized, Err: errors.NoUserIdError, } ErrForbidden = Error{ Code: http.StatusForbidden, Err: errors.NoPermission, } ErrInvalidRequest = Error{ Code: http.StatusBadRequest, Err: errors.ErrReqParams, } ErrServerInternal = Error{ Code: http.StatusInternalServerError, Err: errors.ErrInternal, } ErrUserNotFound = Error{ Code: http.StatusNotFound, Err: errors.ErrUserNotFound, } ErrNotAcceptable = Error{ Code: http.StatusNotAcceptable, Err: errors.ErrNotAcceptable, } ErrUserExists = Error{ Code: http.StatusConflict, Err: errors.UserExistError, } ErrInvalidPassword = Error{ Code: http.StatusUnauthorized, Err: errors.ErrUserPassword, } ErrDuplicatedPassword = Error{ Code: http.StatusConflict, Err: errors.ErrDuplicatedPassword, } ErrClusterNotFound = Error{ Code: http.StatusNotFound, Err: errors.ErrClusterNotFound, } ErrTenantExists = Error{ Code: http.StatusConflict, Err: errors.TenantExistError, } ErrTenantNotFound = Error{ Code: http.StatusNotFound, Err: errors.ErrTenantNotFound, } ErrAuditNotFound = Error{ Code: http.StatusNotFound, Err: errors.ErrAuditNotFound, } ErrAuditExists = Error{ Code: http.StatusConflict, Err: errors.ErrAuditExists, } ErrRBACPolicyExists = Error{ Code: http.StatusConflict, Err: errors.PolicyExistError, } ErrRBACPolicyNotFound = Error{ Code: http.StatusNotFound, Err: errors.PolicyNotExistError, } ErrGroupBindingExists = Error{ Code: http.StatusConflict, Err: errors.PolicyExistError, } ErrGroupBindingNotFound = Error{ Code: http.StatusNotFound, Err: errors.PolicyNotExistError, } ErrRootAlreadyExists = Error{ Code: http.StatusConflict, Err: errors.ErrRootAlreadyExists, } ) ================================================ FILE: api/server/httpstatus/status.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package httpstatus // TODO: 自定义状态码 ================================================ FILE: api/server/httputils/docs.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package httputils // HttpOK 正常返回 type HttpOK struct { Code int `json:"code" example:"200"` Result string `json:"result" example:"any result"` } // HttpError 异常返回 type HttpError struct { Code int `json:"code" example:"400"` Message string `json:"message" example:"status bad request"` } ================================================ FILE: api/server/httputils/httputils.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package httputils import ( "context" goerrors "errors" "fmt" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "github.com/caoyingjunz/pixiu/api/server/errors" validatorutil "github.com/caoyingjunz/pixiu/api/server/validator" "github.com/caoyingjunz/pixiu/pkg/db/model" ) type Response struct { Code int `json:"code"` // 返回的状态码 Result interface{} `json:"result,omitempty"` // 正常返回时的数据,可以为任意数据结构 Message string `json:"message,omitempty"` // 异常返回时的错误信息 } func (r *Response) SetCode(c int) { r.Code = c } func (r *Response) SetMessage(m interface{}) { switch msg := m.(type) { case error: r.Message = msg.Error() case string: r.Message = msg } } func (r *Response) SetMessageWithCode(m interface{}, c int) { r.SetCode(c) r.SetMessage(m) } func (r *Response) Error() string { return r.Message } func (r *Response) String() string { //data, _ := json.Marshal(r) //return string(data) return "" } // NewResponse 构造 http 返回值 // SetSuccess 时设置 code 为 200 并追加 success 的标识 // SetFailed 时设置 code 为 400,也可以自定义设置错误码,并追加报错信息 func NewResponse() *Response { return &Response{} } // SetSuccess 设置成功返回值 func SetSuccess(c *gin.Context, r *Response) { _ = contextBind(c).withResponseCode(http.StatusOK) r.SetMessageWithCode("success", http.StatusOK) c.JSON(http.StatusOK, r) } // SetFailed 设置错误返回值 func SetFailed(c *gin.Context, r *Response, err error) { switch e := err.(type) { case errors.Error: setFailedWithCode(c, r, e.Code, e) case validator.ValidationErrors: setFailedWithValidationError(c, r, validatorutil.TranslateError(e)) default: setFailedWithCode(c, r, http.StatusBadRequest, err) } } // SetFailedWithCode 设置错误返回值 func setFailedWithCode(c *gin.Context, r *Response, code int, err error) { _ = contextBind(c).withResponseCode(code).withRawError(err) r.SetMessageWithCode(err, code) c.JSON(http.StatusOK, r) } func setFailedWithValidationError(c *gin.Context, r *Response, e string) { _ = contextBind(c).withResponseCode(http.StatusBadRequest).withRawError(goerrors.New(e)) r.SetMessageWithCode(e, http.StatusBadRequest) c.JSON(http.StatusOK, r) } // AbortFailedWithCode 设置错误,code 返回值并终止请求 func AbortFailedWithCode(c *gin.Context, code int, err error) { r := NewResponse() _ = contextBind(c).withResponseCode(code).withRawError(err) r.SetMessageWithCode(err, code) c.JSON(http.StatusOK, r) c.Abort() } func ShouldBindAny(c *gin.Context, jsonObject interface{}, uriObject interface{}, queryObject interface{}) error { var err error if jsonObject != nil { if err = c.ShouldBindJSON(jsonObject); err != nil { return err } } if uriObject != nil { if err = c.ShouldBindUri(uriObject); err != nil { return err } } if queryObject != nil { if err = c.ShouldBindQuery(queryObject); err != nil { return err } } return nil } const userKey = "user" func GetUserFromRequest(ctx context.Context) (*model.User, error) { val := ctx.Value(userKey) if val == nil { return nil, fmt.Errorf("get nil user") } user, ok := val.(*model.User) if !ok { return nil, fmt.Errorf("failed to assert user") } return user, nil } func GetUserIdFromContext(ctx context.Context) (int64, error) { user, err := GetUserFromRequest(ctx) if err != nil { return 0, err } return user.Id, nil } func SetUserToContext(c *gin.Context, user *model.User) { c.Set(userKey, user) } func GetObjectFromRequest(c *gin.Context) (string, string, bool) { return getObjectFromRequest(c.Request.URL.Path) } // getObjectFromRequest cuts and returns the object from the request path. // e.g. /pixiu/clusters/1 -> "clusters" "1" true func getObjectFromRequest(path string) (obj, sid string, ok bool) { // must start with / l := len(path) if l == 0 || path[0] != '/' { return } subs := strings.Split(path[1:l], "/") l = len(subs) if l < 2 || subs[0] != "pixiu" { return } if l == 2 { // e.g. /pixiu/clusters -> "clusters" "" true return subs[1], "", subs[1] != "" } return subs[1], subs[2], subs[1] != "" && subs[2] != "" } const ( objIDsKey = "objIDs" ) func SetIdRangeContext(c *gin.Context, ids []int64) { c.Set(objIDsKey, ids) } func GetIdRangeFromListReq(ctx context.Context) (exists bool, ids []int64) { val := ctx.Value(objIDsKey) if val == nil { return } ids, exists = val.([]int64) return } const ( ResponseCodeKey = "response_code" RawErrorKey = "raw_error" ) type ctxBind struct { *gin.Context } func contextBind(c *gin.Context) *ctxBind { return &ctxBind{c} } // withResponseCode puts the response code into the HTTP context. func (cb *ctxBind) withResponseCode(code int) *ctxBind { cb.Set(ResponseCodeKey, code) return cb } // withRawError puts the raw error into the HTTP context. func (cb *ctxBind) withRawError(err error) *ctxBind { cb.Set(RawErrorKey, err) return cb } // GetResponseCode gets the response code from the HTTP context. func GetResponseCode(ctx context.Context) (code int) { val := ctx.Value(ResponseCodeKey) if val == nil { return } code = val.(int) return } // GetRawError gets the raw error from the HTTP context. func GetRawError(ctx context.Context) (err error) { val := ctx.Value(RawErrorKey) if val == nil { return } err = val.(error) return } ================================================ FILE: api/server/httputils/httputils_test.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package httputils import "testing" func Test_getObjectFromRequest(t *testing.T) { tests := []struct { name string path string wantObj string wantSid string wantOk bool }{ { name: "test0", path: "", wantObj: "", wantOk: false, }, { name: "test1", path: "/", wantObj: "", wantOk: false, }, { name: "test2", path: "//", wantObj: "", wantOk: false, }, { name: "test3", path: "pixiu", wantObj: "", wantOk: false, }, { name: "test4", path: "/pixiu", wantObj: "", wantOk: false, }, { name: "test5", path: "/pixiu/", wantObj: "", wantOk: false, }, { name: "test6", path: "/pixiu/users", wantObj: "users", wantSid: "", wantOk: true, }, { name: "test7", path: "/pixiu/users/", wantObj: "users", wantOk: false, }, { name: "test8", path: "/pixiu/users/1", wantObj: "users", wantSid: "1", wantOk: true, }, { name: "test9", path: "/pixiu//", wantObj: "", wantOk: false, }, { name: "test10", path: "///", wantObj: "", wantOk: false, }, { name: "test11", path: "/pixiu/users/1/password", wantObj: "users", wantSid: "1", wantOk: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotObj, gotSid, gotOk := getObjectFromRequest(tt.path) if gotObj != tt.wantObj { t.Errorf("getObjectFromRequest() gotObj = %v, want %v", gotObj, tt.wantObj) } if gotSid != tt.wantSid { t.Errorf("getObjectFromRequest() gotSid = %v, want %v", gotSid, tt.wantSid) } if gotOk != tt.wantOk { t.Errorf("getObjectFromRequest() gotOk = %v, want %v", gotOk, tt.wantOk) } }) } } ================================================ FILE: api/server/middleware/admission.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package middleware import "github.com/gin-gonic/gin" // Admission 准入控制 func Admission() gin.HandlerFunc { return func(c *gin.Context) {} } ================================================ FILE: api/server/middleware/audit.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package middleware import ( "context" "net/http" "strings" "sync" "time" "github.com/gin-contrib/requestid" "github.com/gin-gonic/gin" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/cmd/app/options" "github.com/caoyingjunz/pixiu/pkg/db" "github.com/caoyingjunz/pixiu/pkg/db/model" ) const ( defaultAuditQueueSize = 2048 defaultAuditWorkers = 2 auditWriteTimeout = 3 * time.Second ) type auditRecorder struct { factory db.ShareDaoFactory queue chan *model.Audit } var ( recorderOnce sync.Once recorderInst *auditRecorder ) func getAuditRecorder(o *options.Options) *auditRecorder { recorderOnce.Do(func() { recorderInst = &auditRecorder{ factory: o.Factory, queue: make(chan *model.Audit, defaultAuditQueueSize), } for i := 0; i < defaultAuditWorkers; i++ { go recorderInst.run() } }) return recorderInst } func (r *auditRecorder) run() { for record := range r.queue { r.write(record) } } func (r *auditRecorder) write(record *model.Audit) { ctx, cancel := context.WithTimeout(context.Background(), auditWriteTimeout) defer cancel() if _, err := r.factory.Audit().Create(ctx, record); err != nil { klog.Errorf("failed to create audit record [%s]: %v", record.String(), err) } } func (r *auditRecorder) enqueue(record *model.Audit) { select { case r.queue <- record: default: // 队列满时降级同步写入,确保非 GET 请求都能落库 klog.Warningf("audit queue is full, fallback to direct write: %s", record.Path) r.write(record) } } func Audit(o *options.Options) gin.HandlerFunc { recorder := getAuditRecorder(o) return func(c *gin.Context) { if !shouldAudit(c) { c.Next() return } startTime := time.Now() c.Next() recorder.enqueue(buildAuditRecord(c, startTime)) } } func buildAuditRecord(c *gin.Context, startTime time.Time) *model.Audit { userName := "unknown" if user, err := httputils.GetUserFromRequest(c); err == nil && user != nil { userName = user.Name } cluster, resourceName, resourceNamespace := parseK8sProxyPath(c.Request.URL.Path) return &model.Audit{ RequestId: requestid.Get(c), Action: c.Request.Method, Ip: c.ClientIP(), Operator: userName, Path: c.Request.RequestURI, ObjectType: detectObjectType(c), Status: getAuditStatus(c), Duration: time.Since(startTime).Milliseconds(), ResponseCode: c.Writer.Status(), Cluster: cluster, ResourceName: resourceName, ResourceNamespace: resourceNamespace, } } // parseK8sProxyPath 从 K8s proxy URL 路径中解析集群、资源名称和命名空间。 // 支持以下路径格式: // - /pixiu/proxy/{cluster}/api/v1/namespaces/{namespace}/{resource}/{name} // - /pixiu/proxy/{cluster}/apis/{group}/{version}/namespaces/{namespace}/{resource}/{name} // - /pixiu/proxy/{cluster}/api/v1/{resource}/{name}(集群级资源) func parseK8sProxyPath(path string) (cluster, resourceName, resourceNamespace string) { // /pixiu/proxy/{cluster}/... const proxyPrefix = "/pixiu/proxy/" if !strings.HasPrefix(path, proxyPrefix) { return } rest := path[len(proxyPrefix):] parts := strings.SplitN(rest, "/", 2) if len(parts) < 1 || parts[0] == "" { return } cluster = parts[0] if len(parts) < 2 { return } segments := strings.Split(parts[1], "/") // 查找 namespaces/{ns} 段 for i := 0; i < len(segments)-1; i++ { if segments[i] == "namespaces" { resourceNamespace = segments[i+1] // namespace 之后还有 {resource}/{name} if i+3 < len(segments) { resourceName = segments[i+3] } return } } // 无 namespaces 段,集群级资源:api/v1/{resource}/{name} 或 apis/.../.../{resource}/{name} if len(segments) > 0 { resourceName = segments[len(segments)-1] } return } func shouldAudit(c *gin.Context) bool { if c.Request.Method == http.MethodGet || c.Request.Method == http.MethodOptions { return false } return strings.HasPrefix(c.Request.URL.Path, "/pixiu") } func detectObjectType(c *gin.Context) model.ObjectType { obj, _, ok := httputils.GetObjectFromRequest(c) if !ok { return model.ObjectAll } ot := model.ObjectType(obj) if _, exists := model.ObjectTypeMap[ot]; exists { return ot } return model.ObjectAll } // getAuditStatus returns the status of operation. func getAuditStatus(c *gin.Context) model.AuditOperationStatus { respCode := httputils.GetResponseCode(c) if respCode == 0 { respCode = c.Writer.Status() if respCode == 0 { return model.AuditOpUnknown } } if responseOK(respCode) { return model.AuditOpSuccess } return model.AuditOpFail } func responseOK(code int) bool { return code == http.StatusOK || code == http.StatusCreated || code == http.StatusAccepted } ================================================ FILE: api/server/middleware/authentication.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package middleware import ( "fmt" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/errors" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/cmd/app/options" tokenutil "github.com/caoyingjunz/pixiu/pkg/util/token" ) // Authentication 身份认证 func Authentication(o *options.Options) gin.HandlerFunc { keyBytes := []byte(o.ComponentConfig.Default.JWTKey) return func(c *gin.Context) { if o.ComponentConfig.Default.Mode.InDebug() { // Considered all as root user when running in debug mode. root, err := o.Factory.User().GetRoot(c) if err != nil { httputils.AbortFailedWithCode(c, http.StatusInternalServerError, err) return } httputils.SetUserToContext(c, root) return } if alwaysAllowPath.Has(c.Request.URL.Path) || allowCustomRequest(c) { return } if err := validate(c, o, keyBytes); err != nil { httputils.AbortFailedWithCode(c, http.StatusUnauthorized, err) return } } } func validate(c *gin.Context, o *options.Options, keyBytes []byte) error { token, err := extractToken(c, false) if err != nil { return err } claim, err := tokenutil.ParseToken(token, keyBytes) if err != nil { return err } existToken, err := o.Controller.User().GetLoginToken(c, claim.Id) if err != nil { return fmt.Errorf("未登陆或者密码被修改,请重新登陆") } if token != existToken { return fmt.Errorf("已被他人登陆") } user, err := o.Factory.User().Get(c, claim.Id) if err != nil { return err } if user == nil { return errors.ErrUnauthorized } httputils.SetUserToContext(c, user) return nil } // 从请求头中获取 token func extractToken(c *gin.Context, ws bool) (string, error) { emptyFunc := func(t string) bool { return len(t) == 0 } if ws { wsToken := c.GetHeader("Sec-WebSocket-Protocol") if emptyFunc(wsToken) { return "", fmt.Errorf("authorization header is not provided") } return wsToken, nil } token := c.GetHeader("Authorization") if emptyFunc(token) { return "", fmt.Errorf("authorization header is not provided") } fields := strings.Fields(token) if len(fields) != 2 { return "", fmt.Errorf("invalid authorization header format") } if fields[0] != "Bearer" { return "", fmt.Errorf("unsupported authorization type") } return fields[1], nil } ================================================ FILE: api/server/middleware/authorization.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package middleware import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/api/server/router/cluster" "github.com/caoyingjunz/pixiu/api/server/router/proxy" "github.com/caoyingjunz/pixiu/cmd/app/options" ctrlutil "github.com/caoyingjunz/pixiu/pkg/controller/util" "github.com/caoyingjunz/pixiu/pkg/db/model" ) // HTTP method to operation var operationsMap = map[string]model.Operation{ http.MethodGet: model.OpRead, http.MethodPost: model.OpCreate, http.MethodPatch: model.OpUpdate, http.MethodPut: model.OpUpdate, http.MethodDelete: model.OpDelete, } // Authorization 鉴权 func Authorization(o *options.Options) gin.HandlerFunc { return func(c *gin.Context) { // 允许请求直接通过 if o.ComponentConfig.Default.Mode.InDebug() || alwaysAllowPath.Has(c.Request.URL.Path) || allowCustomRequest(c) { return } user, err := httputils.GetUserFromRequest(c) if err != nil { httputils.AbortFailedWithCode(c, http.StatusMethodNotAllowed, err) return } switch user.Status { case 1: // status 为 1,表示用户只读模式, 只读模式只允许查询请求 if c.Request.Method != http.MethodGet && c.Request.Method != http.MethodOptions { httputils.AbortFailedWithCode(c, http.StatusForbidden, fmt.Errorf("无操作权限")) return } case 2: // 禁用用户无法进行任何操作 httputils.AbortFailedWithCode(c, http.StatusForbidden, fmt.Errorf("用户已被禁用")) return } // Proxy path should be skipped now. // TODO: get object and ID from proxy path if proxy.IsProxyPath(c) || cluster.IsKubeProxyPath(c) || cluster.IsHelmPath(c) { return } obj, id, ok := httputils.GetObjectFromRequest(c) if !ok { return } op := operationsMap[c.Request.Method] // load policy for consistency // ref: https://github.com/casbin/casbin/issues/679#issuecomment-761525328 if err := o.Enforcer.LoadPolicy(); err != nil { httputils.AbortFailedWithCode(c, http.StatusInternalServerError, err) return } ok, err = o.Enforcer.Enforce(user.Name, obj, id, op.String()) if err != nil { httputils.AbortFailedWithCode(c, http.StatusMethodNotAllowed, err) return } if !ok { httputils.AbortFailedWithCode(c, http.StatusForbidden, fmt.Errorf("无操作权限")) } if id != "" { return } // this is a list API if err := ctrlutil.SetIdRangeContext(c, o.Enforcer, user, obj); err != nil { httputils.AbortFailedWithCode(c, http.StatusInternalServerError, err) } } } ================================================ FILE: api/server/middleware/cors.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package middleware import ( "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func Cors() gin.HandlerFunc { c := cors.Config{ AllowAllOrigins: true, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH"}, AllowHeaders: []string{"Content-Type", "Access-Token", "Authorization"}, MaxAge: 6 * time.Hour, } return cors.New(c) } ================================================ FILE: api/server/middleware/limiter.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package middleware import ( "net/http" "time" "github.com/gin-gonic/gin" "github.com/juju/ratelimit" "golang.org/x/time/rate" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/util/errors" "github.com/caoyingjunz/pixiu/pkg/util/lru" ) const ( capacity = 100 quantum = 20 cap = 200 ) // UserRateLimiter 针对每个用户的请求进行限速 // TODO 限速大小从配置中读取 func UserRateLimiter() gin.HandlerFunc { cache := lru.NewLRUCache(cap) return func(c *gin.Context) { clientIP := c.ClientIP() if !cache.Contains(clientIP) { cache.Add(clientIP, ratelimit.NewBucketWithQuantum(time.Second, capacity, quantum)) return } // 通过 ClientIP 取出 bucket val := cache.Get(clientIP) if val == nil { return } // 判断是否还有可用的 bucket bucket := val.(*ratelimit.Bucket) if bucket.TakeAvailable(1) == 0 { httputils.AbortFailedWithCode(c, http.StatusForbidden, errors.ErrBusySystem) } } } func Limiter() gin.HandlerFunc { // 初始化一个限速器,每秒产生 1000 个令牌,桶的大小为 1000 个 // 初始化状态桶是满的 // TODO: 限速的值从配置或者环境变量中获取 limiter := rate.NewLimiter(1000, 1000) return func(c *gin.Context) { if !limiter.Allow() { httputils.AbortFailedWithCode(c, http.StatusForbidden, errors.ErrBusySystem) } } } ================================================ FILE: api/server/middleware/log.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package middleware import ( "github.com/gin-contrib/requestid" "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/db" logutil "github.com/caoyingjunz/pixiu/pkg/util/log" ) func Logger(cfg *logutil.LogOptions) gin.HandlerFunc { return func(c *gin.Context) { l := logutil.NewLogger(cfg) c.Set(db.SQLContextKey, new(db.SQLs)) // set SQL context key // 处理请求操作 c.Next() l.WithLogFields(map[string]interface{}{ "request_id": requestid.Get(c), "method": c.Request.Method, "uri": c.Request.RequestURI, httputils.ResponseCodeKey: httputils.GetResponseCode(c), "client_ip": c.ClientIP(), }) l.Log(c, logutil.InfoLevel, httputils.GetRawError(c)) } } ================================================ FILE: api/server/middleware/middleware.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package middleware import ( "github.com/gin-contrib/requestid" "github.com/gin-gonic/gin" "k8s.io/apimachinery/pkg/util/sets" "github.com/caoyingjunz/pixiu/cmd/app/options" "github.com/caoyingjunz/pixiu/pkg/util" ) var alwaysAllowPath sets.String func init() { alwaysAllowPath = sets.NewString("/pixiu/users/login") } // 允许特定请求不经过验证 func allowCustomRequest(c *gin.Context) bool { // TODO: 其他请求 return false } func InstallMiddlewares(o *options.Options) { // 依次进行跨域,日志,单用户限速,总量限速,验证,鉴权和审计 o.HttpEngine.Use( requestid.New(requestid.WithGenerator(func() string { return util.GenerateRequestID() })), Cors(), Logger(&o.ComponentConfig.Default.LogOptions), UserRateLimiter(), Limiter(), Authentication(o), Authorization(o), Admission(), Audit(o), ) } ================================================ FILE: api/server/router/audit/audit.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/cmd/app/options" "github.com/caoyingjunz/pixiu/pkg/controller" ) type auditRouter struct { c controller.PixiuInterface } func NewRouter(o *options.Options) { router := &auditRouter{ c: o.Controller, } router.initRoutes(o.HttpEngine) } func (a *auditRouter) initRoutes(httpEngine *gin.Engine) { auditRoute := httpEngine.Group("/pixiu/audits") { // get 日志 auditRoute.GET("/:auditId", a.getAudit) auditRoute.GET("", a.listAudits) } } ================================================ FILE: api/server/router/audit/audit_routes.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) type AuditMeta struct { AuditId int64 `uri:"auditId" binding:"required"` } func (a *auditRouter) getAudit(c *gin.Context) { r := httputils.NewResponse() var ( opt AuditMeta err error ) if err = c.ShouldBindUri(&opt); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = a.c.Audit().Get(c, opt.AuditId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (a *auditRouter) listAudits(c *gin.Context) { r := httputils.NewResponse() var ( listOption types.AuditListOptions err error ) if err = httputils.ShouldBindAny(c, nil, nil, &listOption); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = a.c.Audit().List(c, listOption); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } ================================================ FILE: api/server/router/auth/auth.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/cmd/app/options" "github.com/caoyingjunz/pixiu/pkg/controller" ) const ( AuthBasePath = "/pixiu/auth" PolicySubPath = "/policy" BindingSubPath = "/binding" ) type authRouter struct { c controller.PixiuInterface } func NewRouter(o *options.Options) { router := &authRouter{ c: o.Controller, } router.initRoutes(o.HttpEngine) } func (a *authRouter) initRoutes(ge *gin.Engine) { authRoute := ge.Group(AuthBasePath) { policyRoute := authRoute.Group(PolicySubPath) policyRoute.POST("", a.createPolicy) policyRoute.DELETE("", a.deletePolicy) policyRoute.GET("", a.listPolicies) } { bindingRoute := authRoute.Group(BindingSubPath) bindingRoute.POST("", a.createBinding) bindingRoute.DELETE("", a.deleteBinding) bindingRoute.GET("", a.listBindings) } } ================================================ FILE: api/server/router/auth/auth_routes.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) type IdMeta struct { PolicyId int64 `uri:"policyId" binding:"required"` } func (a *authRouter) listPolicies(c *gin.Context) { r := httputils.NewResponse() var ( req types.ListRBACPolicyRequest err error ) if err = c.ShouldBindQuery(&req); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = a.c.Auth().ListRBACPolicies(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (a *authRouter) createPolicy(c *gin.Context) { r := httputils.NewResponse() var req types.RBACPolicyRequest if err := c.ShouldBindJSON(&req); err != nil { httputils.SetFailed(c, r, err) return } if err := a.c.Auth().CreateRBACPolicy(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (a *authRouter) deletePolicy(c *gin.Context) { r := httputils.NewResponse() var req types.RBACPolicyRequest if err := c.ShouldBindJSON(&req); err != nil { httputils.SetFailed(c, r, err) return } if err := a.c.Auth().DeleteRBACPolicy(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (a *authRouter) listBindings(c *gin.Context) { r := httputils.NewResponse() var ( req types.ListGroupBindingRequest err error ) if err = c.ShouldBindQuery(&req); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = a.c.Auth().ListGroupBindings(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (a *authRouter) createBinding(c *gin.Context) { r := httputils.NewResponse() var req types.GroupBindingRequest if err := c.ShouldBindJSON(&req); err != nil { httputils.SetFailed(c, r, err) return } if err := a.c.Auth().CreateGroupBinding(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (a *authRouter) deleteBinding(c *gin.Context) { r := httputils.NewResponse() var req types.GroupBindingRequest if err := c.ShouldBindJSON(&req); err != nil { httputils.SetFailed(c, r, err) return } if err := a.c.Auth().DeleteGroupBinding(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } ================================================ FILE: api/server/router/cluster/cluster.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cluster import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/cmd/app/options" "github.com/caoyingjunz/pixiu/pkg/controller" ) const ( kubeProxyBaseURL = "/pixiu/kubeproxy" helmBaseURL = "/pixiu/helms" indexerBaseURL = "/pixiu/indexer" ) // clusterRouter is a router to talk with the cluster controller type clusterRouter struct { c controller.PixiuInterface } // NewRouter initializes a new cluster router func NewRouter(o *options.Options) { s := &clusterRouter{ c: o.Controller, } s.initRoutes(o.HttpEngine) } func (cr *clusterRouter) initRoutes(httpEngine *gin.Engine) { clusterRoute := httpEngine.Group("/pixiu/clusters") { clusterRoute.POST("", cr.createCluster) clusterRoute.PUT("/:clusterId", cr.updateCluster) clusterRoute.DELETE("/:clusterId", cr.deleteCluster) clusterRoute.GET("/:clusterId", cr.getCluster) clusterRoute.GET("", cr.listClusters) // 检查 kubernetes 的连通性 clusterRoute.POST("/ping", cr.pingCluster) // 设置集群的删除保护模式 clusterRoute.POST("/protect/:clusterId", cr.protectCluster) } // 调用 kubernetes 对象 kubeRoute := httpEngine.Group(kubeProxyBaseURL) { // 获取指定对象的日志 kubeRoute.GET("/clusters/:cluster/namespaces/:namespace/pods/:pod/log", cr.watchPodLog) // Deprecated 聚合 events kubeRoute.GET("/clusters/:cluster/namespaces/:namespace/name/:name/kind/:kind/events", cr.aggregateEvents) // 获取指定对象的 events,支持事件聚合 kubeRoute.GET("/clusters/:cluster/api/v1/events", cr.getEventList) // pod ws kubeRoute.GET("/ws", cr.webShell) // node ws kubeRoute.GET("/nodes/ws", cr.nodeWebShell) // 重启Job action=rerun kubeRoute.POST("/clusters/:cluster/namespaces/:namespace/jobs/:name", cr.ReRunJob) } // 从 pixiu 缓存中获取 kubernetes 对象 //indexerRoute := httpEngine.Group(indexerBaseURL) //{ // // 从缓存中获取指定对象 // indexerRoute.GET("/clusters/:cluster/resources/:resource/namespaces/:namespace/name/:name", cr.getIndexerResource) // // 从缓存中获取对象列表 // indexerRoute.GET("/clusters/:cluster/resources/:resource/namespaces/:namespace", cr.listIndexerResources) //} } ================================================ FILE: api/server/router/cluster/cluster_routes.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cluster import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) type IdMeta struct { ClusterId int64 `uri:"clusterId" binding:"required"` } // CreateCluster godoc // // @Summary Create a cluster // @Description Create by a json cluster // @Tags Clusters // @Accept json // @Produce json // @Param cluster body types.Cluster true "Create cluster" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/clusters/ [post] // @Security Bearer func (cr *clusterRouter) createCluster(c *gin.Context) { r := httputils.NewResponse() var req types.CreateClusterRequest if err := c.ShouldBindJSON(&req); err != nil { httputils.SetFailed(c, r, err) return } if err := cr.c.Cluster().Create(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // UpdateCluster godoc // // @Summary Update an cluster // @Description Update by json cluster // @Tags Clusters // @Accept json // @Produce json // @Param clusterId path int true "Cluster ID" // @Param cluster body types.Cluster true "Update cluster" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/clusters/{clusterId} [put] // @Security Bearer func (cr *clusterRouter) updateCluster(c *gin.Context) { r := httputils.NewResponse() var ( idMeta IdMeta err error ) if err = c.ShouldBindUri(&idMeta); err != nil { httputils.SetFailed(c, r, err) return } var req types.UpdateClusterRequest if err = c.ShouldBindJSON(&req); err != nil { httputils.SetFailed(c, r, err) return } if err = cr.c.Cluster().Update(c, idMeta.ClusterId, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // DeleteCluster godoc // // @Summary Delete cluster by clusterId // @Description Delete by cloud cluster ID // @Tags Clusters // @Accept json // @Produce json // @Param clusterId path int true "Cluster ID" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/clusters/{clusterId} [delete] // @Security Bearer func (cr *clusterRouter) deleteCluster(c *gin.Context) { r := httputils.NewResponse() var ( idMeta IdMeta err error ) if err = c.ShouldBindUri(&idMeta); err != nil { httputils.SetFailed(c, r, err) return } if err = cr.c.Cluster().Delete(c, idMeta.ClusterId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // GetCluster godoc // // @Summary Get Cluster by clusterId // @Description Get by cloud cluster ID // @Tags Clusters // @Accept json // @Produce json // @Param clusterId path int true "Cluster ID" // @Success 200 {object} httputils.Response{result=types.Cluster} // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/clusters/{clusterId} [get] // @Security Bearer func (cr *clusterRouter) getCluster(c *gin.Context) { r := httputils.NewResponse() var ( idMeta IdMeta err error ) if err = c.ShouldBindUri(&idMeta); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = cr.c.Cluster().Get(c, idMeta.ClusterId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // ListClusters godoc // // @Summary List clusters // @Description List clusters with pagination and filter // @Tags Clusters // @Accept json // @Produce json // @Param page query int false "Page number (1-based)" // @Param limit query int false "Page size" // @Param nameSelector query string false "Fuzzy match on alias_name" // @Param status query int false "Cluster status (0-4)" // @Success 200 {object} httputils.Response{result=types.PageResponse} // @Failure 400 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/clusters [get] // @Security Bearer func (cr *clusterRouter) listClusters(c *gin.Context) { r := httputils.NewResponse() var req types.ListClusterRequest if err := c.ShouldBindQuery(&req); err != nil { httputils.SetFailed(c, r, err) return } var err error if r.Result, err = cr.c.Cluster().List(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // PingCluster godoc // // @Summary Ping cluster // @Description Do ping // @Tags Clusters // @Accept json // @Produce json // @Param clusterId path int true "Cluster ID" // @Success 200 {array} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/clusters/{clusterId}/ping [get] // @Security Bearer func (cr *clusterRouter) pingCluster(c *gin.Context) { r := httputils.NewResponse() var ( cluster types.Cluster err error ) if err = c.ShouldBindJSON(&cluster); err != nil { httputils.SetFailed(c, r, err) return } if err = cr.c.Cluster().Ping(c, cluster.KubeConfig); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (cr *clusterRouter) protectCluster(c *gin.Context) { r := httputils.NewResponse() var ( idMeta IdMeta req types.ProtectClusterRequest err error ) if err = httputils.ShouldBindAny(c, &req, &idMeta, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = cr.c.Cluster().Protect(c, idMeta.ClusterId, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (cr *clusterRouter) aggregateEvents(c *gin.Context) { r := httputils.NewResponse() var ( optMeta struct { Cluster string `uri:"cluster" binding:"required"` Namespace string `uri:"namespace" binding:"required"` Name string `uri:"name" binding:"required"` Kind string `uri:"kind" binding:"required"` } err error ) if err = c.ShouldBindUri(&optMeta); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = cr.c.Cluster().AggregateEvents(c, optMeta.Cluster, optMeta.Namespace, optMeta.Name, optMeta.Kind); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (cr *clusterRouter) getEventList(c *gin.Context) { r := httputils.NewResponse() var ( opts struct { Cluster string `uri:"cluster" binding:"required"` } eventOpt types.EventOptions err error ) if err = httputils.ShouldBindAny(c, nil, &opts, &eventOpt); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = cr.c.Cluster().GetEventList(c, opts.Cluster, eventOpt); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (cr *clusterRouter) watchPodLog(c *gin.Context) { r := httputils.NewResponse() var ( opts struct { Cluster string `uri:"cluster" binding:"required"` Namespace string `uri:"namespace" binding:"required"` Pod string `uri:"pod" binding:"required"` //pod name } logOpt types.PodLogOptions err error ) if err = httputils.ShouldBindAny(c, nil, &opts, &logOpt); err != nil { httputils.SetFailed(c, r, err) return } // websocket if err = cr.c.Cluster().WatchPodLog(c, opts.Cluster, opts.Namespace, opts.Pod, logOpt.Container, logOpt.TailLines, c.Writer, c.Request); err != nil { httputils.SetFailed(c, r, err) return } } ================================================ FILE: api/server/router/cluster/helper.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cluster import ( "strings" "github.com/gin-gonic/gin" ) func IsKubeProxyPath(c *gin.Context) bool { return strings.HasPrefix(c.Request.URL.Path, kubeProxyBaseURL) } func IsHelmPath(c *gin.Context) bool { return strings.HasPrefix(c.Request.URL.Path, helmBaseURL) } ================================================ FILE: api/server/router/cluster/informer.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cluster import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) type ResourceMeta struct { Cluster string `uri:"cluster" binding:"required"` Resource string `uri:"resource" binding:"required"` Namespace string `uri:"namespace"` Name string `uri:"name"` } func (cr *clusterRouter) getIndexerResource(c *gin.Context) { r := httputils.NewResponse() var ( resourceMeta ResourceMeta err error ) if err = httputils.ShouldBindAny(c, nil, &resourceMeta, nil); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = cr.c.Cluster().GetIndexerResource(c, resourceMeta.Cluster, resourceMeta.Resource, resourceMeta.Namespace, resourceMeta.Name); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (cr *clusterRouter) listIndexerResources(c *gin.Context) { r := httputils.NewResponse() var ( resourceMeta ResourceMeta listOption types.ListOptions // 分页设置 err error ) if err = httputils.ShouldBindAny(c, nil, &resourceMeta, &listOption); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = cr.c.Cluster().ListIndexerResources(c, resourceMeta.Cluster, resourceMeta.Resource, resourceMeta.Namespace, listOption); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } ================================================ FILE: api/server/router/cluster/proxy.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cluster import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) type Action struct { Act string `form:"action" binding:"required"` ResourceVersion string `form:"resourceVersion" binding:"required"` } func (cr *clusterRouter) ReRunJob(c *gin.Context) { r := httputils.NewResponse() var ( jobMeta types.PixiuObjectMeta action Action err error ) if err = httputils.ShouldBindAny(c, nil, &jobMeta, &action); err != nil { httputils.SetFailed(c, r, err) return } if err = cr.c.Cluster().ReRunJob(c, jobMeta.Cluster, jobMeta.Namespace, jobMeta.Name, action.ResourceVersion); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } ================================================ FILE: api/server/router/cluster/ws.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cluster import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) func (cr *clusterRouter) webShell(c *gin.Context) { r := httputils.NewResponse() var ( err error opt types.WebShellOptions ) if err = c.ShouldBindQuery(&opt); err != nil { httputils.SetFailed(c, r, err) return } if err = cr.c.Cluster().WsHandler(c, &opt, c.Writer, c.Request); err != nil { httputils.SetFailed(c, r, err) return } } func (cr *clusterRouter) nodeWebShell(c *gin.Context) { r := httputils.NewResponse() var ( sshConfig types.WebSSHRequest err error ) if err = httputils.ShouldBindAny(c, nil, nil, &sshConfig); err != nil { httputils.SetFailed(c, r, err) return } if err = cr.c.Cluster().WsNodeHandler(c, &sshConfig, c.Writer, c.Request); err != nil { httputils.SetFailed(c, r, err) return } } ================================================ FILE: api/server/router/helm/helm.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helm import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/cmd/app/options" "github.com/caoyingjunz/pixiu/pkg/controller" ) const ( helmBaseURL = "/pixiu/helms" ) type helmRouter struct { c controller.PixiuInterface } func NewRouter(o *options.Options) { hr := &helmRouter{ c: o.Controller, } hr.initRoutes(o.HttpEngine) } func (hr *helmRouter) initRoutes(httpEngine *gin.Engine) { helmRoute := httpEngine.Group(helmBaseURL) { // helm Repository helmRoute.POST("/repositories", hr.createRepository) helmRoute.PUT("/repositories/:id", hr.updateRepository) helmRoute.DELETE("/repositories/:id", hr.deleteRepository) helmRoute.GET("/repositories/:id", hr.getRepository) helmRoute.GET("/repositories", hr.listRepositories) helmRoute.GET("/repositories/:id/charts", hr.getRepoCharts) helmRoute.GET("/repositories/charts", hr.getRepoChartsByURL) helmRoute.GET("/repositories/values", hr.getChartValues) // Helm Release helmRoute.POST("/clusters/:cluster/namespaces/:namespace/releases", hr.InstallRelease) helmRoute.PUT("/clusters/:cluster/namespaces/:namespace/releases", hr.UpgradeRelease) helmRoute.DELETE("/clusters/:cluster/namespaces/:namespace/releases/:name", hr.UninstallRelease) helmRoute.GET("/clusters/:cluster/namespaces/:namespace/releases/:name", hr.GetRelease) helmRoute.GET("/clusters/:cluster/namespaces/:namespace/releases", hr.ListReleases) helmRoute.GET("/clusters/:cluster/namespaces/:namespace/releases/:name/history", hr.GetReleaseHistory) helmRoute.POST("/clusters/:cluster/namespaces/:namespace/releases/:name/rollback", hr.RollbackRelease) } } ================================================ FILE: api/server/router/helm/release_routes.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helm import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) // GetRelease retrieves a release by its name in the specified namespace and cluster // // @Summary get a release // @Description retrieves a release from the specified namespace and cluster // @Tags helm // @Accept json // @Produce json // @Param cluster path string true "Kubernetes cluster name" // @Param namespace path string true "Kubernetes namespace" // @Param name path string true "Release name" // @Success 200 {object} httputils.Response{result=types.Release} // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /helm/releases/{cluster}/{namespace}/{name} [get] func (hr *helmRouter) GetRelease(c *gin.Context) { r := httputils.NewResponse() var ( err error helmMeta types.PixiuObjectMeta ) if err = c.ShouldBindUri(&helmMeta); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = hr.c.Helm().Release(helmMeta.Cluster, helmMeta.Namespace).Get(c, helmMeta.Name); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // ListReleases lists all releases in the specified namespace and cluster // // @Summary list releases // @Description lists all releases in the specified namespace and cluster // @Tags helm // @Accept json // @Produce json // @Param cluster path string true "Kubernetes cluster name" // @Param namespace path string true "Kubernetes namespace" // @Success 200 {object} httputils.Response{result=[]types.Release} // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /helm/releases/{cluster}/{namespace} [get] func (hr *helmRouter) ListReleases(c *gin.Context) { r := httputils.NewResponse() var ( err error helmMeta types.PixiuObjectMeta ) if err = c.ShouldBindUri(&helmMeta); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = hr.c.Helm().Release(helmMeta.Cluster, helmMeta.Namespace).List(c); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // InstallRelease installs a new release in the specified namespace and cluster // // @Summary install a release // @Description installs a release in the specified Kubernetes namespace and cluster // @Tags helm // @Accept json // @Produce json // @Param cluster path string true "Kubernetes cluster name" // @Param namespace path string true "Kubernetes namespace" // @Param body body types.ReleaseForm true "Release information" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /helm/releases/{cluster}/{namespace} [post] func (hr *helmRouter) InstallRelease(c *gin.Context) { r := httputils.NewResponse() var ( err error helmMeta types.PixiuObjectMeta releaseOpt types.Release ) if err = httputils.ShouldBindAny(c, &releaseOpt, &helmMeta, nil); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = hr.c.Helm().Release(helmMeta.Cluster, helmMeta.Namespace).Install(c, &releaseOpt); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // UninstallRelease uninstalls a release from the specified namespace and cluster // // @Summary uninstall a release // @Description uninstalls a release from the specified Kubernetes namespace and cluster // @Tags helm // @Accept json // @Produce json // @Param cluster path string true "Kubernetes cluster name" // @Param namespace path string true "Kubernetes namespace" // @Param name path string true "Release name" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /helm/releases/{cluster}/{namespace}/{name} [delete] func (hr *helmRouter) UninstallRelease(c *gin.Context) { r := httputils.NewResponse() var ( err error helmMeta types.PixiuObjectMeta ) if err = c.ShouldBindUri(&helmMeta); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = hr.c.Helm().Release(helmMeta.Cluster, helmMeta.Namespace).Uninstall(c, helmMeta.Name); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // UpgradeRelease upgrades a release in the specified namespace and cluster // // @Summary upgrade a release // @Description upgrades a release in the specified Kubernetes namespace and cluster // @Tags helm // @Accept json // @Produce json // @Param cluster path string true "Kubernetes cluster name" // @Param namespace path string true "Kubernetes namespace" // @Param name path string true "Release name" // @Param body body types.ReleaseForm true "Release information" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /helm/releases/{cluster}/{namespace}/{name} [put] func (hr *helmRouter) UpgradeRelease(c *gin.Context) { r := httputils.NewResponse() var ( err error helmMeta types.PixiuObjectMeta releaseOpt types.Release ) if err = httputils.ShouldBindAny(c, &releaseOpt, &helmMeta, nil); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = hr.c.Helm().Release(helmMeta.Cluster, helmMeta.Namespace).Upgrade(c, &releaseOpt); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // GetReleaseHistory retrieves the history of a release in the specified namespace and cluster // // @Summary get a release history // @Description retrieves the history of a release from the specified Kubernetes namespace and cluster // @Tags helm // @Accept json // @Produce json // @Param cluster path string true "Kubernetes cluster name" // @Param namespace path string true "Kubernetes namespace" // @Param name path string true "Release name" // @Success 200 {object} httputils.Response{result=types.ReleaseHistory} // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /helm/releases/history/{cluster}/{namespace}/{name} [get] func (hr *helmRouter) GetReleaseHistory(c *gin.Context) { r := httputils.NewResponse() var ( err error helmMeta types.PixiuObjectMeta ) if err = c.ShouldBindUri(&helmMeta); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = hr.c.Helm().Release(helmMeta.Cluster, helmMeta.Namespace).History(c, helmMeta.Name); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // RollbackRelease rolls back a release in the specified namespace and cluster to the specified revision // // @Summary rollback a release // @Description rolls back a release from the specified Kubernetes namespace and cluster to the specified revision // @Tags helm // @Accept json // @Produce json // @Param cluster path string true "Kubernetes cluster name" // @Param namespace path string true "Kubernetes namespace" // @Param name path string true "Release name" // @Param version query int true "Release revision" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /helm/releases/rollback/{cluster}/{namespace}/{name} [post] func (hr *helmRouter) RollbackRelease(c *gin.Context) { r := httputils.NewResponse() var ( err error helmMeta types.PixiuObjectMeta reverionMeta types.ReleaseHistory ) if err = httputils.ShouldBindAny(c, nil, &helmMeta, &reverionMeta); err != nil { httputils.SetFailed(c, r, err) return } if err = hr.c.Helm().Release(helmMeta.Cluster, helmMeta.Namespace).Rollback(c, helmMeta.Name, reverionMeta.Version); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } ================================================ FILE: api/server/router/helm/respository_routes.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helm import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) // createRepository creates a new repository in the specified cluster // // @Summary create a repository // @Description creates a new repository in the specified Kubernetes cluster // @Tags repositories // @Accept json // @Produce json // @Param cluster query string true "Kubernetes cluster name" // @Param body body types.RepoForm true "Repository information" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /repositories [post] func (hr *helmRouter) createRepository(c *gin.Context) { r := httputils.NewResponse() var ( err error req types.CreateRepository ) if err = httputils.ShouldBindAny(c, &req, nil, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = hr.c.Helm().Repository().Create(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // deleteRepository deletes a repository by its ID // // @Summary delete a repository by ID // @Description deletes a repository from the system using the provided ID // @Tags repositories // @Accept json // @Produce json // @Param id path int true "Repository ID" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /repositories/{id} [delete] func (hr *helmRouter) deleteRepository(c *gin.Context) { r := httputils.NewResponse() var ( err error repoMeta types.RepoId ) if err = c.ShouldBindUri(&repoMeta); err != nil { httputils.SetFailed(c, r, err) return } if err = hr.c.Helm().Repository().Delete(c, repoMeta.Id); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // updateRepository updates a repository by its ID // // @Summary update a repository by ID // @Description updates a repository in the system using the provided ID and update information // @Tags repositories // @Accept json // @Produce json // @Param id path int true "Repository ID" // @Param body body types.RepoUpdateForm true "Repository update information" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response func (hr *helmRouter) updateRepository(c *gin.Context) { r := httputils.NewResponse() var ( err error repoMeta types.RepoId formData types.UpdateRepository ) if err = httputils.ShouldBindAny(c, &formData, &repoMeta, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = hr.c.Helm().Repository().Update(c, repoMeta.Id, &formData); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // getRepository retrieves a repository by its ID // // @Summary get a repository by ID // @Description retrieves a repository from the system using the provided ID // @Tags repositories // @Accept json // @Produce json // @Param id path int true "Repository ID" // @Success 200 {object} httputils.Response{result=types.Repository} // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /repositories/{id} [get] func (hr *helmRouter) getRepository(c *gin.Context) { r := httputils.NewResponse() var ( err error repoMeta types.RepoId ) if err = c.ShouldBindUri(&repoMeta); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = hr.c.Helm().Repository().Get(c, repoMeta.Id); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // listRepositories retrieves a list of all repositories // // @Summary list repositories // @Description retrieves a list of all repositories in the system // @Tags repositories // @Accept json // @Produce json // @Success 200 {object} httputils.Response{result=[]types.Repository} // @Failure 400 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /repositories [get] func (hr *helmRouter) listRepositories(c *gin.Context) { r := httputils.NewResponse() var err error if r.Result, err = hr.c.Helm().Repository().List(c); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // getRepoCharts retrieves charts of a repository by its ID // // @Summary get repository charts by ID // @Description retrieves charts associated with a repository from the system using the provided ID // @Tags repositories // @Accept json // @Produce json // @Param id path int true "Repository ID" // @Success 200 {object} httputils.Response{result=model.ChartIndex} // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /repositories/{id}/charts [get] func (hr *helmRouter) getRepoCharts(c *gin.Context) { r := httputils.NewResponse() var ( err error repoMeta types.RepoId ) if err = c.ShouldBindUri(&repoMeta); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = hr.c.Helm().Repository().GetChartsById(c, repoMeta.Id); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // getRepoChartsByURL retrieves charts of a repository by its URL // // @Summary get repository charts by URL // @Description retrieves charts associated with a repository from the system using the provided URL // @Tags repositories // @Accept json // @Produce json // @Param url query string true "Repository URL" // @Success 200 {object} httputils.Response{result=model.ChartIndex} // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /repositories/charts [get] func (hr *helmRouter) getRepoChartsByURL(c *gin.Context) { r := httputils.NewResponse() var ( err error repoMeta types.RepoURL ) if err = httputils.ShouldBindAny(c, nil, nil, &repoMeta); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = hr.c.Helm().Repository().GetChartsByURL(c, repoMeta.Url); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // getChartValues retrieves the values of a specific chart version // // @Summary get chart values // @Description retrieves values for a specific chart version using the provided chart name and version // @Tags charts // @Accept json // @Produce json // @Param chart query string true "Chart name" // @Param version query string true "Chart version" // @Success 200 {object} httputils.Response{result=types.ChartValues} // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /repositories/chartvalues [get] func (hr *helmRouter) getChartValues(c *gin.Context) { r := httputils.NewResponse() var ( err error repoMeta types.ChartValues ) if err = httputils.ShouldBindAny(c, nil, nil, &repoMeta); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = hr.c.Helm().Repository().GetChartValues(c, repoMeta.Chart, repoMeta.Version); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } ================================================ FILE: api/server/router/plan/config_routes.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) type planConfigMeta struct { planMeta ConfigId int64 `uri:"configId" binding:"required"` } func (t *planRouter) createPlanConfig(c *gin.Context) { r := httputils.NewResponse() var ( opt planMeta req types.CreatePlanConfigRequest err error ) if err = httputils.ShouldBindAny(c, &req, &opt, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Plan().CreateConfig(c, opt.PlanId, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) updatePlanConfig(c *gin.Context) { r := httputils.NewResponse() var ( opt planConfigMeta req types.UpdatePlanConfigRequest err error ) if err = httputils.ShouldBindAny(c, &req, &opt, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Plan().UpdateConfig(c, opt.PlanId, opt.ConfigId, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) deletePlanConfig(c *gin.Context) { r := httputils.NewResponse() var ( opt planConfigMeta err error ) if err = httputils.ShouldBindAny(c, nil, &opt, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Plan().DeleteConfig(c, opt.PlanId, opt.ConfigId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) getPlanConfig(c *gin.Context) { r := httputils.NewResponse() var ( opt planMeta err error ) if err = httputils.ShouldBindAny(c, nil, &opt, nil); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = t.c.Plan().GetConfig(c, opt.PlanId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } ================================================ FILE: api/server/router/plan/node_routes.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) type planNodeMeta struct { planMeta `json:",inline"` NodeId int64 `uri:"nodeId" binding:"required"` } func (t *planRouter) createPlanNode(c *gin.Context) { r := httputils.NewResponse() var ( opt planMeta req types.CreatePlanNodeRequest err error ) if err = httputils.ShouldBindAny(c, &req, &opt, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Plan().CreateNode(c, opt.PlanId, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) updatePlanNode(c *gin.Context) { r := httputils.NewResponse() var ( opt planNodeMeta req types.UpdatePlanNodeRequest err error ) if err = httputils.ShouldBindAny(c, &req, &opt, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Plan().UpdateNode(c, opt.PlanId, opt.NodeId, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) deletePlanNode(c *gin.Context) { r := httputils.NewResponse() var ( opt planNodeMeta err error ) if err = httputils.ShouldBindAny(c, nil, &opt, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Plan().DeleteNode(c, opt.PlanId, opt.NodeId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) getPlanNode(c *gin.Context) { r := httputils.NewResponse() var ( opt planNodeMeta err error ) if err = httputils.ShouldBindAny(c, nil, &opt, nil); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = t.c.Plan().GetNode(c, opt.PlanId, opt.NodeId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) listPlanNodes(c *gin.Context) { r := httputils.NewResponse() var ( opt planMeta err error ) if err = httputils.ShouldBindAny(c, nil, &opt, nil); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = t.c.Plan().ListNodes(c, opt.PlanId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } ================================================ FILE: api/server/router/plan/plan.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/cmd/app/options" "github.com/caoyingjunz/pixiu/pkg/controller" ) type planRouter struct { c controller.PixiuInterface } func NewRouter(o *options.Options) { router := &planRouter{ c: o.Controller, } router.initRoutes(o.HttpEngine) } func (t *planRouter) initRoutes(ginEngine *gin.Engine) { planRoute := ginEngine.Group("/pixiu/plans") { planRoute.POST("", t.createPlan) planRoute.PUT("/:planId", t.updatePlan) planRoute.DELETE("/:planId", t.deletePlan) planRoute.GET("/:planId", t.getPlan) planRoute.GET("", t.listPlans) planRoute.GET("/:planId/resources", t.getPlanWithSubResources) // 启动部署任务 planRoute.POST("/:planId/start", t.startPlan) // 终止部署任务 planRoute.POST("/:planId/stop", t.stopPlan) // 部署计划的节点API planRoute.POST("/:planId/nodes", t.createPlanNode) planRoute.PUT("/:planId/nodes/:nodeId", t.updatePlanNode) planRoute.DELETE("/:planId/nodes/:nodeId", t.deletePlanNode) planRoute.GET("/:planId/nodes/:nodeId", t.getPlanNode) planRoute.GET("/:planId/nodes", t.listPlanNodes) // 部署计划的部署配置 planRoute.POST("/:planId/configs", t.createPlanConfig) planRoute.PUT("/:planId/configs/:configId", t.updatePlanConfig) planRoute.DELETE("/:planId/configs/:configId", t.deletePlanConfig) planRoute.GET("/:planId/configs", t.getPlanConfig) // 执行指定任务 planRoute.POST("/:planId/tasks/:taskId", t.runTasks) // 查询任务列表 planRoute.GET("/:planId/tasks", t.listTasks) // 实时查询任务进度 planRoute.GET("/:planId/tasks/:taskId/logs", t.watchTaskLog) // 获取 os 与 os version planRoute.GET("/distributions", t.getDistributions) } } ================================================ FILE: api/server/router/plan/plan_routes.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) type planMeta struct { PlanId int64 `uri:"planId" binding:"required"` } type watchTaskLogMeta struct { PlanId int64 `uri:"planId" binding:"required"` TaskId int64 `uri:"taskId" binding:"required"` } type WatchMeta struct { Watch bool `form:"watch"` } // 创建部署计划,同时创建配置和节点 func (t *planRouter) createPlan(c *gin.Context) { r := httputils.NewResponse() var req types.CreatePlanRequest if err := c.ShouldBindJSON(&req); err != nil { httputils.SetFailed(c, r, err) return } if err := t.c.Plan().Create(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) updatePlan(c *gin.Context) { r := httputils.NewResponse() var ( opt planMeta req types.UpdatePlanRequest err error ) if err = httputils.ShouldBindAny(c, &req, &opt, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Plan().Update(c, opt.PlanId, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) deletePlan(c *gin.Context) { r := httputils.NewResponse() var ( opt planMeta err error ) if err = c.ShouldBindUri(&opt); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Plan().Delete(c, opt.PlanId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) getPlan(c *gin.Context) { r := httputils.NewResponse() var ( opt planMeta err error ) if err = c.ShouldBindUri(&opt); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = t.c.Plan().Get(c, opt.PlanId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // getPlanWithSubResources // 获取 plan // 获取 configs // 获取 nodes func (t *planRouter) getPlanWithSubResources(c *gin.Context) { r := httputils.NewResponse() var ( opt planMeta err error ) if err = c.ShouldBindUri(&opt); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = t.c.Plan().GetWithSubResources(c, opt.PlanId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) listPlans(c *gin.Context) { r := httputils.NewResponse() var ( req types.ListPlanRequest err error ) if err = c.ShouldBindQuery(&req); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = t.c.Plan().List(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) startPlan(c *gin.Context) { r := httputils.NewResponse() var ( opt planMeta err error ) if err = c.ShouldBindUri(&opt); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Plan().Start(c, opt.PlanId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *planRouter) stopPlan(c *gin.Context) { r := httputils.NewResponse() var ( opt planMeta err error ) if err = c.ShouldBindUri(&opt); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Plan().Stop(c, opt.PlanId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } type DistributionsMeta struct { Centos []string `json:"centos,omitempty"` Ubuntu []string `json:"ubuntu,omitempty"` Debian []string `json:"debian,omitempty"` OpenEuler []string `json:"openEuler,omitempty"` Rocky []string `json:"rocky,omitempty"` } func (t *planRouter) getDistributions(c *gin.Context) { r := httputils.NewResponse() r.Result = &DistributionsMeta{ Centos: []string{"centos7"}, Ubuntu: []string{"ubuntu18.04", "ubuntu20.04", "ubuntu22.04"}, Debian: []string{"debian10", "debian11"}, OpenEuler: []string{"openEuler22.03"}, Rocky: []string{"rocky8.5", "rocky9.2", "rocky9.3"}, } httputils.SetSuccess(c, r) } ================================================ FILE: api/server/router/plan/task_routes.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" ) type taskNodeMeta struct { planMeta `json:",inline"` TaskId int64 `uri:"taskId" binding:"required"` } func (t *planRouter) runTasks(c *gin.Context) { r := httputils.NewResponse() httputils.SetSuccess(c, r) } func (t *planRouter) listTasks(c *gin.Context) { r := httputils.NewResponse() var ( opt planMeta watch WatchMeta err error ) if err = httputils.ShouldBindAny(c, nil, &opt, &watch); err != nil { httputils.SetFailed(c, r, err) return } // 不是长连接请求则直接返回 if !watch.Watch { if r.Result, err = t.c.Plan().ListTasks(c, opt.PlanId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) return } // 长连接请求 t.c.Plan().WatchTasks(c, opt.PlanId, c.Writer, c.Request) } func (t *planRouter) watchTaskLog(c *gin.Context) { r := httputils.NewResponse() var ( opt watchTaskLogMeta err error ) if err = httputils.ShouldBindAny(c, nil, &opt, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Plan().WatchTaskLog(c, opt.PlanId, opt.TaskId, c.Writer, c.Request); err != nil { httputils.SetFailed(c, r, err) return } } ================================================ FILE: api/server/router/proxy/helper.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package proxy import ( "strings" "github.com/gin-gonic/gin" ) // IsProxyPath returns true when the request path is a proxy one. func IsProxyPath(c *gin.Context) bool { return strings.HasPrefix(c.Request.URL.Path, proxyBaseURL) } ================================================ FILE: api/server/router/proxy/proxy.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package proxy import ( "context" "fmt" "net/url" "github.com/gin-gonic/gin" "k8s.io/apimachinery/pkg/util/proxy" "k8s.io/client-go/rest" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/cmd/app/options" "github.com/caoyingjunz/pixiu/pkg/controller" ) const ( proxyBaseURL = "/pixiu/proxy" ) type proxyRouter struct { c controller.PixiuInterface } func NewRouter(o *options.Options) { s := &proxyRouter{ c: o.Controller, } s.initRoutes(o.HttpEngine) } func (p *proxyRouter) initRoutes(ginEngine *gin.Engine) { proxyRoute := ginEngine.Group("/pixiu/") { proxyRoute.Any("/proxy/:clusterName/*act", p.proxyHandler) } } func (p *proxyRouter) proxyHandler(c *gin.Context) { resp := httputils.NewResponse() var cluster struct { Name string `uri:"clusterName" binding:"required"` } if err := c.ShouldBindUri(&cluster); err != nil { httputils.SetFailed(c, resp, err) return } name := cluster.Name config, err := p.c.Cluster().GetKubeConfigByName(context.TODO(), name) if err != nil { httputils.SetFailed(c, resp, fmt.Errorf("failed to get cluster %q kubeconfig", name)) return } transport, err := rest.TransportFor(config) if err != nil { httputils.SetFailed(c, resp, err) return } target, err := p.parseTarget(*c.Request.URL, config.Host, name) if err != nil { httputils.SetFailed(c, resp, err) return } httpProxy := proxy.NewUpgradeAwareHandler(target, transport, false, false, nil) httpProxy.UpgradeTransport = proxy.NewUpgradeRequestRoundTripper(transport, transport) httpProxy.ServeHTTP(c.Writer, c.Request) } func (p *proxyRouter) parseTarget(target url.URL, host string, name string) (*url.URL, error) { kubeURL, err := url.Parse(host) if err != nil { return nil, err } // TODO: 检查 URL 是否规范 target.Path = target.Path[len(proxyBaseURL+"/"+name):] target.Host = kubeURL.Host target.Scheme = kubeURL.Scheme return &target, nil } ================================================ FILE: api/server/router/router.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package router import ( "embed" "net/http" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" // 导入 docs.json 文件 _ "github.com/caoyingjunz/pixiu/api/docs" _ "github.com/caoyingjunz/pixiu/api/server/validator" "github.com/caoyingjunz/pixiu/api/server/middleware" "github.com/caoyingjunz/pixiu/api/server/router/audit" "github.com/caoyingjunz/pixiu/api/server/router/auth" "github.com/caoyingjunz/pixiu/api/server/router/cluster" "github.com/caoyingjunz/pixiu/api/server/router/helm" "github.com/caoyingjunz/pixiu/api/server/router/plan" "github.com/caoyingjunz/pixiu/api/server/router/proxy" "github.com/caoyingjunz/pixiu/api/server/router/tenant" "github.com/caoyingjunz/pixiu/api/server/router/user" "github.com/caoyingjunz/pixiu/cmd/app/options" "github.com/caoyingjunz/pixiu/pkg/static" ) type RegisterFunc func(o *options.Options) //go:embed static var EmbedFS embed.FS func InstallRouters(o *options.Options) { fs := []RegisterFunc{ middleware.InstallMiddlewares, cluster.NewRouter, helm.NewRouter, proxy.NewRouter, tenant.NewRouter, user.NewRouter, plan.NewRouter, audit.NewRouter, auth.NewRouter, } install(o, fs...) // StaticFiles 启用前端集成 o.HttpEngine.Use(static.Serve("/", static.LocalFile(o.ComponentConfig.Default.StaticFiles, true))) // 启动健康检查 o.HttpEngine.GET("/healthz", func(c *gin.Context) { c.String(http.StatusOK, "ok") }) // 启动 APIs 服务 o.HttpEngine.GET("/api-ref/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) } func install(o *options.Options, fs ...RegisterFunc) { for _, f := range fs { f(o) } } ================================================ FILE: api/server/router/static/index.html ================================================ 欢迎使用 Pixiu

欢迎使用 Pixiu

================================================ FILE: api/server/router/tenant/tenant.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tenant import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/cmd/app/options" "github.com/caoyingjunz/pixiu/pkg/controller" ) type tenantRouter struct { c controller.PixiuInterface } func NewRouter(o *options.Options) { router := &tenantRouter{ c: o.Controller, } router.initRoutes(o.HttpEngine) } func (t *tenantRouter) initRoutes(ginEngine *gin.Engine) { tenantRoute := ginEngine.Group("/pixiu/tenants") { tenantRoute.POST("", t.createTenant) tenantRoute.PUT("/:tenantId", t.updateTenant) tenantRoute.DELETE("/:tenantId", t.deleteTenant) tenantRoute.GET("/:tenantId", t.getTenant) tenantRoute.GET("", t.listTenants) } } ================================================ FILE: api/server/router/tenant/tenant_routes.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tenant import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) type TenantMeta struct { TenantId int64 `uri:"tenantId" binding:"required"` } func (t *tenantRouter) createTenant(c *gin.Context) { r := httputils.NewResponse() var req types.CreateTenantRequest if err := c.ShouldBindJSON(&req); err != nil { httputils.SetFailed(c, r, err) return } if err := t.c.Tenant().Create(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *tenantRouter) updateTenant(c *gin.Context) { r := httputils.NewResponse() var ( opt TenantMeta err error ) if err = c.ShouldBindUri(&opt); err != nil { httputils.SetFailed(c, r, err) return } var req types.UpdateTenantRequest if err = c.ShouldBindJSON(&req); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Tenant().Update(c, opt.TenantId, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *tenantRouter) deleteTenant(c *gin.Context) { r := httputils.NewResponse() var ( opt TenantMeta err error ) if err = c.ShouldBindUri(&opt); err != nil { httputils.SetFailed(c, r, err) return } if err = t.c.Tenant().Delete(c, opt.TenantId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *tenantRouter) getTenant(c *gin.Context) { r := httputils.NewResponse() var ( opt TenantMeta err error ) if err = c.ShouldBindUri(&opt); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = t.c.Tenant().Get(c, opt.TenantId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } func (t *tenantRouter) listTenants(c *gin.Context) { r := httputils.NewResponse() var err error if r.Result, err = t.c.Tenant().List(c); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } ================================================ FILE: api/server/router/user/user.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package user import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/cmd/app/options" "github.com/caoyingjunz/pixiu/pkg/controller" ) type userRouter struct { c controller.PixiuInterface } func NewRouter(o *options.Options) { router := &userRouter{ c: o.Controller, } router.initRoutes(o.HttpEngine) } func (u *userRouter) initRoutes(httpEngine *gin.Engine) { // TODO: Base pixiu 后续作为常量定义 userRoute := httpEngine.Group("/pixiu/users") { userRoute.POST("", u.createUser) userRoute.PUT("/:userId", u.updateUser) userRoute.DELETE("/:userId", u.deleteUser) userRoute.GET("/:userId", u.getUser) userRoute.GET("", u.listUsers) // 用户修改密码或者管理员重置密码 userRoute.PUT("/:userId/password", u.updatePassword) // 用户的登陆或者退出 userRoute.POST("/login", u.login) userRoute.POST("/:userId/logout", u.logout) } } ================================================ FILE: api/server/router/user/user_routes.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package user import ( "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/types" ) type IdMeta struct { UserId int64 `uri:"userId" binding:"required"` } // CreateUser godoc // // @Summary Create a user // @Description Create by a json user // @Tags Users // @Accept json // @Produce json // @Param user body types.CreateUserRequest true "Create user" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/users/ [post] // @Security Bearer func (u *userRouter) createUser(c *gin.Context) { r := httputils.NewResponse() var ( req types.CreateUserRequest err error ) if err = c.ShouldBindJSON(&req); err != nil { httputils.SetFailed(c, r, err) return } if err = u.c.User().Create(c, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // UpdateUser godoc // // @Summary Update an user // @Description Update by json user // @Tags Users // @Accept json // @Produce json // @Param userId path int true "User ID" // @Param user body types.UpdateUserRequest true "Update user" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/users/{userId} [put] // @Security Bearer func (u *userRouter) updateUser(c *gin.Context) { r := httputils.NewResponse() var ( idMeta IdMeta req types.UpdateUserRequest err error ) if err = httputils.ShouldBindAny(c, &req, &idMeta, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = u.c.User().Update(c, idMeta.UserId, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // UpdateUserPassword godoc // // @Summary Update user password // @Description Update by json user // @Tags Users // @Accept json // @Produce json // @Param userId path int true "User ID" // @Param user body types.UpdateUserPasswordRequest true "Update user password" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/users/password [put] // @Security Bearer func (u *userRouter) updatePassword(c *gin.Context) { r := httputils.NewResponse() var ( idMeta IdMeta req types.UpdateUserPasswordRequest err error ) if err = httputils.ShouldBindAny(c, &req, &idMeta, nil); err != nil { httputils.SetFailed(c, r, err) return } if err = u.c.User().UpdatePassword(c, idMeta.UserId, &req); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // DeleteUser godoc // // @Summary Delete user by userId // @Description Delete by userID // @Tags Users // @Accept json // @Produce json // @Param userId path int true "User ID" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/users/{userId} [delete] // @Security Bearer func (u *userRouter) deleteUser(c *gin.Context) { r := httputils.NewResponse() var ( idMeta IdMeta err error ) if err = c.ShouldBindUri(&idMeta); err != nil { httputils.SetFailed(c, r, err) return } if err = u.c.User().Delete(c, idMeta.UserId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // Getuser godoc // // @Summary Get user by userId // @Description Get by user ID // @Tags Users // @Accept json // @Produce json // @Param userId path int true "User ID" // @Success 200 {object} httputils.Response{result=types.User} // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/users/{userId} [get] // @Security Bearer func (u *userRouter) getUser(c *gin.Context) { r := httputils.NewResponse() var ( idMeta IdMeta err error ) if err = c.ShouldBindUri(&idMeta); err != nil { httputils.SetFailed(c, r, err) return } if r.Result, err = u.c.User().Get(c, idMeta.UserId); err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // Listusers godoc // // @Summary List users // @Description List users // @Tags Users // @Accept json // @Produce json // @Success 200 {array} httputils.Response{result=[]types.User} // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/users [get] // @Security Bearer func (u *userRouter) listUsers(c *gin.Context) { r := httputils.NewResponse() var req types.ListUserRequest if err := c.ShouldBindQuery(&req); err != nil { httputils.SetFailed(c, r, err) return } var err error r.Result, err = u.c.User().List(c, &req) if err != nil { httputils.SetFailed(c, r, err) return } httputils.SetSuccess(c, r) } // Login godoc // // @Summary User login // @Description Login by a json user // @Tags Login // @Accept json // @Produce json // @Param user body types.LoginRequest true "User login" // @Success 200 {object} httputils.Response // @Failure 400 {object} httputils.Response // @Failure 404 {object} httputils.Response // @Failure 500 {object} httputils.Response // @Router /pixiu/users/login [post] func (u *userRouter) login(c *gin.Context) { r := httputils.NewResponse() var ( req types.LoginRequest err error ) if err = c.ShouldBindJSON(&req); err != nil { httputils.SetFailed(c, r, err) return } loginResp, err := u.c.User().Login(c, &req) if err != nil { httputils.SetFailed(c, r, err) return } r.Result = loginResp httputils.SetUserToContext(c, loginResp.User) httputils.SetSuccess(c, r) } // TODO func (u *userRouter) logout(c *gin.Context) { r := httputils.NewResponse() httputils.SetSuccess(c, r) } ================================================ FILE: api/server/validator/helper.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package validator import ( "strings" "github.com/go-playground/validator/v10" ) // TranslateError returns the translated message of the validation error. func TranslateError(errs validator.ValidationErrors) string { messages := make([]string, len(errs)) for i, err := range errs { messages[i] = err.Translate(tran) } return strings.Join(messages, "; ") } ================================================ FILE: api/server/validator/password.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package validator import ( "github.com/go-playground/validator/v10" "github.com/caoyingjunz/pixiu/pkg/util" ) func init() { register( &passwordValidator{pixiuValidator: newPixiuValidator("password", "密码不符合要求,至少包含一个大写字母、一个小写字母、一个数字")}, ) } // passwordValidator is a customized validator for validating user password. type passwordValidator struct { pixiuValidator } // validate validates the password in request. func (pv *passwordValidator) validate(fl validator.FieldLevel) bool { return util.ValidateStrongPassword(fl.Field().String()) } ================================================ FILE: api/server/validator/rbac.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package validator import ( "strconv" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/go-playground/validator/v10" ) func init() { register( &objectValidator{pixiuValidator: newPixiuValidator("rbac_object", "对象类型不支持")}, &operationValidator{pixiuValidator: newPixiuValidator("rbac_operation", "操作不支持")}, &stringIDValidator{pixiuValidator: newPixiuValidator("rbac_sid", "不合法")}, ) } type objectValidator struct { pixiuValidator } func (ov *objectValidator) validate(fl validator.FieldLevel) bool { obj := fl.Field().Interface().(model.ObjectType) _, ok := model.ObjectTypeMap[obj] return ok } type operationValidator struct { pixiuValidator } func (ov *operationValidator) validate(fl validator.FieldLevel) bool { op := fl.Field().Interface().(model.Operation) _, ok := model.OperationMap[op] return ok } type stringIDValidator struct { pixiuValidator } func (sv *stringIDValidator) validate(fl validator.FieldLevel) bool { sid := fl.Field().String() if sid == "*" { return true } _, err := strconv.Atoi(sid) return err == nil } ================================================ FILE: api/server/validator/validator.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package validator import ( "github.com/gin-gonic/gin/binding" "github.com/go-playground/locales/zh" ut "github.com/go-playground/universal-translator" "github.com/go-playground/validator/v10" zt "github.com/go-playground/validator/v10/translations/zh" ) type customValidator interface { getTag() string translateError(ut ut.Translator) error translate(ut ut.Translator, fe validator.FieldError) string // Should be implemented by the custom validator. validate(fl validator.FieldLevel) bool } var tran ut.Translator var customValidators []customValidator // register adds a new custom validator to the validator list func register(validators ...customValidator) { customValidators = append(customValidators, validators...) } func init() { _zh := zh.New() // default is Chinese uni := ut.New(_zh, _zh) tran, _ = uni.GetTranslator("zh") if v, ok := binding.Validator.Engine().(*validator.Validate); ok { _ = zt.RegisterDefaultTranslations(v, tran) for _, c := range customValidators { _ = v.RegisterValidation(c.getTag(), c.validate) _ = v.RegisterTranslation(c.getTag(), tran, c.translateError, c.translate) } } } type pixiuValidator struct { tag string err string } func newPixiuValidator(tag, err string) pixiuValidator { return pixiuValidator{ tag: tag, err: err, } } func (c pixiuValidator) getTag() string { return c.tag } func (c pixiuValidator) translateError(ut ut.Translator) error { return ut.Add(c.tag, "{0}"+c.err, true) } func (c pixiuValidator) translate(ut ut.Translator, fe validator.FieldError) string { t, _ := ut.T(c.tag, fe.Field()) return t } ================================================ FILE: cmd/app/config/config.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package config import ( "fmt" "github.com/caoyingjunz/pixiu/pkg/jobmanager" logutil "github.com/caoyingjunz/pixiu/pkg/util/log" ) type Mode string const ( DebugMode Mode = "debug" ReleaseMode Mode = "release" ) func (m Mode) InDebug() bool { return m == DebugMode } type Config struct { Default DefaultOptions `yaml:"default"` Mysql MysqlOptions `yaml:"mysql"` Worker WorkerOptions `yaml:"worker"` Audit jobmanager.AuditOptions `yaml:"audit"` TLS *TLS `yaml:"tls"` } type DefaultOptions struct { Mode Mode `yaml:"mode"` Listen int `yaml:"listen"` JWTKey string `yaml:"jwt_key"` // 自动创建指定模型的数据库表结构,不会更新已存在的数据库表 AutoMigrate bool `yaml:"auto_migrate"` logutil.LogOptions `yaml:",inline"` // 静态文件路径 StaticFiles string `yaml:"static_files"` // 超级管理员初始化配置,留空则使用默认值 AdminUser string `yaml:"admin_user"` AdminPassword string `yaml:"admin_password"` } func (o DefaultOptions) Valid() error { if err := o.LogOptions.Valid(); err != nil { return err } return nil } // MysqlOptions 数据库具体配置 type MysqlOptions struct { Host string `yaml:"host"` User string `yaml:"user"` Password string `yaml:"password"` Port int `yaml:"port"` Name string `yaml:"name"` } func (o MysqlOptions) Valid() error { // TODO return nil } type WorkerOptions struct { WorkDir string `yaml:"work_dir"` Engines []Engine `yaml:"engines"` } type Engine struct { Image string `yaml:"image"` OSSupported []string `yaml:"os_supported"` } func (w WorkerOptions) Valid() error { // TODO return nil } type TLS struct { CertFile string `yaml:"cert_file"` KeyFile string `yaml:"key_file"` } func (t *TLS) Valid() error { if t != nil { if len(t.CertFile) == 0 { return fmt.Errorf("listen on tls, no cert_file found") } if len(t.KeyFile) == 0 { return fmt.Errorf("listen on tls, no key_file found") } } return nil } func (c *Config) Valid() (err error) { if err = c.Default.Valid(); err != nil { return } if err = c.Mysql.Valid(); err != nil { return } if err = c.Worker.Valid(); err != nil { return } if err = c.TLS.Valid(); err != nil { return err } return } ================================================ FILE: cmd/app/options/options.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package options import ( "context" "fmt" "os" "time" "github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2/model" gormadapter "github.com/casbin/gorm-adapter/v3" "github.com/gin-gonic/gin" "github.com/spf13/cobra" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/cmd/app/config" "github.com/caoyingjunz/pixiu/pkg/controller" pixiudb "github.com/caoyingjunz/pixiu/pkg/db" pixiuModel "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/jobmanager" "github.com/caoyingjunz/pixiu/pkg/types" logutil "github.com/caoyingjunz/pixiu/pkg/util/log" pixiuConfig "github.com/caoyingjunz/pixiulib/config" ) const ( maxIdleConns = 10 maxOpenConns = 100 defaultListen = 8080 defaultTokenKey = "pixiu" defaultConfigFile = "/etc/pixiu/config.yaml" defaultLogFormat = logutil.LogFormatJson defaultWorkDir = "/etc/pixiu" defaultStaticDir = "/static" defaultAdminUser = "admin" defaultAdminPassword = "Pixiu123456!" defaultSlowSQLDuration = 1 * time.Second rulesTableName = "rules" ) // Options has all the params needed to run a pixiu type Options struct { // The default values. ComponentConfig config.Config HttpEngine *gin.Engine // 数据库接口 db *gorm.DB Factory pixiudb.ShareDaoFactory // 貔貅主控制接口 Controller controller.PixiuInterface // ConfigFile is the location of the pixiu server's configuration file. ConfigFile string // Authorization enforcement and policy management Enforcer *casbin.SyncedEnforcer JobManager *jobmanager.Manager } func NewOptions() (*Options, error) { return &Options{ HttpEngine: gin.Default(), // 初始化默认 api 路由 ConfigFile: defaultConfigFile, }, nil } // Complete completes all the required options func (o *Options) Complete() error { // 配置文件优先级: 默认配置,环境变量,命令行 if len(o.ConfigFile) == 0 { // Try to read config file path from env. if cfgFile := os.Getenv("ConfigFile"); cfgFile != "" { o.ConfigFile = cfgFile } else { o.ConfigFile = defaultConfigFile } } c := pixiuConfig.New() c.SetConfigFile(o.ConfigFile) c.SetConfigType("yaml") if err := c.Binding(&o.ComponentConfig); err != nil { return err } // TODO: move to config initialization? if o.ComponentConfig.Default.Listen == 0 { o.ComponentConfig.Default.Listen = defaultListen } if len(o.ComponentConfig.Default.JWTKey) == 0 { o.ComponentConfig.Default.JWTKey = defaultTokenKey } if o.ComponentConfig.Default.LogFormat == "" { o.ComponentConfig.Default.LogFormat = defaultLogFormat } if o.ComponentConfig.Worker.WorkDir == "" { o.ComponentConfig.Worker.WorkDir = defaultWorkDir } if len(o.ComponentConfig.Default.StaticFiles) == 0 { o.ComponentConfig.Default.StaticFiles = defaultStaticDir } if o.ComponentConfig.Audit.Schedule == "" { o.ComponentConfig.Audit.Schedule = jobmanager.DefaultSchedule } if o.ComponentConfig.Audit.DaysReserved == 0 { o.ComponentConfig.Audit.DaysReserved = jobmanager.DefaultDaysReserved } if len(o.ComponentConfig.Default.AdminUser) == 0 { o.ComponentConfig.Default.AdminUser = defaultAdminUser } if len(o.ComponentConfig.Default.AdminPassword) == 0 { o.ComponentConfig.Default.AdminPassword = defaultAdminPassword } if err := o.ComponentConfig.Valid(); err != nil { return err } o.ComponentConfig.Default.LogOptions.Init() // 注册依赖组件 if err := o.register(); err != nil { return err } o.Controller = controller.New(o.ComponentConfig, o.Factory, o.Enforcer) if err := o.bootstrapRootUser(); err != nil { return err } o.JobManager = jobmanager.NewManager( &o.ComponentConfig.Default.LogOptions, jobmanager.NewAuditsCleaner(o.ComponentConfig.Audit, o.Factory), jobmanager.NewClusterSyncer(o.Factory), ) return nil } // BindFlags binds the pixiu Configuration struct fields func (o *Options) BindFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.ConfigFile, "configfile", defaultConfigFile, "The location of the pixiu configuration file") } func (o *Options) register() error { // 注册数据库 if err := o.registerDatabase(); err != nil { return err } // TODO: 注册其他依赖 if err := o.registerEnforcer(); err != nil { return err } return nil } // This panics if o.db is nil. func (o *Options) registerEnforcer() error { // Casbin a, err := gormadapter.NewAdapterByDBUseTableName(o.db, "", rulesTableName) if err != nil { return err } m, err := model.NewModelFromString(pixiuModel.RBACModel) if err != nil { return err } if o.Enforcer, err = casbin.NewSyncedEnforcer(m, a); err != nil { return err } // Add an super admin policy. _, err = o.Enforcer.AddPolicy(pixiuModel.AdminPolicy.Raw()) return err } func (o *Options) registerDatabase() error { sqlConfig := o.ComponentConfig.Mysql dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", sqlConfig.User, sqlConfig.Password, sqlConfig.Host, sqlConfig.Port, sqlConfig.Name) opt := &gorm.Config{ Logger: pixiudb.NewLogger(logger.Info, defaultSlowSQLDuration), } db, err := gorm.Open(mysql.Open(dsn), opt) if err != nil { return err } o.db = db // 设置数据库连接池 sqlDB, err := db.DB() if err != nil { return err } sqlDB.SetMaxIdleConns(maxIdleConns) sqlDB.SetMaxOpenConns(maxOpenConns) o.Factory, err = pixiudb.NewDaoFactory(db, o.ComponentConfig.Default.AutoMigrate) return err } // Validate validates all the required options. func (o *Options) Validate() error { // TODO return nil } // bootstrapRootUser 启动时自动初始化超级管理员账户 // 若超管已存在则跳过,若不存在则使用配置文件中的用户名和密码创建 // 密码经由 Controller.User().Create() 内部调用 util.EncryptUserPassword() 加密后入库 func (o *Options) bootstrapRootUser() error { ctx := context.Background() root, err := o.Factory.User().GetRoot(ctx) if err != nil { return fmt.Errorf("failed to check root user: %v", err) } if root != nil { klog.Info("root user already exists, skipping") return nil } adminUser := o.ComponentConfig.Default.AdminUser adminPassword := o.ComponentConfig.Default.AdminPassword klog.Infof("initializing root user: %s", adminUser) return o.Controller.User().Create(ctx, &types.CreateUserRequest{ Name: adminUser, Password: adminPassword, Role: pixiuModel.RoleRoot, }) } ================================================ FILE: cmd/app/server.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package app import ( "context" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/spf13/cobra" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/api/server/router" "github.com/caoyingjunz/pixiu/cmd/app/options" ) func NewServerCommand(version string) *cobra.Command { opts, err := options.NewOptions() if err != nil { klog.Fatalf("unable to initialize command options: %v", err) } cmd := &cobra.Command{ Use: "pixiu-server", Long: "The pixiu server controller is a daemon that embeds the core control loops.", Run: func(cmd *cobra.Command, args []string) { if err = opts.Complete(); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } if err = opts.Validate(); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } if err = Run(opts); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } }, Args: func(cmd *cobra.Command, args []string) error { for _, arg := range args { if len(arg) > 0 { return fmt.Errorf("%q does not take any arguments, got %q", cmd.CommandPath(), args) } } return nil }, } // 绑定命令行参数 opts.BindFlags(cmd) verCmd := &cobra.Command{ Use: "version", Short: "Print the version", Long: "Print version and exit.", Run: func(cmd *cobra.Command, args []string) { fmt.Println(version) }, } cmd.AddCommand(verCmd) return cmd } // Run 优雅启动貔貅服务 func Run(opt *options.Options) error { srv := &http.Server{ Addr: fmt.Sprintf(":%d", opt.ComponentConfig.Default.Listen), Handler: opt.HttpEngine, } // TODO: 暂未设置优雅退出 // 启动集群相关控制器 runers := []func(context.Context, int) error{opt.Controller.Plan().Run, opt.Controller.Cluster().Run} for _, runner := range runers { if err := runner(context.TODO(), 5); err != nil { klog.Fatal("failed to start manager: ", err) } } // 安装 http 路由 router.InstallRouters(opt) // Initializing the server in a goroutine so that it won't block the graceful shutdown handling below go func() { var err error if opt.ComponentConfig.TLS != nil { klog.Info("starting pixiu server with TLS") err = srv.ListenAndServeTLS(opt.ComponentConfig.TLS.CertFile, opt.ComponentConfig.TLS.KeyFile) } else { klog.Info("starting pixiu server with no TLS") err = srv.ListenAndServe() } if err != nil && err != http.ErrServerClosed { klog.Fatal("failed to listen pixiu server: ", err) } }() klog.Info("starting job manager") opt.JobManager.Run() // Wait for interrupt signal to gracefully shut down the server with a timeout of 5 seconds. quit := make(chan os.Signal) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit klog.Info("shutting pixiu server down ...") // The context is used to inform the server it has 5 seconds to finish the request ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { klog.Fatalf("pixiu server forced to shutdown: %v", err) } klog.Info("shutting job manager down ...") opt.JobManager.Stop() return nil } ================================================ FILE: cmd/pixiuserver.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "io" "math/rand" "os" "time" "github.com/gin-gonic/gin" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/cmd/app" ) var version string // @title Pixiu API Documentation // @version 1.0 // @termsOfService https://github.com/caoyingjunz/pixiu // @contact.name API Support // @contact.url https://github.com/caoyingjunz/pixiu // @contact.email support@pixiu.io // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @schemes http https // @host localhost:8090 // @securityDefinitions.apikey Bearer // @in header // @name Authorization // @description Use the Pixiu APIs to your cloud // @description Type "Bearer" followed by a space and JWT token func main() { klog.InitFlags(nil) rand.Seed(time.Now().UnixNano()) gin.SetMode(gin.ReleaseMode) gin.DefaultWriter = io.Discard cmd := app.NewServerCommand(version) if err := cmd.Execute(); err != nil { os.Exit(1) } } ================================================ FILE: config.yaml ================================================ default: # 运行模式,可选 debug 和 release mode: debug # 服务监听端口 listen: 8091 # jwt 签名的 key jwt_key: pixiu # 自动创建指定模型的数据库表结构,不会更新已存在的数据库表 auto_migrate: true # 日志的格式,可选 text 和 json log_format: json log_level: info # 静态文件路径 static_files: /static # 超级管理初始化用户名和密码,默认为 admin/Pixiu123456! admin_user: pixiu admin_password: 123456 # 配置前端请求地址 dashboard: url: http://localhost:8080 #tls: # cert_file: test.pem # key_file: test.key # 数据库地址信息 mysql: host: peng user: root password: Pixiu868686 port: 3306 name: pixiu worker: work_dir: /tmp/pixiu engines: - image: ccr.ccs.tencentyun.com/pixiucloud/kubez-ansible:v2.0.1 os_supported: - centos7 - debian10 - ubuntu18.04 - image: ccr.ccs.tencentyun.com/pixiucloud/kubez-ansible:v3.0.1 os_supported: - debian11 - ubuntu20.04 - ubuntu22.04 - rocky8.5 - rocky9.2 - rocky9.3 - openEuler22.03 ================================================ FILE: deploy/README.md ================================================ # Pixiu Pixiu是貔貅的服务端, 貔貅是一个广泛使用的、具有web-ui的、实现多集群管理的、非常好用的kubernetes(k8s)容器管理平台. 本 chart 使用 [Helm](https://helm.sh) 包管理器实现在[Kubernetes](https://kubernetes.io) (k8s)集群上部署pixiu ## 安装Chart 注意: 暂时只支持helm3 - 通过源码安装(本示例默认源码安装) ``` helm install [RELEASE_NAME] pixiu/ ``` - 修改默认参数后, 打包上传到repos后, 例如https://artifacthub.io/, 再行安装 ``` helm repo add pixiu https://xxxx.xxx.xx/pixiu helm repo update helm install [RELEASE_NAME] ./pixiu ``` ## 卸载Chart 通过以下命令卸载: ```console helm delete pixiu ``` ## 更新Chart ``` helm upgrade [RELEASE_NAME] [CHART] --install ``` ## 配置 - 详情请参考: [values.yaml](./values.yaml) - 自定义参数: ``` helm install pixiu pixiu/ --set=service.externalPort=8080,resources.limits.cpu=300m ``` ================================================ FILE: deploy/pixiu/.helmignore ================================================ # Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. .DS_Store # Common VCS dirs .git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/ # Common backup files *.swp *.bak *.tmp *.orig *~ # Various IDEs .project .idea/ *.tmproj .vscode/ ================================================ FILE: deploy/pixiu/Chart.yaml ================================================ apiVersion: v2 name: pixiu description: A Helm chart for Kubernetes # A chart can be either an 'application' or a 'library' chart. # # Application charts are a collection of templates that can be packaged into versioned archives # to be deployed. # # Library charts provide useful utilities or functions for the chart developer. They're included as # a dependency of application charts to inject those utilities and functions into the rendering # pipeline. Library charts do not define any templates and therefore cannot be deployed. type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) version: 0.1.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "1.16.0" ================================================ FILE: deploy/pixiu/templates/NOTES.txt ================================================ 1. Get the application URL by running these commands: {{- if .Values.ingress.enabled }} {{- range $host := .Values.ingress.hosts }} {{- range .paths }} http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} {{- end }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "pixiu.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "pixiu.fullname" . }}' export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "pixiu.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") echo http://$SERVICE_IP:{{ .Values.service.port }} {{- else if contains "ClusterIP" .Values.service.type }} export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "pixiu.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT {{- end }} ================================================ FILE: deploy/pixiu/templates/_helpers.tpl ================================================ {{/* Expand the name of the chart. */}} {{- define "pixiu.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "pixiu.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define "pixiu.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} {{- define "pixiu.labels" -}} helm.sh/chart: {{ include "pixiu.chart" . }} {{ include "pixiu.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "pixiu.selectorLabels" -}} app.kubernetes.io/name: {{ include "pixiu.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} {{- define "pixiu.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "pixiu.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} ================================================ FILE: deploy/pixiu/templates/configmap.yaml ================================================ # Copyright 2021 The Pixiu Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. apiVersion: v1 kind: ConfigMap metadata: labels: {{- include "pixiu.labels" . | nindent 4 }} {{- if .Values.commonLabels }} {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} name: {{ template "pixiu.fullname" . }}-config namespace: {{ .Release.Namespace }} data: config.yaml: | default: listen: {{ .Values.default.listen }} log_type: {{ .Values.default.log_type }} log_level: {{ .Values.default.log_level }} log_dir: {{ .Values.default.log_dir }} jwt_key: {{ .Values.default.jwt_key }} mysql: host: {{ .Values.mysql.host }} user: {{ .Values.mysql.user }} password: {{ .Values.mysql.password }} port: {{ .Values.mysql.port }} name: {{ .Values.mysql.name }} cicd: driver: {{ .Values.cicd.driver }} jenkins: host: {{ .Values.cicd.host }} user: {{ .Values.cicd.user }} password: {{ .Values.cicd.password }} ================================================ FILE: deploy/pixiu/templates/deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "pixiu.fullname" . }} labels: {{- include "pixiu.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: {{- include "pixiu.selectorLabels" . | nindent 6 }} template: metadata: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "pixiu.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "pixiu.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.pixiu }}" imagePullPolicy: {{ .Values.image.pullPolicy }} volumeMounts: - mountPath: /etc/pixiu name: config-volume readOnly: true resources: {{- toYaml .Values.resources | nindent 12 }} - name: dashboard securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.dashboard }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http containerPort: 80 protocol: TCP resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} volumes: - configMap: defaultMode: 420 name: pixiu-config name: config-volume ================================================ FILE: deploy/pixiu/templates/hpa.yaml ================================================ {{- if .Values.autoscaling.enabled }} apiVersion: autoscaling/v2beta1 kind: HorizontalPodAutoscaler metadata: name: {{ include "pixiu.fullname" . }} labels: {{- include "pixiu.labels" . | nindent 4 }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: {{ include "pixiu.fullname" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - type: Resource resource: name: cpu targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} {{- end }} {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - type: Resource resource: name: memory targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- end }} {{- end }} ================================================ FILE: deploy/pixiu/templates/ingress.yaml ================================================ {{- if .Values.ingress.enabled -}} {{- $fullName := include "pixiu.fullname" . -}} {{- $svcPort := .Values.service.port -}} {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1beta1 {{- else -}} apiVersion: extensions/v1beta1 {{- end }} kind: Ingress metadata: name: {{ $fullName }} labels: {{- include "pixiu.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - {{ . | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ .path }} backend: serviceName: {{ $fullName }} servicePort: {{ $svcPort }} {{- end }} {{- end }} {{- end }} ================================================ FILE: deploy/pixiu/templates/service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: {{ include "pixiu.fullname" . }} labels: {{- include "pixiu.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: http protocol: TCP name: http selector: {{- include "pixiu.selectorLabels" . | nindent 4 }} ================================================ FILE: deploy/pixiu/templates/serviceaccount.yaml ================================================ {{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "pixiu.serviceAccountName" . }} labels: {{- include "pixiu.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} {{- end }} ================================================ FILE: deploy/pixiu/values.yaml ================================================ # Default values for pixiu. # This is a YAML-formatted file. # Declare variables to be passed into your templates. default: # 服务监听端口 listen: 8090 # 日志类型,支持 stdout, stderr 和 file, 默认为 stdout log_type: stdout # 日志级别,支持 INFO, WARN 和 ERROR, 默认为 INFO log_level: INFO # 日志路径,在日志类型为 file 的时候生效 log_dir: /var/log/pixiu # jwt 签名的 key jwt_key: pixiu # 数据库地址信息 mysql: host: pixiu user: root password: GoPixiu868686 port: 3306 name: pixiu cicd: enable: false driver: jenkins jenkins: host: http://127.0.0.1:9090 user: adminops password: adminops@321 replicaCount: 1 image: pixiu: jacky06/pixiu:v0.1 dashboard: jacky06/pixiu-dashboard pullPolicy: IfNotPresent imagePullSecrets: [] nameOverride: "" fullnameOverride: "" serviceAccount: # Specifies whether a service account should be created create: false # Annotations to add to the service account annotations: {} # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "" podAnnotations: {} podSecurityContext: {} # fsGroup: 2000 securityContext: {} # capabilities: # drop: # - ALL # readOnlyRootFilesystem: true # runAsNonRoot: true # runAsUser: 1000 service: type: ClusterIP port: 80 ingress: enabled: false annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - host: chart-example.local paths: - path: / backend: serviceName: chart-example.local servicePort: 80 tls: [] # - secretName: chart-example-tls # hosts: # - chart-example.local resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. # limits: # cpu: 100m # memory: 128Mi # requests: # cpu: 100m # memory: 128Mi autoscaling: enabled: false minReplicas: 1 maxReplicas: 100 targetCPUUtilizationPercentage: 80 # targetMemoryUtilizationPercentage: 80 nodeSelector: {} tolerations: [] affinity: {} ================================================ FILE: docker/Dockerfile ================================================ FROM golang:1.17-alpine as builder WORKDIR /app ARG VERSION ENV GOPROXY=https://goproxy.cn COPY ../go.mod ./ COPY ../go.sum ./ #RUN go mod download COPY .. . RUN CGO_ENABLED=0 go build -ldflags "-s -w -X 'main.version=${VERSION}'" -o pixiu ./cmd FROM node:16.18.0-alpine as dashboard-builder WORKDIR /build RUN apk add git RUN git clone https://github.com/pixiu-io/dashboard.git RUN cd dashboard && npm install && npm run build FROM busybox as runner LABEL MAINTAINER="PIXIU" COPY --from=builder /app/pixiu /app COPY --from=dashboard-builder /build/dashboard/dist /static COPY docker/start.sh /usr/local/bin/pixiu_start CMD ["pixiu_start"] ================================================ FILE: docker/Dockerfile-toolbox ================================================ # 使用 Ubuntu 作为基础镜像 FROM ubuntu:22.04 ARG K8S_VERSION ARG HELM_VERSION # 安装vim RUN apt-get update && apt-get install -y vim # 安装必要的软件包 RUN DEBIAN_FRONTEND=noninteractive apt-get install -y curl bash-completion # 安装 kubectl RUN ARCH=$(uname -m|sed 's|x86_64|amd64|'|sed 's|aarch64|arm64|') && \ curl -LO "https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/${ARCH}/kubectl" && \ install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl && \ rm -rf kubectl # 安装 helm RUN ARCH=$(uname -m|sed 's|x86_64|amd64|'|sed 's|aarch64|arm64|') && \ curl -LO "https://get.helm.sh/helm-${HELM_VERSION}-linux-${ARCH}.tar.gz" && \ tar -zxvf helm-${HELM_VERSION}-linux-${ARCH}.tar.gz && \ rm -rf helm-${HELM_VERSION}-linux-${ARCH}.tar.gz && \ mv linux-${ARCH}/helm /usr/local/bin/helm && \ rm -rf linux-${ARCH} # 配置 bash 补全和自定义的 PS1 提示符 RUN echo 'source /etc/profile.d/bash_completion.sh' >> /root/.bashrc \ && echo 'source <(kubectl completion bash)' >> /root/.bashrc \ && echo 'source <(helm completion bash)' >> /root/.bashrc \ && echo "PS1='[\[\033[0;34m\]\u\[\033[0;37m\]@\[\033[0;35m\]\h\[\033[0;33m\] \w\[\033[0;37m\]]\[\033[0;31m\]\$\[\033[00m\] '" >> /root/.bashrc # 设置工作目录 WORKDIR /root # 设置环境变量等(可选) ENV PATH="/usr/local/bin:${PATH}" # 启动 bash 终端 CMD ["sleep", "infinity"] ================================================ FILE: docker/start.sh ================================================ #!/bin/sh set -o errexit set -o xtrace if [[ ! -d "/etc/pixiu" ]]; then mkdir -p /etc/pixiu fi if [[ -e "/static/config.json" ]]; then rm -rf /static/config.json fi URL=$(grep "url" /etc/pixiu/config.yaml | awk -F'url:' '{print $2}' | tr -d '[:space:]') if [[ -z "$URL" ]]; then URL="http://localhost:8080" fi cat >/static/config.json << EOF { "url": "${URL}" } EOF /app ================================================ FILE: docs/OWNERS ================================================ reviewers: - caoyingjun ================================================ FILE: docs/README.md ================================================ # Pixiu ### 初始账号 ```shell pixiu/Pixiu123456! ``` ### 主进程启动 ```shell go run cmd/pixiuserver.go --configfile ./config.yaml ``` ### 访问 APIs doc ```shell curl http://127.0.0.1:8090/api-ref/index.html ``` ================================================ FILE: docs/apis.md ================================================ # Pixiu API Specification - Pixiu API 是对 Pixiu 资源的调用接口,通常接口有 list, show details, create, update 和 delete, 以及对 resource 的具体动作, 如: 启动 cicd 资源的 job. - API 应当遵循(以 cicd 资源的 job 为例): ## List - method: GET - URL: /cicd/jobs - 正常返回码: 200 - 异常返回码: badRequest(400), unauthorized(401), forbidden(403) - 请求参数: - 响应结果: ## Show Details - method: GET - URL: /cicd/jobs/{job_id} - 正常返回码: 200 - 异常返回码: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404) - 请求参数: - 响应结果: ## Create - method: POST - URL: /cicd/jobs - 正常返回码: 200 - 异常返回码: badRequest(400), unauthorized(401), forbidden(403) - 请求参数: - 响应结果: ## Update - method: PUT - URL: /cicd/jobs/{image_id} - 正常返回码: 200 - 异常返回码: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404) - 请求参数: - 响应结果: ## Delete - method: DELETE - URL: /cicd/jobs/{job_id} - 正常返回码: 200 - 异常返回码: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404) - 请求参数: - 响应结果: ## Run - method: POST - URL: /cicd/jobs/{job_id}/run - 正常返回码: 200 - 异常返回码: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404), NotImplemented(501) - 请求参数: - 响应结果: ================================================ FILE: docs/sql.md ================================================ # 创建 `pixiu` 数据库 ```sql CREATE DATABASE pixiu; ``` ## 创建 `clusters` 表 ```sql CREATE TABLE `clusters` ( id int primary key NOT NULL AUTO_INCREMENT COMMENT '主键' , gmt_create datetime COMMENT '创建时间', gmt_modified datetime COMMENT '修改时间', resource_version int COMMENT '版本号', plan_id int COMMENT 'plan表的id号', name varchar(128) COMMENT 'k8s 集群名称', alias_name varchar(128) COMMENT 'k8s 集群中文名称', cluster_type int COMMENT 'Kubernetes 集群的类型', status tinyint(4) COMMENT '集群状态', kubernetes_version varchar(64) COMMENT 'k8s 集群版本', nodes text COMMENT '集群节点详情', protected bool COMMENT '集群删除保护', kube_config text COMMENT 'kubeConfig 文件内容', description text COMMENT 'k8s 集群描述信息', extension text COMMENT '扩展预留', KEY `idx_name` (`name`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB CHARSET=utf8 AUTO_INCREMENT=20220801; ``` ## 创建 `users` 表 ```sql CREATE TABLE `users` ( id int primary key NOT NULL AUTO_INCREMENT COMMENT '主键' , gmt_create datetime COMMENT '创建时间', gmt_modified datetime COMMENT '修改时间', resource_version int COMMENT '版本号', name varchar(128) COMMENT '用户名', password varchar(256) COMMENT '用户密码', email varchar(128) COMMENT '邮箱', status tinyint COMMENT '状态: 1启用,2未启用', role varchar(128) COMMENT '角色', description text COMMENT '描述', extension text COMMENT '扩展字段', KEY `idx_name` (`name`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB CHARSET=utf8 AUTO_INCREMENT=21220801; ``` ### 创建 `pixiu` 用户 ```sql # 用户 pixiu 的初始密码为 Pixiu123456! insert into users(name, password) values ('pixiu', '$2a$10$SamcBWw.aPMDv5QadDr7f.2rDBWiwfTwnbh5sEEhaTkWfVwO96PfW'); ``` ## 创建 `tenants` 表 ```sql CREATE TABLE `tenants` ( id int primary key NOT NULL AUTO_INCREMENT COMMENT '主键' , gmt_create datetime COMMENT '创建时间', gmt_modified datetime COMMENT '修改时间', resource_version int COMMENT '版本号', name varchar(128) COMMENT '租户名', description text COMMENT '描述', extension text COMMENT '扩展字段', KEY `idx_name` (`name`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB CHARSET=utf8 AUTO_INCREMENT=22220801; ``` ## 创建 `roles` 表 ```sql CREATE TABLE `roles` ( id int primary key NOT NULL AUTO_INCREMENT COMMENT '主键' , gmt_create datetime COMMENT '创建时间', gmt_modified datetime COMMENT '修改时间', resource_version int COMMENT '版本号', name varchar(128) COMMENT '用户名', status tinyint(4) COMMENT '状态', sequence bigint(20) NOT NULL, parent_id bigint(20) NOT NULL, memo varchar(128) DEFAULT NULL, KEY `idx_name` (`name`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB CHARSET=utf8 AUTO_INCREMENT=23220801; ``` ## 创建 `clouds` 表 ```sql CREATE TABLE `clouds` ( id int primary key NOT NULL AUTO_INCREMENT COMMENT '主键' , gmt_create datetime COMMENT '创建时间', gmt_modified datetime COMMENT '修改时间', resource_version int COMMENT '版本号', name varchar(128) COMMENT '用户名', alias_name varchar(128) COMMENT '别名', status int COMMENT '集群状态', cloud_type int COMMENT '集群类型', kube_version varchar(128) COMMENT 'k8s 集群版本', node_number int COMMENT '集群节点数量', resources varchar(128) COMMENT '资源数量', description text COMMENT '描述', extension text COMMENT '扩展字段', KEY `idx_name` (`name`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB CHARSET=utf8 AUTO_INCREMENT=22220801; ``` ## 创建 `kube_configs` 表 ```sql CREATE TABLE `kube_configs` ( id int primary key NOT NULL AUTO_INCREMENT COMMENT '主键' , gmt_create datetime COMMENT '创建时间', gmt_modified datetime COMMENT '修改时间', resource_version int COMMENT '版本号', service_account varchar(128) COMMENT 'k8s service account', cloud_name varchar(128) COMMENT '集群名', cloud_id int COMMENT '所属 cloud id', cluster_role varchar(128) COMMENT 'k8s cluster role', config text COMMENT 'k8s kube_config', expiration_timestamp text COMMENT '过期时间', KEY `idx_cloud_name` (`cloud_name`), UNIQUE KEY `service_account` (`service_account`) ) ENGINE=InnoDB CHARSET=utf8 AUTO_INCREMENT=22220801; ``` ## 创建 `nodes` 表 ```sql CREATE TABLE `nodes` ( id int primary key NOT NULL AUTO_INCREMENT COMMENT '主键' , gmt_create datetime COMMENT '创建时间', gmt_modified datetime COMMENT '修改时间', resource_version int COMMENT '版本号', cloud_id int COMMENT 'cloud ID', role varchar(128) COMMENT '节点类型', host_name varchar(128) COMMENT '节点名称', address varchar(128) COMMENT '节点 ip 地址', user varchar(128) COMMENT '用户名', password varchar(128) COMMENT '节点密码', KEY `idx_cloud` (`cloud_id`) ) ENGINE=InnoDB CHARSET=utf8 AUTO_INCREMENT=24220801; ``` ## 创建 `events` 表 ```sql CREATE TABLE `events` ( id int primary key NOT NULL AUTO_INCREMENT COMMENT '主键' , gmt_create datetime COMMENT '创建时间', gmt_modified datetime COMMENT '修改时间', resource_version int COMMENT '版本号', user varchar(128) COMMENT '用户名称', client_ip varchar(128) COMMENT '登陆地址', operator varchar(128) COMMENT '操作类型', object varchar(128) COMMENT '操作对象', message varchar(128) COMMENT '消息' ) ENGINE=InnoDB CHARSET=utf8 AUTO_INCREMENT=26220801; ``` ## 创建 `audit` ```sql CREATE TABLE `audits` ( `id` int primary key NOT NULL AUTO_INCREMENT COMMENT '主键' , `gmt_create` datetime COMMENT '创建时间', `gmt_modified` datetime COMMENT '修改时间', `resource_version` int COMMENT '版本号', `operator` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '操作人', `action` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '动作', `ip` varchar(128) COLLATE utf8mb4_bin NOT NULL COMMENT '来源ip', `status` tinyint(4) COLLATE utf8mb4_bin NOT NULL COMMENT '执行是否成功:0-失败,1-成功', `path` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '详细内容', `resource_type` varchar(128) COLLATE utf8mb4_bin NOT NULL COMMENT '操作的资源类型' ) ENGINE=InnoDB AUTO_INCREMENT=3355 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ``` ================================================ FILE: go.mod ================================================ module github.com/caoyingjunz/pixiu go 1.17 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/caoyingjunz/pixiulib v0.0.0-20220819163605-c3c10ec3ed3c github.com/casbin/casbin/v2 v2.97.0 github.com/casbin/gorm-adapter/v3 v3.12.0 github.com/docker/docker v20.10.12+incompatible github.com/gin-contrib/cors v1.4.0 github.com/gin-contrib/requestid v0.0.6 github.com/gin-gonic/gin v1.8.1 github.com/go-playground/locales v0.14.1 github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/validator/v10 v10.19.0 github.com/go-sql-driver/mysql v1.6.0 github.com/golang-jwt/jwt/v4 v4.4.2 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.4.2 github.com/juju/ratelimit v1.0.2 github.com/mattn/go-isatty v0.0.20 // indirect github.com/pelletier/go-toml/v2 v2.2.0 // indirect github.com/pkg/sftp v1.13.6 github.com/robfig/cron/v3 v3.0.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.5.0 github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a github.com/swaggo/gin-swagger v1.5.3 github.com/swaggo/swag v1.8.6 golang.org/x/crypto v0.21.0 golang.org/x/sync v0.1.0 golang.org/x/time v0.1.0 gorm.io/driver/mysql v1.4.1 gorm.io/gorm v1.24.0 helm.sh/helm/v3 v3.8.2 k8s.io/api v0.23.5 k8s.io/apimachinery v0.23.5 k8s.io/cli-runtime v0.23.5 k8s.io/client-go v0.23.5 k8s.io/klog/v2 v2.80.1 k8s.io/metrics v0.23.5 k8s.io/utils v0.0.0-20221012122500-cfd413dd9e85 // indirect ) require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/BurntSushi/toml v1.2.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/Masterminds/sprig/v3 v3.2.2 // indirect github.com/Masterminds/squirrel v1.5.2 // indirect github.com/Microsoft/go-winio v0.5.1 // indirect github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/casbin/govaluate v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect github.com/containerd/containerd v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/cli v20.10.11+incompatible // indirect github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fatih/color v1.13.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/glebarez/go-sqlite v1.20.3 // indirect github.com/glebarez/sqlite v1.5.0 // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/go-logr/logr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/spec v0.20.7 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.13.0 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.1 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgtype v1.12.0 // indirect github.com/jackc/pgx/v4 v4.17.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/jmoiron/sqlx v1.3.4 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/kr/fs v0.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.10.4 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/microsoft/go-mssqldb v0.17.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.11.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.30.0 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc // indirect github.com/russross/blackfriday v1.5.2 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.9.0 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.22.0 // indirect golang.org/x/oauth2 v0.1.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.6.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90 // indirect google.golang.org/grpc v1.47.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/gorp.v1 v1.7.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/postgres v1.4.4 // indirect gorm.io/driver/sqlserver v1.4.1 // indirect gorm.io/plugin/dbresolver v1.3.0 // indirect k8s.io/apiextensions-apiserver v0.23.5 // indirect k8s.io/apiserver v0.23.5 // indirect k8s.io/component-base v0.23.5 // indirect k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect k8s.io/kubectl v0.23.5 // indirect modernc.org/libc v1.22.2 // indirect modernc.org/mathutil v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect modernc.org/sqlite v1.20.3 // indirect oras.land/oras-go v1.1.1 // indirect sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect sigs.k8s.io/kustomize/api v0.10.1 // indirect sigs.k8s.io/kustomize/kyaml v0.13.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) replace ( // github.com/glebarez/sqlite => github.com/glebarez/sqlite v1.4.0 github.com/pelletier/go-toml/v2 => github.com/pelletier/go-toml/v2 v2.1.1 golang.org/x/net => golang.org/x/net v0.17.0 // gorm.io/driver/sqlserver => gorm.io/driver/sqlserver v1.4.1 ) ================================================ FILE: go.sum ================================================ bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM= github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest v0.11.20/go.mod h1:o3tqFY+QR40VOlk+pV4d77mORO64jOXSgEnPQgLK6JY= github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/adal v0.9.15/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/squirrel v1.5.2 h1:UiOEi2ZX4RCSkpiNDQN5kro/XIBpSRk9iTqdIRPzUXE= github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= github.com/Microsoft/hcsshim v0.8.20/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= github.com/Microsoft/hcsshim v0.9.2 h1:wB06W5aYFfUB3IvootYAY2WnOmIdgPGfqSI6tufQNnY= github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.34.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/caoyingjunz/pixiulib v0.0.0-20220819163605-c3c10ec3ed3c h1:SyWVmVBugbQp3dQ3xz8rlkxU43MxmG86fY8JoxqtSBs= github.com/caoyingjunz/pixiulib v0.0.0-20220819163605-c3c10ec3ed3c/go.mod h1:rQFvtjWLu4D7VSo6H/bI8h0tyWneUtuJJM+AcZBalPk= github.com/casbin/casbin/v2 v2.55.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= github.com/casbin/casbin/v2 v2.97.0 h1:FFHIzY+6fLIcoAB/DKcG5xvscUo9XqRpBniRYhlPWkg= github.com/casbin/casbin/v2 v2.97.0/go.mod h1:jX8uoN4veP85O/n2674r2qtfSXI6myvxW85f6TH50fw= github.com/casbin/gorm-adapter/v3 v3.12.0 h1:jUJCwaLnB+QfUadOETD6ymxeDmOR9jQXTmUTzLMpS3g= github.com/casbin/gorm-adapter/v3 v3.12.0/go.mod h1:jqaf4bUITbCyMPUellaTd8IQJ77JfVAbe77gZZnx98w= github.com/casbin/govaluate v1.1.0 h1:6xdCWIpE9CwHdZhlVQW+froUrCsjb6/ZYNcXODfLT+E= github.com/casbin/govaluate v1.1.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 h1:7aWHqerlJ41y6FOsEUvknqgXnGmJyJSbjhAWq5pO4F8= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= github.com/containerd/cgroups v1.0.3 h1:ADZftAkglvCiD44c77s5YmMqaP2pzVCFZvBmAlBdAP4= github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= github.com/containerd/containerd v1.6.1 h1:oa2uY0/0G+JX4X7hpGCYvkp9FjUancz56kSNnb1sG3o= github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= github.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk= github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= github.com/containerd/go-cni v1.1.0/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= github.com/containerd/go-cni v1.1.3/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA= github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= github.com/containerd/imgcrypt v1.1.3/go.mod h1:/TPA1GIDXMzbj01yd8pIbQiLdQxed5ue1wb8bP7PQu4= github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= github.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y= github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= github.com/containernetworking/plugins v1.0.1/go.mod h1:QHCfGpaTwYTbbH+nZXKVTxNBDZcxSOplJT5ico8/FLE= github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= github.com/containers/ocicrypt v1.1.2/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684 h1:DBZ2sN7CK6dgvHVpQsQj4sRMCbWTmd17l+5SUCjnQSY= github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684/go.mod h1:UfCu3YXJJCI+IdnqGgYP82dk2+Joxmv+mUTVBES6wac= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v20.10.11+incompatible h1:tXU1ezXcruZQRrMP8RN2z9N91h+6egZTS1gsPsKantc= github.com/docker/cli v20.10.11+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.12+incompatible h1:CEeNmFM0QZIsJCZKMkZx0ZcahTiewkrgiwfYD+dfl1U= github.com/docker/docker v20.10.12+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/requestid v0.0.6 h1:mGcxTnHQ45F6QU5HQRgQUDsAfHprD3P7g2uZ4cSZo9o= github.com/gin-contrib/requestid v0.0.6/go.mod h1:9i4vKATX/CdggbkY252dPVasgVucy/ggBeELXuQztm4= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/glebarez/go-sqlite v1.19.1/go.mod h1:9AykawGIyIcxoSfpYWiX1SgTNHTNsa/FVc75cDkbp4M= github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0= github.com/glebarez/sqlite v1.5.0 h1:+8LAEpmywqresSoGlqjjT+I9m4PseIM3NcerIJ/V7mk= github.com/glebarez/sqlite v1.5.0/go.mod h1:0wzXzTvfVJIN2GqRhCdMbnYd+m+aH5/QV7B30rM6NgY= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gqc= github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM= github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM= github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= github.com/gobuffalo/packr/v2 v2.8.1 h1:tkQpju6i3EtMXJ9uoF5GT6kB+LMTimDWD8Xvbz6zDVA= github.com/gobuffalo/packr/v2 v2.8.1/go.mod h1:c/PLlOuTU+p3SybaJATW3H6lX/iK7xEz5OeMf+NnJpg= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/karrick/godirwalk v1.15.8 h1:7+rWAZPn9zuRxaIqqT8Ohs2Q2Ac0msBqwRdxNCr2VVs= github.com/karrick/godirwalk v1.15.8/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= github.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.30.0 h1:JEkYlQnpzrzQFxi6gnukFPdQ+ac82oRhzMcIduJu/Ug= github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M= github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc h1:BD7uZqkN8CpjJtN/tScAKiccBikU4dlqe/gNrkRaPY4= github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc/go.mod h1:HFLT6i9iR4QBOF5rdCyjddC9t59ArqWJV2xx+jwcCMo= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q= github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.8.6 h1:2rgOaLbonWu1PLP6G+/rYjSvPg0jQE0HtrEKuE380eg= github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/oauth2 v0.1.0 h1:isLCZuhj4v+tYv7eskaN4v/TM+A1begWWgyVJDdl1+Y= golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90 h1:4SPz2GL2CXJt28MTF8V6Ap/9ZiVbQlJeGSd9qtA7DLs= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.3.2/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U= gorm.io/driver/mysql v1.4.1 h1:4InA6SOaYtt4yYpV1NF9B2kvUKe9TbvUd1iWrvxnjic= gorm.io/driver/mysql v1.4.1/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= gorm.io/driver/postgres v1.4.4 h1:zt1fxJ+C+ajparn0SteEnkoPg0BQ6wOWXEQ99bteAmw= gorm.io/driver/postgres v1.4.4/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw= gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74= gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= gorm.io/plugin/dbresolver v1.3.0 h1:uFDX3bIuH9Lhj5LY2oyqR/bU6pqWuDgas35NAPF4X3M= gorm.io/plugin/dbresolver v1.3.0/go.mod h1:Pr7p5+JFlgDaiM6sOrli5olekJD16YRunMyA2S7ZfKk= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= helm.sh/helm/v3 v3.8.2 h1:HDhe2nKek976VLMPZlIgJbNqwcqvHYBp1qy+sXQ4jiY= helm.sh/helm/v3 v3.8.2/go.mod h1:NxtE2KObf2PrzDl6SIamPFPKyAqWi10iWuvKlQn/Yao= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= k8s.io/api v0.21.2/go.mod h1:Lv6UGJZ1rlMI1qusN8ruAp9PUBFyBwpEHAdG24vIsiU= k8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs= k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= k8s.io/apiextensions-apiserver v0.23.5 h1:5SKzdXyvIJKu+zbfPc3kCbWpbxi+O+zdmAJBm26UJqI= k8s.io/apiextensions-apiserver v0.23.5/go.mod h1:ntcPWNXS8ZPKN+zTXuzYMeg731CP0heCTl6gYBxLcuQ= k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= k8s.io/apimachinery v0.21.2/go.mod h1:CdTY8fU/BlvAbJ2z/8kBwimGki5Zp8/fbVuLY8gJumM= k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= k8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U= k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= k8s.io/apiserver v0.22.5/go.mod h1:s2WbtgZAkTKt679sYtSudEQrTGWUSQAPe6MupLnlmaQ= k8s.io/apiserver v0.23.5 h1:2Ly8oUjz5cnZRn1YwYr+aFgDZzUmEVL9RscXbnIeDSE= k8s.io/apiserver v0.23.5/go.mod h1:7wvMtGJ42VRxzgVI7jkbKvMbuCbVbgsWFT7RyXiRNTw= k8s.io/cli-runtime v0.23.5 h1:Z7XUpGoJZYZB2uNjQfJjMbyDKyVkoBGye62Ap0sWQHY= k8s.io/cli-runtime v0.23.5/go.mod h1:oY6QDF2qo9xndSq32tqcmRp2UyXssdGrLfjAVymgbx4= k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= k8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y= k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= k8s.io/component-base v0.22.5/go.mod h1:VK3I+TjuF9eaa+Ln67dKxhGar5ynVbwnGrUiNF4MqCI= k8s.io/component-base v0.23.5 h1:8qgP5R6jG1BBSXmRYW+dsmitIrpk8F/fPEvgDenMCCE= k8s.io/component-base v0.23.5/go.mod h1:c5Nq44KZyt1aLl0IpHX82fhsn84Sb0jjzwjpcA42bY0= k8s.io/component-helpers v0.23.5/go.mod h1:5riXJgjTIs+ZB8xnf5M2anZ8iQuq37a0B/0BgoPQuSM= k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= k8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/kubectl v0.23.5 h1:DmDULqCaF4qstj0Im143XmncvqWtJxHzK8IrW2BzlU0= k8s.io/kubectl v0.23.5/go.mod h1:lLgw7cVY8xbd7o637vOXPca/w6HC205KsPCRDYRCxwE= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/metrics v0.23.5 h1:u+3oGwdUWr30LT2v6MHdSJQ4Pbht9s0cVIWyuY5nf9c= k8s.io/metrics v0.23.5/go.mod h1:WNAtV2a5BYbmDS8+7jSqYYV6E3efuGTpIwJ8PTD1wgs= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20221012122500-cfd413dd9e85 h1:cTdVh7LYu82xeClmfzGtgyspNh6UxpwLWGi8R4sspNo= k8s.io/utils v0.0.0-20221012122500-cfd413dd9e85/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI= modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g= modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA= modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0= modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0= modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0= modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.19.1/go.mod h1:UfQ83woKMaPW/ZBruK0T7YaFCrI+IE0LeWVY6pmnVms= modernc.org/sqlite v1.20.3 h1:SqGJMMxjj1PHusLxdYxeQSodg7Jxn9WWkaAQjKrntZs= modernc.org/sqlite v1.20.3/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/tcl v1.14.0/go.mod h1:gQ7c1YPMvryCHCcmf8acB6VPabE59QBeuRQLL7cTUlM= modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.6.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= oras.land/oras-go v1.1.1 h1:gI00ftziRivKXaw1BdMeEoIA4uBgga33iVlOsEwefFs= oras.land/oras-go v1.1.1/go.mod h1:n2TE1ummt9MUyprGhT+Q7kGZUF4kVUpYysPFxeV2IpQ= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= sigs.k8s.io/kustomize/api v0.10.1 h1:KgU7hfYoscuqag84kxtzKdEC3mKMb99DPI3a0eaV1d0= sigs.k8s.io/kustomize/api v0.10.1/go.mod h1:2FigT1QN6xKdcnGS2Ppp1uIWrtWN28Ms8A3OZUZhwr8= sigs.k8s.io/kustomize/cmd/config v0.10.2/go.mod h1:K2aW7nXJ0AaT+VA/eO0/dzFLxmpFcTzudmAgDwPY1HQ= sigs.k8s.io/kustomize/kustomize/v4 v4.4.1/go.mod h1:qOKJMMz2mBP+vcS7vK+mNz4HBLjaQSWRY22EF6Tb7Io= sigs.k8s.io/kustomize/kyaml v0.13.0 h1:9c+ETyNfSrVhxvphs+K2dzT3dh5oVPPEqPOE/cUpScY= sigs.k8s.io/kustomize/kyaml v0.13.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E= sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= ================================================ FILE: hack/tools/licfmt/licfmt.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "bufio" "bytes" "flag" "fmt" "io" "log" "os" "path/filepath" "runtime" "time" "github.com/bmatcuk/doublestar/v4" "golang.org/x/sync/errgroup" ) const tpml = `/* Copyright %s The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ ` var ( ignorePatterns = stringSlice{"vendor/**", "docs/**"} year = flag.String("y", fmt.Sprint(time.Now().Year()), "copyright year(s)") verbose = flag.Bool("v", false, "verbose mode: print the name of the files that are modified or were skipped") ) func init() { flag.Var(&ignorePatterns, "i", "file patterns to ignore, for example: -i vendor/**") } type stringSlice []string func (i *stringSlice) String() string { return fmt.Sprint(*i) } func (i *stringSlice) Set(value string) error { *i = append(*i, value) return nil } type file struct { path string mode os.FileMode } func main() { flag.Parse() if flag.NArg() == 0 { flag.Usage() os.Exit(1) } for _, p := range ignorePatterns { if !doublestar.ValidatePattern(p) { log.Fatalf("ignore pattern %q is invalid", p) } } headerText := fmt.Sprintf(tpml, *year) // process at most CPU number files in parallel ch := make(chan *file, runtime.NumCPU()) done := make(chan struct{}) go func() { var wg errgroup.Group for f := range ch { f := f wg.Go(func() error { updated, err := addLicenseHeader(f.path, headerText, f.mode) if err != nil { log.Printf("%s: %v", f.path, err) return err } if *verbose && updated { log.Printf("%s updated", f.path) } return nil }) } err := wg.Wait() close(done) if err != nil { os.Exit(1) } }() for _, d := range flag.Args() { if err := walk(ch, d); err != nil { log.Fatal(err) } } close(ch) <-done } // walk walks the file tree from root, sends go file to task queue. func walk(ch chan<- *file, root string) error { return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { if err != nil { log.Printf("%s error: %v", path, err) } if fi.IsDir() { return nil } if match(path, ignorePatterns) || filepath.Ext(path) != ".go" { if *verbose { log.Printf("skipping: %s", path) } return nil } ch <- &file{path, fi.Mode()} return nil }) } // match returns if path matches one of the provided file patterns. func match(path string, patterns []string) bool { for _, p := range patterns { if ok, _ := doublestar.Match(p, path); ok { return true } } return false } // hasLicense returns if there is a license in the file header already. func hasLicense(header []byte) bool { return bytes.Contains(header, []byte("Copyright")) && bytes.Contains(header, []byte("Apache License")) } func notAlowEditd(header []byte) bool { return bytes.Contains(header, []byte("DO NOT EDIT")) || bytes.Contains(header, []byte("generated by")) } // addLicenseHeader adds a license header to the file if it does not exist. func addLicenseHeader(path, license string, fmode os.FileMode) (bool, error) { f, err := os.OpenFile(path, os.O_RDONLY, fmode) if err != nil { return false, err } defer f.Close() buf, dropped := scanLines(f) if hasLicense(buf.Bytes()) || notAlowEditd(buf.Bytes()) { return false, nil } // add license header at the beginning new := bytes.NewBuffer(buf.Bytes()) if _, err := new.WriteString(license + "\n"); err != nil { return false, err } // Caculate position of the line contains `package `. offset := int64(buf.Len()) if dropped > 1 { // CR characters are dropped, they should be added back. offset += dropped - 1 } // Move the cursor of original go file to the position. if _, err := f.Seek(offset, io.SeekStart); err != nil { return false, err } if _, err := new.ReadFrom(f); err != nil { return false, err } // create a temporary file tmpFile, err := os.CreateTemp("", "licfmt-") if err != nil { return false, err } defer os.Remove(tmpFile.Name()) defer tmpFile.Close() if err = os.Chmod(tmpFile.Name(), fmode); err != nil { return false, err } // rewrite the whole file to the temporary file if _, err := new.WriteTo(tmpFile); err != nil { return false, err } if err = tmpFile.Sync(); err != nil { return false, err } // rename the tmp file to the original file err = os.Rename(tmpFile.Name(), path) return err == nil, err } // scanLines handles the file line by line // buf: the lines before the package declaration // dropped: the number of CR (`\r`) characters dropped func scanLines(file *os.File) (buf bytes.Buffer, dropped int64) { scanner := bufio.NewScanner(file) dropCR := func(data []byte) []byte { if len(data) > 0 && data[len(data)-1] == '\r' { dropped++ return data[0 : len(data)-1] } return data } scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { if atEOF && len(data) == 0 { return 0, nil, nil } if i := bytes.IndexByte(data, '\n'); i >= 0 { // We have a full newline-terminated line. return i + 1, dropCR(data[0:i]), nil } // If we're at EOF, we have a final, non-terminated line. Return it. if atEOF { return len(data), dropCR(data), nil } // Request more data. return 0, nil, nil }) for scanner.Scan() { line := scanner.Bytes() if bytes.HasPrefix(line, []byte("package ")) { // reading until the line contains `package p` break } buf.Write(line) buf.WriteString("\n") } return } ================================================ FILE: hack/update-gofmt.sh ================================================ #!/bin/bash # Copyright 2021 The Pixiu Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -o nounset set -o errexit set -o pipefail find . -name "*.go" | grep -v vendor | xargs gofmt -s -w ================================================ FILE: hack/update-image.sh ================================================ #!/bin/bash function update() { docker ccr.ccs.tencentyun.com/pixiucloud/pixiu-aio:latest docker rm -f pixiu-aio sleep 3 docker run -d --net host --restart=always --privileged=true -v /etc/pixiu:/etc/pixiu -v /var/run/docker.sock:/var/run/docker.sock --name pixiu-aio ccr.ccs.tencentyun.com/pixiucloud/pixiu-aio:latest } update ================================================ FILE: hack/verify-gofmt.sh ================================================ #!/bin/bash # Copyright 2021 The Pixiu Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -o errexit set -o nounset set -o pipefail HELM_ROOT=$(dirname "${BASH_SOURCE}")/.. cd "${HELM_ROOT}" find_files() { find . -not \( \ \( \ -wholename './output' \ -o -wholename '*/vendor/*' \ \) -prune \ \) -name '*.go' } GOFMT="gofmt -s" bad_files=$(find_files | xargs $GOFMT -l) if [[ -n "${bad_files}" ]]; then echo "Please run hack/update-gofmt.sh to fix the following files:" echo "${bad_files}" exit 1 fi ================================================ FILE: install.md ================================================ # 前置准备 ```bash 确保 docker 已经安装 ``` # 数据库 ```bash # 选择1:直接提供可用数据库 # 选择2:快速启动数据库 docker run -d --net host --restart=always --privileged=true --name mariadb -e MYSQL_ROOT_PASSWORD="Pixiu868686" ccr.ccs.tencentyun.com/pixiucloud/mysql:5.7 # 创建 pixiu 数据库 CREATE DATABASE pixiu; ``` # 获取部署驱动镜像 ```shell docker pull ccr.ccs.tencentyun.com/pixiucloud/kubez-ansible:v2.0.1 docker pull ccr.ccs.tencentyun.com/pixiucloud/kubez-ansible:v3.0.1 ``` # 启动 pixiu 服务端 ```bash # 创建配置文件夹 mkdir -p /etc/pixiu/ # 后端配置(host 根据实际情况调整) vim /etc/pixiu/config.yaml 写入后端如下配置 default: # 运行模式,可选 debug 和 release mode: debug # 服务监听端口 listen: 8090 # 自动创建指定模型的数据库表结构,不会更新已存在的数据库表 auto_migrate: true # 前端配置(ip 根据实际情况调整,如果是虚拟机,则配置成虚拟机的公网IP,安全组放通80(http)或者443(https)端口) # 前端的端口需要和后端监听端口保持一致 dashboard: url: http://localhost:8090 # 数据库地址信息, 根据实际情况配置 mysql: host: pixiu # 数据库的ip user: root password: Pixiu868686 port: 3306 name: pixiu worker: engines: - image: ccr.ccs.tencentyun.com/pixiucloud/kubez-ansible:v2.0.1 os_supported: - centos7 - debian10 - ubuntu18.04 - image: ccr.ccs.tencentyun.com/pixiucloud/kubez-ansible:v3.0.1 os_supported: - debian11 - ubuntu20.04 - ubuntu22.04 - rocky8.5 - rocky9.2 - rocky9.3 - openEuler22.03 ``` # 启动 pixiu ```bash docker run -d --net host --restart=always --privileged=true -v /etc/pixiu:/etc/pixiu -v /var/run/docker.sock:/var/run/docker.sock --name pixiu ccr.ccs.tencentyun.com/pixiucloud/pixiu 登录效果 浏览器登陆: http://192.168.16.156 ``` ================================================ FILE: pkg/client/cache.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "context" "sync" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" appsv1 "k8s.io/client-go/listers/apps/v1" batchv1 "k8s.io/client-go/listers/batch/v1" v1 "k8s.io/client-go/listers/core/v1" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" resourceclient "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1" ) var ( groupVersionResources = []schema.GroupVersionResource{ {Group: "", Version: "v1", Resource: "pods"}, {Group: "", Version: "v1", Resource: "nodes"}, {Group: "apps", Version: "v1", Resource: "deployments"}, {Group: "apps", Version: "v1", Resource: "statefulsets"}, {Group: "apps", Version: "v1", Resource: "daemonsets"}, {Group: "batch", Version: "v1", Resource: "cronjobs"}, {Group: "batch", Version: "v1", Resource: "jobs"}, } ) type PixiuInformer struct { Shared informers.SharedInformerFactory Cancel context.CancelFunc } func (p PixiuInformer) NodesLister() v1.NodeLister { return p.Shared.Core().V1().Nodes().Lister() } func (p PixiuInformer) PodsLister() v1.PodLister { return p.Shared.Core().V1().Pods().Lister() } func (p PixiuInformer) NamespacesLister() v1.NamespaceLister { return p.Shared.Core().V1().Namespaces().Lister() } func (p PixiuInformer) DeploymentsLister() appsv1.DeploymentLister { return p.Shared.Apps().V1().Deployments().Lister() } func (p *PixiuInformer) StatefulSetsLister() appsv1.StatefulSetLister { return p.Shared.Apps().V1().StatefulSets().Lister() } func (p *PixiuInformer) DaemonSetsLister() appsv1.DaemonSetLister { return p.Shared.Apps().V1().DaemonSets().Lister() } func (p *PixiuInformer) CronJobsLister() batchv1.CronJobLister { return p.Shared.Batch().V1().CronJobs().Lister() } func (p *PixiuInformer) JobsLister() batchv1.JobLister { return p.Shared.Batch().V1().Jobs().Lister() } type ClusterSet struct { Client *kubernetes.Clientset Config *restclient.Config Metric *resourceclient.MetricsV1beta1Client Informer *PixiuInformer } func (cs *ClusterSet) Complete(cfg []byte) error { var err error if cs.Config, err = clientcmd.RESTConfigFromKubeConfig(cfg); err != nil { return err } if cs.Client, err = kubernetes.NewForConfig(cs.Config); err != nil { return err } if cs.Metric, err = resourceclient.NewForConfig(cs.Config); err != nil { return err } sharedInformer, cancel, err := NewSharedInformers(cs.Config) if err != nil { return err } cs.Informer = &PixiuInformer{ Shared: sharedInformer, Cancel: cancel, } return nil } func NewSharedInformers(c *restclient.Config) (informers.SharedInformerFactory, context.CancelFunc, error) { // 重新构造客户端 clientSet, err := kubernetes.NewForConfig(c) if err != nil { return nil, nil, err } informerFactory := informers.NewSharedInformerFactory(clientSet, 0) for _, gvr := range groupVersionResources { if _, err = informerFactory.ForResource(gvr); err != nil { return nil, nil, err } } ctx, cancel := context.WithCancel(context.Background()) // Start all informers. informerFactory.Start(ctx.Done()) // Wait for all caches to sync. informerFactory.WaitForCacheSync(ctx.Done()) return informerFactory, cancel, nil } type store map[string]ClusterSet type Cache struct { sync.RWMutex store } func NewClusterCache() *Cache { return &Cache{ store: make(store), } } func (s *Cache) Get(name string) (ClusterSet, bool) { s.RLock() defer s.RUnlock() cluster, ok := s.store[name] return cluster, ok } func (s *Cache) GetConfig(name string) (*restclient.Config, bool) { s.RLock() defer s.RUnlock() clusterSet, ok := s.store[name] if !ok { return nil, false } return clusterSet.Config, true } func (s *Cache) GetClient(name string) (*kubernetes.Clientset, bool) { s.RLock() defer s.RUnlock() clusterSet, ok := s.store[name] if !ok { return nil, false } return clusterSet.Client, true } func (s *Cache) Set(name string, cs ClusterSet) { s.Lock() defer s.Unlock() if s.store == nil { s.store = store{} } s.store[name] = cs } func (s *Cache) Delete(name string) { s.Lock() defer s.Unlock() // Cancel informer cluster, ok := s.store[name] if !ok { return } cluster.Informer.Cancel() // 从缓存移除集群数据 delete(s.store, name) } func (s *Cache) List() store { s.Lock() defer s.Unlock() return s.store } func (s *Cache) Clear() { s.Lock() defer s.Unlock() s.store = store{} } ================================================ FILE: pkg/client/client.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "encoding/base64" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" ) func ParseKubeConfigBytes(cfg string) ([]byte, error) { kubeConfigBytes, err := base64.StdEncoding.DecodeString(cfg) if err != nil { return nil, err } return kubeConfigBytes, err } func NewClientSetFromBytes(data []byte) (*kubernetes.Clientset, error) { config, err := clientcmd.RESTConfigFromKubeConfig(data) if err != nil { return nil, err } return kubernetes.NewForConfig(config) } func NewClientSetFromString(cfg string) (*kubernetes.Clientset, error) { kubeConfigBytes, err := ParseKubeConfigBytes(cfg) if err != nil { return nil, err } return NewClientSetFromBytes(kubeConfigBytes) } func NewClusterSet(cfg string) (*ClusterSet, error) { kubeConfigBytes, err := ParseKubeConfigBytes(cfg) if err != nil { return nil, err } cs := &ClusterSet{} if err = cs.Complete(kubeConfigBytes); err != nil { return nil, err } return cs, nil } ================================================ FILE: pkg/client/helm.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/discovery" memory "k8s.io/client-go/discovery/cached" "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" "k8s.io/client-go/tools/clientcmd" ) type HelmRESTClientGetter struct { kubeConfig *rest.Config } var _ genericclioptions.RESTClientGetter = &HelmRESTClientGetter{} // ToDiscoveryClient implements action.RESTClientGetter. func (h *HelmRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { h.kubeConfig.Burst = 100 discoveryClient, err := discovery.NewDiscoveryClientForConfig(h.kubeConfig) if err != nil { return nil, err } return memory.NewMemCacheClient(discoveryClient), nil } // ToRESTConfig implements action.RESTClientGetter. func (h *HelmRESTClientGetter) ToRESTConfig() (*rest.Config, error) { return h.kubeConfig, nil } // ToRESTMapper implements action.RESTClientGetter. func (h *HelmRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) { discoveryClient, err := h.ToDiscoveryClient() if err != nil { return nil, err } mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) expander := restmapper.NewShortcutExpander(mapper, discoveryClient) return expander, nil } func (h *HelmRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig { loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults} return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) } func NewHelmRESTClientGetter(kubeConfig *rest.Config) *HelmRESTClientGetter { return &HelmRESTClientGetter{ kubeConfig: kubeConfig, } } ================================================ FILE: pkg/client/task.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import ( "context" "fmt" "sync" "time" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/pkg/db" "github.com/caoyingjunz/pixiu/pkg/db/model" ) type WrapObject struct { LatestTime time.Time // 最近一次获取时间 Object []model.Task // TODO,临时实现,使用task的结构,后续优化成任意类型 } type Task struct { sync.RWMutex Lister func(ctx context.Context, planId int64, opts ...db.Options) ([]model.Task, error) items map[int64]WrapObject } func NewTaskCache() *Task { t := &Task{items: make(map[int64]WrapObject)} t.Run() return t } func (t *Task) SetLister(Lister func(ctx context.Context, planId int64, opts ...db.Options) ([]model.Task, error)) { t.Lock() defer t.Unlock() t.Lister = Lister } func (t *Task) Get(planId int64) ([]model.Task, bool) { t.Lock() defer t.Unlock() wrapObject, ok := t.items[planId] if !ok { return nil, false } wrapObject.LatestTime = time.Now() t.items[planId] = wrapObject return wrapObject.Object, ok } func (t *Task) Set(planId int64, tasks []model.Task) { t.Lock() defer t.Unlock() if t.items == nil { t.items = map[int64]WrapObject{} } now := time.Now() klog.Infof("add plan(%d) tasks into cache at %v", planId, now) t.items[planId] = WrapObject{ Object: tasks, LatestTime: now, } } func (t *Task) SetByTask(planId int64, task model.Task) { t.Lock() defer t.Unlock() wrapObject, ok := t.items[planId] if !ok { return } var ( index int found bool ) for i, s := range wrapObject.Object { if s.Id == task.Id { index = i found = true break } } if !found { return } wrapObject.Object[index] = task wrapObject.LatestTime = time.Now() t.items[planId] = wrapObject } func (t *Task) Delete(planId int64) { t.Lock() defer t.Unlock() delete(t.items, planId) } // WaitForCacheSync // 判断缓存中是否已经存在,如果不存在则先写入 func (t *Task) WaitForCacheSync(planId int64) error { _, ok := t.Get(planId) if ok { return nil } tasks, err := t.Lister(context.TODO(), planId) if err != nil { return fmt.Errorf("failed to get plan(%d) tasks from database: %v", planId, err) } t.Set(planId, tasks) return nil } func (t *Task) syncTasks() { if t.Lister == nil || len(t.items) == 0 { klog.V(2).Infof("syncing and waiting for the next loop") return } t.Lock() defer t.Unlock() for planId, wrapObject := range t.items { now := time.Now() if now.Sub(wrapObject.LatestTime) > 5*time.Second { // 如果对象5分钟未被操作,则从缓存中清理 klog.Infof("remove plan(%d) tasks from cache due to it not be handled for 5s", planId) delete(t.items, planId) // 处理下一个对象 continue } newTasks, err := t.Lister(context.TODO(), planId) if err != nil { klog.Errorf("[syncTasks] failed to list plan(%d) tasks: %v", planId, err) delete(t.items, planId) continue } t.items[planId] = WrapObject{ Object: newTasks, LatestTime: now, } } } // Run 启动 syncTasks // TODO: 后续优化 stopCh func (t *Task) Run() { stopCh := make(<-chan struct{}) // 10s 主动加载一次 task // 不发起长连接情况下,无任何开销 go wait.Until(t.syncTasks, 10*time.Second, stopCh) } ================================================ FILE: pkg/client/token_cache.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import "sync" // TokenCache // TODO: 临时实现,后续优化 type TokenCache struct { sync.RWMutex items map[int64]string } func NewTokenCache() *TokenCache { return &TokenCache{ items: map[int64]string{}, } } func (s *TokenCache) Get(uid int64) (string, bool) { s.RLock() defer s.RUnlock() t, ok := s.items[uid] return t, ok } func (s *TokenCache) Set(uid int64, token string) { s.RLock() defer s.RUnlock() if s.items == nil { s.items = map[int64]string{} } s.items[uid] = token } func (s *TokenCache) Delete(uid int64) { s.RLock() defer s.RUnlock() delete(s.items, uid) } func (s *TokenCache) Clear() { s.RLock() defer s.RUnlock() s.items = map[int64]string{} } ================================================ FILE: pkg/client/user_cache.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package client import "sync" type UserCache struct { sync.RWMutex items map[int64]int } func NewUserCache() *UserCache { return &UserCache{ items: map[int64]int{}, } } func (s *UserCache) Get(uid int64) (int, bool) { s.RLock() defer s.RUnlock() status, ok := s.items[uid] return status, ok } func (s *UserCache) Set(uid int64, status int) { s.RLock() defer s.RUnlock() if s.items == nil { s.items = map[int64]int{} } s.items[uid] = status } func (s *UserCache) Delete(uid int64) { s.RLock() defer s.RUnlock() delete(s.items, uid) } func (s *UserCache) Clear() { s.RLock() defer s.RUnlock() s.items = map[int64]int{} } ================================================ FILE: pkg/controller/audit/audit.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package audit import ( "context" "time" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/api/server/errors" "github.com/caoyingjunz/pixiu/cmd/app/config" "github.com/caoyingjunz/pixiu/pkg/db" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" ) type AuditGetter interface { Audit() Interface } type Interface interface { List(ctx context.Context, listOption types.AuditListOptions) (interface{}, error) Get(ctx context.Context, aid int64) (*types.Audit, error) } type audit struct { cc config.Config factory db.ShareDaoFactory } func (a *audit) Get(ctx context.Context, aid int64) (*types.Audit, error) { object, err := a.factory.Audit().Get(ctx, aid) if err != nil { klog.Errorf("failed to get audit %d: %v", aid, err) return nil, errors.ErrServerInternal } if object == nil { return nil, errors.ErrAuditNotFound } return a.model2Type(object), nil } func (a *audit) List(ctx context.Context, listOption types.AuditListOptions) (interface{}, error) { // 构建公共过滤 opts filterOpts := buildAuditFilterOpts(listOption) // 使用相同过滤条件获取总数 total, err := a.factory.Audit().Count(ctx, filterOpts...) if err != nil { klog.Errorf("failed to get audits count: %v", err) return nil, err } page := listOption.Page if page <= 0 { page = 1 } limit := int(listOption.Limit) if limit <= 0 { limit = 20 } paginationOpts := append(filterOpts, db.WithOffset((page-1)*limit), db.WithLimit(limit), db.WithOrderByDesc(), ) objects, err := a.factory.Audit().List(ctx, paginationOpts...) if err != nil { klog.Errorf("failed to get audit events: %v", err) return nil, errors.ErrServerInternal } var ts []types.Audit for _, object := range objects { ts = append(ts, *a.model2Type(&object)) } return types.PageResponse{ PageRequest: listOption.PageRequest, Total: int(total), Items: ts, }, nil } func buildAuditFilterOpts(opt types.AuditListOptions) []db.Options { var opts []db.Options if opt.Operator != "" { opts = append(opts, db.WithAuditOperatorLike(opt.Operator)) } if opt.Action != "" { opts = append(opts, db.WithAuditAction(opt.Action)) } if opt.ObjectType != "" { opts = append(opts, db.WithAuditObjectType(opt.ObjectType)) } if opt.Cluster != "" { opts = append(opts, db.WithAuditCluster(opt.Cluster)) } if opt.Status != nil { opts = append(opts, db.WithAuditStatus(*opt.Status)) } if opt.StartTime != "" { if t, err := time.Parse(time.RFC3339, opt.StartTime); err == nil { opts = append(opts, db.WithAuditCreatedAfter(t)) } } if opt.EndTime != "" { if t, err := time.Parse(time.RFC3339, opt.EndTime); err == nil { opts = append(opts, db.WithCreatedBefore(t)) } } return opts } func (a *audit) model2Type(o *model.Audit) *types.Audit { return &types.Audit{ PixiuMeta: types.PixiuMeta{ Id: o.Id, ResourceVersion: o.ResourceVersion, }, TimeMeta: types.TimeMeta{ GmtCreate: o.GmtCreate, GmtModified: o.GmtModified, }, Ip: o.Ip, Action: o.Action, Status: o.Status, Operator: o.Operator, Path: o.Path, ObjectType: o.ObjectType, Duration: o.Duration, ResponseCode: o.ResponseCode, Cluster: o.Cluster, ResourceName: o.ResourceName, ResourceNamespace: o.ResourceNamespace, } } func NewAudit(cfg config.Config, f db.ShareDaoFactory) *audit { return &audit{ cc: cfg, factory: f, } } ================================================ FILE: pkg/controller/auth/auth.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package auth import ( "context" "fmt" "net/http" "github.com/casbin/casbin/v2" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/api/server/errors" ctrlutil "github.com/caoyingjunz/pixiu/pkg/controller/util" "github.com/caoyingjunz/pixiu/pkg/db" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" ) type AuthGetter interface { Auth() Interface } type ( Interface interface { CreateRBACPolicy(ctx context.Context, req *types.RBACPolicyRequest) error DeleteRBACPolicy(ctx context.Context, req *types.RBACPolicyRequest) error ListRBACPolicies(ctx context.Context, req *types.ListRBACPolicyRequest) ([]types.RBACPolicy, error) CreateGroupBinding(ctx context.Context, req *types.GroupBindingRequest) error DeleteGroupBinding(ctx context.Context, req *types.GroupBindingRequest) error ListGroupBindings(ctx context.Context, req *types.ListGroupBindingRequest) ([]types.RBACPolicy, error) } ) type auth struct { enforcer *casbin.SyncedEnforcer factory db.ShareDaoFactory } func NewAuth(factory db.ShareDaoFactory, enforcer *casbin.SyncedEnforcer) Interface { return &auth{ factory: factory, enforcer: enforcer, } } // getPolicy returns the RBAC policy represented by the request body func (a *auth) getPolicy(ctx context.Context, req *types.RBACPolicyRequest) (model.Policy, error) { if req.UserId != nil { // user RBAC policy user, err := a.factory.User().Get(ctx, *req.UserId) if err != nil { klog.Errorf("failed to get user(%d): %v", *req.UserId, err) return nil, errors.ErrServerInternal } if user == nil { return nil, errors.NewError(fmt.Errorf("user(%d) is not found", *req.UserId), http.StatusBadRequest) } return model.NewUserPolicy(user.Name, req.ObjectType, req.SID, req.Operation), nil } // group RBAC policy return model.NewGroupPolicy(*req.GroupName, req.ObjectType, req.SID, req.Operation), nil } // getBinding returns the group binding policy represented by the request body func (a *auth) getBinding(ctx context.Context, req *types.GroupBindingRequest) (model.Policy, error) { user, err := a.factory.User().Get(ctx, req.UserId) if err != nil { klog.Errorf("failed to get user(%d): %v", req.UserId, err) return nil, errors.ErrServerInternal } if user == nil { return nil, errors.NewError(fmt.Errorf("user(%d) is not found", req.UserId), http.StatusBadRequest) } policy, err := ctrlutil.GetGroupPolicy(a.enforcer, req.GroupName) if err != nil { klog.Errorf("failed to get group(%d): %v", req.GroupName, err) return nil, errors.ErrServerInternal } if policy == nil { return nil, errors.NewError(fmt.Errorf("group(%s) is not found", req.GroupName), http.StatusBadRequest) } return model.NewGroupBinding(user.Name, req.GroupName), nil } func (a *auth) CreateRBACPolicy(ctx context.Context, req *types.RBACPolicyRequest) error { policy, err := a.getPolicy(ctx, req) if err != nil { return err } ok, err := a.enforcer.AddPolicy(policy.Raw()) if err != nil { klog.Errorf("failed to create policy %v: %v", policy.Raw(), err) return errors.ErrServerInternal } if !ok { return errors.ErrRBACPolicyExists } return nil } func (a *auth) DeleteRBACPolicy(ctx context.Context, req *types.RBACPolicyRequest) error { policy, err := a.getPolicy(ctx, req) if err != nil { return err } switch p := policy.(type) { case model.UserPolicy: klog.Infof("delete user policy: %v", policy.Raw()) case model.GroupPolicy: // check existing group bindings klog.Infof("delete group policy: %v", policy.Raw()) bindings, err := ctrlutil.GetGroupBindings(a.enforcer, ctrlutil.QueryWithGroupName(p.GetGroupName())) if err != nil { klog.Errorf("failed to get bindings of group policy(%v): %v", policy.Raw(), err) return errors.ErrServerInternal } if len(bindings) > 0 { return errors.NewError(fmt.Errorf("用户组 %s 已绑定至某些用户", p.GetGroupName()), http.StatusForbidden) } } ok, err := a.enforcer.RemovePolicy(policy.Raw()) if err != nil { klog.Errorf("failed to delete policy %v: %v", policy.Raw(), err) return errors.ErrServerInternal } if !ok { return errors.ErrRBACPolicyNotFound } return nil } func (a *auth) ListRBACPolicies(ctx context.Context, req *types.ListRBACPolicyRequest) ([]types.RBACPolicy, error) { user, err := a.factory.User().Get(ctx, req.UserId) if err != nil { klog.Errorf("failed to get user(%d): %v", req.UserId, err) return nil, errors.ErrServerInternal } if user == nil { return nil, errors.NewError(fmt.Errorf("user(%d) is not found", req.UserId), http.StatusBadRequest) } conds := make([]ctrlutil.PolicyCondition, 0) if req.ObjectType != nil { conds = append(conds, ctrlutil.WithObjectType(*req.ObjectType)) } if req.SID != nil { conds = append(conds, ctrlutil.WithStringID(*req.SID)) } if req.Operation != nil { conds = append(conds, ctrlutil.WithOperation(*req.Operation)) } policies, err := ctrlutil.GetUserPolicies(a.enforcer, user, conds...) if err != nil { klog.Errorf("failed to list policies: %v", err) return nil, errors.ErrServerInternal } rbacPolicies := make([]types.RBACPolicy, len(policies)) for i, policy := range policies { rbacPolicies[i] = *model2Type(policy) } return rbacPolicies, nil } func (a *auth) CreateGroupBinding(ctx context.Context, req *types.GroupBindingRequest) error { binding, err := a.getBinding(ctx, req) if err != nil { return err } ok, err := a.enforcer.AddGroupingPolicy(binding.Raw()) if err != nil { klog.Errorf("failed to create group binding %v: %v", binding.Raw(), err) return errors.ErrServerInternal } if !ok { return errors.ErrGroupBindingExists } return nil } func (a *auth) DeleteGroupBinding(ctx context.Context, req *types.GroupBindingRequest) error { binding, err := a.getBinding(ctx, req) if err != nil { return err } ok, err := a.enforcer.RemoveGroupingPolicy(binding.Raw()) if err != nil { klog.Errorf("failed to delete group binding %v: %v", binding.Raw(), err) return errors.ErrServerInternal } if !ok { return errors.ErrGroupBindingNotFound } return nil } func (a *auth) ListGroupBindings(ctx context.Context, req *types.ListGroupBindingRequest) ([]types.RBACPolicy, error) { conds := make([]ctrlutil.BindingQueryCondition, 0) if req.UserId != nil { user, err := a.factory.User().Get(ctx, *req.UserId) if err != nil { klog.Errorf("failed to get user(%d): %v", *req.UserId, err) return nil, errors.ErrServerInternal } if user == nil { return nil, errors.NewError(fmt.Errorf("user(%d) is not found", *req.UserId), http.StatusBadRequest) } conds = append(conds, ctrlutil.QueryWithUserName(user.Name)) } if req.GroupName != nil { policy, err := ctrlutil.GetGroupPolicy(a.enforcer, *req.GroupName) if err != nil { klog.Errorf("failed to get group(%s): %v", *req.GroupName, err) return nil, errors.ErrServerInternal } if policy == nil { return nil, errors.NewError(fmt.Errorf("group(%s) is not found", *req.GroupName), http.StatusBadRequest) } conds = append(conds, ctrlutil.QueryWithGroupName(*req.GroupName)) } bindings, err := ctrlutil.GetGroupBindings(a.enforcer, conds...) if err != nil { klog.Errorf("failed to get group bindings: %v", *req.GroupName, err) return nil, errors.ErrServerInternal } bindingPolicies := make([]types.RBACPolicy, len(bindings)) for i, binding := range bindings { bindingPolicies[i] = *model2Type(binding) } return bindingPolicies, nil } func model2Type(policy model.Policy) *types.RBACPolicy { switch p := policy.(type) { case model.UserPolicy: return &types.RBACPolicy{ UserName: p.GetUserName(), ObjectType: p.GetObjectType(), StringID: p.GetSID(), Operation: p.GetOperation(), } case model.GroupPolicy: return &types.RBACPolicy{ GroupName: p.GetGroupName(), ObjectType: p.GetObjectType(), StringID: p.GetSID(), Operation: p.GetOperation(), } case model.GroupBinding: return &types.RBACPolicy{ UserName: p.GetUserName(), GroupName: p.GetGroupName(), } } return nil } ================================================ FILE: pkg/controller/cluster/cluster.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cluster import ( "context" "fmt" "io" "net/http" "regexp" "strconv" "strings" "sync" "time" "github.com/casbin/casbin/v2" "github.com/gorilla/websocket" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" apitypes "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" "k8s.io/klog/v2" "k8s.io/metrics/pkg/apis/metrics/v1beta1" "github.com/caoyingjunz/pixiu/api/server/errors" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/cmd/app/config" "github.com/caoyingjunz/pixiu/pkg/client" ctrlutil "github.com/caoyingjunz/pixiu/pkg/controller/util" "github.com/caoyingjunz/pixiu/pkg/db" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" "github.com/caoyingjunz/pixiu/pkg/util" "github.com/caoyingjunz/pixiu/pkg/util/uuid" ) type ClusterGetter interface { Cluster() Interface } type Interface interface { Create(ctx context.Context, req *types.CreateClusterRequest) error Update(ctx context.Context, cid int64, req *types.UpdateClusterRequest) error Delete(ctx context.Context, cid int64) error Get(ctx context.Context, cid int64) (*types.Cluster, error) List(ctx context.Context, req *types.ListClusterRequest) (*types.PageResponse, error) // Ping 检查和 k8s 集群的连通性 Ping(ctx context.Context, kubeConfig string) error // Protect 设置集群的保护策略 Protect(ctx context.Context, cid int64, req *types.ProtectClusterRequest) error // GetEventList 获取指定对象的事件,支持做聚合 GetEventList(ctx context.Context, cluster string, options types.EventOptions) (*v1.EventList, error) // AggregateEvents 聚合指定资源的 events AggregateEvents(ctx context.Context, cluster string, namespace string, name string, kind string) (*v1.EventList, error) // WsHandler pod 的 webShell WsHandler(ctx context.Context, webShellOptions *types.WebShellOptions, w http.ResponseWriter, r *http.Request) error // WsNodeHandler node 的 webShell WsNodeHandler(ctx context.Context, sshConfig *types.WebSSHRequest, w http.ResponseWriter, r *http.Request) error // WatchPodLog 实时获取 pod 的日志 WatchPodLog(ctx context.Context, cluster string, namespace string, podName string, containerName string, tailLine int64, w http.ResponseWriter, r *http.Request) error // ReRunJob 重新执行指定任务 ReRunJob(ctx context.Context, cluster string, namespace string, jobName string, resourceVersion string) error GetKubeConfigByName(ctx context.Context, name string) (*restclient.Config, error) GetIndexerResource(ctx context.Context, cluster string, resource string, namespace string, name string) (interface{}, error) ListIndexerResources(ctx context.Context, cluster string, resource string, namespace string, listOption types.ListOptions) (interface{}, error) // Run 启动 cluster worker 处理协程 Run(ctx context.Context, workers int) error } var ClusterIndexer client.Cache func init() { ClusterIndexer = *client.NewClusterCache() } type ( listerFunc func(ctx context.Context, informer *client.PixiuInformer, namespace string, listOption types.ListOptions) (interface{}, error) getterFunc func(ctx context.Context, informer *client.PixiuInformer, namespace, name string) (interface{}, error) ) type InformerResource struct { // k8s 资源类型,比如 deployment, sts, daemonset 等 ResourceType string ListerFunc listerFunc GetterFunc getterFunc } type cluster struct { cc config.Config factory db.ShareDaoFactory enforcer *casbin.SyncedEnforcer listerFuncs map[string]listerFunc getterFuncs map[string]getterFunc } func (c *cluster) preCreate(ctx context.Context, req *types.CreateClusterRequest) error { // 实际创建前,先创建集群的连通性 if err := c.Ping(ctx, req.KubeConfig); err != nil { return fmt.Errorf("尝试连接 kubernetes API 失败: %v", err) } return nil } func (c *cluster) Create(ctx context.Context, req *types.CreateClusterRequest) error { user, err := httputils.GetUserFromRequest(ctx) if err != nil { return errors.NewError(err, http.StatusInternalServerError) } if err := c.preCreate(ctx, req); err != nil { return errors.NewError(err, http.StatusBadRequest) } // TODO: 集群名称必须是由英文,数字组成 if len(req.Name) == 0 { req.Name = uuid.NewRandName(8) } var cs *client.ClusterSet var txFunc = func(cluster *model.Cluster) (err error) { if cs, err = client.NewClusterSet(req.KubeConfig); err != nil { return } // insert a user RBAC policy policy := model.NewPolicyFromModels(user, model.ObjectCluster, cluster.Model, model.OpAll) _, err = c.enforcer.AddPolicy(policy.Raw()) return } kubeNode := types.KubeNode{} nodes, _ := kubeNode.Marshal() if _, err := c.factory.Cluster().Create(ctx, &model.Cluster{ Name: req.Name, AliasName: req.AliasName, ClusterType: req.Type, Protected: req.Protected, KubeConfig: req.KubeConfig, Description: req.Description, Nodes: nodes, }, txFunc); err != nil { klog.Errorf("failed to create cluster %s: %v", req.Name, err) return errors.ErrServerInternal } // TODO: 暂时不做创建后动作 ClusterIndexer.Set(req.Name, *cs) return nil } func (c *cluster) Update(ctx context.Context, cid int64, req *types.UpdateClusterRequest) error { object, err := c.factory.Cluster().Get(ctx, cid) if err != nil { klog.Errorf("failed to get cluster(%d): %v", cid, err) return errors.ErrServerInternal } if object == nil { return errors.ErrClusterNotFound } updates := make(map[string]interface{}) if req.AliasName != nil { updates["alias_name"] = *req.AliasName } if req.Description != nil { updates["description"] = *req.Description } if len(updates) == 0 { return errors.ErrInvalidRequest } if err = c.factory.Cluster().Update(ctx, cid, *req.ResourceVersion, updates); err != nil { klog.Errorf("failed to update cluster(%d): %v", cid, err) return errors.ErrServerInternal } return nil } // 删除前置检查 // 开启集群删除保护,则不允许删除 func (c *cluster) preDelete(ctx context.Context, cid int64) (cluster *model.Cluster, err error) { if cluster, err = c.factory.Cluster().Get(ctx, cid); err != nil { klog.Errorf("failed to get cluster(%d): %v", cid, err) return } if cluster == nil { return nil, errors.ErrClusterNotFound } // 开启集群删除保护,则不允许删除 if cluster.Protected { return nil, errors.NewError(fmt.Errorf("已开启集群删除保护功能,不允许删除 %s", cluster.AliasName), http.StatusForbidden) } // TODO: 其他删除策略检查 return } func (c *cluster) Delete(ctx context.Context, cid int64) error { user, err := httputils.GetUserFromRequest(ctx) if err != nil { return errors.NewError(err, http.StatusInternalServerError) } cluster, err := c.preDelete(ctx, cid) if err != nil { return err } var txFunc = func(cluster *model.Cluster) (err error) { _, err = c.enforcer.RemoveNamedPolicy("p", user.Name, model.ObjectCluster.String(), cluster.GetSID()) return } if err := c.factory.Cluster().Delete(ctx, cluster, txFunc); err != nil { klog.Errorf("failed to delete cluster(%d): %v", cid, err) return errors.ErrServerInternal } // 从缓存中移除 clusterSet ClusterIndexer.Delete(cluster.Name) return nil } func (c *cluster) Get(ctx context.Context, cid int64) (*types.Cluster, error) { object, err := c.factory.Cluster().Get(ctx, cid) if err != nil { return nil, errors.ErrServerInternal } if object == nil { return nil, errors.ErrClusterNotFound } return c.model2Type(object), nil } func (c *cluster) List(ctx context.Context, req *types.ListClusterRequest) (*types.PageResponse, error) { opts := ctrlutil.MakeDbOptions(ctx) if req != nil && req.NameSelector != "" { opts = append(opts, db.WithAliasNameLike(req.NameSelector)) } if req != nil && req.Status != nil { opts = append(opts, db.WithClusterStatus(*req.Status)) } total, err := c.factory.Cluster().Count(ctx, opts...) if err != nil { return nil, err } pageReq := types.PageRequest{} if req != nil { pageReq = req.PageRequest if req.Page > 0 && req.Limit > 0 { opts = append(opts, db.WithOffset((req.Page-1)*req.Limit), db.WithLimit(req.Limit)) } } objects, err := c.factory.Cluster().List(ctx, opts...) if err != nil { return nil, err } cs := make([]types.Cluster, len(objects)) for i, object := range objects { cs[i] = *c.model2Type(&object) } return &types.PageResponse{ PageRequest: pageReq, Total: int(total), Items: cs, }, nil } // Ping 检查和 k8s 集群的连通性 // 如果能获取到 k8s 接口的正常返回,则返回 nil,否则返回具体 error // kubeConfig 为 k8s 证书的 base64 字符串 func (c *cluster) Ping(ctx context.Context, kubeConfig string) error { clientSet, err := client.NewClientSetFromString(kubeConfig) if err != nil { return err } // 调用 ns 资源,确保连通 var timeout int64 = 1 if _, err = clientSet.CoreV1().Namespaces().List(ctx, metav1.ListOptions{ TimeoutSeconds: &timeout, }); err != nil { klog.Errorf("failed to ping kubernetes: %v", err) // 处理原始报错信息,仅返回连接不通的信息 return fmt.Errorf("kubernetes 集群连接测试失败") } return nil } func (c *cluster) Protect(ctx context.Context, cid int64, req *types.ProtectClusterRequest) error { if err := c.factory.Cluster().Update(ctx, cid, *req.ResourceVersion, map[string]interface{}{ "protected": req.Protected, }); err != nil { klog.Errorf("failed to protect cluster(%d): %v", cid, err) return err } return nil } func (c *cluster) GetEventList(ctx context.Context, cluster string, options types.EventOptions) (*v1.EventList, error) { if options.Limit == 0 { options.Limit = 500 } opt := metav1.ListOptions{Limit: options.Limit} fs := c.makeFieldSelector(apitypes.UID(options.Uid), options.Name, options.Namespace, options.Kind) if len(fs) != 0 { opt.FieldSelector = fs } clusterSet, err := c.GetClusterSetByName(ctx, cluster) if err != nil { return nil, err } return clusterSet.Client.CoreV1().Events(options.Namespace).List(ctx, opt) } // WatchPodLog streams the logs of a pod in a cluster to a websocket connection. // // Parameters: // - ctx: The context.Context object for the request. // - cluster: The name of the cluster. // - namespace: The namespace of the pod. // - podName: The name of the pod. // - containerName: The name of the container. // - tailLine: The number of lines to show from the end of the logs. // - w: The http.ResponseWriter object for the websocket connection. // - r: The *http.Request object for the websocket connection. // // Returns: // - error: An error if there was a problem streaming the logs. func (c *cluster) WatchPodLog(ctx context.Context, cluster string, namespace string, podName string, containerName string, tailLine int64, w http.ResponseWriter, r *http.Request) error { clusterSet, err := c.GetClusterSetByName(ctx, cluster) if err != nil { klog.Errorf("failed to get cluster(%s) clientSet: %v", cluster, err) return err } req := clusterSet.Client.CoreV1().Pods(namespace).GetLogs(podName, &v1.PodLogOptions{ Container: containerName, Follow: true, TailLines: &tailLine, Timestamps: false, }) if req == nil { klog.Errorf("failed to get stream") return fmt.Errorf("failed to get stream") } withTimeout, cancelFunc := context.WithTimeout(ctx, time.Minute*10) defer cancelFunc() reader, err := req.Stream(withTimeout) if err != nil { klog.Errorf("failed to get stream: %v", err) return err } defer reader.Close() conn, err := util.BuildWebSocketConnection(w, r) if err != nil { klog.Errorf("failed to build websocket connection: %v", err) return err } defer conn.Close() for { buf := make([]byte, 1024) n, err := reader.Read(buf) if err != nil && err != io.EOF { break } err = conn.WriteMessage(websocket.TextMessage, buf[0:n]) if err != nil { klog.Errorf("failed to write message: %v ,this websocket connection will be closed", err) break } } return nil } const Retries = 3 // ReRunJob 重新运行(创建)任务,通过先删除在创建的方式实现,极端情况下可能导致 job 丢失 func (c *cluster) ReRunJob(ctx context.Context, cluster string, namespace string, jobName string, resourceVersion string) error { cs, err := c.GetClusterSetByName(ctx, cluster) if err != nil { return err } job, err := cs.Client.BatchV1().Jobs(namespace).Get(ctx, jobName, metav1.GetOptions{}) if err != nil { return err } if job.ResourceVersion != resourceVersion { return fmt.Errorf("please apply your changes to the latest and re-run") } newJob := *job // 重置不必要字段 newJob.ResourceVersion = "" newJob.ObjectMeta.UID = "" newJob.Status = batchv1.JobStatus{} // 重置 uid 和 label delete(newJob.Spec.Selector.MatchLabels, "controller-uid") delete(newJob.Spec.Selector.MatchLabels, "batch.kubernetes.io/controller-uid") delete(newJob.Spec.Template.ObjectMeta.Labels, "controller-uid") delete(newJob.Spec.Template.ObjectMeta.Labels, "batch.kubernetes.io/controller-uid") delete(newJob.Spec.Template.ObjectMeta.Labels, "batch.kubernetes.io/job-name") delete(newJob.Spec.Template.ObjectMeta.Labels, "job-name") // TODO: 备份一次job,避免失败job丢失 // 2. 删除job if err = cs.Client.BatchV1().Jobs(namespace).Delete(ctx, jobName, metav1.DeleteOptions{}); err != nil { return fmt.Errorf("failed to rerun job(%s) %v", jobName, err) } var jobErr error // 3. 新建job,最多重试 3 次 for i := 0; i < Retries; i++ { _, jobErr = cs.Client.BatchV1().Jobs(namespace).Create(ctx, &newJob, metav1.CreateOptions{}) if jobErr != nil { time.Sleep(time.Second) continue } break } if jobErr != nil { return fmt.Errorf("failed to rerun job(%s) %v", jobName, err) } return nil } // AggregateEvents 聚合 k8s 资源的所有 events,比如 kind 为 deployment 时,则聚合 deployment,所属 rs 以及 pod 的事件 func (c *cluster) AggregateEvents(ctx context.Context, cluster string, namespace string, name string, kind string) (*v1.EventList, error) { clusterSet, err := c.GetClusterSetByName(ctx, cluster) if err != nil { return nil, err } var fieldSelectors []string switch kind { case "deployment": // TODO: 临时聚合方式,后续继续优化(简化) // 获取 deployment deployment, err := clusterSet.Client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { klog.Errorf("failed to get deployment (%s/%s), err: %v", namespace, name, err) return nil, err } fieldSelectors = append(fieldSelectors, c.makeFieldSelector(deployment.UID, deployment.Name, deployment.Namespace, "Deployment")) var labels []string for k, v := range deployment.Spec.Selector.MatchLabels { labels = append(labels, fmt.Sprintf("%s=%s", k, v)) } labelSelector := strings.Join(labels, ",") kubeObject, err := c.GetKubeObjectByLabel(clusterSet.Client, namespace, labelSelector, "ReplicaSet", "Pod") if err != nil { return nil, err } // 获取 rs allReplicaSets := kubeObject.GetReplicaSets() var replicaSetUIDs []apitypes.UID for _, rs := range allReplicaSets { for _, ownerReference := range rs.OwnerReferences { if ownerReference.Kind == "Deployment" && ownerReference.UID == deployment.UID { fieldSelectors = append(fieldSelectors, c.makeFieldSelector(rs.UID, rs.Name, rs.Namespace, "ReplicaSet")) replicaSetUIDs = append(replicaSetUIDs, rs.UID) } } } // 获取 pods allPods := kubeObject.GetPods() for _, p := range allPods { for _, ownerReference := range p.OwnerReferences { for _, replicaSetUID := range replicaSetUIDs { if ownerReference.UID == replicaSetUID && ownerReference.Kind == "ReplicaSet" { fieldSelectors = append(fieldSelectors, c.makeFieldSelector(p.UID, p.Name, p.Namespace, "Pod")) } } } } default: return nil, fmt.Errorf("unsupported kubernetes object kind %s", kind) } diff := len(fieldSelectors) errCh := make(chan error, diff) eventCh := make(chan *v1.EventList, diff) var wg sync.WaitGroup wg.Add(diff) for _, fieldSelector := range fieldSelectors { go func(fs string) { defer wg.Done() events, err := clusterSet.Client.CoreV1().Events(namespace).List(context.TODO(), metav1.ListOptions{ FieldSelector: fs, Limit: 500, }) if err != nil { klog.Errorf("failed to get object(%s) events: %v", namespace, err) errCh <- err } eventCh <- events }(fieldSelector) } wg.Wait() select { case err := <-errCh: if err != nil { return nil, err } default: } eventList := &v1.EventList{Items: []v1.Event{}} for i := 0; i < diff; i++ { es := <-eventCh if es == nil { continue } eventList.Items = append(eventList.Items, es.Items...) } return eventList, nil } // GetKubeObjectByLabel // TODO: 并发优化 func (c *cluster) GetKubeObjectByLabel(Client *kubernetes.Clientset, namespace string, labelSelector string, kinds ...string) (*types.KubeObject, error) { object := &types.KubeObject{} kindSet := sets.NewString(kinds...) errCh := make(chan error, kindSet.Len()) var wg sync.WaitGroup wg.Add(kindSet.Len()) // 后续优化 if kindSet.Has("ReplicaSet") { go func() { defer wg.Done() allReplicaSets, err := Client.AppsV1().ReplicaSets(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector, Limit: 500}) if err != nil { errCh <- err } else { object.SetReplicaSets(allReplicaSets.Items) } }() } if kindSet.Has("Pod") { go func() { defer wg.Done() allPods, err := Client.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector, Limit: 500}) if err != nil { errCh <- err } else { object.SetPods(allPods.Items) } }() } wg.Wait() select { case err := <-errCh: if err != nil { return nil, err } default: } return object, nil } func (c *cluster) GetKubeConfigByName(ctx context.Context, name string) (*restclient.Config, error) { cs, err := c.GetClusterSetByName(ctx, name) if err != nil { return nil, err } return cs.Config, nil } // GetClusterSetByName 获取 ClusterSet, 缓存中不存在时,构建缓存再返回 func (c *cluster) GetClusterSetByName(ctx context.Context, name string) (client.ClusterSet, error) { cs, ok := ClusterIndexer.Get(name) if ok { klog.Infof("Get %s clusterSet from cache", name) return cs, nil } klog.Infof("building clusterSet for %s", name) // 缓存中不存在,则新建并重写回缓存 object, err := c.factory.Cluster().GetClusterByName(ctx, name) if err != nil { return client.ClusterSet{}, err } if object == nil { return client.ClusterSet{}, errors.ErrClusterNotFound } newClusterSet, err := client.NewClusterSet(object.KubeConfig) if err != nil { return client.ClusterSet{}, err } klog.Infof("set %s clusterSet into cache", name) ClusterIndexer.Set(name, *newClusterSet) return *newClusterSet, nil } // GetKubernetesMeta // TODO:临时构造 client,后续通过 informer 的方式维护缓存 func (c *cluster) GetKubernetesMeta(ctx context.Context, clusterName string) (*types.KubernetesMeta, error) { clusterSet, err := c.GetClusterSetByName(ctx, clusterName) if err != nil { return nil, err } // 获取 k8s 的节点信息 nodeList, err := clusterSet.Client.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) if err != nil { return nil, err } nodes := nodeList.Items // 在集群启动,但是没有节点加入时,命中该场景 if len(nodes) == 0 { return nil, fmt.Errorf("no nodes found") } // 构造 kubernetes 资源数据格式 km := types.KubernetesMeta{ Nodes: len(nodes), KubernetesVersion: nodes[0].Status.NodeInfo.KubeletVersion, } // TODO: 并发优化 // 获取集群所有节点的资源数据,并做整合 //metricList, err := clusterSet.Metric.NodeMetricses().List(ctx, metav1.ListOptions{}) //if err != nil { // return nil, err //} //km.Resources = c.parseKubernetesResource(metricList.Items) return &km, nil } func (c *cluster) GetKubernetesMetaFromPlan(ctx context.Context, planId int64) (*types.KubernetesMeta, error) { planConfig, err := c.factory.Plan().GetConfigByPlan(ctx, planId) if err != nil { return nil, err } ks := &types.KubernetesSpec{} if err = ks.Unmarshal(planConfig.Kubernetes); err != nil { return nil, err } nodes, err := c.factory.Plan().ListNodes(ctx, planId) if err != nil { return nil, err } return &types.KubernetesMeta{ KubernetesVersion: "v" + ks.KubernetesVersion, Nodes: len(nodes), }, nil } // 构造事件的 FieldSelector, 如果参数为空则忽略 func (c *cluster) makeFieldSelector(uid apitypes.UID, name string, namespace string, kind string) string { eventFS := make([]string, 0) // 追加对象的 uid if util.IsEmptyS(string(uid)) { eventFS = append(eventFS, "involvedObject.uid="+string(uid)) } if util.IsEmptyS(name) { eventFS = append(eventFS, "involvedObject.name="+name) } if util.IsEmptyS(namespace) { eventFS = append(eventFS, "involvedObject.namespace="+namespace) } if util.IsEmptyS(kind) { eventFS = append(eventFS, "involvedObject.kind="+kind) } // 构造 kubernetes 原生 FieldSelector 参数格式 return strings.Join(eventFS, ",") } func (c *cluster) parseKubernetesResource(nodeMetrics []v1beta1.NodeMetrics) types.Resources { // 初始化集群资源 resourceList := v1.ResourceList{ v1.ResourceCPU: resource.Quantity{}, v1.ResourceMemory: resource.Quantity{}, } // 遍历所有 metric 数据,算集群总和,仅计算 cpu 和 memory for _, metric := range nodeMetrics { // 1. Cpu cpuMetric := metric.Usage.Cpu() if cpuMetric != nil { cpuSum := resourceList[v1.ResourceCPU] cpuSum.Add(*cpuMetric) resourceList[v1.ResourceCPU] = cpuSum } // 2. Memory memoryMetric := metric.Usage.Memory() if memoryMetric != nil { memSum := resourceList[v1.ResourceMemory] memSum.Add(*memoryMetric) resourceList[v1.ResourceMemory] = memSum } } cpuSum := resourceList[v1.ResourceCPU] memSum := resourceList[v1.ResourceMemory] return types.Resources{ Cpu: strconv.FormatFloat(parseFloat64FromString(cpuSum.String())/1000/1000/1000, 'f', 2, 64) + " Core", Memory: strconv.FormatFloat(parseFloat64FromString(memSum.String())/1024/1024, 'f', 2, 64) + " Gi"} } // parseFloat64FromString 从字符串中解析出包含的数字,并以 float64 返回。 // 无法解析时,返回 0 // 仅解析最先遇到的数字,效果: // "666ddd" -> 666 // "666ddd888" -> 666 // "" 或者 "ddd"- > 0 func parseFloat64FromString(s string) float64 { matcher := regexp.MustCompile(`\d+`) fs := matcher.FindString(s) if len(fs) == 0 { return 0 } f, err := strconv.ParseFloat(fs, 64) if err != nil { return 0 } return f } func (c *cluster) model2Type(o *model.Cluster) *types.Cluster { nodes := types.KubeNode{} if strings.TrimSpace(o.Nodes) != "" { if err := nodes.Unmarshal(o.Nodes); err != nil { // 非核心数据 klog.Warningf("failed to unmarshal cluster nodes: %v", err) } } tc := &types.Cluster{ PixiuMeta: types.PixiuMeta{ Id: o.Id, ResourceVersion: o.ResourceVersion, }, TimeMeta: types.TimeMeta{ GmtCreate: o.GmtCreate, GmtModified: o.GmtModified, }, Name: o.Name, AliasName: o.AliasName, ClusterType: o.ClusterType, KubernetesVersion: o.KubernetesVersion, Nodes: nodes, PlanId: o.PlanId, Status: o.ClusterStatus, // 默认是运行中状态,自建集群会根据实际任务状态修改状态 Protected: o.Protected, Description: o.Description, } //var ( // kubernetesMeta *types.KubernetesMeta // err error //) // //if o.ClusterType == model.ClusterTypeStandard { // // 导入的集群通过API获取相关数据 // // 获取失败时,返回空的 kubernetes Meta, 不终止主流程 // // TODO: 后续改成并发处理 // kubernetesMeta, err = c.GetKubernetesMeta(context.TODO(), o.Name) //} else { // // 自建的集群通过plan配置获取版本信息 // kubernetesMeta, err = c.GetKubernetesMetaFromPlan(context.TODO(), o.PlanId) // // // 自建的集群需要从 plan task 获取状态 // tc.Status, _ = c.GetClusterStatusFromPlanTask(o.PlanId) //} //if err != nil { // klog.Warning("failed to get kubernetes Meta: %v", err) //} else { // tc.KubernetesMeta = *kubernetesMeta //} return tc } func (c *cluster) GetClusterStatusFromPlanTask(planId int64) (model.ClusterStatus, error) { status := model.ClusterStatusRunning // 尝试获取最新的任务状态 // 获取失败也不中断返回 if tasks, err := c.factory.Plan().ListTasks(context.TODO(), planId); err == nil { if len(tasks) == 0 { status = model.ClusterStatusUnStart } else { for _, task := range tasks { if task.Status != model.SuccessPlanStatus { if task.Status == model.FailedPlanStatus { status = model.ClusterStatusFailed } else { status = model.ClusterStatusDeploy } break } } } } return status, nil } func (c *cluster) registerIndexers(informerResources ...InformerResource) { for _, informerResource := range informerResources { c.listerFuncs[informerResource.ResourceType] = informerResource.ListerFunc c.getterFuncs[informerResource.ResourceType] = informerResource.GetterFunc } } func (c *cluster) Run(ctx context.Context, workers int) error { klog.Infof("starting cluster manager") // 同步集群状态,节点数,版本 go wait.UntilWithContext(ctx, c.Sync, 5*time.Second) return nil } func (c *cluster) Sync(ctx context.Context) { // TODO: 后续添加同步任务 } func NewCluster(cfg config.Config, f db.ShareDaoFactory, e *casbin.SyncedEnforcer) *cluster { c := &cluster{ cc: cfg, factory: f, enforcer: e, listerFuncs: make(map[string]listerFunc), getterFuncs: make(map[string]getterFunc), } // TODO: code generation? //c.registerIndexers([]InformerResource{ // { // ResourceType: ResourcePod, // ListerFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace string, listOption types.ListOptions) (interface{}, error) { // return c.ListPods(ctx, informer.PodsLister(), namespace, listOption) // }, // GetterFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace, name string) (interface{}, error) { // return c.GetPod(ctx, informer.PodsLister(), namespace, name) // }, // }, // { // ResourceType: ResourceDeployment, // ListerFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace string, listOption types.ListOptions) (interface{}, error) { // return c.ListDeployments(ctx, informer.DeploymentsLister(), namespace, listOption) // }, // GetterFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace, name string) (interface{}, error) { // return c.GetDeployment(ctx, informer.DeploymentsLister(), namespace, name) // }, // }, // { // ResourceType: ResourceStatefulSet, // ListerFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace string, listOption types.ListOptions) (interface{}, error) { // return c.ListStatefulSets(ctx, informer.StatefulSetsLister(), namespace, listOption) // }, // GetterFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace, name string) (interface{}, error) { // return c.GetStatefulSet(ctx, informer.StatefulSetsLister(), namespace, name) // }, // }, // { // ResourceType: ResourceDaemonSet, // ListerFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace string, listOption types.ListOptions) (interface{}, error) { // return c.ListDaemonSets(ctx, informer.DaemonSetsLister(), namespace, listOption) // }, // GetterFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace, name string) (interface{}, error) { // return c.GetDaemonSet(ctx, informer.DaemonSetsLister(), namespace, name) // }, // }, // { // ResourceType: ResourceCronJob, // ListerFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace string, listOption types.ListOptions) (interface{}, error) { // return c.ListCronJobs(ctx, informer.CronJobsLister(), namespace, listOption) // }, // GetterFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace, name string) (interface{}, error) { // return c.GetCronJob(ctx, informer.CronJobsLister(), namespace, name) // }, // }, // { // ResourceType: ResourceJob, // ListerFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace string, listOption types.ListOptions) (interface{}, error) { // return c.ListJobs(ctx, informer.JobsLister(), namespace, listOption) // }, // GetterFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace, name string) (interface{}, error) { // return c.GetJob(ctx, informer.JobsLister(), namespace, name) // }, // }, // { // ResourceType: ResourceNode, // ListerFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace string, listOption types.ListOptions) (interface{}, error) { // return c.ListNodes(ctx, informer.NodesLister(), namespace, listOption) // }, // GetterFunc: func(ctx context.Context, informer *client.PixiuInformer, namespace, name string) (interface{}, error) { // return c.GetNode(ctx, informer.NodesLister(), namespace, name) // }, // }, // // TODO: 补充更多资源实现 //}...) return c } ================================================ FILE: pkg/controller/cluster/informer.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cluster import ( "context" "fmt" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" listersv1 "k8s.io/client-go/listers/apps/v1" listersbatchv1 "k8s.io/client-go/listers/batch/v1" v1 "k8s.io/client-go/listers/core/v1" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/pkg/types" ) const ( ResourceNode = "node" ResourcePod = "pod" ResourceDeployment = "deployment" ResourceStatefulSet = "statefulset" ResourceDaemonSet = "daemonset" ResourceCronJob = "cronjob" ResourceJob = "job" ) func (c *cluster) GetIndexerResource(ctx context.Context, cluster string, resource string, namespace string, name string) (interface{}, error) { if len(namespace) == 0 || len(name) == 0 { return nil, fmt.Errorf("namespace or name is empty") } cs, err := c.GetClusterSetByName(ctx, cluster) if err != nil { return nil, err } // getter functions should be registered in NewCluster function fn, ok := c.getterFuncs[resource] if !ok { return nil, fmt.Errorf("unsupported resource type %s", resource) } return fn(ctx, cs.Informer, namespace, name) } func (c *cluster) GetPod(ctx context.Context, podsLister v1.PodLister, namespace string, name string) (interface{}, error) { pod, err := podsLister.Pods(namespace).Get(name) if err != nil { klog.Error("failed to get pod (%s/%s) from indexer: %v", namespace, name, err) return nil, err } return pod, nil } func (c *cluster) GetDeployment(ctx context.Context, deploymentsLister listersv1.DeploymentLister, namespace string, name string) (interface{}, error) { deploy, err := deploymentsLister.Deployments(namespace).Get(name) if err != nil { klog.Error("failed to get deployment (%s/%s) from indexer: %v", namespace, name, err) return nil, err } return deploy, nil } func (c *cluster) GetStatefulSet(ctx context.Context, statefulSetsLister listersv1.StatefulSetLister, namespace string, name string) (interface{}, error) { statefulSet, err := statefulSetsLister.StatefulSets(namespace).Get(name) if err != nil { klog.Error("failed to get statefulSet (%s/%s) from indexer: %v", namespace, name, err) return nil, err } return statefulSet, nil } func (c *cluster) GetDaemonSet(ctx context.Context, daemonSetsLister listersv1.DaemonSetLister, namespace string, name string) (interface{}, error) { daemonSet, err := daemonSetsLister.DaemonSets(namespace).Get(name) if err != nil { klog.Error("failed to get daemonset (%s/%s) from indexer: %v", namespace, name, err) return nil, err } return daemonSet, nil } func (c *cluster) GetCronJob(ctx context.Context, cronJobsLister listersbatchv1.CronJobLister, namespace string, name string) (interface{}, error) { cronJob, err := cronJobsLister.CronJobs(namespace).Get(name) if err != nil { klog.Error("failed to get cronjob (%s/%s) from indexer: %v", namespace, name, err) return nil, err } return cronJob, nil } func (c *cluster) GetJob(ctx context.Context, cronJobsLister listersbatchv1.JobLister, namespace string, name string) (interface{}, error) { job, err := cronJobsLister.Jobs(namespace).Get(name) if err != nil { klog.Error("failed to get job (%s/%s) from indexer: %v", namespace, name, err) return nil, err } return job, nil } func (c *cluster) GetNode(ctx context.Context, nodesLister v1.NodeLister, namespace string, name string) (interface{}, error) { node, err := nodesLister.Get(name) if err != nil { klog.Error("failed to get node (%s/%s) from indexer: %v", namespace, name, err) return nil, err } return node, nil } func (c *cluster) ListIndexerResources(ctx context.Context, cluster string, resource string, namespace string, listOption types.ListOptions) (interface{}, error) { // 获取客户端缓存 cs, err := c.GetClusterSetByName(ctx, cluster) if err != nil { return nil, err } // lister functions should be registered in NewCluster function fn, ok := c.listerFuncs[resource] if !ok { return nil, fmt.Errorf("unsupported resource type %s", resource) } if namespace == "all_namespaces" { namespace = "" } return fn(ctx, cs.Informer, namespace, listOption) } func (c *cluster) ListPods(ctx context.Context, podsLister v1.PodLister, namespace string, listOption types.ListOptions) (interface{}, error) { pods, err := podsLister.Pods(namespace).List(labels.Everything()) if err != nil { return nil, err } // 构造通用的 objects objects := make([]metav1.Object, 0) for _, pod := range pods { objects = append(objects, pod) } return c.listObjects(objects, namespace, listOption) } // ListDeployments 从缓存中获取 deployment 列表 func (c *cluster) ListDeployments(ctx context.Context, deploymentsLister listersv1.DeploymentLister, namespace string, listOption types.ListOptions) (interface{}, error) { deployments, err := deploymentsLister.Deployments(namespace).List(labels.Everything()) if err != nil { return nil, err } objects := make([]metav1.Object, 0) for _, deployment := range deployments { objects = append(objects, deployment) } return c.listObjects(objects, namespace, listOption) } func (c *cluster) ListStatefulSets(ctx context.Context, statefulSetsLister listersv1.StatefulSetLister, namespace string, listOption types.ListOptions) (interface{}, error) { statefulSets, err := statefulSetsLister.StatefulSets(namespace).List(labels.Everything()) if err != nil { return nil, err } objects := make([]metav1.Object, 0) for _, statefulSet := range statefulSets { objects = append(objects, statefulSet) } return c.listObjects(objects, namespace, listOption) } func (c *cluster) ListDaemonSets(ctx context.Context, daemonSetsLister listersv1.DaemonSetLister, namespace string, listOption types.ListOptions) (interface{}, error) { daemonSets, err := daemonSetsLister.DaemonSets(namespace).List(labels.Everything()) if err != nil { return nil, err } objects := make([]metav1.Object, 0) for _, daemonSet := range daemonSets { objects = append(objects, daemonSet) } return c.listObjects(objects, namespace, listOption) } func (c *cluster) ListCronJobs(ctx context.Context, cronJobsLister listersbatchv1.CronJobLister, namespace string, listOption types.ListOptions) (interface{}, error) { cronJobs, err := cronJobsLister.CronJobs(namespace).List(labels.Everything()) if err != nil { return nil, err } objects := make([]metav1.Object, 0) for _, cronJob := range cronJobs { objects = append(objects, cronJob) } return c.listObjects(objects, namespace, listOption) } func (c *cluster) ListJobs(ctx context.Context, jobsLister listersbatchv1.JobLister, namespace string, listOption types.ListOptions) (interface{}, error) { jobs, err := jobsLister.Jobs(namespace).List(labels.Everything()) if err != nil { return nil, err } objects := make([]metav1.Object, 0) for _, job := range jobs { objects = append(objects, job) } return c.listObjects(objects, namespace, listOption) } func (c *cluster) ListNodes(ctx context.Context, nodesLister v1.NodeLister, namespace string, listOption types.ListOptions) (interface{}, error) { nodes, err := nodesLister.List(labels.Everything()) if err != nil { return nil, err } objects := make([]metav1.Object, 0) for _, node := range nodes { objects = append(objects, node) } return c.listObjects(objects, namespace, listOption) } ================================================ FILE: pkg/controller/cluster/util.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cluster import ( "sort" "strings" "github.com/caoyingjunz/pixiu/pkg/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" ) func (c *cluster) forQuery(objects []metav1.Object, queryOption types.QueryOption) []metav1.Object { if len(queryOption.LabelSelector) == 0 && len(queryOption.NameSelector) == 0 { return objects } queryObjects := make([]metav1.Object, 0) for _, object := range objects { // 标签搜索 // TODO: 多个标签存在时,存在乱序时无法生效 // 名称搜索 if (len(queryOption.LabelSelector) != 0 && strings.Contains(labels.FormatLabels(object.GetLabels()), queryOption.LabelSelector)) || (len(queryOption.NameSelector) != 0 && strings.Contains(object.GetName(), queryOption.NameSelector)) { queryObjects = append(queryObjects, object) } } return queryObjects } func (c *cluster) forPage(objects []metav1.Object, pageOption types.PageRequest) []metav1.Object { if !pageOption.IsPaged() { return objects } offset, end, err := pageOption.Offset(len(objects)) if err != nil { return nil } return objects[offset:end] } func (c *cluster) forSorted(objects []metav1.Object, namespace string) []metav1.Object { sort.SliceStable(objects, func(i, j int) bool { return objects[i].GetName() < objects[j].GetName() }) // 全量获取 pod 时,以命名空间排序 if len(namespace) == 0 { sort.SliceStable(objects, func(i, j int) bool { return objects[i].GetNamespace() < objects[j].GetNamespace() }) } return objects } func (c *cluster) listObjects(objects []metav1.Object, namespace string, listOption types.ListOptions) (types.PageResponse, error) { objects = c.forQuery(objects, listOption.QueryOption) objects = c.forSorted(objects, namespace) return types.PageResponse{ PageRequest: listOption.PageRequest, Total: len(objects), Items: c.forPage(objects, listOption.PageRequest), }, nil } ================================================ FILE: pkg/controller/cluster/ws.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cluster import ( "bytes" "context" "net/http" "sync" "time" "github.com/gorilla/websocket" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/remotecommand" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/pkg/types" sshutil "github.com/caoyingjunz/pixiu/pkg/util/ssh" ) func (c *cluster) WsHandler(ctx context.Context, opt *types.WebShellOptions, w http.ResponseWriter, r *http.Request) error { cs, err := c.GetClusterSetByName(ctx, opt.Cluster) if err != nil { klog.Errorf("failed to get cluster(%s) client set: %v", opt.Cluster, err) return err } session, err := types.NewTerminalSession(w, r) if err != nil { return err } // 处理关闭 defer func() { _ = session.Close() }() klog.Infof("connecting to %s/%s,", opt.Namespace, opt.Pod) cmd := opt.Command if len(cmd) == 0 { cmd = "/bin/bash" } // 组装 POST 请求 req := cs.Client.CoreV1().RESTClient().Post(). Resource("pods"). Name(opt.Pod). Namespace(opt.Namespace). SubResource("exec"). VersionedParams(&v1.PodExecOptions{ Container: opt.Container, Command: []string{cmd}, Stderr: true, Stdin: true, Stdout: true, TTY: true, }, scheme.ParameterCodec) // remotecommand 主要实现了http 转 SPDY 添加X-Stream-Protocol-Version相关header 并发送请求 executor, err := remotecommand.NewSPDYExecutor(cs.Config, "POST", req.URL()) if err != nil { return err } // 与 kubelet 建立 stream 连接 if err = executor.Stream(remotecommand.StreamOptions{ Stdout: session, Stdin: session, Stderr: session, TerminalSizeQueue: session, Tty: true, }); err != nil { _, _ = session.Write([]byte("exec pod command failed," + err.Error())) // 标记关闭terminal session.Done() } return nil } var BufPool = sync.Pool{New: func() interface{} { return new(bytes.Buffer) }} func (c *cluster) WsNodeHandler(ctx context.Context, sshConfig *types.WebSSHRequest, w http.ResponseWriter, r *http.Request) error { upgrader := &websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024 * 10, HandshakeTimeout: time.Second * 2, CheckOrigin: func(r *http.Request) bool { return true }, Subprotocols: []string{r.Header.Get("Sec-WebSocket-Protocol")}, } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return err } defer conn.Close() sshClient, err := sshutil.NewSSHClient(sshConfig) if err != nil { return err } defer sshClient.Close() turn, err := types.NewTurn(conn, sshClient) if err != nil { return err } defer turn.Close() // 处理连接 handler(turn) return nil } func handler(turn *types.Turn) { logBuff := BufPool.Get().(*bytes.Buffer) logBuff.Reset() defer BufPool.Put(logBuff) wg := &sync.WaitGroup{} wg.Add(2) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go turn.StartLoopRead(ctx, wg, logBuff) go turn.StartSessionWait(wg) wg.Wait() } ================================================ FILE: pkg/controller/controller.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "github.com/casbin/casbin/v2" "github.com/caoyingjunz/pixiu/cmd/app/config" "github.com/caoyingjunz/pixiu/pkg/controller/audit" "github.com/caoyingjunz/pixiu/pkg/controller/auth" "github.com/caoyingjunz/pixiu/pkg/controller/cluster" "github.com/caoyingjunz/pixiu/pkg/controller/helm" "github.com/caoyingjunz/pixiu/pkg/controller/plan" "github.com/caoyingjunz/pixiu/pkg/controller/tenant" "github.com/caoyingjunz/pixiu/pkg/controller/user" "github.com/caoyingjunz/pixiu/pkg/db" ) type PixiuInterface interface { cluster.ClusterGetter tenant.TenantGetter user.UserGetter plan.PlanGetter audit.AuditGetter auth.AuthGetter helm.HelmGetter } type pixiu struct { cc config.Config factory db.ShareDaoFactory enforcer *casbin.SyncedEnforcer } func (p *pixiu) Cluster() cluster.Interface { return cluster.NewCluster(p.cc, p.factory, p.enforcer) } func (p *pixiu) Tenant() tenant.Interface { return tenant.NewTenant(p.cc, p.factory) } func (p *pixiu) User() user.Interface { return user.NewUser(p.cc, p.factory, p.enforcer) } func (p *pixiu) Plan() plan.Interface { return plan.NewPlan(p.cc, p.factory) } func (p *pixiu) Audit() audit.Interface { return audit.NewAudit(p.cc, p.factory) } func (p *pixiu) Auth() auth.Interface { return auth.NewAuth(p.factory, p.enforcer) } func (p *pixiu) Helm() helm.Interface { return helm.NewHelm(p.factory) } func New(cfg config.Config, f db.ShareDaoFactory, enforcer *casbin.SyncedEnforcer) PixiuInterface { return &pixiu{ cc: cfg, factory: f, enforcer: enforcer, } } ================================================ FILE: pkg/controller/helm/helm.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helm import ( "context" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/cli" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/pkg/client" "github.com/caoyingjunz/pixiu/pkg/controller/cluster" "github.com/caoyingjunz/pixiu/pkg/db" ) type HelmGetter interface { Helm() Interface } type Interface interface { Release(cluster, namespace string) ReleaseInterface Repository() RepositoryInterface } type Helm struct { factory db.ShareDaoFactory } func (h *Helm) Release(cluster, namespace string) ReleaseInterface { cs := h.MustGetClusterSetByName(context.Background(), cluster) settings := cli.New() settings.SetNamespace(namespace) actionConfig := new(action.Configuration) resetClientGetter := client.NewHelmRESTClientGetter(cs.Config) actionConfig.Init( resetClientGetter, settings.Namespace(), "secrets", klog.Infof, ) return NewReleases(actionConfig, settings) } func (h *Helm) Repository() RepositoryInterface { return NewRepository(h.factory) } func NewHelm(factory db.ShareDaoFactory) Interface { return &Helm{ factory: factory, } } func (h *Helm) MustGetClusterSetByName(ctx context.Context, name string) client.ClusterSet { cs, ok := cluster.ClusterIndexer.Get(name) if ok { klog.Infof("Get %s clusterSet from indexer", name) return cs } klog.Infof("building clusterSet for %s", name) // 缓存中不存在,则新建并重写回缓存 object, err := h.factory.Cluster().GetClusterByName(ctx, name) if err != nil { return client.ClusterSet{} } if object == nil { return client.ClusterSet{} } newClusterSet, err := client.NewClusterSet(object.KubeConfig) if err != nil { return client.ClusterSet{} } klog.Infof("set %s clusterSet into indexer", name) cluster.ClusterIndexer.Set(name, *newClusterSet) return *newClusterSet } ================================================ FILE: pkg/controller/helm/releases.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helm import ( "context" "fmt" "io" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/release" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/pkg/types" ) type ReleaseInterface interface { Install(ctx context.Context, form *types.Release) (*release.Release, error) Get(ctx context.Context, name string) (*release.Release, error) List(ctx context.Context) ([]*release.Release, error) Uninstall(ctx context.Context, name string) (*release.UninstallReleaseResponse, error) Upgrade(ctx context.Context, form *types.Release) (*release.Release, error) History(ctx context.Context, name string) ([]*release.Release, error) Rollback(ctx context.Context, name string, toVersion int) error } type Releases struct { settings *cli.EnvSettings actionConfig *action.Configuration } func NewReleases(actionConfig *action.Configuration, settings *cli.EnvSettings) *Releases { return &Releases{ actionConfig: actionConfig, settings: settings, } } var _ ReleaseInterface = &Releases{} func (r *Releases) Get(ctx context.Context, name string) (*release.Release, error) { client := action.NewGet(r.actionConfig) return client.Run(name) } func (r *Releases) List(ctx context.Context) ([]*release.Release, error) { client := action.NewList(r.actionConfig) return client.Run() } // InstallRelease install release func (r *Releases) Install(ctx context.Context, form *types.Release) (*release.Release, error) { client := action.NewInstall(r.actionConfig) client.ReleaseName = form.Name client.Namespace = r.settings.Namespace() client.Version = form.Version client.DryRun = form.Preview if client.DryRun { client.Description = "server" } chart, err := r.locateChart(client.ChartPathOptions, form.Chart, r.settings) if err != nil { return nil, err } out, err := client.Run(chart, form.Values) if err != nil { return nil, err } return out, nil } func (r *Releases) Uninstall(ctx context.Context, name string) (*release.UninstallReleaseResponse, error) { client := action.NewUninstall(r.actionConfig) return client.Run(name) } // UpgradeRelease upgrade release func (r *Releases) Upgrade(ctx context.Context, form *types.Release) (*release.Release, error) { client := action.NewUpgrade(r.actionConfig) client.Namespace = r.settings.Namespace() client.DryRun = form.Preview if client.DryRun { client.Description = "server" } chart, err := r.locateChart(client.ChartPathOptions, form.Chart, r.settings) if err != nil { return nil, err } out, err := client.Run(form.Name, chart, form.Values) if err != nil { return nil, err } return out, nil } func (r *Releases) History(ctx context.Context, name string) ([]*release.Release, error) { client := action.NewHistory(r.actionConfig) return client.Run(name) } func (r *Releases) Rollback(ctx context.Context, name string, toVersion int) error { klog.Error("version: ", toVersion) _, err := r.Get(ctx, name) if err != nil { return err } client := action.NewRollback(r.actionConfig) client.Version = toVersion return client.Run(name) } func (r *Releases) locateChart(pathOpts action.ChartPathOptions, chart string, settings *cli.EnvSettings) (*chart.Chart, error) { // from cmd/helm/install.go and cmd/helm/upgrade.go cp, err := pathOpts.LocateChart(chart, settings) if err != nil { return nil, err } p := getter.All(settings) // Check chart dependencies to make sure all are present in /charts chartRequested, err := loader.Load(cp) if err != nil { return nil, err } if err := checkIfInstallable(chartRequested); err != nil { return nil, err } registryClient, err := registry.NewClient( registry.ClientOptDebug(false), //registry.ClientOptWriter(out), registry.ClientOptCredentialsFile(settings.RegistryConfig), ) if err != nil { return nil, fmt.Errorf("failed to crete helm config object %v", err) } if req := chartRequested.Metadata.Dependencies; req != nil { // If CheckDependencies returns an error, we have unfulfilled dependencies. // As of Helm 2.4.0, this is treated as a stopping condition: // https://github.com/helm/helm/issues/2209 if err := action.CheckDependencies(chartRequested, req); err != nil { err = fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: %v", err) if true { // client.DependencyUpdate man := &downloader.Manager{ Out: io.Discard, ChartPath: cp, Keyring: pathOpts.Keyring, SkipUpdate: false, Getters: p, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, Debug: settings.Debug, RegistryClient: registryClient, // added on top of Helm code } if err := man.Update(); err != nil { return nil, err } // Reload the chart with the updated Chart.lock file. if chartRequested, err = loader.Load(cp); err != nil { return nil, fmt.Errorf("failed reloading chart after repo update : %v", err) } } else { return nil, err } } } return chartRequested, nil } func checkIfInstallable(ch *chart.Chart) error { switch ch.Metadata.Type { case "", "application": return nil } return fmt.Errorf("%s charts are not installable", ch.Metadata.Type) } ================================================ FILE: pkg/controller/helm/repository.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package helm import ( "context" "fmt" "net/url" "strings" "k8s.io/klog/v2" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/repo" "k8s.io/apimachinery/pkg/util/yaml" "github.com/caoyingjunz/pixiu/pkg/db" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" ) type RepositoryGetter interface { Repository() RepositoryInterface } type RepositoryInterface interface { Create(ctx context.Context, repo *types.CreateRepository) error Delete(ctx context.Context, id int64) error Get(ctx context.Context, id int64) (*model.Repository, error) List(ctx context.Context) ([]*model.Repository, error) Update(ctx context.Context, id int64, update *types.UpdateRepository) error GetChartsById(ctx context.Context, id int64) (*model.ChartIndex, error) GetChartsByURL(ctx context.Context, repoURL string) (*model.ChartIndex, error) GetChartValues(ctx context.Context, chart, version string) (string, error) } type Repository struct { settings *cli.EnvSettings actionConfig *action.Configuration factory db.ShareDaoFactory } func NewRepository(f db.ShareDaoFactory) *Repository { settings := cli.New() actionConfig := new(action.Configuration) actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), "secrets", klog.Infof) return &Repository{factory: f, settings: settings, actionConfig: actionConfig} } var _ RepositoryInterface = &Repository{} func (r *Repository) Create(ctx context.Context, repo *types.CreateRepository) error { repoModel := &model.Repository{ Name: repo.Name, URL: repo.URL, } if res, _ := r.GetByName(ctx, repoModel.Name); res != nil { return fmt.Errorf("repository %s already exists", repoModel.Name) } _, err := r.factory.Repository().Create(ctx, repoModel) return err } func (r *Repository) Delete(ctx context.Context, id int64) error { return r.factory.Repository().Delete(ctx, id) } func (r *Repository) Get(ctx context.Context, id int64) (*model.Repository, error) { return r.factory.Repository().Get(ctx, id) } func (r *Repository) GetByName(ctx context.Context, name string) (*model.Repository, error) { return r.factory.Repository().GetByName(ctx, name) } func (r *Repository) List(ctx context.Context) ([]*model.Repository, error) { return r.factory.Repository().List(ctx) } func (r *Repository) Update(ctx context.Context, id int64, update *types.UpdateRepository) error { updates := map[string]interface{}{ "name": update.Name, "url": update.URL, "username": update.Username, "password": update.Password, } return r.factory.Repository().Update(ctx, id, *update.ResourceVersion, updates) } func (r *Repository) GetChartsById(ctx context.Context, id int64) (*model.ChartIndex, error) { repository, err := r.Get(ctx, id) if err != nil { return nil, err } entry := &repo.Entry{ Name: repository.Name, URL: repository.URL, Username: repository.Username, Password: repository.Password, } return r.fetch(ctx, entry) } func (r *Repository) GetChartsByURL(ctx context.Context, repoURL string) (*model.ChartIndex, error) { entry := &repo.Entry{ URL: repoURL, } return r.fetch(ctx, entry) } func (r *Repository) GetChartValues(_ context.Context, chart, version string) (string, error) { client := action.NewShowWithConfig(action.ShowValues, r.actionConfig) client.Version = version cp, err := client.ChartPathOptions.LocateChart(chart, r.settings) if err != nil { return "", err } out, err := client.Run(cp) if err != nil { return "", err } return out, nil } func (r *Repository) resolveReferenceURL(baseURL, refURL string) (string, error) { parsedRefURL, err := url.Parse(refURL) if err != nil { return "", fmt.Errorf("failed to parse %s as URL, %v", refURL, err) } if parsedRefURL.IsAbs() { return refURL, nil } parsedBaseURL, err := url.Parse(baseURL) if err != nil { return "", fmt.Errorf("failed to parse %s as URL, %v", baseURL, err) } parsedBaseURL.RawPath = strings.TrimSuffix(parsedBaseURL.RawPath, "/") + "/" parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/" resolvedURL := parsedBaseURL.ResolveReference(parsedRefURL) resolvedURL.RawQuery = parsedBaseURL.RawQuery return resolvedURL.String(), nil } func (r *Repository) fetch(_ context.Context, entry *repo.Entry) (*model.ChartIndex, error) { var charts model.ChartIndex rep, err := repo.NewChartRepository(entry, getter.All(r.settings)) if err != nil { return nil, err } // download index if _, err = rep.DownloadIndexFile(); err != nil { return nil, err } indexURL, err := r.resolveReferenceURL(rep.Config.URL, "index.yaml") if err != nil { return nil, err } resp, err := rep.Client.Get(indexURL, getter.WithURL(rep.Config.URL), getter.WithInsecureSkipVerifyTLS(rep.Config.InsecureSkipTLSverify), getter.WithTLSClientConfig(rep.Config.CertFile, rep.Config.KeyFile, rep.Config.CAFile), getter.WithBasicAuth(rep.Config.Username, rep.Config.Password), getter.WithPassCredentialsAll(rep.Config.PassCredentialsAll), ) if err != nil { return nil, err } err = yaml.Unmarshal(resp.Bytes(), &charts) if err != nil { return nil, err } return &charts, nil } ================================================ FILE: pkg/controller/plan/bootstrap_servers.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "context" "time" "github.com/caoyingjunz/pixiu/pkg/util/container" ) type BootStrap struct { handlerTask dir string runner string } func (b BootStrap) Name() string { return "初始化部署环境" } // Run 以容器的形式执行 BootStrap 任务,如果存在旧的容器,则先删除在执行 func (b BootStrap) Run() error { cli, err := container.NewContainer("bootstrap-servers", b.GetPlanId(), b.dir) if err != nil { return err } defer cli.Close() ctx, cancel := context.WithTimeout(context.Background(), 600*time.Second) defer cancel() // 启动执行容器 if err = cli.StartAndWaitForContainer(ctx, b.runner); err != nil { return err } return nil } ================================================ FILE: pkg/controller/plan/checker.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan type Check struct { handlerTask } func (c Check) Name() string { return "部署预检查" } func (c Check) Run() error { if err := c.data.validate(); err != nil { return err } return nil } ================================================ FILE: pkg/controller/plan/deploy.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "context" "time" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/util/container" ) type Deploy struct { handlerTask dir string runner string } func (b Deploy) Name() string { return "部署Master" } // Run 以容器的形式执行 BootStrap 任务,如果存在旧的容器,则先删除在执行 func (b Deploy) Run() error { cli, err := container.NewContainer("deploy", b.GetPlanId(), b.dir) if err != nil { return err } defer cli.Close() ctx, cancel := context.WithTimeout(context.Background(), 600*time.Second) defer cancel() // 启动执行容器 if err = cli.StartAndWaitForContainer(ctx, b.runner); err != nil { return err } return nil } type DeployNode struct { handlerTask } func (b DeployNode) Name() string { return "部署Node" } // Run 以容器的形式执行 BootStrap 任务,如果存在旧的容器,则先删除在执行 func (b DeployNode) Run() error { return nil } type DeployChart struct { handlerTask } func (b DeployChart) Name() string { return "部署基础组件" } func (b DeployChart) Step() model.PlanStep { return model.CompletedPlanStep } // Run 以容器的形式执行 BootStrap 任务,如果存在旧的容器,则先删除在执行 func (b DeployChart) Run() error { return nil } ================================================ FILE: pkg/controller/plan/plan.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "context" "encoding/json" "fmt" "net/http" "sync" "gorm.io/gorm" "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/api/server/errors" "github.com/caoyingjunz/pixiu/cmd/app/config" "github.com/caoyingjunz/pixiu/pkg/client" "github.com/caoyingjunz/pixiu/pkg/db" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" "github.com/caoyingjunz/pixiu/pkg/util/uuid" ) type PlanGetter interface { Plan() Interface } type Interface interface { Create(ctx context.Context, req *types.CreatePlanRequest) error Update(ctx context.Context, planID int64, req *types.UpdatePlanRequest) error Delete(ctx context.Context, pid int64) error Get(ctx context.Context, pid int64) (*types.Plan, error) List(ctx context.Context, req *types.ListPlanRequest) (*types.PageResponse, error) GetWithSubResources(ctx context.Context, planId int64) (*types.Plan, error) // Start 启动部署任务 Start(ctx context.Context, pid int64) error // Stop 终止部署任务 Stop(ctx context.Context, pid int64) error CreateNode(ctx context.Context, pid int64, req *types.CreatePlanNodeRequest) error UpdateNode(ctx context.Context, pid int64, nodeId int64, req *types.UpdatePlanNodeRequest) error DeleteNode(ctx context.Context, pid int64, nodeId int64) error GetNode(ctx context.Context, pid int64, nodeId int64) (*types.PlanNode, error) ListNodes(ctx context.Context, pid int64) ([]types.PlanNode, error) CreateConfig(ctx context.Context, planId int64, req *types.CreatePlanConfigRequest) error UpdateConfig(ctx context.Context, pid int64, cfgId int64, req *types.UpdatePlanConfigRequest) error DeleteConfig(ctx context.Context, pid int64, cfgId int64) error GetConfig(ctx context.Context, planId int64) (*types.PlanConfig, error) // Run 启动 plan worker 处理协程 Run(ctx context.Context, workers int) error RunTask(ctx context.Context, planId int64, taskId int64) error ListTasks(ctx context.Context, planId int64) ([]types.PlanTask, error) WatchTasks(ctx context.Context, planId int64, w http.ResponseWriter, r *http.Request) WatchTaskLog(ctx context.Context, planId int64, taskId int64, w http.ResponseWriter, r *http.Request) error } var taskQueue workqueue.RateLimitingInterface var taskC *client.Task func init() { taskQueue = workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "tasks") taskC = client.NewTaskCache() } type plan struct { cc config.Config factory db.ShareDaoFactory } // Create // 1. 创建部署计划 // 2. 创建部署配置 // 3. 创建节点列表 // 4. 创建扩展组件 // 5. 创建容器服务 func (p *plan) Create(ctx context.Context, req *types.CreatePlanRequest) error { planModel := &model.Plan{ Name: req.Name, Description: req.Description, } createdPlan, err := p.factory.Plan().Create(ctx, planModel, p.createPlanSubResources(ctx, req)) if err != nil { klog.Errorf("failed to create plan %s: %v", req.Name, err) return errors.ErrServerInternal } planId := createdPlan.Id // 如果启用pixiu注册功能,则创建容器服务 kubeNode := types.KubeNode{Ready: []string{}, NotReady: []string{}} nodes, _ := kubeNode.Marshal() _, err = p.factory.Cluster().Create(ctx, &model.Cluster{ Name: uuid.NewRandName(8), AliasName: req.Name, Description: req.Description, ClusterType: model.ClusterTypeCustom, PlanId: planId, Protected: true, Nodes: nodes, }) if err != nil { klog.Errorf("failed to register cluster for plan: %v", err) _ = p.Delete(ctx, planId) return errors.ErrServerInternal } return nil } func (p *plan) createPlanSubResources(ctx context.Context, req *types.CreatePlanRequest) db.CreatePlanOption { return func(planModel *model.Plan, tx *gorm.DB) (*gorm.DB, error) { if err := p.preCreateConfig(ctx, planModel.Id, &req.Config); err != nil { return nil, err } planConfig, err := p.buildPlanConfig(ctx, &req.Config) if err != nil { return nil, err } planConfig.PlanId = planModel.Id if err := p.factory.Plan().TxCreateConfig(ctx, tx, planConfig); err != nil { klog.Errorf("failed to create plan(%d) config: %v", planModel.Id, err) return nil, err } for i := range req.Nodes { nodeReq := &req.Nodes[i] node, err := p.buildNodeFromRequest(planModel.Id, nodeReq) if err != nil { klog.Errorf("failed to build plan(%d) node from request: %v", planModel.Id, err) return nil, err } if err := p.factory.Plan().TxCreateNode(ctx, tx, node); err != nil { klog.Errorf("failed to create node(%s): %v", nodeReq.Name, err) return nil, err } } return tx, nil } } // Update // 更新部署计划 func (p *plan) Update(ctx context.Context, planId int64, req *types.UpdatePlanRequest) error { oldPlan, err := p.factory.Plan().Get(ctx, planId) if err != nil { klog.Errorf("failed to get plan(%d) %v", planId, err) return errors.ErrServerInternal } // 必要时更新 plan if oldPlan.Description != req.Description { if err := p.factory.Plan().Update(ctx, planId, *req.ResourceVersion, map[string]interface{}{"description": req.Description}); err != nil { klog.Errorf("failed to update plan %d: %v", planId, err) return errors.ErrServerInternal } } // 必要时更新部署计划配置 if err = p.UpdateConfigIfNeeded(ctx, planId, req); err != nil { klog.Errorf("failed to update plan(%d) config: %v", planId, err) return errors.ErrServerInternal } // 必要时更新部署计划 nodes if err = p.updateNodesIfNeeded(ctx, planId, req); err != nil { klog.Errorf("failed to update plan(%d) nodes: %v", planId, err) return errors.ErrServerInternal } return nil } // 删除前检查 // 有正在运行中的任务则不允许删除 func (p *plan) preDelete(ctx context.Context, planId int64) error { isRunning, err := p.TaskIsRunning(ctx, planId) if err != nil { return errors.ErrServerInternal } if isRunning { return errors.ErrNotAcceptable } return nil } // Delete // 1. 删除部署计划 // 2. 删除关联任务 // 3. 删除关联配置 // 4. 删除关联节点 func (p *plan) Delete(ctx context.Context, planId int64) error { // 删除前校验 if err := p.preDelete(ctx, planId); err != nil { return err } // 执行实际的删除逻辑 _, err := p.factory.Plan().Delete(ctx, planId) if err != nil { klog.Errorf("failed to delete plan %d: %v", planId, err) return errors.ErrServerInternal } // 删除 plan 关联资源 // 2. 删除部署计划后,同步删除任务,删除任务失败时,可直接忽略 if err = p.factory.Plan().DeleteTask(ctx, planId); err != nil { klog.Errorf("failed to delete plan(%d) task: %v", planId, err) return err } // 3. 删除关联配置 if err = p.factory.Plan().DeleteConfigByPlan(ctx, planId); err != nil { klog.Errorf("failed to delete plan(%d) config: %v", planId, err) return err } // 4. 删除关联nodes if err = p.factory.Plan().DeleteNodesByPlan(ctx, planId); err != nil { klog.Errorf("failed to delete plan(%d) nodes: %v", planId, err) return err } return nil } func (p *plan) Get(ctx context.Context, pid int64) (*types.Plan, error) { object, err := p.factory.Plan().Get(ctx, pid) if err != nil { klog.Errorf("failed to get plan %d: %v", pid, err) return nil, errors.ErrServerInternal } return p.model2Type(object) } // GetWithSubResources // 获取 plan // 获取 configs // 获取 nodes func (p *plan) GetWithSubResources(ctx context.Context, planId int64) (*types.Plan, error) { result, err := p.Get(ctx, planId) if err != nil { return nil, err } // 追加配置 cfg, err := p.GetConfig(ctx, planId) if err != nil { return nil, err } result.Config = *cfg // 追加节点 result.Nodes, err = p.ListNodes(ctx, planId) if err != nil { return nil, err } return result, nil } func (p *plan) List(ctx context.Context, req *types.ListPlanRequest) (*types.PageResponse, error) { var opts []db.Options if req != nil && req.NameSelector != "" { opts = append(opts, db.WithPlanNameLike(req.NameSelector)) } total, err := p.factory.Plan().Count(ctx, opts...) if err != nil { klog.Errorf("failed to count plans: %v", err) return nil, errors.ErrServerInternal } pageReq := types.PageRequest{} if req != nil { pageReq = req.PageRequest if req.Page > 0 && req.Limit > 0 { opts = append(opts, db.WithOffset((req.Page-1)*req.Limit), db.WithLimit(req.Limit)) } } objects, err := p.factory.Plan().List(ctx, opts...) if err != nil { klog.Errorf("failed to get plans: %v", err) return nil, errors.ErrServerInternal } ps := make([]types.Plan, 0, len(objects)) for _, object := range objects { no, err := p.model2Type(&object) if err != nil { return nil, err } // 状态过滤(在内存中过滤,因为状态来自关联表) if req != nil && req.Step != "" && string(no.Step) != req.Step { continue } ps = append(ps, *no) } return &types.PageResponse{ PageRequest: pageReq, Total: int(total), Items: ps, }, nil } // listAll 内部使用,返回所有计划(不带分页和过滤) func (p *plan) listAll(ctx context.Context) ([]types.Plan, error) { objects, err := p.factory.Plan().List(ctx) if err != nil { klog.Errorf("failed to get plans: %v", err) return nil, errors.ErrServerInternal } var ps []types.Plan for _, object := range objects { no, err := p.model2Type(&object) if err != nil { return nil, err } ps = append(ps, *no) } return ps, nil } func (p *plan) SyncTaskStatus(ctx context.Context) error { plans, err := p.listAll(ctx) if err != nil { return err } var wg sync.WaitGroup errChan := make(chan error, len(plans)) for _, planP := range plans { wg.Add(1) go func(planId int64) { defer wg.Done() if err = p.syncStatus(ctx, planId); err != nil { errChan <- err } }(planP.Id) } wg.Wait() select { case err := <-errChan: return err default: } return nil } // 启动前校验 // 1. 配置 // 2. 节点 // 3. 校验runner // 3. 运行任务 func (p *plan) preStart(ctx context.Context, pid int64) error { // 1. 校验配置 cfg, err := p.GetConfig(ctx, pid) if err != nil { return fmt.Errorf("failed to get plan(%d) config %v", pid, err) } // TODO: 根据具体情况对参数 // 2. 校验节点 nodes, err := p.ListNodes(ctx, pid) if err != nil { return fmt.Errorf("failed to get plan(%d) nodes %v", pid, err) } if len(nodes) == 0 { return fmt.Errorf("部署计划暂无关联节点") } // 3. 校验runner runner, err := p.GetRunner(cfg.OSImage) if err != nil { return err } klog.Infof("plan(%d) runner is %s", pid, runner) // 4. 校验运行任务 isRunning, err := p.TaskIsRunning(ctx, pid) if err != nil { return errors.ErrServerInternal } if isRunning { return errors.ErrNotAcceptable } return nil } // TaskIsRunning // 校验是否有任务正在运行 func (p *plan) TaskIsRunning(ctx context.Context, planId int64) (bool, error) { tasks, err := p.factory.Plan().ListTasks(ctx, planId) if err != nil { klog.Errorf("failed to get tasks of plan %d: %v", planId, err) return false, errors.ErrServerInternal } for _, task := range tasks { if task.Status == model.RunningPlanStatus { klog.Warningf("task %d of plan %d is running", task.Id, planId) return true, nil } } return false, nil } func (p *plan) Start(ctx context.Context, pid int64) error { // 启动前校验 if err := p.preStart(ctx, pid); err != nil { return err } taskQueue.Add(pid) return nil } func (p *plan) Stop(ctx context.Context, pid int64) error { return nil } func (p *plan) model2Type(o *model.Plan) (*types.Plan, error) { status := model.SuccessPlanStatus // 尝试获取最新的任务状态 // 获取失败也不中断返回 if tasks, err := p.factory.Plan().ListTasks(context.TODO(), o.Id); err == nil { if len(tasks) == 0 { status = model.UnStartPlanStatus } else { for _, task := range tasks { if task.Status != model.SuccessPlanStatus { status = task.Status break } } } } // 获取 kubernetes 版本,获取失败不中断 kubernetesVersion := "" if cfg, err := p.factory.Plan().GetConfigByPlan(context.TODO(), o.Id); err == nil && cfg != nil { var kubeSpec struct { KubernetesVersion string `json:"kubernetes_version"` } if err := json.Unmarshal([]byte(cfg.Kubernetes), &kubeSpec); err == nil { kubernetesVersion = kubeSpec.KubernetesVersion } } // 获取节点总数,获取失败不中断 nodeCount := 0 if nodes, err := p.factory.Plan().ListNodes(context.TODO(), o.Id); err == nil { nodeCount = len(nodes) } return &types.Plan{ PixiuMeta: types.PixiuMeta{ Id: o.Id, ResourceVersion: o.ResourceVersion, }, TimeMeta: types.TimeMeta{ GmtCreate: o.GmtCreate, GmtModified: o.GmtModified, }, Name: o.Name, Description: o.Description, Step: status, KubernetesVersion: kubernetesVersion, NodeCount: nodeCount, }, nil } func NewPlan(cfg config.Config, f db.ShareDaoFactory) *plan { return &plan{ cc: cfg, factory: f, } } ================================================ FILE: pkg/controller/plan/plan_config.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "context" "fmt" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/api/server/errors" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" ) func (p *plan) preCreateConfig(ctx context.Context, planId int64, req *types.CreatePlanConfigRequest) error { _, err := p.factory.Plan().GetConfigByPlan(ctx, planId) if err == nil { return fmt.Errorf("plan(%d) 配置已存在", planId) } return nil } func (p *plan) CreateConfig(ctx context.Context, pid int64, req *types.CreatePlanConfigRequest) error { // 创建前检查 if err := p.preCreateConfig(ctx, pid, req); err != nil { return err } planConfig, err := p.buildPlanConfig(ctx, req) if err != nil { return err } planConfig.PlanId = pid // 创建配置 if _, err = p.factory.Plan().CreateConfig(ctx, planConfig); err != nil { klog.Errorf("failed to create plan(%s) config: %v", pid, err) return err } return nil } // UpdateConfig // TODO func (p *plan) UpdateConfig(ctx context.Context, pid int64, cfgId int64, req *types.UpdatePlanConfigRequest) error { return nil } // UpdateConfigIfNeeded // 更新部署计划配置 func (p *plan) UpdateConfigIfNeeded(ctx context.Context, planId int64, req *types.UpdatePlanRequest) error { oldConfig, err := p.factory.Plan().GetConfigByPlan(ctx, planId) if err != nil { return errors.ErrServerInternal } newConfig := req.Config updates := make(map[string]interface{}) if oldConfig.Region != newConfig.Region { updates["region"] = newConfig.Region } if oldConfig.OSImage != newConfig.OSImage { updates["os_image"] = newConfig.OSImage } newKubernetes, err := p.buildAndCleanKubernetesConfig(newConfig.Kubernetes) if err != nil { return err } if oldConfig.Kubernetes != newKubernetes { updates["kubernetes"] = newKubernetes } newNetwork, err := newConfig.Network.Marshal() if err != nil { return err } if oldConfig.Network != newNetwork { updates["network"] = newNetwork } newRuntime, err := newConfig.Runtime.Marshal() if err != nil { return err } if oldConfig.Runtime != newRuntime { updates["runtime"] = newRuntime } newComponent, err := newConfig.Component.Marshal() if err != nil { return err } if oldConfig.Component != newComponent { updates["component"] = newComponent } // 没有更新,则直接返回 if len(updates) == 0 { return nil } if err = p.factory.Plan().UpdateConfig(ctx, oldConfig.Id, oldConfig.ResourceVersion, updates); err != nil { klog.Errorf("failed to update plan(%d) config: %v", planId, err) return errors.ErrServerInternal } return nil } func (p *plan) DeleteConfig(ctx context.Context, pid int64, cfgId int64) error { if _, err := p.factory.Plan().DeleteConfig(ctx, cfgId); err != nil { klog.Errorf("failed to delete plan(%d) config(%d): %v", pid, cfgId, err) return errors.ErrServerInternal } return nil } func (p *plan) GetConfig(ctx context.Context, pid int64) (*types.PlanConfig, error) { object, err := p.factory.Plan().GetConfigByPlan(ctx, pid) if err != nil { klog.Errorf("failed to get plan(%d) config: %v", pid, err) return nil, errors.ErrServerInternal } return p.modelConfig2Type(object) } func (p *plan) buildAndCleanKubernetesConfig(ks types.KubernetesSpec) (string, error) { if ks.EnablePublicIp { if len(ks.ApiServer) == 0 { return "", fmt.Errorf("启用 ApiServer 地址,但是未配置关联 IP") } } else { if len(ks.ApiServer) != 0 { ks.ApiServer = "" } } return ks.Marshal() } func (p *plan) buildPlanConfig(ctx context.Context, req *types.CreatePlanConfigRequest) (*model.Config, error) { kubeConfig, err := p.buildAndCleanKubernetesConfig(req.Kubernetes) if err != nil { return nil, err } networkConfig, err := req.Network.Marshal() if err != nil { return nil, err } runtimeConfig, err := req.Runtime.Marshal() if err != nil { return nil, err } componentConfig, err := req.Component.Marshal() if err != nil { return nil, err } return &model.Config{ Region: req.Region, OSImage: req.OSImage, Kubernetes: kubeConfig, Network: networkConfig, Runtime: runtimeConfig, Component: componentConfig, }, nil } func (p *plan) modelConfig2Type(o *model.Config) (*types.PlanConfig, error) { ks := &types.KubernetesSpec{} if err := ks.Unmarshal(o.Kubernetes); err != nil { return nil, err } ns := &types.NetworkSpec{} if err := ns.Unmarshal(o.Network); err != nil { return nil, err } rs := &types.RuntimeSpec{} if err := rs.Unmarshal(o.Runtime); err != nil { return nil, err } cs := &types.ComponentSpec{} if err := cs.Unmarshal(o.Component); err != nil { return nil, err } return &types.PlanConfig{ PixiuMeta: types.PixiuMeta{ Id: o.Id, ResourceVersion: o.ResourceVersion, }, TimeMeta: types.TimeMeta{ GmtCreate: o.GmtCreate, GmtModified: o.GmtModified, }, PlanId: o.PlanId, Region: o.Region, OSImage: o.OSImage, Kubernetes: *ks, Network: *ns, Runtime: *rs, Component: *cs, }, nil } ================================================ FILE: pkg/controller/plan/plan_node.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "context" "strings" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/api/server/errors" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" utilerrors "github.com/caoyingjunz/pixiu/pkg/util/errors" ) // 创建前预检查 // 1. 创建 node 时 plan 必须存在 func (p *plan) preCreateNode(ctx context.Context, pid int64, req *types.CreatePlanNodeRequest) error { _, err := p.Get(ctx, pid) if err != nil { return err } return nil } func (p *plan) CreateNode(ctx context.Context, pid int64, req *types.CreatePlanNodeRequest) error { if err := p.preCreateNode(ctx, pid, req); err != nil { return err } if err := p.createNode(ctx, pid, req); err != nil { return err } return nil } func (p *plan) CreateNodes(ctx context.Context, planId int64, nodes []types.CreatePlanNodeRequest) error { _, err := p.Get(ctx, planId) if err != nil { return err } for _, node := range nodes { if err = p.createNode(ctx, planId, &node); err != nil { return err } } return nil } func (p *plan) UpdateNode(ctx context.Context, pid int64, nodeId int64, req *types.UpdatePlanNodeRequest) error { return nil } // 删除多余的节点 // 新增没有的节点 // 更新已存在的节点 func (p *plan) updateNodesIfNeeded(ctx context.Context, planId int64, req *types.UpdatePlanRequest) error { oldNodes, err := p.factory.Plan().ListNodes(ctx, planId) if err != nil { return err } newNodes := req.Nodes newMap := make(map[string]types.CreatePlanNodeRequest) for _, newNode := range newNodes { newMap[newNode.Name] = newNode } // 遍历寻找待删除节点然后执行删除 var delNodes []string for _, oldNode := range oldNodes { name := oldNode.Name _, found := newMap[name] if !found { delNodes = append(delNodes, name) } } if len(delNodes) != 0 { if err = p.factory.Plan().DeleteNodesByNames(ctx, planId, delNodes); err != nil { klog.Errorf("failed deleting nodes %v %v", delNodes, err) return err } } for _, newNode := range newNodes { node, err := p.buildNodeFromRequest(planId, &newNode) if err != nil { return err } if err = p.CreateOrUpdateNode(ctx, node); err != nil { return err } } return nil } func (p *plan) buildNodeFromRequest(planId int64, req *types.CreatePlanNodeRequest) (*model.Node, error) { auth, err := req.Auth.Marshal() if err != nil { return nil, err } return &model.Node{ Name: req.Name, PlanId: planId, Role: strings.Join(req.Role, ","), CRI: req.CRI, Ip: req.Ip, Auth: auth, }, nil } func (p *plan) createNode(ctx context.Context, planId int64, req *types.CreatePlanNodeRequest) error { node, err := p.buildNodeFromRequest(planId, req) if err != nil { klog.Errorf("failed to build plan(%d) node from request: %v", planId, err) return err } if _, err = p.factory.Plan().CreateNode(ctx, node); err != nil { klog.Errorf("failed to create node(%s): %v", req.Name, err) return err } return nil } func (p *plan) DeleteNode(ctx context.Context, pid int64, nodeId int64) error { if _, err := p.factory.Plan().DeleteNode(ctx, nodeId); err != nil { klog.Errorf("failed to delete plan(%d) node(%d): %v", pid, nodeId, err) return errors.ErrServerInternal } return nil } func (p *plan) GetNode(ctx context.Context, pid int64, nodeId int64) (*types.PlanNode, error) { object, err := p.factory.Plan().GetNode(ctx, nodeId) if err != nil { klog.Errorf("failed to get plan(%d) node(%d): %v", pid, nodeId, err) return nil, errors.ErrServerInternal } return p.modelNode2Type(object) } func (p *plan) ListNodes(ctx context.Context, pid int64) ([]types.PlanNode, error) { objects, err := p.factory.Plan().ListNodes(ctx, pid) if err != nil { klog.Errorf("failed to get plan(%d) nodes: %v", pid, err) return nil, errors.ErrServerInternal } var nodes []types.PlanNode for _, object := range objects { n, err := p.modelNode2Type(&object) if err != nil { return nil, err } nodes = append(nodes, *n) } return nodes, nil } // CreateOrUpdateNode // TODO: 优化 func (p *plan) CreateOrUpdateNode(ctx context.Context, object *model.Node) error { old, err := p.factory.Plan().GetNodeByName(ctx, object.PlanId, object.Name) if err != nil { if !utilerrors.IsRecordNotFound(err) { return err } // 不存在则创建 klog.Infof("plan(%d) node(%s) not exist, try to create it.", object.PlanId, object.Name) _, err = p.factory.Plan().CreateNode(ctx, object) if err != nil { return err } return nil } klog.Infof("plan(%d) node(%s) already exist", object.PlanId, object.Name) // 已存在尝试更新 updates := p.buildNodeUpdates(old, object) if len(updates) == 0 { return nil } klog.Infof("plan(%d) node(%s) already exist and need to update %v", object.PlanId, object.Name, updates) return p.factory.Plan().UpdateNode(ctx, old.Id, old.ResourceVersion, updates) } func (p *plan) modelNode2Type(o *model.Node) (*types.PlanNode, error) { auth := types.PlanNodeAuth{} if err := auth.Unmarshal(o.Auth); err != nil { return nil, err } return &types.PlanNode{ PixiuMeta: types.PixiuMeta{ Id: o.Id, ResourceVersion: o.ResourceVersion, }, TimeMeta: types.TimeMeta{ GmtCreate: o.GmtCreate, GmtModified: o.GmtModified, }, PlanId: o.PlanId, Name: o.Name, Role: strings.Split(o.Role, ","), Ip: o.Ip, Auth: auth, }, nil } func (p *plan) buildNodeUpdates(old, object *model.Node) map[string]interface{} { updates := make(map[string]interface{}) if old.Ip != object.Ip { updates["ip"] = object.Ip } if old.Role != object.Role { updates["role"] = object.Role } if old.Auth != object.Auth { updates["auth"] = object.Auth } return updates } ================================================ FILE: pkg/controller/plan/plan_task.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "bufio" "context" "encoding/json" "fmt" "io" "net/http" "time" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" "github.com/caoyingjunz/pixiu/pkg/util/container" ) // RunTask // 只运行指定的计划任务 func (p *plan) RunTask(ctx context.Context, planId int64, taskId int64) error { return nil } func (p *plan) ListTasks(ctx context.Context, planId int64) ([]types.PlanTask, error) { objects, err := p.factory.Plan().ListTasks(ctx, planId) if err != nil { klog.Errorf("failed to get plan(%d) tasks: %v", planId, err) return nil, err } var tasks []types.PlanTask for _, object := range objects { tasks = append(tasks, *p.modelTask2Type(&object)) } return tasks, nil } func (p *plan) WatchTasks(ctx context.Context, planId int64, w http.ResponseWriter, r *http.Request) { flush, _ := w.(http.Flusher) w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") // 初始化 Lister if taskC.Lister == nil { taskC.SetLister(p.factory.Plan().ListTasks) } // 等待缓存同步 if err := taskC.WaitForCacheSync(planId); err != nil { return } for { select { case <-r.Context().Done(): klog.Infof("plan(%d) watch API has been closed by client and cache will be removed after 5s", planId) return default: tasks, ok := taskC.Get(planId) if ok { var ts []types.PlanTask for _, object := range tasks { ts = append(ts, *p.modelTask2Type(&object)) } if err := json.NewEncoder(w).Encode(ts); err != nil { klog.Errorf("failed to encode tasks: %v", err) break } flush.Flush() } // 同步事件间隔为 3s time.Sleep(3 * time.Second) } } } func (p *plan) WatchTaskLog(ctx context.Context, planId int64, taskId int64, w http.ResponseWriter, r *http.Request) error { task, err := p.factory.Plan().GetTaskById(ctx, taskId) if err != nil { klog.Errorf("failed to get tasks of plan %d: %v", planId, err) return err } if task.Status == model.UnStartPlanStatus { return fmt.Errorf("任务尚未开始") } c, err := container.NewContainer("WatchTaskLog", planId, "") if err != nil { return err } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") // TODO 临时指定,后期根据步骤id去做查询判断 var step string switch task.Name { case "初始化部署环境": step = "bootstrap-servers" case "部署Master": step = "deploy" case "部署Node": step = "deploy" case "部署基础组件": step = "deploy" default: step = "bootstrap-servers" } containerId := fmt.Sprintf("%s-%d", step, planId) readCloser, err := c.WatchContainerLog(ctx, containerId, "") if err != nil { return err } defer readCloser.Close() // 读取日志 scanner := bufio.NewScanner(readCloser) flush, _ := w.(http.Flusher) for scanner.Scan() { line := append(scanner.Bytes(), byte('\n')) // 去掉前8不可见字符 _, err = w.Write(line[8:]) if err == io.EOF { break } if err != nil { return err } flush.Flush() } return nil } func (p *plan) modelTask2Type(o *model.Task) *types.PlanTask { return &types.PlanTask{ PixiuMeta: types.PixiuMeta{ Id: o.Id, ResourceVersion: o.ResourceVersion, }, TimeMeta: types.TimeMeta{ GmtCreate: o.GmtCreate, GmtModified: o.GmtModified, }, Name: o.Name, PlanId: o.PlanId, Status: o.Status, Message: o.Message, } } func (p *plan) modelTask2TypeList(o []*model.Task) []types.PlanTask { var tasks []types.PlanTask for _, object := range o { tasks = append(tasks, *p.modelTask2Type(object)) } return tasks } ================================================ FILE: pkg/controller/plan/register.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "context" "encoding/base64" "fmt" "io" "net" "strings" "time" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/pkg/db" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" ) const ( KubeConfigFile = "/etc/kubernetes/admin.conf" ) type Register struct { handlerTask factory db.ShareDaoFactory } func (c Register) Name() string { return "集群注册" } func (c Register) Run() error { ks := &types.KubernetesSpec{} if err := ks.Unmarshal(c.data.Config.Kubernetes); err != nil { return err } // 如果未启用自注册功能,则直接跳过 if !ks.Register { klog.Infof("部署计划未启用自注册功能,skipping") } // 从 master 节点获取 kubeConfig 内容,注入集群服务 var masterNodes []model.Node for _, node := range c.data.Nodes { if strings.Contains(node.Role, model.MasterRole) { masterNodes = append(masterNodes, node) } } var ( kubeConfig []byte err error ) for _, masterNode := range masterNodes { kubeConfig, err = getKubeConfigFromMasterNode(masterNode) if err == nil { break } else { klog.Warningf("failed to get kubeConfig from master(%s): %v, trying the other masters", masterNode.Name, err) } } if len(kubeConfig) == 0 { return fmt.Errorf("get the empty kubeconfig from master nodes") } config64 := base64.StdEncoding.EncodeToString(kubeConfig) if err = c.factory.Cluster().UpdateByPlan(context.TODO(), c.data.PlanId, map[string]interface{}{"kube_config": config64}); err != nil { return err } return nil } func getKubeConfigFromMasterNode(maserNode model.Node) ([]byte, error) { sftpClient, err := newSftpClient(maserNode) if err != nil { return nil, err } defer sftpClient.Close() srcFile, err := sftpClient.Open(KubeConfigFile) if err != nil { return nil, err } defer srcFile.Close() buf, err := io.ReadAll(srcFile) if err != nil { return nil, err } return buf, nil } func newSftpClient(node model.Node) (*sftp.Client, error) { nodeAuth := types.PlanNodeAuth{} if err := nodeAuth.Unmarshal(node.Auth); err != nil { return nil, err } var clientConfig *ssh.ClientConfig switch nodeAuth.Type { case types.PasswordAuth: // 1. 使用密码 clientConfig = &ssh.ClientConfig{ User: nodeAuth.Password.User, Auth: []ssh.AuthMethod{ ssh.Password(nodeAuth.Password.Password), }, Timeout: 30 * time.Second, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }, } case types.KeyAuth: //2. 使用秘钥 key := []byte(nodeAuth.Key.Data) signer, err := ssh.ParsePrivateKey(key) if err != nil { return nil, err } clientConfig = &ssh.ClientConfig{ User: "root", // 秘钥登陆时,默认 root Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, Timeout: 30 * time.Second, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } default: return nil, fmt.Errorf("unsupported ssh auth type: %s", nodeAuth.Type) } addr := fmt.Sprintf("%s:%d", node.Ip, 22) sshClient, err := ssh.Dial("tcp", addr, clientConfig) if err != nil { return nil, err } return sftp.NewClient(sshClient) } ================================================ FILE: pkg/controller/plan/render.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "bytes" "fmt" "path/filepath" "strings" "text/template" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" "github.com/caoyingjunz/pixiu/pkg/util" pixiutpl "github.com/caoyingjunz/pixiu/template" ) // Render 渲染 pixiu 部署配置 // 1. 渲染 hosts // 2. 渲染 globals.yaml // 3. 渲染 multinode // 具体参考 https://github.com/pixiu-io/kubez-ansible type Render struct { handlerTask dir string } func (r Render) Name() string { return "配置渲染" } func (r Render) Run() error { // 渲染 hosts if err := r.doRender("hosts", pixiutpl.HostTemplate, r.data); err != nil { return err } // 渲染 multiNode nodes, err := ParseMultinode(r.data, r.dir) if err != nil { return err } if err := r.doRender("multinode", pixiutpl.MultiModeTemplate, nodes); err != nil { return err } // 渲染 globals cfg, err := ParseConfig(r.data) if err != nil { return err } if err := r.doRender("globals.yml", pixiutpl.GlobalsTemplate, cfg); err != nil { return err } return nil } func (r Render) doRender(name string, text string, data interface{}) error { tpl := template.New(name) tpl = template.Must(tpl.Parse(text)) var buf bytes.Buffer if err := tpl.Execute(&buf, data); err != nil { return err } filename, err := GetRenderFile(r.GetPlanId(), r.dir, name) if err != nil { return err } if err = util.WriteToFile(filename, buf.Bytes()); err != nil { return err } return nil } type Multinode struct { DockerMaster []types.PlanNode DockerNode []types.PlanNode ContainerdMaster []types.PlanNode ContainerdNode []types.PlanNode } func ParseMultinode(data TaskData, workDir string) (Multinode, error) { multinode := Multinode{ DockerMaster: make([]types.PlanNode, 0), DockerNode: make([]types.PlanNode, 0), ContainerdMaster: make([]types.PlanNode, 0), ContainerdNode: make([]types.PlanNode, 0), } runtime := types.RuntimeSpec{} if err := runtime.Unmarshal(data.Config.Runtime); err != nil { return multinode, err } for _, node := range data.Nodes { nodeAuth := types.PlanNodeAuth{} err := nodeAuth.Unmarshal(node.Auth) if err != nil { return multinode, err } // 生成rsa的渲染文件 _, err = RenderRSA(data.PlanId, node.Name, workDir, nodeAuth) if err != nil { return multinode, err } if nodeAuth.Type == types.KeyAuth { if nodeAuth.Key == nil { return multinode, fmt.Errorf("node(%s) key auth config is empty", node.Name) } nodeAuth.Key.File = fmt.Sprintf("/configs/ssh/%s/id_rsa", node.Name) } if nodeAuth.Type == types.PasswordAuth && nodeAuth.Password == nil { return multinode, fmt.Errorf("node(%s) password auth config is empty", node.Name) } planNode := types.PlanNode{Name: node.Name, Auth: nodeAuth} roles := strings.Split(node.Role, ",") if runtime.IsDocker() { for _, role := range roles { if role == model.MasterRole { multinode.DockerMaster = append(multinode.DockerMaster, planNode) } if role == model.NodeRole { multinode.DockerNode = append(multinode.DockerNode, planNode) } } } if runtime.IsContainerd() { for _, role := range roles { if role == model.MasterRole { multinode.ContainerdMaster = append(multinode.ContainerdMaster, planNode) } if role == model.NodeRole { multinode.ContainerdNode = append(multinode.ContainerdNode, planNode) } } } } return multinode, nil } // GetRenderFile // TODO: 后续优化 func GetRenderFile(planId int64, workDir string, f string) (string, error) { planDir := filepath.Join(workDir, fmt.Sprintf("%d", planId)) if err := util.EnsureDirectoryExists(planDir); err != nil { return "", err } return filepath.Join(planDir, f), nil } func RenderRSA(planId int64, name string, workDir string, auth types.PlanNodeAuth) (string, error) { if auth.Type == types.KeyAuth { if auth.Key == nil { return "", fmt.Errorf("node(%s) key auth config is empty", name) } f, err := GetRSAFile(planId, workDir, name) if err != nil { return "", err } if err = util.WriteToFile(f, []byte(auth.Key.Data)); err != nil { return "", err } return f, nil } return "", nil } func GetRSAFile(planId int64, workDir string, name string) (string, error) { rsaDir := filepath.Join(workDir, fmt.Sprintf("%d", planId), "ssh", name) if err := util.EnsureDirectoryExists(rsaDir); err != nil { return "", err } return filepath.Join(rsaDir, "id_rsa"), nil } func ParseConfig(data TaskData) (*types.PlanConfig, error) { config := data.Config network := types.NetworkSpec{} if err := network.Unmarshal(config.Network); err != nil { return nil, err } kubernetes := types.KubernetesSpec{} if err := kubernetes.Unmarshal(config.Kubernetes); err != nil { return nil, err } component := types.ComponentSpec{} if err := component.Unmarshal(config.Component); err != nil { return nil, err } return &types.PlanConfig{ Kubernetes: kubernetes, Network: network, Component: component, }, nil } ================================================ FILE: pkg/controller/plan/worker.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plan import ( "context" "fmt" "time" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/util/errors" ) type Handler interface { GetPlanId() int64 Name() string // 检查项名称 Step() model.PlanStep // 未开始,运行中,异常和完成 Run() error // 执行 } type handlerTask struct { data TaskData } func (t handlerTask) GetPlanId() int64 { return t.data.PlanId } func (t handlerTask) Step() model.PlanStep { return model.RunningPlanStep } func newHandlerTask(data TaskData) handlerTask { return handlerTask{data: data} } func (p *plan) Run(ctx context.Context, workers int) error { // 进程启动时,尝试同步任务状态 klog.Infof("starting to sync task manager") if err := p.SyncTaskStatus(ctx); err != nil { return err } // 启动部署计划控制器 klog.Infof("starting deployment manager") for i := 0; i < workers; i++ { go wait.UntilWithContext(ctx, p.worker, time.Second) } return nil } func (p *plan) worker(ctx context.Context) { for p.process(ctx) { } } func (p *plan) process(ctx context.Context) bool { key, quit := taskQueue.Get() if quit { return false } defer taskQueue.Done(key) p.syncHandler(ctx, key.(int64)) return true } type TaskData struct { PlanId int64 Config *model.Config Nodes []model.Node } func (t TaskData) validate() error { return nil } func (p *plan) getTaskData(ctx context.Context, planId int64) (TaskData, error) { nodes, err := p.factory.Plan().ListNodes(ctx, planId) if err != nil { return TaskData{}, err } cfg, err := p.factory.Plan().GetConfigByPlan(ctx, planId) if err != nil { return TaskData{}, err } return TaskData{ PlanId: planId, Config: cfg, Nodes: nodes, }, nil } // 实际处理函数 // 处理步骤: // 1. 检查部署参数是否符合要求 // 2. 渲染环境 // 3. 执行部署 // 4. 部署后环境清理 func (p *plan) syncHandler(ctx context.Context, planId int64) { klog.Infof("starting plan(%d) task", planId) defer klog.Infof("completed plan(%d) task", planId) taskData, err := p.getTaskData(ctx, planId) if err != nil { klog.Errorf("failed to get task data: %v", err) return } runner, err := p.GetRunner(taskData.Config.OSImage) if err != nil { klog.Errorf("failed to get image(%s) for worker: %v", taskData.Config.OSImage, err) return } // Runner的工作目录 dir := p.WorkDir() task := newHandlerTask(taskData) handlers := []Handler{ Check{handlerTask: task}, Render{handlerTask: task, dir: dir}, BootStrap{handlerTask: task, dir: dir, runner: runner}, Deploy{handlerTask: task, dir: dir, runner: runner}, DeployNode{handlerTask: task}, Register{handlerTask: task, factory: p.factory}, DeployChart{handlerTask: task}, } if err = p.syncTasks(handlers...); err != nil { klog.Errorf("failed to sync task: %v", err) } } func (p *plan) createPlanTasksIfNotExist(tasks ...Handler) error { for _, task := range tasks { planId := task.GetPlanId() name := task.Name() step := task.Step() _, err := p.factory.Plan().GetTaskByName(context.TODO(), planId, name) // 存在则直接返回 if err == nil { return nil } // 非不存在报错则报异常 if !errors.IsRecordNotFound(err) { klog.Infof("failed to get plan(%d) tasks(%s) for first created: %v", planId, name, err) return err } // 不存在记录则新建 if _, err = p.factory.Plan().CreateTask(context.TODO(), &model.Task{ Name: name, PlanId: planId, Step: step, Status: model.UnStartPlanStatus, }); err != nil { klog.Errorf("failed to init plan(%d) task(%s): %v", planId, name, err) return err } } return nil } func (p *plan) WorkDir() string { return p.cc.Worker.WorkDir } func (p *plan) GetRunner(osImage string) (string, error) { engines := p.cc.Worker.Engines for _, engine := range engines { for _, os := range engine.OSSupported { if os == osImage { return engine.Image, nil } } } return "", fmt.Errorf("osImage(%s) runner not found", osImage) } // 同步任务状态 // 任务启动时设置为运行中,结束时同步为结束状态(成功或者失败) // TODO: 后续优化,判断对应部署容器是否在运行,根据容器的运行结果同步状态 func (p *plan) syncStatus(ctx context.Context, planId int64) error { tasks, err := p.factory.Plan().ListTasks(ctx, planId) if err != nil { return err } for _, task := range tasks { if task.Status != model.RunningPlanStatus { continue } if _, err = p.factory.Plan().UpdateTask(ctx, planId, task.Name, map[string]interface{}{ "status": model.FailedPlanStatus, "step": model.FailedPlanStep, "message": "服务异常修正,请重新启动部署计划", "gmt_modified": time.Now(), }); err != nil { klog.Errorf("failed to update plan(%d) status: %v", planId, err) return err } } return nil } func (p *plan) syncTasks(tasks ...Handler) error { // 初始化记录 if err := p.createPlanTasksIfNotExist(tasks...); err != nil { return err } // 执行任务并更新状态 for _, task := range tasks { planId := task.GetPlanId() name := task.Name() klog.Infof("starting plan(%d) task(%s)", planId, name) // TODO: 通过闭包方式优化 start, err := p.factory.Plan().UpdateTask(context.TODO(), planId, name, map[string]interface{}{ "status": model.RunningPlanStatus, "message": "", "gmt_create": time.Now(), }) if err != nil { klog.Errorf("failed to update plan(%d) status before run task(%s): %v", planId, name, err) return err } taskC.SetByTask(planId, *start) status := model.SuccessPlanStatus step := task.Step() message := "" // 执行检查 runErr := task.Run() if runErr != nil { status = model.FailedPlanStatus step = model.FailedPlanStep message = runErr.Error() } // 执行完成之后更新状态 end, err := p.factory.Plan().UpdateTask(context.TODO(), planId, name, map[string]interface{}{ "status": status, "message": message, "step": step, "gmt_modified": time.Now(), }) if err != nil { klog.Errorf("failed to update plan(%d) status after run task(%s): %v", planId, name, err) return err } taskC.SetByTask(planId, *end) klog.Infof("completed plan(%d) task(%s)", planId, name) if runErr != nil { return runErr } } return nil } ================================================ FILE: pkg/controller/tenant/tenant.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package tenant import ( "context" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/api/server/errors" "github.com/caoyingjunz/pixiu/cmd/app/config" "github.com/caoyingjunz/pixiu/pkg/db" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" ) type TenantGetter interface { Tenant() Interface } type Interface interface { Create(ctx context.Context, req *types.CreateTenantRequest) error Update(ctx context.Context, tid int64, req *types.UpdateTenantRequest) error Delete(ctx context.Context, tid int64) error Get(ctx context.Context, tid int64) (*types.Tenant, error) List(ctx context.Context) ([]types.Tenant, error) } type tenant struct { cc config.Config factory db.ShareDaoFactory } func (t *tenant) Create(ctx context.Context, req *types.CreateTenantRequest) error { object, err := t.factory.Tenant().GetTenantByName(ctx, req.Name) if err != nil { klog.Errorf("failed to get tenant %s: %v", req.Name, err) return errors.ErrServerInternal } if object != nil { return errors.ErrTenantExists } tenant := &model.Tenant{ Name: req.Name, } if req.Description != nil { tenant.Description = *req.Description } if _, err = t.factory.Tenant().Create(ctx, tenant); err != nil { klog.Errorf("failed to create tenant %s: %v", req.Name, err) return errors.ErrServerInternal } return nil } func (t *tenant) Update(ctx context.Context, tid int64, req *types.UpdateTenantRequest) error { object, err := t.factory.Tenant().Get(ctx, tid) if err != nil { klog.Errorf("failed to get tenant %d: %v", tid, err) return errors.ErrServerInternal } if object == nil { return errors.ErrTenantNotFound } updates := make(map[string]interface{}) if req.Name != nil { updates["name"] = *req.Name } if req.Description != nil { updates["description"] = *req.Description } if len(updates) == 0 { return errors.ErrInvalidRequest } if err := t.factory.Tenant().Update(ctx, tid, *req.ResourceVersion, updates); err != nil { klog.Errorf("failed to update tenant %d: %v", tid, err) return errors.ErrServerInternal } return nil } func (t *tenant) Delete(ctx context.Context, tid int64) error { _, err := t.factory.Tenant().Delete(ctx, tid) if err != nil { klog.Errorf("failed to delete tenant %d: %v", tid, err) return errors.ErrServerInternal } return nil } func (t *tenant) Get(ctx context.Context, tid int64) (*types.Tenant, error) { object, err := t.factory.Tenant().Get(ctx, tid) if err != nil { klog.Errorf("failed to get tenant %d: %v", tid, err) return nil, errors.ErrServerInternal } if object == nil { return nil, errors.ErrTenantNotFound } return t.model2Type(object), nil } func (t *tenant) List(ctx context.Context) ([]types.Tenant, error) { objects, err := t.factory.Tenant().List(ctx) if err != nil { klog.Errorf("failed to get tenants: %v", err) return nil, errors.ErrServerInternal } var ts []types.Tenant for _, object := range objects { ts = append(ts, *t.model2Type(&object)) } return ts, nil } func (t *tenant) model2Type(o *model.Tenant) *types.Tenant { return &types.Tenant{ PixiuMeta: types.PixiuMeta{ Id: o.Id, ResourceVersion: o.ResourceVersion, }, TimeMeta: types.TimeMeta{ GmtCreate: o.GmtCreate, GmtModified: o.GmtModified, }, Name: o.Name, Description: o.Description, } } func NewTenant(cfg config.Config, f db.ShareDaoFactory) *tenant { return &tenant{ cc: cfg, factory: f, } } ================================================ FILE: pkg/controller/user/user.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package user import ( "context" "fmt" "github.com/casbin/casbin/v2" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/api/server/errors" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/cmd/app/config" "github.com/caoyingjunz/pixiu/pkg/client" "github.com/caoyingjunz/pixiu/pkg/db" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" "github.com/caoyingjunz/pixiu/pkg/util" tokenutil "github.com/caoyingjunz/pixiu/pkg/util/token" ) var ( userIndexer client.UserCache tokenIndexer client.TokenCache ) func init() { userIndexer = *client.NewUserCache() tokenIndexer = *client.NewTokenCache() } type UserGetter interface { User() Interface } type Interface interface { Create(ctx context.Context, req *types.CreateUserRequest) error Update(ctx context.Context, userId int64, req *types.UpdateUserRequest) error Delete(ctx context.Context, userId int64) error Get(ctx context.Context, userId int64) (*types.User, error) List(ctx context.Context, req *types.ListUserRequest) (*types.PageResponse, error) // UpdatePassword 用户修改密码或者管理员重置密码 UpdatePassword(ctx context.Context, userId int64, req *types.UpdateUserPasswordRequest) error // GetCount 仅获取用户数量 GetCount(ctx context.Context, opts types.ListOptions) (int64, error) // GetStatus 获取用户状态,优先从缓存获取,如果没有则从库里获取,然后同步到缓存 GetStatus(ctx context.Context, uid int64) (int, error) Login(ctx context.Context, req *types.LoginRequest) (*types.LoginResponse, error) Logout(ctx context.Context, userId int64) error GetLoginToken(ctx context.Context, userId int64) (string, error) } type user struct { cc config.Config factory db.ShareDaoFactory enforcer *casbin.SyncedEnforcer } func (u *user) Create(ctx context.Context, req *types.CreateUserRequest) error { encrypt, err := util.EncryptUserPassword(req.Password) if err != nil { klog.Errorf("failed to encrypt user password: %v", err) return errors.ErrServerInternal } object, err := u.factory.User().GetUserByName(ctx, req.Name) if err != nil { klog.Errorf("failed to get user %s: %v", req.Name, err) return errors.ErrServerInternal } if object != nil { err = errors.ErrUserExists // 记录错误 return err } // 超级管理员全局只允许存在一个 if req.Role == model.RoleRoot { root, err := u.factory.User().GetRoot(ctx) if err != nil { klog.Errorf("failed to check root user: %v", err) return errors.ErrServerInternal } if root != nil { return errors.ErrRootAlreadyExists } } txFunc := func() (err error) { if req.Role == model.RoleRoot { bindings := model.NewGroupBinding(req.Name, model.AdminGroup) _, err = u.enforcer.AddGroupingPolicy(bindings.Raw()) } return } if _, err = u.factory.User().Create(ctx, &model.User{ Name: req.Name, Password: encrypt, Status: req.Status, Role: req.Role, Email: req.Email, Phone: req.Phone, Description: req.Description, }, txFunc); err != nil { klog.Errorf("failed to create user %s: %v", req.Name, err) return errors.ErrServerInternal } return nil } func (u *user) Update(ctx context.Context, uid int64, req *types.UpdateUserRequest) error { updates := map[string]interface{}{ "status": req.Status, "email": req.Email, "phone": req.Phone, "description": req.Description, } if err := u.factory.User().Update(ctx, uid, *req.ResourceVersion, updates); err != nil { klog.Errorf("failed to update user(%d): %v", uid, err) return errors.ErrServerInternal } userIndexer.Set(uid, int(req.Status)) return nil } func (u *user) preResetPassword(ctx context.Context, userId int64, operatorId int64, req *types.UpdateUserPasswordRequest) error { // 操作人必须具备管理员权限 operator, err := u.Get(ctx, operatorId) if err != nil { return err } if operator.Role != model.RoleRoot { return fmt.Errorf("非超级管理员,不允许重置用户密码") } return nil } func (u *user) preChangePassword(ctx context.Context, userId int64, operatorId int64, req *types.UpdateUserPasswordRequest) error { if operatorId != userId { return fmt.Errorf("用户只能修改自己的密码") } object, err := u.Get(ctx, userId) if err != nil { return err } // 校验旧密码是否正确 if err = util.ValidateUserPassword(object.Password, req.Old); err != nil { klog.Errorf("检验用户密码失败: %v", err) return errors.ErrInvalidPassword } return nil } // UpdatePassword 支持用户修改密码和管理员重置密码 // 修改密码: 用户只能修改自己密码 // 重启密码: 管理员可以重置他人密码 func (u *user) UpdatePassword(ctx context.Context, userId int64, req *types.UpdateUserPasswordRequest) error { // 新老密码不允许相同 if req.New == req.Old { return errors.ErrDuplicatedPassword } operatorId, err := httputils.GetUserIdFromContext(ctx) if err != nil { return err } if req.Reset { // 管理员重置密码前置检查 if err = u.preResetPassword(ctx, userId, operatorId, req); err != nil { return err } } else { // 用户修改密码前置检查 if err = u.preChangePassword(ctx, userId, operatorId, req); err != nil { return err } } newPass, err := util.EncryptUserPassword(req.New) if err != nil { klog.Errorf("failed to encrypt user password: %v", err) return errors.ErrServerInternal } if err = u.factory.User().Update(ctx, userId, *req.ResourceVersion, map[string]interface{}{ "password": newPass, }); err != nil { klog.Errorf("failed to update user(%d) password: %v", userId, err) return errors.ErrServerInternal } tokenIndexer.Delete(userId) return nil } func (u *user) Delete(ctx context.Context, userId int64) error { if err := u.factory.User().Delete(ctx, userId); err != nil { klog.Errorf("failed to delete user(%d): %v", userId, err) return errors.ErrServerInternal } userIndexer.Delete(userId) tokenIndexer.Delete(userId) return nil } func (u *user) Get(ctx context.Context, userId int64) (*types.User, error) { object, err := u.factory.User().Get(ctx, userId) if err != nil { klog.Errorf("failed to get user(%d): %v", userId, err) return nil, errors.ErrServerInternal } if object == nil { return nil, errors.ErrUserNotFound } return model2Type(object), nil } func (u *user) List(ctx context.Context, req *types.ListUserRequest) (*types.PageResponse, error) { opts := []db.Options{db.WithOrderByDesc()} if req != nil { opts = append(opts, db.WithUserNameLike(req.UserName), db.WithUserPhoneLike(req.UserPhone), db.WithUserEmailLike(req.UserEmail), ) if req.Status != nil { opts = append(opts, db.WithUserStatus(*req.Status)) } } total, err := u.factory.User().Count(ctx, opts...) if err != nil { klog.Errorf("failed to get user counts: %v", err) return nil, errors.ErrServerInternal } pageReq := types.PageRequest{} if req != nil { pageReq = req.PageRequest if req.Page > 0 && req.Limit > 0 { opts = append(opts, db.WithOffset((req.Page-1)*req.Limit), db.WithLimit(req.Limit)) } } objects, err := u.factory.User().List(ctx, opts...) if err != nil { klog.Errorf("failed to get user list: %v", err) return nil, errors.ErrServerInternal } var users []types.User for _, object := range objects { users = append(users, *model2Type(&object)) } return &types.PageResponse{ PageRequest: pageReq, Total: int(total), Items: users, }, nil } func (u *user) GetCount(ctx context.Context, opts types.ListOptions) (int64, error) { userCount, err := u.factory.User().Count(ctx) if err != nil { klog.Errorf("failed to get user counts: %v", err) return 0, errors.ErrServerInternal } return userCount, nil } // GetStatus 获取用户状态,优先从缓存获取,如果没有则从库里获取,然后同步到缓存 func (u *user) GetStatus(ctx context.Context, uid int64) (int, error) { status, ok := userIndexer.Get(uid) if ok { return status, nil } object, err := u.factory.User().Get(ctx, uid) if err != nil { klog.Errorf("failed to get user(%d): %v", uid, err) return 0, errors.ErrServerInternal } if object == nil { return 0, errors.ErrUserNotFound } userIndexer.Set(uid, int(object.Status)) return int(object.Status), nil } func (u *user) Login(ctx context.Context, req *types.LoginRequest) (*types.LoginResponse, error) { object, err := u.factory.User().GetUserByName(ctx, req.Name) if err != nil { return nil, errors.ErrServerInternal } if object == nil { return nil, errors.ErrUserNotFound } // 如果用户已被禁用,则不允许登陆 if object.Status == 2 { return nil, fmt.Errorf("用户已被禁用") } if err = util.ValidateUserPassword(object.Password, req.Password); err != nil { klog.Errorf("检验用户密码失败: %v", err) return nil, errors.ErrInvalidPassword } // 生成登陆的 token 信息 key := u.GetTokenKey() token, err := tokenutil.GenerateToken(object.Id, object.Name, key) if err != nil { return nil, fmt.Errorf("生成用户 token 失败: %v", err) } tokenIndexer.Set(object.Id, token) return &types.LoginResponse{ UserId: object.Id, UserName: object.Name, Token: token, Role: object.Role, User: object, }, nil } // Logout // 允许用户登出登陆状态 // TODO: 临时实现,后续优化 func (u *user) Logout(ctx context.Context, userId int64) error { tokenIndexer.Delete(userId) return nil } func (u *user) GetLoginToken(ctx context.Context, userId int64) (string, error) { t, exists := tokenIndexer.Get(userId) if !exists { return "", fmt.Errorf("invalid empty token") } return t, nil } func (u *user) GetTokenKey() []byte { k := u.cc.Default.JWTKey return []byte(k) } // 将 model user 转换成 types func model2Type(o *model.User) *types.User { return &types.User{ PixiuMeta: types.PixiuMeta{ Id: o.Id, ResourceVersion: o.ResourceVersion, }, Name: o.Name, Description: o.Description, Status: o.Status, Role: o.Role, Email: o.Email, Phone: o.Phone, TimeMeta: types.TimeMeta{ GmtCreate: o.GmtCreate, GmtModified: o.GmtModified, }, } } func NewUser(cfg config.Config, f db.ShareDaoFactory, e *casbin.SyncedEnforcer) *user { return &user{ cc: cfg, factory: f, enforcer: e, } } ================================================ FILE: pkg/controller/user/user_test.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package user ================================================ FILE: pkg/controller/util/util.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "context" "github.com/casbin/casbin/v2" "github.com/gin-gonic/gin" "github.com/caoyingjunz/pixiu/api/server/httputils" "github.com/caoyingjunz/pixiu/pkg/db" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/util" ) func MakeDbOptions(ctx context.Context) (opts []db.Options) { exists, ids := httputils.GetIdRangeFromListReq(ctx) if exists { opts = append(opts, db.WithIDIn(ids...)) } return } func SetIdRangeContext(c *gin.Context, enforcer *casbin.SyncedEnforcer, user *model.User, obj string) error { bindings, err := GetGroupBindings(enforcer, QueryWithUserName(user.Name)) if err != nil { return err } if model.BindingToAdmin(bindings) { // This user is an admin/root, it's unnecessary to set object IDs list to context. return nil } ups, err := GetUserPolicies(enforcer, user, WithObjectType(model.ObjectType(obj))) if err != nil { return err } policies := make([]model.Policy, len(ups)) for i, up := range ups { policies[i] = up } if all, ids := model.GetIdRangeFromPolicy(policies); !all { // Set a list of object IDs to context. httputils.SetIdRangeContext(c, ids) } // If policy with all operation(*) exists, it's unnecessary to set object IDs list to context. return nil } type BindingQueryCondition func(c *policyConditions) (index int) func QueryWithGroupName(name string) BindingQueryCondition { return func(c *policyConditions) int { c.conds[1] = &name return 1 } } func QueryWithUserName(name string) BindingQueryCondition { return func(c *policyConditions) int { c.conds[0] = &name return 0 } } func GetGroupBindings(enforcer *casbin.SyncedEnforcer, conds ...BindingQueryCondition) ([]model.GroupBinding, error) { var index int pc := &policyConditions{ conds: [4]*string{}, } for _, cond := range conds { i := cond(pc) index = util.Less(i, index) } rp, err := enforcer.GetFilteredNamedGroupingPolicy("g", index, pc.get()...) if err != nil { return nil, err } bindings := make([]model.GroupBinding, len(rp)) for i, p := range rp { copy(bindings[i][:], p) } return bindings, nil } type policyConditions struct { conds [4]*string } func newPolicyConditions(name string) *policyConditions { return &policyConditions{ conds: [4]*string{&name}, } } func (c *policyConditions) get() (conds []string) { for _, cond := range c.conds { if cond != nil { conds = append(conds, *cond) } else { break } } return } type PolicyCondition func(c *policyConditions) func WithObjectType(t model.ObjectType) PolicyCondition { return func(c *policyConditions) { s := t.String() c.conds[1] = &s } } func WithStringID(sid string) PolicyCondition { return func(c *policyConditions) { c.conds[2] = &sid } } func WithOperation(op model.Operation) PolicyCondition { return func(c *policyConditions) { s := op.String() c.conds[3] = &s } } func GetUserPolicies(enforcer *casbin.SyncedEnforcer, user *model.User, conds ...PolicyCondition) ([]model.UserPolicy, error) { pc := newPolicyConditions(user.Name) for _, cond := range conds { cond(pc) } rp, err := enforcer.GetFilteredNamedPolicy("p", 0, pc.get()...) if err != nil { return nil, err } policies := make([]model.UserPolicy, len(rp)) for i, p := range rp { _ = copy(policies[i][:], p) } return policies, nil } func GetGroupPolicy(enforcer *casbin.SyncedEnforcer, name string) (*model.GroupPolicy, error) { rp, err := enforcer.GetFilteredNamedPolicy("p", 0, name) if err != nil { return nil, err } if len(rp) == 0 { return nil, nil } policy := model.GroupPolicy{} _ = copy(policy[:], rp[0]) return &policy, nil } ================================================ FILE: pkg/db/audit.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package db import ( "context" "time" "gorm.io/gorm" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/util/errors" ) type AuditInterface interface { List(ctx context.Context, opts ...Options) ([]model.Audit, error) Get(ctx context.Context, id int64) (*model.Audit, error) Create(ctx context.Context, object *model.Audit) (*model.Audit, error) BatchDelete(ctx context.Context, opts ...Options) (int64, error) Count(ctx context.Context, opts ...Options) (int64, error) } type audit struct { db *gorm.DB } func newAudit(db *gorm.DB) AuditInterface { return &audit{db: db} } func (a *audit) Create(ctx context.Context, object *model.Audit) (*model.Audit, error) { now := time.Now() object.GmtCreate = now object.GmtModified = now if err := a.db.WithContext(ctx).Create(object).Error; err != nil { return nil, err } return object, nil } func (a *audit) Get(ctx context.Context, aid int64) (*model.Audit, error) { var object model.Audit if err := a.db.WithContext(ctx).Where("id = ?", aid).First(&object).Error; err != nil { if errors.IsRecordNotFound(err) { return nil, nil } return nil, err } return &object, nil } func (a *audit) List(ctx context.Context, opts ...Options) ([]model.Audit, error) { var audits []model.Audit tx := a.db.WithContext(ctx) for _, opt := range opts { tx = opt(tx) } if err := tx.Find(&audits).Error; err != nil { return nil, err } return audits, nil } func (a *audit) BatchDelete(ctx context.Context, opts ...Options) (int64, error) { tx := a.db.WithContext(ctx) for _, opt := range opts { tx = opt(tx) } err := tx.Delete(&model.Audit{}).Error return tx.RowsAffected, err } func (a *audit) Count(ctx context.Context, opts ...Options) (int64, error) { tx := a.db.WithContext(ctx) for _, opt := range opts { tx = opt(tx) } var total int64 err := tx.Model(&model.Audit{}).Count(&total).Error return total, err } ================================================ FILE: pkg/db/cluster.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package db import ( "context" "time" "gorm.io/gorm" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/util/errors" ) type ClusterInterface interface { Create(ctx context.Context, object *model.Cluster, fns ...func(*model.Cluster) error) (*model.Cluster, error) Update(ctx context.Context, cid int64, resourceVersion int64, updates map[string]interface{}) error Delete(ctx context.Context, cluster *model.Cluster, fns ...func(*model.Cluster) error) error Get(ctx context.Context, cid int64, opts ...Options) (*model.Cluster, error) List(ctx context.Context, opts ...Options) ([]model.Cluster, error) Count(ctx context.Context, opts ...Options) (int64, error) // InternalUpdate 内部更新,不更新版本号 InternalUpdate(ctx context.Context, cid int64, updates map[string]interface{}) error GetClusterByName(ctx context.Context, name string) (*model.Cluster, error) UpdateByPlan(ctx context.Context, planId int64, updates map[string]interface{}) error } type cluster struct { db *gorm.DB } func (c *cluster) Create(ctx context.Context, object *model.Cluster, fns ...func(*model.Cluster) error) (*model.Cluster, error) { now := time.Now() object.GmtCreate = now object.GmtModified = now if err := c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Create(object).Error; err != nil { return err } for _, fn := range fns { if err := fn(object); err != nil { return err } } return nil }); err != nil { return nil, err } return object, nil } func (c *cluster) Update(ctx context.Context, cid int64, resourceVersion int64, updates map[string]interface{}) error { // 系统维护字段 updates["gmt_modified"] = time.Now() updates["resource_version"] = resourceVersion + 1 f := c.db.WithContext(ctx).Model(&model.Cluster{}).Where("id = ? and resource_version = ?", cid, resourceVersion).Updates(updates) if f.Error != nil { return f.Error } if f.RowsAffected == 0 { return errors.ErrRecordNotUpdate } return nil } // InternalUpdate 程序内部更新 func (c *cluster) InternalUpdate(ctx context.Context, cid int64, updates map[string]interface{}) error { // 系统维护字段 updates["gmt_modified"] = time.Now() f := c.db.WithContext(ctx).Model(&model.Cluster{}).Where("id = ?", cid).Updates(updates) if f.Error != nil { return f.Error } if f.RowsAffected == 0 { return errors.ErrRecordNotUpdate } return nil } func (c *cluster) Delete(ctx context.Context, cluster *model.Cluster, fns ...func(*model.Cluster) error) error { // 仅当数据库支持回写功能时才能正常 //if err := c.db.Clauses(clause.Returning{}).Where("id = ?", cid).Delete(&object).Error; err != nil { // return nil, err //} return c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Delete(cluster).Error; err != nil { return err } for _, fn := range fns { if err := fn(cluster); err != nil { return err } } return nil }) } func (c *cluster) Get(ctx context.Context, cid int64, opts ...Options) (*model.Cluster, error) { var object model.Cluster tx := c.db.WithContext(ctx) for _, opt := range opts { tx = opt(tx) } if err := tx.First(&object, cid).Error; err != nil { if errors.IsRecordNotFound(err) { return nil, nil } return nil, err } return &object, nil } func (c *cluster) List(ctx context.Context, opts ...Options) ([]model.Cluster, error) { var cs []model.Cluster tx := c.db.WithContext(ctx) for _, opt := range opts { tx = opt(tx) } if err := tx.Find(&cs).Error; err != nil { return nil, err } return cs, nil } func (c *cluster) Count(ctx context.Context, opts ...Options) (int64, error) { var total int64 tx := c.db.WithContext(ctx).Model(&model.Cluster{}) for _, opt := range opts { tx = opt(tx) } if err := tx.Count(&total).Error; err != nil { return 0, err } return total, nil } func (c *cluster) GetClusterByName(ctx context.Context, name string) (*model.Cluster, error) { var object model.Cluster if err := c.db.WithContext(ctx).Where("name = ?", name).First(&object).Error; err != nil { if errors.IsRecordNotFound(err) { return nil, nil } return nil, err } return &object, nil } func (c *cluster) UpdateByPlan(ctx context.Context, planId int64, updates map[string]interface{}) error { updates["gmt_modified"] = time.Now() f := c.db.WithContext(ctx).Model(&model.Cluster{}).Where("plan_id = ?", planId).Updates(updates) if f.Error != nil { return f.Error } if f.RowsAffected == 0 { return errors.ErrRecordNotUpdate } return nil } func newCluster(db *gorm.DB) ClusterInterface { return &cluster{db} } ================================================ FILE: pkg/db/factory.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package db import ( "gorm.io/gorm" ) type ShareDaoFactory interface { Cluster() ClusterInterface Tenant() TenantInterface User() UserInterface Plan() PlanInterface Audit() AuditInterface Repository() RepositoryInterface } type shareDaoFactory struct { db *gorm.DB } func (f *shareDaoFactory) Cluster() ClusterInterface { return newCluster(f.db) } func (f *shareDaoFactory) Tenant() TenantInterface { return newTenant(f.db) } func (f *shareDaoFactory) User() UserInterface { return newUser(f.db) } func (f *shareDaoFactory) Plan() PlanInterface { return newPlan(f.db) } func (f *shareDaoFactory) Audit() AuditInterface { return newAudit(f.db) } func (f *shareDaoFactory) Repository() RepositoryInterface { return newRepository(f.db) } func NewDaoFactory(db *gorm.DB, migrate bool) (ShareDaoFactory, error) { if migrate { // 自动创建指定模型的数据库表结构 if err := newMigrator(db).AutoMigrate(); err != nil { return nil, err } } return &shareDaoFactory{ db: db, }, nil } ================================================ FILE: pkg/db/logger.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package db import ( "context" "time" "gorm.io/gorm/logger" ) type ( SQLs []string DBLogger struct { logger.LogLevel SlowThreshold time.Duration // slow SQL queries } ) const SQLContextKey = "sqls" func NewLogger(level logger.LogLevel, slowThreshold time.Duration) *DBLogger { return &DBLogger{ LogLevel: level, SlowThreshold: slowThreshold, } } func (l *DBLogger) LogMode(level logger.LogLevel) logger.Interface { l.LogLevel = level return l } func (l *DBLogger) Info(ctx context.Context, msg string, data ...interface{}) {} func (l *DBLogger) Warn(ctx context.Context, msg string, data ...interface{}) {} func (l *DBLogger) Error(ctx context.Context, msg string, data ...interface{}) {} func (l *DBLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { if l.LogLevel <= logger.Silent { return } sql, _ := fc() if v := ctx.Value(SQLContextKey); v != nil { sqls := v.(*SQLs) *sqls = append(*sqls, sql) } } func WithDBContext(ctx context.Context) context.Context { return context.WithValue(ctx, SQLContextKey, new(SQLs)) } // GetSQLs returns all the SQL statements executed in the current context. func GetSQLs(ctx context.Context) SQLs { if v := ctx.Value(SQLContextKey); v != nil { return *v.(*SQLs) } return SQLs{} } ================================================ FILE: pkg/db/migrator.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package db import ( "github.com/caoyingjunz/pixiu/pkg/db/model" "gorm.io/gorm" ) type migrator struct { db *gorm.DB } // AutoMigrate 自动创建指定模型的数据库表结构 func (m *migrator) AutoMigrate() error { return m.CreateTables(model.GetMigrationModels()...) } func (m *migrator) CreateTables(dst ...interface{}) error { db := m.db.Set("gorm:table_options", "AUTO_INCREMENT=20220801 DEFAULT CHARSET=utf8") for _, d := range dst { if db.Migrator().HasTable(d) { continue } if err := db.Migrator().CreateTable(d); err != nil { return err } } return nil } func newMigrator(db *gorm.DB) *migrator { return &migrator{db} } ================================================ FILE: pkg/db/model/audit.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package model import ( "fmt" "github.com/caoyingjunz/pixiu/pkg/db/model/pixiu" ) func init() { register(&Audit{}) } type AuditOperationStatus uint8 const ( AuditOpFail AuditOperationStatus = iota // 执行失败 AuditOpSuccess // 执行成功 AuditOpUnknown // 获取执行状态失败 ) func (s AuditOperationStatus) String() string { switch s { case AuditOpFail: return "failed" case AuditOpSuccess: return "succeed" default: return "unknown" } } type Audit struct { pixiu.Model RequestId string `gorm:"column:request_id;type:varchar(32);index" json:"request_id"` // 请求 ID Ip string `gorm:"type:varchar(128)" json:"ip"` // 客户端 IP Action string `gorm:"type:varchar(255)" json:"action"` // HTTP 方法 [POST/DELETE/PUT/GET] Operator string `gorm:"type:varchar(255)" json:"operator"` // 操作人 ID Path string `gorm:"type:varchar(255)" json:"path"` // HTTP 路径 ObjectType ObjectType `gorm:"column:resource_type;type:varchar(128)" json:"resource_type"` // 操作资源类型 [cluster/plan...] Status AuditOperationStatus `gorm:"type:tinyint" json:"status"` // 记录操作运行结果[OperationStatus] Duration int64 `gorm:"column:duration;type:bigint;default:0" json:"duration"` // 请求耗时 ms ResponseCode int `gorm:"column:response_code;type:int;default:0" json:"response_code"` // HTTP 响应码 Cluster string `gorm:"column:cluster;type:varchar(255)" json:"cluster"` // K8s 集群名 ResourceName string `gorm:"column:resource_name;type:varchar(255)" json:"resource_name"` // 资源名称 ResourceNamespace string `gorm:"column:resource_namespace;type:varchar(255)" json:"resource_namespace"` // 资源命名空间 } func (a *Audit) String() string { return fmt.Sprintf("user %s(ip addr: %s) access %s with %s then %s (duration: %dms)", a.Operator, a.Ip, a.Path, a.Action, a.Status.String(), a.Duration) } func (a *Audit) TableName() string { return "audits" } ================================================ FILE: pkg/db/model/cluster.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package model import "github.com/caoyingjunz/pixiu/pkg/db/model/pixiu" func init() { register(&Cluster{}) } // ClusterType Kubernetes 集群的类型 type ClusterType uint8 const ( ClusterTypeStandard ClusterType = iota // 标准集群 ClusterTypeCustom // 自建集群 ) type ClusterStatus uint8 const ( ClusterStatusRunning ClusterStatus = iota // 运行中 ClusterStatusDeploy // 部署中 ClusterStatusUnStart // 等待部署 ClusterStatusFailed // 部署失败 ClusterStatusError // 集群失联,API不可用 ) // Cluster kubernetes 集群信息 type Cluster struct { pixiu.Model // 集群名称,全局唯一 Name string `gorm:"index:idx_name,unique" json:"name"` // 集群别名,可以重复,允许为中文 AliasName string `json:"alias_name"` // 0:标准集群 1: 自建集群 ClusterType `gorm:"type:tinyint" json:"cluster_type"` // 自建集群关联的 PlanId PlanId int64 // 集群运行状态 0: 运行中 1: 部署中 2: 等待部署 3: 部署失败 4: 运行中断 5: 所有的 node 不健康 ClusterStatus `gorm:"column:status;type:tinyint" json:"status"` // 集群的版本 KubernetesVersion string `gorm:"type:varchar(255)" json:"kubernetes_version,omitempty"` // 集群节点健康数,json 字符串 Nodes string `gorm:"type:text" json:"nodes"` // 集群删除保护,开启集群删除保护时不允许删除集群 // 0: 关闭集群删除保护 1: 开启集群删除保护 Protected bool `json:"protected"` // k8s kubeConfig base64 字段 KubeConfig string `json:"kube_config"` // 集群用途描述,可以为空 Description string `gorm:"type:text" json:"description"` } func (*Cluster) TableName() string { return "clusters" } ================================================ FILE: pkg/db/model/model.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package model var models = make([]interface{}, 0) func register(model ...interface{}) { models = append(models, model...) } // GetMigrationModels is a helper function returns all models for table initalization. func GetMigrationModels() []interface{} { return models } ================================================ FILE: pkg/db/model/pixiu/model.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package pixiu import ( "strconv" "time" ) type Model struct { Id int64 `gorm:"column:id;primaryKey;autoIncrement;not null" json:"id"` GmtCreate time.Time `gorm:"column:gmt_create;type:datetime;default:current_timestamp;not null" json:"gmt_create"` GmtModified time.Time `gorm:"column:gmt_modified;type:datetime;default:current_timestamp;not null" json:"gmt_modified"` ResourceVersion int64 `gorm:"column:resource_version;default:0;not null" json:"resource_version"` } func (m Model) GetSID() string { return strconv.FormatInt(m.Id, 10) } ================================================ FILE: pkg/db/model/plan.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package model import ( "github.com/caoyingjunz/pixiu/pkg/db/model/pixiu" ) func init() { register(&Plan{}, &Node{}, &Config{}, &Task{}) } type Plan struct { pixiu.Model Name string `gorm:"index:idx_name,unique" json:"name"` Description string `gorm:"type:text" json:"description"` } func (plan *Plan) TableName() string { return "plans" } type KubeRole string const ( MasterRole string = "master" // kubernetes master role NodeRole string = "node" // kubernetes node role ) type CRI string const ( DockerCRI CRI = "docker" ContainerdCRI CRI = "containerd" ) type Node struct { pixiu.Model Name string `json:"name"` // 主机名,相同plan内不允许重复 PlanId int64 `json:"plan_id"` Role string `json:"role"` // k8s 节点的角色,master 和 node CRI CRI `json:"cri"` Ip string `json:"ip"` Auth string `json:"auth"` } func (node *Node) TableName() string { return "nodes" } type Config struct { pixiu.Model PlanId int64 `json:"plan_id"` Region string `json:"region"` OSImage string `json:"os_image"` Kubernetes string `json:"kubernetes"` Network string `json:"network"` Runtime string `json:"runtime"` Component string `json:"component"` } func (config *Config) TableName() string { return "configs" } type PlanStep int const ( UnStartedPlanStep PlanStep = iota RunningPlanStep FailedPlanStep CompletedPlanStep ) type TaskStatus string const ( FailedPlanStatus TaskStatus = "已失败" SuccessPlanStatus TaskStatus = "已成功" UnStartPlanStatus TaskStatus = "未开始" RunningPlanStatus TaskStatus = "运行中" ) type Task struct { pixiu.Model Name string `json:"name"` PlanId int64 `json:"plan_id"` Step PlanStep `json:"step"` Status TaskStatus `json:"status"` Message string `json:"message"` } func (task *Task) TableName() string { return "tasks" } ================================================ FILE: pkg/db/model/rbac.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package model import ( "reflect" "strconv" "github.com/caoyingjunz/pixiu/pkg/db/model/pixiu" "github.com/caoyingjunz/pixiu/pkg/util" ) const ( AdminGroup = "root" SidAll = "*" ) type Operation string const ( OpRead Operation = "read" OpCreate Operation = "create" OpUpdate Operation = "update" OpDelete Operation = "delete" OpAll Operation = "*" ) func (o Operation) String() string { return string(o) } var OperationMap = map[Operation]struct{}{ OpRead: {}, OpCreate: {}, OpUpdate: {}, OpDelete: {}, OpAll: {}, } type ObjectType string const ( ObjectUser ObjectType = "users" ObjectCluster ObjectType = "clusters" ObjectTenant ObjectType = "tenants" ObjectPlan ObjectType = "plans" ObjectAuth ObjectType = "auth" ObjectAll ObjectType = "*" ) func (o ObjectType) String() string { return string(o) } var ObjectTypeMap = map[ObjectType]struct{}{ ObjectUser: {}, ObjectCluster: {}, ObjectTenant: {}, ObjectPlan: {}, ObjectAuth: {}, ObjectAll: {}, } // TODO: type RBACInterface interface{} // Casbin RBAC model // ref: https://github.com/casbin/casbin/blob/master/examples/rbac_model.conf const RBACModel = ` [request_definition] r = sub, obj, id, op [policy_definition] p = sub, obj, id, op [role_definition] g = _, _ [policy_effect] e = some(where (p.eft == allow)) [matchers] m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && keyMatch(r.id, p.id) && keyMatch(r.op, p.op)` // TODO: type CasbinRBACImpl struct{} type Policy interface { Raw() []string } // UserPolicy is a RBAC policy for user. // e.g. ["foo", "clusters", "*", "read"] type UserPolicy [4]string // NewUserPolicy returns a policy slice for user. // e.g. ["foo", "clusters", "*", "read"]: foo is a user name func NewUserPolicy(userName string, obj ObjectType, sid string, op Operation) UserPolicy { return UserPolicy{userName, obj.String(), sid, op.String()} } func (p UserPolicy) Raw() []string { return p[:] } func (p UserPolicy) GetUserName() string { return p[0] } func (p UserPolicy) GetObjectType() ObjectType { return ObjectType(p[1]) } func (p UserPolicy) GetSID() string { return p[2] } func (p UserPolicy) GetOperation() Operation { return Operation(p[3]) } // GroupPolicy is a RBAC policy for group. // e.g. ["master", "clusters", "*", "*"] type GroupPolicy [4]string // NewGroupPolicy returns a policy slice for group. // e.g. ["master", "clusters", "*", "*"]: master is a group name func NewGroupPolicy(groupName string, obj ObjectType, sid string, op Operation) GroupPolicy { return GroupPolicy{groupName, obj.String(), sid, op.String()} } func (p GroupPolicy) Raw() []string { return p[:] } func (p GroupPolicy) GetGroupName() string { return p[0] } func (p GroupPolicy) GetObjectType() ObjectType { return ObjectType(p[1]) } func (p GroupPolicy) GetSID() string { return p[2] } func (p GroupPolicy) GetOperation() Operation { return Operation(p[3]) } // GroupBinding binds a user to a group. // e.g. ["foo", "master"]: user foo belongs to group master type GroupBinding [2]string // NewGroupBinding returns a binding slice for relationship between user and group. func NewGroupBinding(userName, groupName string) GroupBinding { return GroupBinding{userName, groupName} } func (p GroupBinding) Raw() []string { return p[:] } func (p GroupBinding) GetUserName() string { return p[0] } func (p GroupBinding) GetGroupName() string { return p[1] } // AdminPolicy is the specific policy for admin/root user. var AdminPolicy = NewGroupPolicy(AdminGroup, ObjectAll, SidAll, OpAll) // IsAdminPolicy returns true if the policy is the admin policy. func IsAdminPolicy(policy Policy) bool { switch p := policy.(type) { case GroupPolicy: return reflect.DeepEqual(p.Raw(), AdminPolicy.Raw()) default: return false } } // BindingToAdmin returns true if policy binding to admin group exists. func BindingToAdmin(policies []GroupBinding) bool { for _, policy := range policies { if policy.GetGroupName() == AdminGroup { return true } } return false } // NewPolicyFromModels returns a policy slice. // e.g. ["foo", "clusters", "*", "*"] func NewPolicyFromModels(user *User, obj ObjectType, model pixiu.Model, op Operation) Policy { return NewUserPolicy(user.Name, obj, model.GetSID(), op) } // NOTE: GetIdRangeFromPolicy is only used for listing API request. // GetIdRangeFromPolicy returns true and an empty list when policy with all operation(*) are allowed exists, // otherwise it returns false and a list of object IDs. func GetIdRangeFromPolicy(policies []Policy) (all bool, ids []int64) { ids = make([]int64, 0) for _, policy := range policies { if _, ok := policy.(GroupBinding); ok { continue } raw := policy.Raw() // e.g. ["foo", "clusters", "*", "read"] sid := raw[2] switch sid { case "": continue case SidAll: // permit to read all return true, []int64{} } // operation if !(raw[3] == OpRead.String() || raw[3] == OpAll.String()) { continue } id, err := strconv.ParseInt(sid, 10, 64) if err != nil { // invalid sid continue } ids = append(ids, id) } return false, util.DeduplicateIntSlice(ids) } ================================================ FILE: pkg/db/model/rbac_test.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package model import ( "reflect" "strconv" "testing" ) func TestGetIdRangeFromPolicies(t *testing.T) { type args struct { policies []Policy } tests := []struct { name string args args wantAll bool wantIds []int64 }{ { name: "case 1", args: args{ policies: []Policy{}, }, wantAll: false, wantIds: []int64{}, }, { name: "case 2", args: args{ policies: []Policy{ NewUserPolicy("foo", ObjectCluster, strconv.Itoa(1), OpRead), }, }, wantAll: false, wantIds: []int64{1}, }, { name: "case 3", args: args{ policies: []Policy{ NewUserPolicy("foo", ObjectCluster, strconv.Itoa(1), OpRead), NewUserPolicy("foo", ObjectCluster, strconv.Itoa(2), OpRead), }, }, wantAll: false, wantIds: []int64{1, 2}, }, { name: "case 4", args: args{ policies: []Policy{ NewUserPolicy("foo", ObjectCluster, "", OpRead), }, }, wantAll: false, wantIds: []int64{}, }, { name: "case 5", args: args{ policies: []Policy{ NewUserPolicy("foo", ObjectCluster, "*", OpRead), }, }, wantAll: true, wantIds: []int64{}, }, { name: "case 6", args: args{ policies: []Policy{ NewUserPolicy("foo", ObjectCluster, strconv.Itoa(1), OpRead), NewUserPolicy("foo", ObjectCluster, "*", OpRead), }, }, wantAll: true, wantIds: []int64{}, }, { name: "case 7", args: args{ policies: []Policy{ NewUserPolicy("foo", ObjectCluster, strconv.Itoa(1), OpRead), NewUserPolicy("foo", ObjectCluster, strconv.Itoa(1), OpRead), }, }, wantAll: false, wantIds: []int64{1}, }, { name: "case 8", args: args{ policies: []Policy{ NewUserPolicy("foo", ObjectCluster, strconv.Itoa(1), OpRead), NewUserPolicy("foo", ObjectCluster, strconv.Itoa(1), OpRead), NewUserPolicy("foo", ObjectCluster, "*", OpRead), }, }, wantAll: true, wantIds: []int64{}, }, { name: "case 9", args: args{ policies: []Policy{ NewUserPolicy("foo", ObjectCluster, strconv.Itoa(1), OpDelete), NewUserPolicy("foo", ObjectCluster, strconv.Itoa(2), OpUpdate), NewUserPolicy("foo", ObjectCluster, strconv.Itoa(3), OpRead), }, }, wantAll: false, wantIds: []int64{3}, }, { name: "case 10", args: args{ policies: []Policy{ NewUserPolicy("foo", ObjectCluster, strconv.Itoa(1), OpDelete), NewUserPolicy("foo", ObjectCluster, strconv.Itoa(2), OpUpdate), NewUserPolicy("foo", ObjectCluster, strconv.Itoa(3), OpRead), NewUserPolicy("foo", ObjectCluster, "*", OpRead), }, }, wantAll: true, wantIds: []int64{}, }, { name: "case 11", args: args{ policies: []Policy{ NewUserPolicy("foo", ObjectCluster, strconv.Itoa(1), OpDelete), NewUserPolicy("foo", ObjectCluster, strconv.Itoa(2), OpUpdate), NewUserPolicy("foo", ObjectCluster, strconv.Itoa(3), OpRead), }, }, wantAll: false, wantIds: []int64{3}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotAll, gotIds := GetIdRangeFromPolicy(tt.args.policies) if gotAll != tt.wantAll { t.Errorf("GetIdRangeFromPolicies() gotAll = %v, want %v", gotAll, tt.wantAll) } if !reflect.DeepEqual(gotIds, tt.wantIds) { t.Errorf("GetIdRangeFromPolicies() gotIds = %v, want %v", gotIds, tt.wantIds) } }) } } func TestIsAdminPolicy(t *testing.T) { type args struct { policy Policy } tests := []struct { name string args args want bool }{ { name: "case 1", args: args{ policy: NewUserPolicy("foo", ObjectCluster, "*", OpRead), }, want: false, }, { name: "case 1", args: args{ policy: AdminPolicy, }, want: true, }, { name: "case 2", args: args{ policy: GroupPolicy{}, }, want: false, }, { name: "case 3", args: args{ policy: NewGroupPolicy("*", "*", "*", "*"), }, want: false, }, { name: "case 4", args: args{ policy: NewGroupPolicy("*", "root", "*", "*"), }, want: false, }, { name: "case 5", args: args{ policy: NewGroupPolicy("*", "*", "root", "*"), }, want: false, }, { name: "case 6", args: args{ policy: NewGroupPolicy("*", "*", "*", "root"), }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := IsAdminPolicy(tt.args.policy); got != tt.want { t.Errorf("IsAdminPolicy() = %v, want %v", got, tt.want) } }) } } func TestBindingToAdmin(t *testing.T) { type args struct { policies []GroupBinding } tests := []struct { name string args args want bool }{ { name: "case 1", args: args{}, want: false, }, { name: "case 2", args: args{ policies: []GroupBinding{ NewGroupBinding("foo", "bar"), }, }, want: false, }, { name: "case 3", args: args{ policies: []GroupBinding{ NewGroupBinding("foo", AdminGroup), }, }, want: true, }, { name: "case 4", args: args{ policies: []GroupBinding{ NewGroupBinding("foo", "bar"), NewGroupBinding("foo", AdminGroup), }, }, want: true, }, { name: "case 5", args: args{ policies: []GroupBinding{ NewGroupBinding("foo", "bar"), NewGroupBinding("foo", "baz"), }, }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := BindingToAdmin(tt.args.policies); got != tt.want { t.Errorf("HasAdminGroupPolicy() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/db/model/repository.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package model import ( "time" "github.com/caoyingjunz/pixiu/pkg/db/model/pixiu" ) func init() { register(&Repository{}) } type Repository struct { pixiu.Model Name string `gorm:"column:name; index:idx_name,unique; not null" json:"name"` URL string `gorm:"column:url;not null" json:"url"` Username string `gorm:"column:username" json:"username"` Password string `gorm:"column:password" json:"password"` } func (*Repository) TableName() string { return "repositories" } type ChartIndex struct { APIVersion string `json:"apiVersion"` Entries Entries `json:"entries"` } type Entries map[string][]ChartVersion type ChartVersion struct { Annotations map[string]string `json:"annotations"` APIVersion string `json:"apiVersion"` AppVersion string `json:"appVersion"` Created time.Time `json:"created"` Dependencies []Dependency `json:"dependencies"` Description string `json:"description"` Digest string `json:"digest"` Icon string `json:"icon"` Maintainers []Maintainer `json:"maintainers"` Name string `json:"name"` Sources []string `json:"sources"` Type string `json:"type"` URLs []string `json:"urls"` Version string `json:"version"` } type Dependency struct { Condition string `json:"condition"` Name string `json:"name"` Repository string `json:"repository"` Version string `json:"version"` Alias string `json:"alias,omitempty"` } type Maintainer struct { Name string `json:"name"` } ================================================ FILE: pkg/db/model/tenant.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package model import "github.com/caoyingjunz/pixiu/pkg/db/model/pixiu" func init() { register(&Tenant{}) } type Tenant struct { pixiu.Model Name string `gorm:"index:idx_name,unique" json:"name"` Description string `gorm:"type:text" json:"description"` Extension string `gorm:"type:text" json:"extension,omitempty"` } func (tenant *Tenant) TableName() string { return "tenants" } ================================================ FILE: pkg/db/model/user.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package model import "github.com/caoyingjunz/pixiu/pkg/db/model/pixiu" func init() { register(&User{}) } type UserRole uint8 const ( RoleUser UserRole = iota // 普通用户 RoleAdmin // 管理员 RoleRoot // 超级管理员 ) type UserStatus uint8 // TODO type User struct { pixiu.Model Name string `gorm:"index:idx_name,unique" json:"name"` Password string `gorm:"type:varchar(256)" json:"-"` Status UserStatus `gorm:"type:tinyint" json:"status"` Role UserRole `gorm:"type:tinyint" json:"role"` Email string `gorm:"type:varchar(128)" json:"email"` Phone string `gorm:"column:phone;type:varchar(32)" json:"phone"` Description string `gorm:"type:text" json:"description"` Extension string `gorm:"type:text" json:"extension,omitempty"` } func (user *User) TableName() string { return "users" } ================================================ FILE: pkg/db/options.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package db import ( "time" "gorm.io/gorm" ) type Options func(*gorm.DB) *gorm.DB func WithOrderByASC() Options { return func(tx *gorm.DB) *gorm.DB { return tx.Order("id ASC") } } func WithOrderByDesc() Options { return func(tx *gorm.DB) *gorm.DB { return tx.Order("id DESC") } } func WithOffset(offset int) Options { return func(tx *gorm.DB) *gorm.DB { return tx.Offset(offset) } } func WithCreatedBefore(t time.Time) Options { return func(tx *gorm.DB) *gorm.DB { return tx.Where("gmt_create < ?", t) } } func WithLimit(limit int) Options { return func(tx *gorm.DB) *gorm.DB { if limit == 0 { // `LIMIT 0` statement will return 0 rows, it's meaningless. return tx } return tx.Limit(limit) } } func WithIDIn(ids ...int64) Options { return func(tx *gorm.DB) *gorm.DB { // e.g. `WHERE id IN (1, 2, 3)` return tx.Where("id IN ?", ids) } } func WithAliasNameLike(name string) Options { return func(tx *gorm.DB) *gorm.DB { if name == "" { return tx } return tx.Where("alias_name like ?", "%"+name+"%") } } func WithClusterStatus(status int) Options { return func(tx *gorm.DB) *gorm.DB { return tx.Where("status = ?", status) } } func WithUserNameLike(name string) Options { return func(tx *gorm.DB) *gorm.DB { if name == "" { return tx } return tx.Where("name like ?", "%"+name+"%") } } func WithUserPhoneLike(phone string) Options { return func(tx *gorm.DB) *gorm.DB { if phone == "" { return tx } return tx.Where("phone like ?", "%"+phone+"%") } } func WithUserEmailLike(email string) Options { return func(tx *gorm.DB) *gorm.DB { if email == "" { return tx } return tx.Where("email like ?", "%"+email+"%") } } func WithUserStatus(status int) Options { return func(tx *gorm.DB) *gorm.DB { return tx.Where("status = ?", status) } } func WithAuditOperatorLike(operator string) Options { return func(tx *gorm.DB) *gorm.DB { if operator == "" { return tx } return tx.Where("operator like ?", "%"+operator+"%") } } func WithAuditAction(action string) Options { return func(tx *gorm.DB) *gorm.DB { if action == "" { return tx } return tx.Where("action = ?", action) } } func WithAuditObjectType(ot string) Options { return func(tx *gorm.DB) *gorm.DB { if ot == "" { return tx } return tx.Where("resource_type = ?", ot) } } func WithAuditStatus(status uint8) Options { return func(tx *gorm.DB) *gorm.DB { return tx.Where("status = ?", status) } } func WithAuditCreatedAfter(t time.Time) Options { return func(tx *gorm.DB) *gorm.DB { if t.IsZero() { return tx } return tx.Where("gmt_create >= ?", t) } } func WithAuditCluster(cluster string) Options { return func(tx *gorm.DB) *gorm.DB { return tx.Where("cluster = ?", cluster) } } func WithPlanNameLike(name string) Options { return func(tx *gorm.DB) *gorm.DB { if name == "" { return tx } return tx.Where("name like ?", "%"+name+"%") } } ================================================ FILE: pkg/db/plan.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package db import ( "context" "time" "gorm.io/gorm" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/util/errors" ) type PlanInterface interface { Create(ctx context.Context, object *model.Plan, opts ...CreatePlanOption) (*model.Plan, error) Update(ctx context.Context, pid int64, resourceVersion int64, updates map[string]interface{}) error Delete(ctx context.Context, pid int64) (*model.Plan, error) Get(ctx context.Context, pid int64) (*model.Plan, error) List(ctx context.Context, opts ...Options) ([]model.Plan, error) Count(ctx context.Context, opts ...Options) (int64, error) CreateNode(ctx context.Context, object *model.Node) (*model.Node, error) TxCreateNode(ctx context.Context, tx *gorm.DB, object *model.Node) error UpdateNode(ctx context.Context, nodeId int64, resourceVersion int64, updates map[string]interface{}) error DeleteNode(ctx context.Context, nodeId int64) (*model.Node, error) GetNode(ctx context.Context, nodeId int64) (*model.Node, error) ListNodes(ctx context.Context, pid int64, opts ...Options) ([]model.Node, error) DeleteNodesByPlan(ctx context.Context, planId int64) error GetNodeByName(ctx context.Context, planId int64, name string) (*model.Node, error) DeleteNodesByNames(ctx context.Context, planId int64, names []string) error CreateConfig(ctx context.Context, object *model.Config) (*model.Config, error) TxCreateConfig(ctx context.Context, tx *gorm.DB, object *model.Config) error UpdateConfig(ctx context.Context, cfgId int64, resourceVersion int64, updates map[string]interface{}) error DeleteConfig(ctx context.Context, cfgId int64) (*model.Config, error) GetConfig(ctx context.Context, cfgId int64) (*model.Config, error) ListConfigs(ctx context.Context, opts ...Options) ([]model.Config, error) DeleteConfigByPlan(ctx context.Context, planId int64) error GetConfigByPlan(ctx context.Context, planId int64) (*model.Config, error) CreateTask(ctx context.Context, object *model.Task) (*model.Task, error) UpdateTask(ctx context.Context, pid int64, name string, updates map[string]interface{}) (*model.Task, error) DeleteTask(ctx context.Context, pid int64) error ListTasks(ctx context.Context, pid int64, opts ...Options) ([]model.Task, error) GetNewestTask(ctx context.Context, pid int64) (*model.Task, error) GetTaskByName(ctx context.Context, planId int64, name string) (*model.Task, error) GetTaskById(ctx context.Context, taskId int64) (*model.Task, error) } type plan struct { db *gorm.DB } type CreatePlanOption func(plan *model.Plan, tx *gorm.DB) (*gorm.DB, error) func (p *plan) Create(ctx context.Context, object *model.Plan, opts ...CreatePlanOption) (*model.Plan, error) { now := time.Now() object.GmtCreate = now object.GmtModified = now if len(opts) == 0 { // no transaction if err := p.db.WithContext(ctx).Create(object).Error; err != nil { return nil, err } return object, nil } if err := p.db.WithContext(ctx).Transaction(func(tx *gorm.DB) (err error) { if err = tx.Create(object).Error; err != nil { return } for _, opt := range opts { if tx, err = opt(object, tx); err != nil { return } } return }); err != nil { return nil, err } return object, nil } func (p *plan) Update(ctx context.Context, pid int64, resourceVersion int64, updates map[string]interface{}) error { // 系统维护字段 updates["gmt_modified"] = time.Now() updates["resource_version"] = resourceVersion + 1 f := p.db.WithContext(ctx).Model(&model.Plan{}).Where("id = ? and resource_version = ?", pid, resourceVersion).Updates(updates) if f.Error != nil { return f.Error } if f.RowsAffected == 0 { return errors.ErrRecordNotFound } return nil } func (p *plan) Delete(ctx context.Context, pid int64) (*model.Plan, error) { object, err := p.Get(ctx, pid) if err != nil { return nil, err } if err = p.db.WithContext(ctx).Where("id = ?", pid).Delete(&model.Plan{}).Error; err != nil { return nil, err } return object, nil } func (p *plan) Get(ctx context.Context, pid int64) (*model.Plan, error) { var object model.Plan if err := p.db.WithContext(ctx).Where("id = ?", pid).First(&object).Error; err != nil { return nil, err } return &object, nil } func (p *plan) List(ctx context.Context, opts ...Options) ([]model.Plan, error) { var objects []model.Plan tx := p.db.WithContext(ctx) for _, opt := range opts { tx = opt(tx) } if err := tx.Find(&objects).Error; err != nil { return nil, err } return objects, nil } func (p *plan) Count(ctx context.Context, opts ...Options) (int64, error) { var count int64 tx := p.db.WithContext(ctx).Model(&model.Plan{}) for _, opt := range opts { tx = opt(tx) } if err := tx.Count(&count).Error; err != nil { return 0, err } return count, nil } func (p *plan) CreateNode(ctx context.Context, object *model.Node) (*model.Node, error) { now := time.Now() object.GmtCreate = now object.GmtModified = now if err := p.db.WithContext(ctx).Create(object).Error; err != nil { return nil, err } return object, nil } // TxCreateNode creates a node object in the given transaction. func (p *plan) TxCreateNode(ctx context.Context, tx *gorm.DB, object *model.Node) error { now := time.Now() object.GmtCreate = now object.GmtModified = now return tx.WithContext(ctx).Create(object).Error } func (p *plan) UpdateNode(ctx context.Context, nodeId int64, resourceVersion int64, updates map[string]interface{}) error { // 系统维护字段 updates["gmt_modified"] = time.Now() updates["resource_version"] = resourceVersion + 1 f := p.db.WithContext(ctx).Model(&model.Node{}).Where("id = ? and resource_version = ?", nodeId, resourceVersion).Updates(updates) if f.Error != nil { return f.Error } if f.RowsAffected == 0 { return errors.ErrRecordNotFound } return nil } func (p *plan) DeleteNode(ctx context.Context, nodeId int64) (*model.Node, error) { object, err := p.GetNode(ctx, nodeId) if err != nil { return nil, err } if err = p.db.WithContext(ctx).Where("id = ?", nodeId).Delete(&model.Node{}).Error; err != nil { return nil, err } return object, nil } func (p *plan) DeleteNodesByPlan(ctx context.Context, planId int64) error { if err := p.db.WithContext(ctx).Where("plan_id = ?", planId).Delete(&model.Node{}).Error; err != nil { return err } return nil } func (p *plan) DeleteNodesByNames(ctx context.Context, planId int64, names []string) error { if err := p.db.WithContext(ctx).Where("plan_id = ? and name in (?)", planId, names).Delete(&model.Node{}).Error; err != nil { return err } return nil } func (p *plan) GetNodeByName(ctx context.Context, planId int64, name string) (*model.Node, error) { var object model.Node if err := p.db.WithContext(ctx).Where("plan_id = ? and name = ?", planId, name).First(&object).Error; err != nil { return nil, err } return &object, nil } func (p *plan) GetNode(ctx context.Context, nodeId int64) (*model.Node, error) { var object model.Node if err := p.db.WithContext(ctx).Where("id = ?", nodeId).First(&object).Error; err != nil { return nil, err } return &object, nil } func (p *plan) ListNodes(ctx context.Context, pid int64, opts ...Options) ([]model.Node, error) { var objects []model.Node tx := p.db.WithContext(ctx).Where("plan_id = ?", pid) for _, opt := range opts { tx = opt(tx) } if err := tx.Find(&objects).Error; err != nil { return nil, err } return objects, nil } func (p *plan) CreateConfig(ctx context.Context, object *model.Config) (*model.Config, error) { now := time.Now() object.GmtCreate = now object.GmtModified = now if err := p.db.WithContext(ctx).Create(object).Error; err != nil { return nil, err } return object, nil } // TxCreateConfig creates a config object in the given transaction. func (p *plan) TxCreateConfig(ctx context.Context, tx *gorm.DB, object *model.Config) error { now := time.Now() object.GmtCreate = now object.GmtModified = now return tx.WithContext(ctx).Create(object).Error } func (p *plan) UpdateConfig(ctx context.Context, cid int64, resourceVersion int64, updates map[string]interface{}) error { // 系统维护字段 updates["gmt_modified"] = time.Now() updates["resource_version"] = resourceVersion + 1 f := p.db.WithContext(ctx).Model(&model.Config{}).Where("id = ? and resource_version = ?", cid, resourceVersion).Updates(updates) if f.Error != nil { return f.Error } if f.RowsAffected == 0 { return errors.ErrRecordNotFound } return nil } func (p *plan) DeleteConfig(ctx context.Context, cid int64) (*model.Config, error) { object, err := p.GetConfig(ctx, cid) if err != nil { return nil, err } if err = p.db.WithContext(ctx).Where("id = ?", cid).Delete(&model.Config{}).Error; err != nil { return nil, err } return object, nil } func (p *plan) DeleteConfigByPlan(ctx context.Context, planId int64) error { if err := p.db.WithContext(ctx).Where("plan_id = ?", planId).Delete(&model.Config{}).Error; err != nil { return err } return nil } func (p *plan) GetConfig(ctx context.Context, cid int64) (*model.Config, error) { var object model.Config if err := p.db.WithContext(ctx).Where("id = ?", cid).First(&object).Error; err != nil { return nil, err } return &object, nil } func (p *plan) ListConfigs(ctx context.Context, opts ...Options) ([]model.Config, error) { var objects []model.Config tx := p.db.WithContext(ctx) for _, opt := range opts { tx = opt(tx) } if err := tx.Find(&objects).Error; err != nil { return nil, err } return objects, nil } func (p *plan) GetConfigByPlan(ctx context.Context, planId int64) (*model.Config, error) { var object model.Config if err := p.db.WithContext(ctx).Where("plan_id = ?", planId).First(&object).Error; err != nil { return nil, err } return &object, nil } func (p *plan) CreateTask(ctx context.Context, object *model.Task) (*model.Task, error) { now := time.Now() object.GmtCreate = now object.GmtModified = now if err := p.db.WithContext(ctx).Create(object).Error; err != nil { return nil, err } return object, nil } func (p *plan) UpdateTask(ctx context.Context, pid int64, name string, updates map[string]interface{}) (*model.Task, error) { f := p.db.WithContext(ctx).Model(&model.Task{}).Where("plan_id = ? and name = ?", pid, name).Updates(updates) if f.Error != nil { return nil, f.Error } if f.RowsAffected == 0 { return nil, errors.ErrRecordNotFound } return p.GetTaskByName(ctx, pid, name) } func (p *plan) DeleteTask(ctx context.Context, pid int64) error { if err := p.db.WithContext(ctx).Where("plan_id = ?", pid).Delete(&model.Task{}).Error; err != nil { return err } return nil } func (p *plan) ListTasks(ctx context.Context, pid int64, opts ...Options) ([]model.Task, error) { var objects []model.Task tx := p.db.WithContext(ctx).Where("plan_id = ?", pid) for _, opt := range opts { tx = opt(tx) } if err := tx.Find(&objects).Error; err != nil { return nil, err } return objects, nil } func (p *plan) GetNewestTask(ctx context.Context, pid int64) (*model.Task, error) { var objects []model.Task if err := p.db.WithContext(ctx).Where("plan_id = ?", pid).Order("id DESC").Limit(1).Find(&objects).Error; err != nil { return nil, err } if len(objects) == 0 { return nil, errors.ErrRecordNotFound } return &objects[0], nil } func (p *plan) GetTaskById(ctx context.Context, taskId int64) (*model.Task, error) { var object model.Task if err := p.db.WithContext(ctx).Where("id = ?", taskId).First(&object).Error; err != nil { return nil, err } return &object, nil } func (p *plan) GetTaskByName(ctx context.Context, planId int64, name string) (*model.Task, error) { var object model.Task if err := p.db.WithContext(ctx).Where("plan_id = ? and name = ?", planId, name).First(&object).Error; err != nil { return nil, err } return &object, nil } func newPlan(db *gorm.DB) *plan { return &plan{db} } ================================================ FILE: pkg/db/repository.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package db import ( "context" "time" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/util/errors" "gorm.io/gorm" ) type RepositoryInterface interface { Create(ctx context.Context, object *model.Repository) (*model.Repository, error) Update(ctx context.Context, id int64, resourceVersion int64, updates map[string]interface{}) error Delete(ctx context.Context, id int64) error Get(ctx context.Context, id int64) (*model.Repository, error) GetByName(ctx context.Context, name string) (*model.Repository, error) List(ctx context.Context) ([]*model.Repository, error) } type repository struct { db *gorm.DB } func newRepository(db *gorm.DB) RepositoryInterface { return &repository{db} } var _ RepositoryInterface = &repository{} func (r *repository) Create(ctx context.Context, object *model.Repository) (*model.Repository, error) { now := time.Now() object.GmtCreate = now object.GmtModified = now if err := r.db.WithContext(ctx).Create(object).Error; err != nil { return nil, err } return object, nil } func (r *repository) Update(ctx context.Context, id int64, resourceVersion int64, updates map[string]interface{}) error { updates["gmt_modified"] = time.Now() updates["resource_version"] = resourceVersion + 1 f := r.db.WithContext(ctx).Model(&model.Repository{}).Where("id = ? and resource_version = ? ", id, resourceVersion).Updates(updates) if f.Error != nil { return f.Error } if f.RowsAffected == 0 { return errors.ErrRecordNotFound } return nil } func (r *repository) Delete(ctx context.Context, id int64) error { f := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Repository{}) if f.Error != nil { return f.Error } if f.RowsAffected == 0 { return errors.ErrRecordNotFound } return nil } func (r *repository) Get(ctx context.Context, id int64) (*model.Repository, error) { var repo model.Repository if err := r.db.WithContext(ctx).Where("id = ?", id).First(&repo).Error; err != nil { return nil, err } return &repo, nil } func (r *repository) GetByName(ctx context.Context, name string) (*model.Repository, error) { var repo model.Repository if err := r.db.WithContext(ctx).Where("name = ?", name).First(&repo).Error; err != nil { return nil, err } return &repo, nil } func (r *repository) List(ctx context.Context) ([]*model.Repository, error) { var repos []*model.Repository if err := r.db.WithContext(ctx).Find(&repos).Error; err != nil { return nil, err } return repos, nil } ================================================ FILE: pkg/db/tenant.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package db import ( "context" "time" "gorm.io/gorm" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/util/errors" ) type TenantInterface interface { Create(ctx context.Context, object *model.Tenant) (*model.Tenant, error) Update(ctx context.Context, cid int64, resourceVersion int64, updates map[string]interface{}) error Delete(ctx context.Context, cid int64) (*model.Tenant, error) Get(ctx context.Context, cid int64) (*model.Tenant, error) List(ctx context.Context, opts ...Options) ([]model.Tenant, error) GetTenantByName(ctx context.Context, name string) (*model.Tenant, error) } type tenant struct { db *gorm.DB } func (t *tenant) Create(ctx context.Context, object *model.Tenant) (*model.Tenant, error) { now := time.Now() object.GmtCreate = now object.GmtModified = now if err := t.db.WithContext(ctx).Create(object).Error; err != nil { return nil, err } return object, nil } func (t *tenant) Update(ctx context.Context, tid int64, resourceVersion int64, updates map[string]interface{}) error { // 系统维护字段 updates["gmt_modified"] = time.Now() updates["resource_version"] = resourceVersion + 1 f := t.db.WithContext(ctx).Model(&model.Tenant{}).Where("id = ? and resource_version = ?", tid, resourceVersion).Updates(updates) if f.Error != nil { return f.Error } if f.RowsAffected == 0 { return errors.ErrRecordNotFound } return nil } func (t *tenant) Delete(ctx context.Context, tid int64) (*model.Tenant, error) { object, err := t.Get(ctx, tid) if err != nil { return nil, err } if object == nil { return nil, nil } if err = t.db.WithContext(ctx).Where("id = ?", tid).Delete(&model.Tenant{}).Error; err != nil { return nil, err } return object, nil } func (t *tenant) Get(ctx context.Context, tid int64) (*model.Tenant, error) { var object model.Tenant if err := t.db.WithContext(ctx).Where("id = ?", tid).First(&object).Error; err != nil { if errors.IsRecordNotFound(err) { return nil, nil } return nil, err } return &object, nil } func (t *tenant) List(ctx context.Context, opts ...Options) ([]model.Tenant, error) { var objects []model.Tenant tx := t.db.WithContext(ctx) for _, opt := range opts { tx = opt(tx) } if err := tx.Find(&objects).Error; err != nil { return nil, err } return objects, nil } func (t *tenant) GetTenantByName(ctx context.Context, name string) (*model.Tenant, error) { var object model.Tenant if err := t.db.WithContext(ctx).Where("name = ?", name).First(&object).Error; err != nil { if errors.IsRecordNotFound(err) { return nil, nil } return nil, err } return &object, nil } func newTenant(db *gorm.DB) *tenant { return &tenant{db} } ================================================ FILE: pkg/db/user.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package db import ( "context" "time" "gorm.io/gorm" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/util/errors" ) type UserInterface interface { Create(ctx context.Context, object *model.User, fns ...func() error) (*model.User, error) Update(ctx context.Context, uid int64, resourceVersion int64, updates map[string]interface{}) error Delete(ctx context.Context, uid int64) error Get(ctx context.Context, uid int64) (*model.User, error) GetRoot(ctx context.Context) (*model.User, error) List(ctx context.Context, opts ...Options) ([]model.User, error) Count(ctx context.Context, opts ...Options) (int64, error) GetUserByName(ctx context.Context, userName string) (*model.User, error) } type user struct { db *gorm.DB } func (u *user) Create(ctx context.Context, object *model.User, fns ...func() error) (*model.User, error) { now := time.Now() object.GmtCreate = now object.GmtModified = now if err := u.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Create(object).Error; err != nil { return err } for _, fn := range fns { if err := fn(); err != nil { return err } } return nil }); err != nil { return nil, err } return object, nil } func (u *user) Update(ctx context.Context, uid int64, resourceVersion int64, updates map[string]interface{}) error { // 系统维护字段 updates["gmt_modified"] = time.Now() updates["resource_version"] = resourceVersion + 1 f := u.db.WithContext(ctx).Model(&model.User{}).Where("id = ? and resource_version = ?", uid, resourceVersion).Updates(updates) if f.Error != nil { return f.Error } if f.RowsAffected == 0 { return errors.ErrRecordNotUpdate } return nil } func (u *user) Delete(ctx context.Context, uid int64) error { return u.db.WithContext(ctx).Where("id = ?", uid).Delete(&model.User{}).Error } func (u *user) Get(ctx context.Context, uid int64) (*model.User, error) { var object model.User if err := u.db.WithContext(ctx).Where("id = ?", uid).First(&object).Error; err != nil { if errors.IsRecordNotFound(err) { return nil, nil } return nil, err } return &object, nil } func (u *user) GetRoot(ctx context.Context) (*model.User, error) { var object model.User if err := u.db.WithContext(ctx).Where("role = ?", model.RoleRoot).First(&object).Error; err != nil { if errors.IsRecordNotFound(err) { return nil, nil } return nil, err } return &object, nil } // List 获取用户列表 // TODO: 暂时不做分页考虑 func (u *user) List(ctx context.Context, opts ...Options) ([]model.User, error) { var objects []model.User tx := u.db.WithContext(ctx) for _, opt := range opts { tx = opt(tx) } if err := tx.Find(&objects).Error; err != nil { return nil, err } return objects, nil } func (u *user) Count(ctx context.Context, opts ...Options) (int64, error) { var total int64 tx := u.db.WithContext(ctx).Model(&model.User{}) for _, opt := range opts { tx = opt(tx) } if err := tx.Count(&total).Error; err != nil { return 0, err } return total, nil } func (u *user) GetUserByName(ctx context.Context, userName string) (*model.User, error) { var object model.User if err := u.db.WithContext(ctx).Where("name = ?", userName).First(&object).Error; err != nil { if errors.IsRecordNotFound(err) { return nil, nil } return nil, err } return &object, nil } func newUser(db *gorm.DB) *user { return &user{db} } ================================================ FILE: pkg/jobmanager/audit_cleaner.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package jobmanager import ( "time" "github.com/caoyingjunz/pixiu/pkg/db" logutil "github.com/caoyingjunz/pixiu/pkg/util/log" ) const ( DefaultSchedule = "0 0 * * 6" // 每周六 0 点执行 DefaultDaysReserved = 30 // 保留 30 天的审计日志 ) type AuditsCleaner struct { cfg AuditOptions dao db.ShareDaoFactory } type AuditOptions struct { Schedule string `yaml:"schedule"` DaysReserved int `yaml:"days_reserved"` } func DefaultOptions() AuditOptions { return AuditOptions{ Schedule: DefaultSchedule, DaysReserved: DefaultDaysReserved, } } func NewAuditsCleaner(cfg AuditOptions, dao db.ShareDaoFactory) *AuditsCleaner { return &AuditsCleaner{ cfg: cfg, dao: dao, } } func (ac *AuditsCleaner) Name() string { return "audits-cleaner" } func (ac *AuditsCleaner) CronSpec() string { return ac.cfg.Schedule } func (ac *AuditsCleaner) LogLevel() logutil.LogLevel { return logutil.InfoLevel } func (ac *AuditsCleaner) Do(ctx *JobContext) (err error) { resv := ac.cfg.DaysReserved before := time.Now().AddDate(0, 0, -resv) entries := map[string]interface{}{ "days_reserved": resv, "deadline": before, } entries["records_deleted"], err = ac.dao.Audit().BatchDelete(ctx, db.WithCreatedBefore(before)) ctx.WithLogFields(entries) return } func (a *AuditOptions) Valid() error { // TODO return nil } ================================================ FILE: pkg/jobmanager/cluster_syncer.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package jobmanager import ( "context" "sync" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/pkg/client" "github.com/caoyingjunz/pixiu/pkg/db" "github.com/caoyingjunz/pixiu/pkg/db/model" "github.com/caoyingjunz/pixiu/pkg/types" logutil "github.com/caoyingjunz/pixiu/pkg/util/log" ) const ( DefaultSyncInterval = "@every 5s" ) type ClusterSyncer struct { factory db.ShareDaoFactory } func NewClusterSyncer(f db.ShareDaoFactory) *ClusterSyncer { return &ClusterSyncer{ factory: f, } } func (cs *ClusterSyncer) Name() string { return "cluster-syncer" } func (cs *ClusterSyncer) CronSpec() string { return DefaultSyncInterval } func (cs *ClusterSyncer) LogLevel() logutil.LogLevel { return logutil.DebugLevel } func (cs *ClusterSyncer) Do(ctx *JobContext) (err error) { clusters, err := cs.factory.Cluster().List(ctx) if err != nil { klog.Error("[ClusterSyncer] failed to get clusters: %v", err) return err } diff := len(clusters) errCh := make(chan error, diff) var wg sync.WaitGroup wg.Add(diff) for _, cluster := range clusters { go func(c model.Cluster) { defer wg.Done() if err = doSync(cs.factory, c); err != nil { errCh <- err } }(cluster) } wg.Wait() select { case err = <-errCh: if err != nil { klog.Error("failed to sync cluster status: %v", err) } default: } return nil } func doSync(f db.ShareDaoFactory, cluster model.Cluster) error { // 处理自建集群正在部署的集群 if cluster.ClusterType == model.ClusterTypeCustom { // 自建环境,状态是部署未完成时,则直接不做同步,包含:部署中,等待部署,部署失败 if cluster.ClusterStatus == model.ClusterStatusUnStart || cluster.ClusterStatus == model.ClusterStatusDeploy || cluster.ClusterStatus == model.ClusterStatusFailed { return nil } } var ( kubernetesVersion string nodeData string err error ) status := model.ClusterStatusRunning nodeData, kubernetesVersion, err = getNewestKubeStatus(cluster) if err != nil { status = model.ClusterStatusError } updates := make(map[string]interface{}) parseStatus(updates, status, kubernetesVersion, nodeData, cluster) if len(updates) == 0 { return nil } if err = f.Cluster().InternalUpdate(context.TODO(), cluster.Id, updates); err != nil { klog.Error("failed to update cluster(%s) status: %v", cluster.Name, err) } return nil } func parseStatus(update map[string]interface{}, status model.ClusterStatus, kubernetesVersion string, nodeData string, cluster model.Cluster) { if status != cluster.ClusterStatus { update["status"] = status } if kubernetesVersion != cluster.KubernetesVersion { update["kubernetes_version"] = kubernetesVersion } if nodeData != cluster.Nodes { update["nodes"] = nodeData } } func getNewestKubeStatus(cluster model.Cluster) (string, string, error) { clusterSet, err := client.NewClusterSet(cluster.KubeConfig) if err != nil { return "", "", err } nodeList, err := clusterSet.Client.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) if err != nil { return "", "", err } nodes := nodeList.Items kubeNode := &types.KubeNode{Ready: make([]string, 0), NotReady: make([]string, 0)} // 获取存储状态 for _, node := range nodes { nodeStatus := parseKubeNodeStatus(&node) switch nodeStatus { case "Ready": kubeNode.Ready = append(kubeNode.Ready, node.Name) case "NotReady": kubeNode.NotReady = append(kubeNode.NotReady, node.Name) } } nodeData, err := kubeNode.Marshal() if err != nil { return "", "", err } var kubernetesVersion string if len(nodes) != 0 { kubernetesVersion = nodes[0].Status.NodeInfo.KubeletVersion } return nodeData, kubernetesVersion, nil } func parseKubeNodeStatus(node *v1.Node) string { status := "Ready" for _, condition := range node.Status.Conditions { if condition.Type != v1.NodeReady { continue } if condition.Status != "True" { status = "NotReady" } } return status } ================================================ FILE: pkg/jobmanager/context.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package jobmanager import ( "context" "github.com/caoyingjunz/pixiu/pkg/db" logutil "github.com/caoyingjunz/pixiu/pkg/util/log" ) type JobContext struct { context.Context *logutil.Logger } func NewJobContext(name string, cfg *logutil.LogOptions) *JobContext { jc := &JobContext{ Context: db.WithDBContext(context.Background()), Logger: logutil.NewLogger(cfg), } jc.WithLogField("job", name) return jc } func (c *JobContext) Log(level logutil.LogLevel, err error) { c.Logger.Log(c.Context, level, err) } ================================================ FILE: pkg/jobmanager/manager.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package jobmanager import ( "github.com/robfig/cron/v3" logutil "github.com/caoyingjunz/pixiu/pkg/util/log" ) type Job interface { // Name returns the job name Name() string // CronSpec returns the cron expression of the job // e.g. "* * * * *" CronSpec() string // LogLevel returns the log level of the job LogLevel() logutil.LogLevel // Do is the job handler Do(ctx *JobContext) error } type Manager struct { cron *cron.Cron } func NewManager(lc *logutil.LogOptions, jobs ...Job) *Manager { c := cron.New() for _, job := range jobs { c.AddFunc(job.CronSpec(), func() { ctx := NewJobContext(job.Name(), lc) ctx.Log(job.LogLevel(), job.Do(ctx)) }) } return &Manager{ c, } } func (m *Manager) Run() { m.cron.Start() } func (m *Manager) Stop() { ctx := m.cron.Stop() <-ctx.Done() } ================================================ FILE: pkg/static/localfile.go ================================================ /* Copyright 2025 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package static import ( "github.com/gin-gonic/gin" "net/http" "os" "path" "strings" ) type localFileSystem struct { http.FileSystem root string indexes bool } func LocalFile(root string, indexes bool) *localFileSystem { return &localFileSystem{ FileSystem: gin.Dir(root, indexes), root: root, indexes: indexes, } } func (l *localFileSystem) Exists(prefix string, file string) bool { if p := strings.TrimPrefix(file, prefix); len(p) < len(file) { name := path.Join(l.root, path.Clean(p)) if strings.Contains(name, "\\") || strings.Contains(name, "..") { return false } stats, err := os.Stat(name) if err != nil { return false } if stats.IsDir() { if !l.indexes { _, err := os.Stat(path.Join(name, "index.html")) if err != nil { return false } } } return true } return false } ================================================ FILE: pkg/static/static.go ================================================ /* Copyright 2025 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package static import ( "fmt" "net/http" "github.com/gin-gonic/gin" ) type ServeFileSystem interface { http.FileSystem Exists(prefix string, path string) bool } func ServeRoot(urlPrefix, root string) gin.HandlerFunc { return Serve(urlPrefix, LocalFile(root, false)) } // Serve returns a middleware handler that serves static files in the given directory. func Serve(urlPrefix string, fs ServeFileSystem) gin.HandlerFunc { return ServeCached(urlPrefix, fs, 0) } // ServeCached returns a middleware handler that similar as Serve // but with the Cache-Control Header set as passed in the cacheAge parameter func ServeCached(urlPrefix string, fs ServeFileSystem, cacheAge uint) gin.HandlerFunc { fileserver := http.FileServer(fs) if urlPrefix != "" { fileserver = http.StripPrefix(urlPrefix, fileserver) } return func(c *gin.Context) { if fs.Exists(urlPrefix, c.Request.URL.Path) { if cacheAge != 0 { c.Writer.Header().Add("Cache-Control", fmt.Sprintf("max-age=%d", cacheAge)) } fileserver.ServeHTTP(c.Writer, c.Request) c.Abort() } } } ================================================ FILE: pkg/types/helm.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package types type Release struct { Name string `json:"name" binding:"required"` Chart string `json:"chart" binding:"required"` Version string `json:"version" binding:"required"` Values map[string]interface{} `json:"values"` Preview bool `json:"preview"` } type RepoId struct { Id int64 `uri:"id" binding:"required"` } type RepoName struct { Cluster string `uri:"cluster" binding:"required"` Name string `uri:"name" binding:"required"` } type RepoURL struct { Url string `form:"url" binding:"required"` } type ChartValues struct { Chart string `form:"chart" binding:"required"` Version string `form:"version" binding:"required"` } type ReleaseHistory struct { Version int `form:"version"` } type CreateRepository struct { Name string `json:"name" binding:"required"` URL string `json:"url" binding:"required"` Username string `json:"username"` Password string `json:"password"` } type UpdateRepository struct { Name string `json:"name" binding:"required"` URL string `json:"url" binding:"required"` Username string `json:"username"` Password string `json:"password"` ResourceVersion *int64 `json:"resource_version" binding:"required"` } ================================================ FILE: pkg/types/meta.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package types import ( "bytes" "context" "encoding/base64" "encoding/json" "fmt" "net/http" "sync" "time" "github.com/gorilla/websocket" "golang.org/x/crypto/ssh" appv1 "k8s.io/api/apps/v1" "k8s.io/api/core/v1" "k8s.io/client-go/tools/remotecommand" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/pkg/db/model" ) const ( timeLayout = "2006-01-02 15:04:05.999999999" MsgData = '1' MsgResize = '2' ) func (c *Cluster) SetId(i int64) { c.Id = i } func (o *KubeObject) SetReplicaSets(replicaSets []appv1.ReplicaSet) { o.lock.Lock() defer o.lock.Unlock() o.ReplicaSets = replicaSets } func (o *KubeObject) GetReplicaSets() []appv1.ReplicaSet { o.lock.Lock() defer o.lock.Unlock() return o.ReplicaSets } func (o *KubeObject) SetPods(pods []v1.Pod) { o.lock.Lock() defer o.lock.Unlock() o.Pods = pods } func (o *KubeObject) GetPods() []v1.Pod { o.lock.Lock() defer o.lock.Unlock() return o.Pods } func FormatTime(GmtCreate time.Time, GmtModified time.Time) TimeSpec { return TimeSpec{ GmtCreate: GmtCreate.Format(timeLayout), GmtModified: GmtModified.Format(timeLayout), } } // NewTerminalSession 该方法用于升级 http 协议至 websocket,并new一个 TerminalSession 类型的对象返回 func NewTerminalSession(w http.ResponseWriter, r *http.Request) (*TerminalSession, error) { // 初始化 Upgrader 类型的对象,用于http协议升级为 websocket 协议 upgrader := &websocket.Upgrader{ HandshakeTimeout: time.Second * 2, // 检测请求来源 CheckOrigin: func(r *http.Request) bool { return true }, Subprotocols: []string{r.Header.Get("Sec-WebSocket-Protocol")}, } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return nil, err } session := &TerminalSession{ wsConn: conn, sizeChan: make(chan remotecommand.TerminalSize), doneChan: make(chan struct{}), } return session, nil } // 用于读取web端的输入,接收web端输入的指令内容 func (t *TerminalSession) Read(p []byte) (int, error) { _, message, err := t.wsConn.ReadMessage() if err != nil { return copy(p, "\u0004"), err } // 反序列化 var msg TerminalMessage if err = json.Unmarshal(message, &msg); err != nil { return copy(p, "\u0004"), err } // 逻辑判断 switch msg.Operation { // 如果是标准输入 case "stdin": return copy(p, msg.Data), nil // 窗口调整大小 case "resize": t.sizeChan <- remotecommand.TerminalSize{Width: msg.Cols, Height: msg.Rows} return 0, nil // ping 无内容交互 case "ping": return 0, nil default: return copy(p, "\u0004"), fmt.Errorf("unknown message type") } } // 写数据的方法,拿到 api-server 的返回内容,向web端输出 func (t *TerminalSession) Write(p []byte) (int, error) { msg, err := json.Marshal(TerminalMessage{ Operation: "stdout", Data: string(p), }) if err != nil { return 0, err } if err = t.wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { return 0, err } return len(p), nil } // Done 标记关闭doneChan,关闭后触发退出终端 func (t *TerminalSession) Done() { close(t.doneChan) } // Close 用于关闭websocket连接 func (t *TerminalSession) Close() error { return t.wsConn.Close() } // Next 获取web端是否resize,以及是否退出终端 func (t *TerminalSession) Next() *remotecommand.TerminalSize { select { case size := <-t.sizeChan: return &size case <-t.doneChan: return nil } } func NewTurn(wsConn *websocket.Conn, sshClient *ssh.Client) (*Turn, error) { session, err := sshClient.NewSession() if err != nil { return nil, err } stdinPipe, err := session.StdinPipe() if err != nil { return nil, err } turn := &Turn{StdinPipe: stdinPipe, Session: session, WsConn: wsConn} session.Stdout = turn session.Stderr = turn modes := ssh.TerminalModes{ ssh.ECHO: 1, // disable echo ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud } if err = session.RequestPty("xterm", 150, 30, modes); err != nil { return nil, err } if err = session.Shell(); err != nil { return nil, err } return turn, nil } func (t *Turn) Write(p []byte) (n int, err error) { writer, err := t.WsConn.NextWriter(websocket.BinaryMessage) if err != nil { return 0, err } defer writer.Close() return writer.Write(p) } func (t *Turn) Close() error { if t.Session != nil { t.Session.Close() } return t.WsConn.Close() } func (t *Turn) Read(p []byte) (n int, err error) { for { msgType, reader, err := t.WsConn.NextReader() if err != nil { return 0, err } if msgType != websocket.BinaryMessage { continue } return reader.Read(p) } } func (t *Turn) StartLoopRead(ctx context.Context, wg *sync.WaitGroup, logBuff *bytes.Buffer) { defer wg.Done() err := t.loopRead(logBuff, ctx) if err != nil { klog.Errorf("LoopRead exit, err:%s", err) } } func (t *Turn) loopRead(logBuff *bytes.Buffer, context context.Context) error { for { select { case <-context.Done(): return fmt.Errorf("LoopRead exit") default: _, wsData, err := t.WsConn.ReadMessage() if err != nil { return fmt.Errorf("reading webSocket message err:%s", err) } body := decode(wsData[1:]) switch wsData[0] { case MsgResize: if err := t.resizeDo(body); err != nil { return err } case MsgData: if err := t.dataDo(body, logBuff); err != nil { return err } } } } } func (t *Turn) dataDo(body []byte, logBuff *bytes.Buffer) error { if _, err := t.StdinPipe.Write(body); err != nil { return fmt.Errorf("StdinPipe write err:%s", err) } if _, err := logBuff.Write(body); err != nil { return fmt.Errorf("logBuff write err:%s", err) } return nil } type Resize struct { Columns int Rows int } func (t *Turn) resizeDo(body []byte) error { var args Resize err := json.Unmarshal(body, &args) if err != nil { return fmt.Errorf("ssh pty resize windows err:%s", err) } if args.Columns > 0 && args.Rows > 0 { if err := t.Session.WindowChange(args.Rows, args.Columns); err != nil { return fmt.Errorf("ssh pty resize windows err:%s", err) } } return nil } func (t *Turn) sessionWait() error { if err := t.Session.Wait(); err != nil { return err } return nil } func (t *Turn) StartSessionWait(wg *sync.WaitGroup) { defer wg.Done() err := t.sessionWait() if err != nil { klog.Errorf("SessionWait exit, err:%s", err) } } func decode(p []byte) []byte { decodeString, _ := base64.StdEncoding.DecodeString(string(p)) return decodeString } func (a *PlanNodeAuth) Marshal() (string, error) { data, err := json.Marshal(a) if err != nil { return "", err } return string(data), nil } func (a *PlanNodeAuth) Unmarshal(s string) error { if err := json.Unmarshal([]byte(s), a); err != nil { return err } return nil } func (ks *KubernetesSpec) Marshal() (string, error) { data, err := json.Marshal(ks) if err != nil { return "", err } return string(data), nil } func (ks *KubernetesSpec) Unmarshal(s string) error { if err := json.Unmarshal([]byte(s), ks); err != nil { return err } return nil } func (ns *NetworkSpec) Marshal() (string, error) { data, err := json.Marshal(ns) if err != nil { return "", err } return string(data), nil } func (ns *NetworkSpec) Unmarshal(s string) error { if err := json.Unmarshal([]byte(s), ns); err != nil { return err } return nil } func (rs *RuntimeSpec) Marshal() (string, error) { data, err := json.Marshal(rs) if err != nil { return "", err } return string(data), nil } func (rs *RuntimeSpec) Unmarshal(s string) error { if err := json.Unmarshal([]byte(s), rs); err != nil { return err } return nil } func (cs ComponentSpec) Marshal() (string, error) { data, err := json.Marshal(cs) if err != nil { return "", err } return string(data), nil } func (cs *ComponentSpec) Unmarshal(s string) error { if err := json.Unmarshal([]byte(s), cs); err != nil { return err } return nil } func (rs *RuntimeSpec) IsDocker() bool { return rs.Runtime == string(model.DockerCRI) } func (rs *RuntimeSpec) IsContainerd() bool { return rs.Runtime == string(model.ContainerdCRI) } func (p PageRequest) IsPaged() bool { return p.Page != 0 && p.Limit != 0 } func (p PageRequest) Offset(total int) (int, int, error) { offset := (p.Page - 1) * p.Limit if offset > total { return 0, 0, fmt.Errorf("invaild offset") } end := offset + p.Limit if end > total { end = total } return offset, end, nil } func (node *KubeNode) Marshal() (string, error) { data, err := json.Marshal(node) if err != nil { return "", err } return string(data), nil } func (node *KubeNode) Unmarshal(s string) error { if err := json.Unmarshal([]byte(s), node); err != nil { return err } return nil } ================================================ FILE: pkg/types/request.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package types import "github.com/caoyingjunz/pixiu/pkg/db/model" const AllNamespace = "all_namespaces" type ( // LoginRequest is the request body struct for user login. LoginRequest struct { Name string `json:"name" binding:"required"` // required Password string `json:"password" binding:"required"` // required } CreateUserRequest struct { Name string `json:"name" binding:"required"` // required Password string `json:"password" binding:"required,password"` // required Role model.UserRole `json:"role" binding:"omitempty,oneof=0 1 2"` // optional Status model.UserStatus `json:"status" binding:"omitempty"` Email string `json:"email" binding:"omitempty,email"` // optional Phone string `json:"phone" binding:"omitempty"` // optional Description string `json:"description" binding:"omitempty"` // optional } // UpdateUserRequest // !Note: if you want to update description only, email also must be provided with current value UpdateUserRequest struct { Role model.UserRole `json:"role" binding:"omitempty,oneof=0 1 2"` // required Status model.UserStatus `json:"status" binding:"omitempty,oneof=0 1 2"` // required Email string `json:"email" binding:"omitempty,email"` // optional Phone string `json:"phone" binding:"omitempty"` // optional Description string `json:"description" binding:"omitempty"` // optional ResourceVersion *int64 `json:"resource_version" binding:"required"` // required } UpdateUserPasswordRequest struct { New string `json:"new" binding:"required,password"` // required Old string `json:"old" binding:"required"` // required ResourceVersion *int64 `json:"resource_version" binding:"required"` // required Reset bool `json:"reset"` } CreateClusterRequest struct { Name string `json:"name" binding:"omitempty"` // optional AliasName string `json:"alias_name" binding:"omitempty"` // optional Type model.ClusterType `json:"cluster_type" binding:"omitempty,oneof=0 1"` // optional KubeConfig string `json:"kube_config" binding:"required"` // required Description string `json:"description" binding:"omitempty"` // optional Protected bool `json:"protected" binding:"omitempty"` // optional } UpdateClusterRequest struct { AliasName *string `json:"alias_name" binding:"omitempty"` // optional Description *string `json:"description" binding:"omitempty"` // optional // TODO: put resource version in a common struct for updating request only ResourceVersion *int64 `json:"resource_version" binding:"required"` // required } ProtectClusterRequest struct { ResourceVersion *int64 `json:"resource_version" binding:"required"` // required Protected bool `json:"protected" binding:"omitempty"` // optional } CreateTenantRequest struct { Name string `json:"name" binding:"required"` // required Description *string `json:"description" binding:"omitempty"` // optional } UpdateTenantRequest struct { Name *string `json:"name" binding:"omitempty"` // optional Description *string `json:"description" binding:"omitempty"` // optional ResourceVersion *int64 `json:"resource_version" binding:"required"` // required } CreatePlanRequest struct { Name string `json:"name" binding:"required"` // required Description string `json:"description" binding:"omitempty"` // optional Config CreatePlanConfigRequest `json:"config"` Nodes []CreatePlanNodeRequest `json:"nodes"` } UpdatePlanRequest struct { Name string `json:"name" binding:"required"` // required ResourceVersion *int64 `json:"resource_version" binding:"required"` // required Description string `json:"description" binding:"omitempty"` // optional Config CreatePlanConfigRequest `json:"config"` Nodes []CreatePlanNodeRequest `json:"nodes"` } CreatePlanNodeRequest struct { Name string `json:"name" binding:"omitempty"` // required PlanId int64 `json:"plan_id"` Role []string `json:"role"` // k8s 节点的角色,master 和 node CRI model.CRI `json:"cri"` Ip string `json:"ip"` Auth PlanNodeAuth `json:"auth"` } UpdatePlanNodeRequest struct { ResourceVersion int64 `json:"resource_version" binding:"required"` // required Name string `json:"name" binding:"omitempty"` // required PlanId int64 `json:"plan_id"` Role []string `json:"role"` // k8s 节点的角色,master 为 1 和 node 为 0 CRI model.CRI `json:"cri"` Ip string `json:"ip"` Auth PlanNodeAuth `json:"auth"` } CreatePlanConfigRequest struct { PlanId int64 `json:"plan_id"` Region string `json:"region"` OSImage string `json:"os_image" binding:"required"` // 操作系统 Description string `json:"description" binding:"omitempty"` // optional Kubernetes KubernetesSpec `json:"kubernetes"` Network NetworkSpec `json:"network"` Runtime RuntimeSpec `json:"runtime"` Component ComponentSpec `json:"component"` // 支持的扩展组件配置 } UpdatePlanConfigRequest struct { // TODO: } RBACPolicyRequest struct { // user ID or group name is required UserId *int64 `json:"user_id" binding:"required_without=GroupName,excluded_with=GroupName"` GroupName *string `json:"group_name" binding:"required_without=UserId,excluded_with=UserId"` ObjectType model.ObjectType `json:"object_type" binding:"required,rbac_object"` SID string `json:"sid" binding:"omitempty,rbac_sid"` Operation model.Operation `json:"operation" binding:"required,rbac_operation"` } ListRBACPolicyRequest struct { UserId int64 `form:"user_id" binding:"required"` ObjectType *model.ObjectType `form:"object_type" binding:"omitempty,required_with=UserId,rbac_object"` SID *string `form:"sid" binding:"omitempty,required_with=ObjectType,rbac_sid"` Operation *model.Operation `form:"operation" binding:"omitempty,required_with=SID,rbac_operation"` } GroupBindingRequest struct { UserId int64 `json:"user_id" binding:"required"` GroupName string `json:"group_name" binding:"required"` } ListGroupBindingRequest struct { UserId *int64 `form:"user_id" binding:"omitempty"` GroupName *string `form:"group_name" binding:"omitempty"` } // PageRequest 分页配置 PageRequest struct { Page int `form:"page" json:"page"` // 页数,表示第几页 Limit int `form:"limit" json:"limit"` // 每页数量 } // QueryOption 搜索配置 QueryOption struct { LabelSelector string `form:"labelSelector" json:"labelSelector"` // 标签搜索 NameSelector string `form:"nameSelector" json:"nameSelector"` // 名称搜索 } // ListClusterRequest 集群列表查询参数 ListClusterRequest struct { PageRequest `form:",inline"` QueryOption `form:",inline"` Status *int `form:"status" json:"status"` // 集群状态过滤,不传则不过滤 } // ListPlanRequest 部署计划列表查询参数 ListPlanRequest struct { PageRequest `form:",inline"` NameSelector string `form:"nameSelector" json:"nameSelector"` // 名称模糊搜索 Step string `form:"step" json:"step"` // 状态过滤,不传则不过滤 } // ListUserRequest 用户列表查询参数 ListUserRequest struct { PageRequest `form:",inline"` UserName string `form:"userName" json:"userName"` UserPhone string `form:"userPhone" json:"userPhone"` UserEmail string `form:"userEmail" json:"userEmail"` Status *int `form:"status" json:"status"` } // WebSSHRequest 主机 ssh 跳转请求 WebSSHRequest struct { Host string `form:"host" json:"host" binding:"required"` Port int `form:"port" json:"port"` User string `form:"user" json:"user" binding:"required"` Password string `form:"password" json:"password"` } ) type ( LoginResponse struct { UserId int64 `json:"user_id"` UserName string `json:"user_name"` Token string `json:"token"` Role model.UserRole `json:"role"` *model.User `json:"-"` } // PageResponse 分页查询返回值 PageResponse struct { PageRequest `json:",inline"` // 分页请求属性 Total int `json:"total"` // 分页总数 Items interface{} `json:"items"` // 指定页的元素列表 } ) ================================================ FILE: pkg/types/types.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package types import ( "io" "sync" "time" "github.com/gorilla/websocket" "golang.org/x/crypto/ssh" appv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" "k8s.io/client-go/tools/remotecommand" "github.com/caoyingjunz/pixiu/pkg/db/model" ) type PixiuObjectMeta struct { Cluster string `uri:"cluster" binding:"required"` Namespace string `uri:"namespace" binding:"required"` Name string `uri:"name"` } type PixiuMeta struct { // pixiu 对象 ID Id int64 `json:"id"` // Pixiu 对象版本号 ResourceVersion int64 `json:"resource_version"` } type TimeMeta struct { // pixiu 对象创建时间 GmtCreate time.Time `json:"gmt_create"` // pixiu 对象修改时间 GmtModified time.Time `json:"gmt_modified"` } type KubeNode struct { Ready []string `json:"ready"` NotReady []string `json:"not_ready"` } type Cluster struct { PixiuMeta `json:",inline"` Name string `json:"name"` AliasName string `json:"alias_name"` Status model.ClusterStatus `json:"status"` // 0: 运行中 1: 部署中 2: 等待部署 3: 部署失败 4: 集群失联,API不可用 // 0: 标准集群 1: 自建集群 ClusterType model.ClusterType `json:"cluster_type"` PlanId int64 `json:"plan_id"` // 自建集群关联的 PlanId,如果是自建的集群,planId 不为 0 // kubernetes 集群的版本和状态 KubernetesVersion string `json:"kubernetes_version"` Nodes KubeNode `json:"nodes"` // 集群删除保护,开启集群删除保护时不允许删除集群 // 0: 关闭集群删除保护 1: 开启集群删除保护 Protected bool `json:"protected"` // k8s kubeConfig base64 字段 KubeConfig string `json:"kube_config,omitempty"` // 集群用途描述,可以为空 Description string `json:"description"` KubernetesMeta `json:",inline"` TimeMeta `json:",inline"` } // KubernetesMeta 记录 kubernetes 集群的数据 type KubernetesMeta struct { // 集群的版本 KubernetesVersion string `json:"kubernetes_version,omitempty"` // 节点数量 Nodes int `json:"nodes"` // The memory and cpu usage Resources Resources `json:"resources"` } // Resources kubernetes 的资源信息 // The memory and cpu usage type Resources struct { Cpu string `json:"cpu"` Memory string `json:"memory"` } type User struct { PixiuMeta `json:",inline"` Name string `json:"name"` // 用户名称 Password string `json:"password" binding:"required,password"` // 用户密码 Status model.UserStatus `json:"status"` // 用户状态标识 Role model.UserRole `json:"role"` // 用户角色,目前只实现管理员,0: 普通用户 1: 管理员 2: 超级管理员 Email string `json:"email"` // 用户注册邮件 Phone string `json:"phone"` // 用户手机号 Description string `json:"description"` // 用户描述信息 TimeMeta `json:",inline"` } type Tenant struct { PixiuMeta `json:",inline"` TimeMeta `json:",inline"` Name string `json:"name"` // 用户名称 Description string `json:"description"` // 用户描述信息 } type Plan struct { PixiuMeta `json:",inline"` TimeMeta `json:",inline"` Name string `json:"name"` // 用户名称 Step model.TaskStatus `json:"step"` Description string `json:"description"` // 用户描述信息 KubernetesVersion string `json:"kubernetes_version"` // k8s 版本 NodeCount int `json:"node_count"` // 节点总数 Config PlanConfig `json:"config"` Nodes []PlanNode `json:"nodes"` } type PlanNode struct { PixiuMeta `json:",inline"` TimeMeta `json:",inline"` Name string `json:"name"` // required PlanId int64 `json:"plan_id,omitempty"` Role []string `json:"role"` // k8s 节点的角色,master 和 node CRI model.CRI `json:"cri"` Ip string `json:"ip"` Auth PlanNodeAuth `json:"auth,omitempty"` } type Audit struct { PixiuMeta `json:",inline"` TimeMeta `json:",inline"` Ip string `json:"ip"` Action string `json:"action"` // 操作动作 Status model.AuditOperationStatus `json:"status"` // 操作状态 Operator string `json:"operator"` // 操作人 Path string `json:"path"` // 操作路径 ObjectType model.ObjectType `json:"resource_type"` // 资源类型 Duration int64 `json:"duration"` // 请求耗时 ms ResponseCode int `json:"response_code"` // HTTP 响应码 Cluster string `json:"cluster"` // K8s 集群名 ResourceName string `json:"resource_name"` // 资源名称 ResourceNamespace string `json:"resource_namespace"` // 资源命名空间 } type AuthType string const ( NoneAuth AuthType = "none" // 已开启密码 KeyAuth AuthType = "key" // 密钥 PasswordAuth AuthType = "password" // 密码 ) type PlanNodeAuth struct { Type AuthType `json:"type"` // 节点认证模式,支持 key 和 password Key *KeySpec `json:"key,omitempty"` Password *PasswordSpec `json:"password,omitempty"` } type PlanTask struct { PixiuMeta `json:",inline"` TimeMeta `json:",inline"` Name string `json:"name"` PlanId int64 `json:"plan_id" binding:"required"` Status model.TaskStatus `json:"status"` Message string `json:"message"` } type KeySpec struct { Data string `json:"data,omitempty"` File string `json:"-"` } type PasswordSpec struct { User string `json:"user,omitempty"` Password string `json:"password,omitempty"` } type PlanConfig struct { PixiuMeta `json:",inline"` TimeMeta `json:",inline"` PlanId int64 `json:"plan_id,omitempty"` // required Region string `json:"region"` OSImage string `json:"os_image"` // 操作系统 Kubernetes KubernetesSpec `json:"kubernetes"` Network NetworkSpec `json:"network"` Runtime RuntimeSpec `json:"runtime"` Component ComponentSpec `json:"component"` // 支持的扩展组件配置 } // TimeSpec 通用时间规格 type TimeSpec struct { GmtCreate interface{} `json:"gmt_create,omitempty"` GmtModified interface{} `json:"gmt_modified,omitempty"` } type KubeObject struct { lock sync.RWMutex ReplicaSets []appv1.ReplicaSet Pods []v1.Pod } // WebShellOptions ws API 参数定义 type WebShellOptions struct { Cluster string `form:"cluster"` Namespace string `form:"namespace"` Pod string `form:"pod"` Container string `form:"container"` Command string `form:"command"` } // TerminalMessage 定义了终端和容器 shell 交互内容的格式 Operation 是操作类型 // Data 是具体数据内容 Rows和Cols 可以理解为终端的行数和列数,也就是宽、高 type TerminalMessage struct { Operation string `json:"operation"` Data string `json:"data"` Rows uint16 `json:"rows"` Cols uint16 `json:"cols"` } // TerminalSession 定义 TerminalSession 结构体,实现 PtyHandler 接口 // wsConn 是 websocket 连接 // sizeChan 用来定义终端输入和输出的宽和高 // doneChan 用于标记退出终端 type TerminalSession struct { wsConn *websocket.Conn sizeChan chan remotecommand.TerminalSize doneChan chan struct{} } type Turn struct { StdinPipe io.WriteCloser Session *ssh.Session WsConn *websocket.Conn } // ListOptions is the query options to a standard REST list call. type ListOptions struct { Count bool `form:"count"` Limit int64 `form:"limit"` PageRequest `json:",inline"` // 分页请求属性 QueryOption `json:",inline"` // 搜索内容 } type EventOptions struct { Uid string `form:"uid"` Namespace string `form:"namespace"` Name string `form:"name"` Kind string `form:"kind"` Namespaced bool `form:"namespaced"` Limit int64 `form:"limit"` } type PodLogOptions struct { Container string `form:"container"` TailLines int64 `form:"tailLines"` } type KubernetesSpec struct { EnablePublicIp bool `json:"enable_public_ip"` ApiServer string `json:"api_server"` ApiPort string `json:"api_port"` KubernetesVersion string `json:"kubernetes_version"` EnableHA bool `json:"enable_ha"` Register bool `json:"register"` } type NetworkSpec struct { NetworkInterface string `json:"network_interface"` // 网口,默认 eth0 Cni string `json:"cni"` PodNetwork string `json:"pod_network"` ServiceNetwork string `json:"service_network"` KubeProxy string `json:"kube_proxy"` } type RuntimeSpec struct { Runtime string `json:"runtime"` } type ComponentSpec struct { Helm *Helm `json:"helm,omitempty"` // 忽略,则使用默认值 Prometheus *Prometheus `json:"prometheus,omitempty"` Grafana *Grafana `json:"grafana,omitempty"` Haproxy *Haproxy `json:"haproxy,omitempty"` } type Helm struct { Enable bool `json:"enable"` HelmRelease string `json:"helm_release"` } type Prometheus struct { EnablePrometheus string `json:"enable_prometheus"` Enable bool `json:"enable"` } type Grafana struct { Enable bool `json:"enable"` GrafanaAdminUser string `json:"grafana_admin_user"` GrafanaAdminPassword string `json:"grafana_admin_password"` } // Haproxy Options // This configuration is usually enabled when self-created VMs require high availability. type Haproxy struct { Enable bool `json:"enable"` // Enable haproxy and keepalived, KeepalivedVirtualRouterId string `json:"keepalived_virtual_router_id"` // Arbitrary unique number from 0..255 } type RBACPolicy struct { UserName string `json:"username,omitempty"` GroupName string `json:"groupname,omitempty"` ObjectType model.ObjectType `json:"resource_type,omitempty"` StringID string `json:"sid,omitempty"` Operation model.Operation `json:"operation,omitempty"` } // AuditListOptions 审计列表查询选项,支持过滤 type AuditListOptions struct { ListOptions `json:",inline"` Operator string `form:"operator"` // 模糊匹配操作人 Action string `form:"action"` // 精确匹配 HTTP 方法(POST/PUT/DELETE/PATCH) ObjectType string `form:"object_type"` // 资源类型 Cluster string `form:"cluster"` // 集群名称 Status *uint8 `form:"status"` // 操作状态(0:失败 1:成功 2:未知) StartTime string `form:"start_time"` // 时间范围起(RFC3339,留空忽略) EndTime string `form:"end_time"` // 时间范围止(RFC3339,留空忽略) } ================================================ FILE: pkg/util/container/container.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package container import ( "context" "fmt" "io" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" "k8s.io/klog/v2" "github.com/caoyingjunz/pixiu/pkg/util/errors" ) type Container struct { client *client.Client action string name string planId int64 dir string } func NewContainer(action string, planId int64, dir string) (*Container, error) { client, err := client.NewClientWithOpts(client.FromEnv) if err != nil { return nil, err } return &Container{ client: client, action: action, name: fmt.Sprintf("%s-%d", action, planId), planId: planId, dir: dir}, nil } // StartAndWaitForContainer 创建,启动容器,并等待容器退出 func (c *Container) StartAndWaitForContainer(ctx context.Context, image string) error { // 已经存在,则先删除运行的容器 if err := c.ClearContainer(ctx); err != nil { return err } config := &container.Config{ Labels: map[string]string{ "author": "caoyingjunz", "pixiuName": c.name, }, Image: image, Env: []string{fmt.Sprintf("COMMAND=%s", c.action)}, } hostConfig := &container.HostConfig{ Binds: []string{fmt.Sprintf("%s/%d:/configs", c.dir, c.planId)}, } netConfig := &network.NetworkingConfig{} resp, err := c.client.ContainerCreate(ctx, config, hostConfig, netConfig, nil, c.name) if err != nil { return err } // 启动容器 if err = c.client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { return err } // 等待容器运行完成退出 return c.WaitContainer(ctx, resp.ID, 180) } func (c *Container) Close() error { return c.client.Close() } // ClearContainer 清理已存在的老容器 func (c *Container) ClearContainer(ctx context.Context) error { old, err := c.GetContainer(ctx, c.name) if err != nil { // 如果不存在则直接返回 if err == errors.ErrContainerNotFound { return nil } return err } containerId := old.ID timeout := 5 * time.Second if err = c.client.ContainerStop(ctx, containerId, &timeout); err != nil { return err } return c.client.ContainerRemove(ctx, containerId, types.ContainerRemoveOptions{Force: true}) } func (c *Container) ListContainers(ctx context.Context) ([]types.Container, error) { cs, err := c.client.ContainerList(ctx, types.ContainerListOptions{All: true}) if err != nil { return nil, err } return cs, nil } func (c *Container) GetContainer(ctx context.Context, containerName string) (*types.Container, error) { containers, err := c.ListContainers(ctx) if err != nil { return nil, err } for _, container := range containers { for _, name := range container.Names { if name == "/"+containerName { return &container, nil } } } return nil, errors.ErrContainerNotFound } // WaitContainer // 等待容器运行退出 // 官方的客户端实现有问题,先通过探针的方式规避,后续优化 // 循环检查容器状态,直到出现异常或符合预期 func (c *Container) WaitContainer(ctx context.Context, containerId string, times int) error { //_, errCh := c.client.ContainerWait(ctx, resp.ID, container.WaitConditionNextExit) //if err = <-errCh; err != nil { // fmt.Println("结束", err) // // return err //} for i := 0; i < times; i++ { klog.Infof("waiting for container at %d times", i+1) // 先等待 5s 再执行,开始等待符合业务场景,且后续的逻辑处理不受影响 time.Sleep(5 * time.Second) // 实际开始检查 containerInfo, err := c.client.ContainerInspect(ctx, containerId) if err != nil { return err } if containerInfo.State != nil { // Can be one of "created", "running", "paused", "restarting", "removing", "exited", or "dead" state := containerInfo.State // 容器还在运行,等待下一次检查 if state.Status == "running" && state.Running { continue } // 状态异常,直接退出 if state.Status == "paused" || state.Status == "removing" || state.Status == "dead" { return fmt.Errorf("容器状态异常(%s),退出等待", state.Status) } // 容器已经退出 if state.Status == "exited" { if state.ExitCode == 0 { // 正常退出 return nil } else { // 异常退出返回错误信息 return fmt.Errorf(state.Error) } } // 其他状态,继续等待 } } return fmt.Errorf("等待容器(%s)运行完成超时", containerId) } func (c *Container) WatchContainerLog(ctx context.Context, containerId, since string) (io.ReadCloser, error) { return c.client.ContainerLogs(ctx, containerId, types.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, Follow: true, Timestamps: false, }) } ================================================ FILE: pkg/util/errors/errors.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package errors import ( "errors" "gorm.io/gorm" "github.com/go-sql-driver/mysql" ) var ( ErrRecordNotFound = gorm.ErrRecordNotFound ErrRecordNotUpdate = errors.New("record not updated") ErrBusySystem = errors.New("系统繁忙,请稍后再试") ErrReqParams = errors.New("请求参数错误") ErrCloudNotRegister = errors.New("cloud 集群未注册") ErrUserNotFound = errors.New("用户不存在") ErrNotAcceptable = errors.New("有任务正在执行,请稍后再试") ErrClusterNotFound = errors.New("集群不存在") ErrUserPassword = errors.New("密码错误") ErrInternal = errors.New("服务器内部错误") ErrTenantNotFound = errors.New("租户不存在") ErrDuplicatedPassword = errors.New("新密码与旧密码相同") ErrAuditNotFound = errors.New("审计记录不存在") ErrContainerNotFound = errors.New("容器不存在") ParamsError = errors.New("参数错误") OperateFailed = errors.New("操作失败") NoPermission = errors.New("无权限") InnerError = errors.New("内部错误") NoUserIdError = errors.New("请登录") UserExistError = errors.New("用户已存在") RoleExistError = errors.New("角色已存在") RoleNotExistError = errors.New("角色不存在") PolicyExistError = errors.New("策略已存在") PolicyNotExistError = errors.New("策略不存在") TenantExistError = errors.New("租户已存在") ErrAuditExists = errors.New("审计记录已存在") ErrRootAlreadyExists = errors.New("超级管理员已存在") ) func IsRecordNotFound(err error) bool { return errors.Is(err, gorm.ErrRecordNotFound) } func IsNotUpdated(err error) bool { return errors.Is(err, ErrRecordNotUpdate) } func IsUniqueConstraintError(err error) bool { mysqlErr, ok := err.(*mysql.MySQLError) if !ok { return false } // 数据库的 1062 错误码为固定的主键冲突号 return mysqlErr.Number == 1062 } ================================================ FILE: pkg/util/log/log.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package log import ( "context" "errors" "fmt" "sync" "time" klog "github.com/sirupsen/logrus" "github.com/caoyingjunz/pixiu/pkg/db" ) var once sync.Once type LogFormat string const ( LogFormatJson LogFormat = "json" LogFormatText LogFormat = "text" ) var ErrInvalidLogFormat = errors.New("invalid log format") type LogLevel = klog.Level // Providing 3 log levels now. const ( ErrorLevel LogLevel = klog.ErrorLevel InfoLevel LogLevel = klog.InfoLevel DebugLevel LogLevel = klog.DebugLevel ) type LogOptions struct { LogFormat `yaml:"log_format"` LogSQL bool `yaml:"log_sql"` LogLevel `yaml:"log_level"` } // DefaultLogOptions returns the default configs. func DefaultLogOptions() *LogOptions { return &LogOptions{ LogFormat: LogFormatJson, LogSQL: false, LogLevel: InfoLevel, } } func (o *LogOptions) Valid() error { switch o.LogFormat { case LogFormatJson, LogFormatText: return nil default: return ErrInvalidLogFormat } } // Init sets the log format only once. func (o *LogOptions) Init() { once.Do(func() { klog.SetLevel(o.LogLevel) switch o.LogFormat { case LogFormatJson: klog.SetFormatter(&klog.JSONFormatter{ TimestampFormat: time.RFC3339Nano, }) default: klog.SetFormatter(&klog.TextFormatter{ FullTimestamp: true, TimestampFormat: time.RFC3339Nano, }) } }) } const ( SuccessMsg = "SUCCESS" ErrorMsg = "ERROR" FailMsg = "FAIL" ) type Logger struct { startTime time.Time logSQL bool logEntry *klog.Entry } func NewLogger(cfg *LogOptions) *Logger { return &Logger{ startTime: time.Now(), logSQL: cfg.LogSQL, logEntry: klog.NewEntry(klog.StandardLogger()), } } func (l *Logger) WithLogField(key string, value interface{}) { l.logEntry = l.logEntry.WithField(key, value) } func (l *Logger) WithLogFields(fields map[string]interface{}) { l.logEntry = l.logEntry.WithFields(fields) } func (l *Logger) Log(ctx context.Context, level LogLevel, err error) { fields := make(map[string]interface{}) if l.logSQL { if sqls := db.GetSQLs(ctx); len(sqls) > 0 { fields["sqls"] = sqls } } fields["latency"] = fmt.Sprintf("%dµs", time.Since(l.startTime).Microseconds()) if err != nil { fields["error"] = err l.logEntry.WithFields(fields).Error(FailMsg) return } switch level { case DebugLevel: l.logEntry.WithFields(fields).Debug(SuccessMsg) case InfoLevel: l.logEntry.WithFields(fields).Info(SuccessMsg) } } ================================================ FILE: pkg/util/lru/lru.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package lru import ( "container/list" "sync" ) type LRUCache struct { cap int evictList *list.List items map[interface{}]*list.Element mu sync.RWMutex } type entry struct { key interface{} value interface{} } func NewLRUCache(cap int) *LRUCache { return &LRUCache{ cap: cap, evictList: list.New(), items: make(map[interface{}]*list.Element), } } func (c *LRUCache) Contains(key interface{}) bool { _, exists := c.items[key] return exists } func (c *LRUCache) Add(key, value interface{}) { c.mu.Lock() defer c.mu.Unlock() // TODO: 重复代码优化 if ent, ok := c.items[key]; ok { // 当前元素存在, 覆盖当前 value, 并移动到 list 头部 ent.Value.(*entry).value = value c.evictList.MoveToFront(ent) } else { // 当前元素不存在, 并移动到 list 头部 c.items[key] = c.evictList.PushFront(&entry{key, value}) } // 超出 LRUCache 的容量, 删除 list 尾部元素 if c.evictList.Len() > c.cap { lastElement := c.evictList.Back() c.evictList.Remove(lastElement) delete(c.items, lastElement.Value.(*entry).key) } } func (c *LRUCache) Get(key interface{}) (value interface{}) { c.mu.RLock() defer c.mu.RUnlock() if ent, ok := c.items[key]; ok { value = ent.Value.(*entry).value c.evictList.MoveToFront(ent) } return } func (c *LRUCache) Len() int { return c.evictList.Len() } ================================================ FILE: pkg/util/ssh/ssh.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package ssh import ( "fmt" "time" "golang.org/x/crypto/ssh" "github.com/caoyingjunz/pixiu/pkg/types" ) func NewSSHClient(sshConfig *types.WebSSHRequest) (*ssh.Client, error) { // TODO:利用 gin 的解析,直接设置默认值 port := sshConfig.Port if port == 0 { port = 22 } // TODO 补充支持 PrivateKey 场景 return ssh.Dial("tcp", fmt.Sprintf("%s:%d", sshConfig.Host, port), &ssh.ClientConfig{ Timeout: time.Second * 5, User: sshConfig.User, Auth: []ssh.AuthMethod{ssh.Password(sshConfig.Password)}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 忽略 know_hosts 检查 }) } ================================================ FILE: pkg/util/token/token.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package token import ( "fmt" "time" "github.com/golang-jwt/jwt/v4" ) type Claims struct { jwt.RegisteredClaims Id int64 `json:"id"` Name string `json:"name"` Role string `json:"role"` } // GenerateToken 生成 token func GenerateToken(uid int64, name string, jwtKey []byte) (string, error) { // Generate jwt, 临时有效期 360 分钟 nowTime := time.Now() expiresTime := nowTime.Add(360 * time.Minute) claims := &Claims{ RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expiresTime), // 过期时间 IssuedAt: jwt.NewNumericDate(nowTime), // 签发时间 NotBefore: jwt.NewNumericDate(nowTime), // 生效时间 }, Id: uid, Name: name, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(jwtKey) } func ParseToken(tokenStr string, jwtKey []byte) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (interface{}, error) { return jwtKey, nil }) if err != nil { if ve, ok := err.(*jwt.ValidationError); ok { if ve.Errors == jwt.ValidationErrorExpired { return nil, fmt.Errorf("登录已过期,请重新登录") } } return nil, err } if claims, ok := token.Claims.(*Claims); ok && token.Valid { return claims, nil } return nil, fmt.Errorf("failed to parse token") } ================================================ FILE: pkg/util/util.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "fmt" "math/rand" "net/http" "os" "time" "github.com/gorilla/websocket" "golang.org/x/crypto/bcrypt" ) func init() { rand.Seed(time.Now().UnixNano()) } // EncryptUserPassword 生成加密密码 // 前端传的密码为明文,需要加密存储 // TODO: 后续确认是否有必要在前端加密 func EncryptUserPassword(origin string) (string, error) { pwd, err := bcrypt.GenerateFromPassword([]byte(origin), bcrypt.DefaultCost) if err != nil { return "", err } return string(pwd), nil } // ValidateUserPassword 验证用户的密码是否正确 func ValidateUserPassword(old, new string) error { return bcrypt.CompareHashAndPassword([]byte(old), []byte(new)) } // ValidateStrongPassword validates the password is strong enough. func ValidateStrongPassword(password string) bool { if len(password) < 8 { return false } var oneUpper bool var oneLower bool var oneNumber bool for _, l := range password { if oneUpper && oneLower && oneNumber { return true } if !oneUpper && l >= 'A' && l <= 'Z' { oneUpper = true continue } if !oneLower && l >= 'a' && l <= 'z' { oneLower = true continue } if !oneNumber && l >= '0' && l <= '9' { oneNumber = true continue } } return oneUpper && oneLower && oneNumber } // GenerateRequestID return a request ID string with random suffix. func GenerateRequestID() string { return fmt.Sprintf("%s-%06d", time.Now().Format("20060102150405"), rand.Intn(1000000)) } func IsEmptyS(s string) bool { return len(s) != 0 } func IsDirectoryExists(path string) bool { stat, err := os.Stat(path) if err != nil { return false } if stat.IsDir() { return true } return false } func IsFileExists(path string) bool { stat, err := os.Stat(path) if err != nil { return false } if stat.IsDir() { return false } return true } func EnsureDirectoryExists(path string) error { if !IsDirectoryExists(path) { if err := os.MkdirAll(path, 0755); err != nil { return err } } return nil } func WriteToFile(filename string, data []byte) error { return os.WriteFile(filename, data, 0600) } func BuildWebSocketConnection(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) { upgrader := websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }} upgrader.Subprotocols = []string{r.Header.Get("Sec-WebSocket-Protocol")} return upgrader.Upgrade(w, r, nil) } // DeduplicateIntSlice returns a new slice with duplicated elements removed. func DeduplicateIntSlice(s []int64) (ret []int64) { ret = make([]int64, 0) m := make(map[int64]struct{}) for _, v := range s { if _, ok := m[v]; ok { continue } m[v] = struct{}{} ret = append(ret, v) } return } // More returns the larger one. func More(a, b int) int { if a > b { return a } return b } // Less returns the smaller one. func Less(a, b int) int { if a < b { return a } return b } ================================================ FILE: pkg/util/util_test.go ================================================ /* Copyright 2024 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "reflect" "testing" ) func TestValidateStrongPassword(t *testing.T) { tests := []struct { name string password string want bool }{ { name: "case1", password: "123456", want: false, }, { name: "case2", password: "12345678", want: false, }, { name: "case3", password: "12345678a", want: false, }, { name: "case4", password: "12345678A", want: false, }, { name: "case5", password: "12345678aA", want: true, }, { name: "case6", password: "123456Aa", want: true, }, { name: "case7", password: "abcdefgh", want: false, }, { name: "case8", password: "ABCDEFGH", want: false, }, { name: "case9", password: "abcdef12", want: false, }, { name: "case10", password: "ABCDEF12", want: false, }, { name: "case11", password: "Abcdef12", want: true, }, { name: "case12", password: "aBCDEF12", want: true, }, { name: "case13", password: "$$$$$$$$", want: false, }, { name: "case14", password: "$$$$$aA1", want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ValidateStrongPassword(tt.password); got != tt.want { t.Errorf("ValidateStrongPassword() = %v, want %v", got, tt.want) } }) } } func TestDeduplicateIntSlice(t *testing.T) { type args struct { s []int64 } tests := []struct { name string args args want []int64 }{ { name: "case 1", args: args{ s: []int64{1, 2, 3, 4, 5}, }, want: []int64{1, 2, 3, 4, 5}, }, { name: "case 2", args: args{ s: []int64{1, 1, 1, 1, 1}, }, want: []int64{1}, }, { name: "case 3", args: args{ s: []int64{1, 1, 2, 3, 4, 5}, }, want: []int64{1, 2, 3, 4, 5}, }, { name: "case 4", args: args{ s: []int64{1, 1, 2, 1, 3, 4, 5}, }, want: []int64{1, 2, 3, 4, 5}, }, { name: "case 5", args: args{ s: []int64{1, 2, 3, 4, 5, 5, 5, 5}, }, want: []int64{1, 2, 3, 4, 5}, }, { name: "case 6", args: args{ s: []int64{5, 1, 4, 2, 3, 3, 2, 4, 1, 5}, }, want: []int64{5, 1, 4, 2, 3}, }, { name: "case 7", args: args{ s: []int64{1, 1, 1, 2, 3, 3, 4, 4, 5, 5, 5}, }, want: []int64{1, 2, 3, 4, 5}, }, { name: "case 8", args: args{ s: []int64{1, 2, 3, 4, 5, 5, 5, 5, 4, 3, 2, 1}, }, want: []int64{1, 2, 3, 4, 5}, }, { name: "case 9", args: args{ s: []int64{1, 2, 3, 4, 5, 5, 5, 5, 4, 3, 2, 1, 1, 1}, }, want: []int64{1, 2, 3, 4, 5}, }, { name: "case 10", args: args{ s: []int64{1, 2, 3, 4, 5, 5, 5, 5, 4, 3, 2, 1, 1, 1, 2, 3, 4, 5}, }, want: []int64{1, 2, 3, 4, 5}, }, { name: "case 11", args: args{ s: []int64{}, }, want: []int64{}, }, { name: "case 12", args: args{ s: nil, }, want: []int64{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := DeduplicateIntSlice(tt.args.s); !reflect.DeepEqual(got, tt.want) { t.Errorf("DeduplicateIntSlice() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/util/uuid/uuid.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package uuid import ( "math/rand" "github.com/google/uuid" ) const ( namePrefix = "pixiu-" ) func NewUUID() string { return uuid.New().String() } func NewRandName(length int) string { chars := []rune("abcdefghijklmnopqrstuvwxyz") rs := make([]rune, length) for i := 0; i < length; i++ { rs[i] = chars[rand.Intn(len(chars))] } return namePrefix + string(rs) } ================================================ FILE: template/globals.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package template const GlobalsTemplate = `# Render below by Pixiu --- {{- if .Kubernetes.EnableHA }} enable_kubernetes_ha: "yes" {{- end }} {{- if .Kubernetes.EnablePublicIp }} kube_vip_address: "{{ .Kubernetes.ApiServer }}" {{- end }} {{- if .Kubernetes.ApiPort }} kube_vip_port: "{{ .Kubernetes.ApiPort }}" {{- end }} kube_release: {{ .Kubernetes.KubernetesVersion }} cluster_cidr: "{{ .Network.PodNetwork }}" service_cidr: "{{ .Network.ServiceNetwork }}" network_interface: "{{ .Network.NetworkInterface }}" {{- if and .Component.Haproxy .Component.Haproxy.Enable }} enable_haproxy: "yes" {{- if .Component.Haproxy.KeepalivedVirtualRouterId }} keepalived_virtual_router_id: "{{ .Component.Haproxy.KeepalivedVirtualRouterId }}" {{- end }} {{- end }} {{- if eq .Network.Cni "calico" }} enable_calico: "yes" {{- end }} enable_nfs: "no" ` ================================================ FILE: template/hosts.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package template const HostTemplate = `# Render below by Pixiu engine 127.0.0.1 localhost ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 {{- range .Nodes }} {{ .Ip }} {{ .Name }} {{- end }} ` ================================================ FILE: template/multinode.go ================================================ /* Copyright 2021 The Pixiu Authors. Licensed under the Apache License, Version 2.0 (phe "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package template const MultiModeTemplate = `# Render below by Pixiu engine [docker-master] {{- range .DockerMaster }} {{- if eq .Auth.Type "password" }} {{ .Name }} ansible_ssh_user={{ .Auth.Password.User }} ansible_ssh_pass={{ .Auth.Password.Password }} {{- end }} {{- if eq .Auth.Type "key" }} {{ .Name }} ansible_ssh_user=root ansible_ssh_private_key_file={{ .Auth.Key.File }} {{- end }} {{- end }} [docker-node] {{- range .DockerNode }} {{- if eq .Auth.Type "password" }} {{ .Name }} ansible_ssh_user={{ .Auth.Password.User }} ansible_ssh_pass={{ .Auth.Password.Password }} {{- end }} {{- if eq .Auth.Type "key" }} {{ .Name }} ansible_ssh_user=root ansible_ssh_private_key_file={{ .Auth.Key.File }} {{- end }} {{- end }} [containerd-master] {{- range .ContainerdMaster }} {{- if eq .Auth.Type "password" }} {{ .Name }} ansible_ssh_user={{ .Auth.Password.User }} ansible_ssh_pass={{ .Auth.Password.Password }} {{- end }} {{- if eq .Auth.Type "key" }} {{ .Name }} ansible_ssh_user=root ansible_ssh_private_key_file={{ .Auth.Key.File }} {{- end }} {{- end }} [containerd-node] {{- range .ContainerdNode }} {{- if eq .Auth.Type "password" }} {{ .Name }} ansible_ssh_user={{ .Auth.Password.User }} ansible_ssh_pass={{ .Auth.Password.Password }} {{- end }} {{- if eq .Auth.Type "key" }} {{ .Name }} ansible_ssh_user=root ansible_ssh_private_key_file={{ .Auth.Key.File }} {{- end }} {{- end }} [storage] # Don't change the bellow groups [kube-master:children] docker-master containerd-master [kube-node:children] docker-node containerd-node [baremetal:children] kube-master kube-node storage [kubernetes:children] kube-master kube-node [nfs-server:children] storage [haproxy:children] kube-master `