├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── aigc-dashboard ├── .eslintrc.cjs ├── .gitignore ├── .pnp.cjs ├── .pnp.loader.mjs ├── .prettierrc.json ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── src │ ├── App.vue │ ├── assets │ │ ├── base.css │ │ ├── logo.svg │ │ └── main.css │ ├── components │ │ ├── engine.vue │ │ └── engines.vue │ ├── main.js │ ├── router │ │ └── index.js │ ├── stores │ │ └── counter.js │ └── views │ │ ├── dashboard.vue │ │ └── welcome.vue ├── vite.config.js ├── vitest.config.js └── yarn.lock ├── deploy ├── helm │ ├── aigc-gateway │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── templates │ │ │ └── aigc-gateway.yaml │ │ └── values.yaml │ └── logto │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── templates │ │ ├── logto.yaml │ │ └── postgress.yaml │ │ └── values.yaml └── yaml │ ├── aigc-gateway.yaml │ ├── logto.yaml │ └── postgres.yaml ├── docs ├── Dashboard介绍.md ├── en │ └── README.md ├── images │ ├── Oops-wrong.png │ ├── SD-dash.png │ ├── Traditional-Web-console.png │ ├── Traditional-Web-setting.jpeg │ ├── admin-login.png │ ├── arch.png │ ├── auth.png │ ├── console.png │ ├── customdata-delete-1.png │ ├── customdata-delete-2.png │ ├── dashboard-installed.png │ ├── dashboard-login.png │ ├── dashboard-recover.png │ ├── dashboard-uninstalled.png │ ├── logout.png │ ├── m2m-console.png │ ├── m2m-role-assign.png │ ├── m2m-role-setting.png │ ├── m2m-setting.png │ ├── quick-setup-realse.png │ └── user-login.png ├── 安装部署.md ├── 常见问题.md ├── 架构原理.md └── 模版管理.md ├── go.mod ├── go.sum ├── main.go └── pkg ├── controller └── controller.go ├── resources ├── error.go ├── error_test.go ├── resource.go ├── resource_manager.go └── resource_manager_test.go ├── routers ├── resource.go └── sign.go ├── session └── session.go ├── user └── user.go └── utils ├── config.go ├── config_test.go ├── string.go ├── string_test.go └── test.config /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | **/.DS_Store 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | .mvn 18 | mvnw* 19 | 20 | # vscode 21 | .vscode 22 | 23 | # Test binary, built with `go test -c` 24 | *.test 25 | 26 | # Output of the go coverage tool, specifically when used with LiteIDE 27 | *.out 28 | 29 | # Dependency directories (remove the comment below to include it) 30 | # vendor/ 31 | 32 | # Go workspace file 33 | go.work 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM golang:1.19 as builder 3 | 4 | # Set destination for COPY 5 | WORKDIR /app 6 | 7 | # Download Go modules 8 | COPY go.mod go.sum ./ 9 | RUN go mod download 10 | 11 | # Copy the source code. Note the slash at the end, as explained in 12 | # https://docs.docker.com/engine/reference/builder/#copy 13 | COPY . ./ 14 | 15 | # Build 16 | #RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go 17 | 18 | RUN CGO_ENABLED=0 GOOS=linux go build -a -o aigc-gateway main.go 19 | 20 | # Optional: 21 | # To bind to a TCP port, runtime parameters must be supplied to the docker command. 22 | # But we can document in the Dockerfile what ports 23 | # the application is going to listen on by default. 24 | # https://docs.docker.com/engine/reference/builder/#expose 25 | 26 | FROM alpine:3.17 27 | 28 | RUN apk add --no-cache ca-certificates bash expat curl \ 29 | && rm -rf /var/cache/apk/* 30 | 31 | WORKDIR /app 32 | COPY --from=builder /app/aigc-gateway . 33 | #COPY ./aigc-gateway . 34 | COPY ./aigc-dashboard ./aigc-dashboard 35 | 36 | # Run 37 | CMD ["./aigc-gateway"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/Makefile -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AIGC-Gateway 2 | 3 | 中文 | [English](./docs/en/README.md) 4 | 5 | ## AIGC-Gateway概述 6 | 7 | 2023年,AIGC的潮流引爆了泛娱乐内容的生产力革命。文生图、文生音频、文生视频等AIGC范式成为了各大公司争相投入和落地生产的主要场景。目前AIGC领域主要分为两个大的场景,一个是面向终端客户的场景,例如:Midjourney或者Hugging Face;另外一个是面向内容生产者的场景,例如:Stable Diffusion。前者更多的是面向AIGC的服务化与商业化,而后者更多的面向企业内部数字资产生成与提效。大部分企业会通过第二种形态在企业内部落地AIGC的能力。 8 | 9 | 相比传统的互联网型业务而言,AIGC引擎的使用与运维具有如下特性: 10 | 11 | - **运行时间碎片且弹性** 12 | 13 | 通常 AIGC引擎的使用时间与内容生产者的使用周期是强相关的。因为目前为止,AIGC引擎生成内容的质量需要人进行评估与交互式的调整。这会导致从单个内容生产者的角度而言,使用的时间会非常碎片,而且使用的时间周期性区间会非常确定。一般情况下,只有在工作日的4-6个小时是AIGC引擎集中使用的时间,而有效的使用时长会更短暂,工作日的晚上与双休日正常情况下也是没有任何使用时长的。 14 | 15 | 16 | - **数字资产隔离与安全** 17 | 18 | 对于很多以内容生产为主体的行业与公司而言,AIGC引擎中的自训练模型是数字核心资产。不同的内容生产者之间是非主动不自动共享模型的。而且不同的内容生产者之间,生成的内容需要做到访问隔离、防止篡改、数据隔离。 19 | 20 | 21 | - **多种资源类型与弹性** 22 | 23 | AIGC引擎对GPU的异构算力是强依赖。但是,GPU的种类丰富,不同类型的GPU在成本和效率上的差异非常大。以Stable Diffusion为例,A100 40G显存与A100 80G显存在生成图片的效率上相差1s左右,但是成本上会相差1倍以上。此外,很多早期就涉足AIGC的公司在本地机房中都会有比较多的消费卡(例如:NV3090)。在企业内部落地AIGC引擎的时候,需要考虑如何将这些资源利用起来,并统一管理和提效。 24 | 25 | 26 | - **引擎种类与版本繁多** 27 | 28 | AIGC引擎的迭代速度非常快,不同版本之间会存在一定的不兼容。而作为数字资产生成的工具,通常为了保障模型的稳定运行,一旦投入生产,大部分时间不会轻易变更版本。而不同的内容生产者之间依赖的版本极有可能也会有所不同。所以,多个版本如何统一的管理和升级,也是企业内部落地AIGC的时候需要考虑的事情。 29 | 30 | 31 | ## AIGC-Gateway特性 32 | 33 | 为了解决企业内部落地AIGC引擎的通用性问题,阿里云与行者AI一起开源了AIGC-Gateway项目,降低企业内部AIGC落地的难度与费用,真正做到开箱即用,即开即用。 34 | 35 | AIGC-Gateway整体架构如下: 36 | 37 | ![architecture](./docs/images/arch.png) 38 | 39 | AIGC-Gateway具有如下特性: 40 | - **动态拉起按需释放** 41 | 42 | 对于任意一种AIGC引擎,AIGC-Gateway都可以做到,访问的时候拉起引擎,退出或者离开的时候,释放引擎所需的GPU资源,并保留数据存储。当下一次同一个用户访问的时候,快速拉起新的资源并挂载归属于这个客户的数据存储,实现GPU卡的分时复用,降低整体的成本投入。 43 | 44 | 45 | - **租户独享访问隔离** 46 | 47 | 通过AIGC-Gateway拉起的AIGC引擎资源独享,数据隔离。除此之外,访问地址通过OIDC的方式进行了安全防护,对任意一种AIGC引擎都可以无侵入式地实现访问的权限控制。此外,AIGC-Gateway通过与开源认证体系Logto的集成,实现了企业内部账号体系的自动单点登录与认证,简化了 48 | 49 | 50 | - **统一GPU资源管理** 51 | 52 | AIGC-Gateway是一个与云厂商无关的开源项目,支持企业的IT工程师进行本地部署、混合云部署、多云部署。对于不同类型的GPU资源,例如:本地GPU、云上单卡GPU、云上多卡GPU、云上显存共享GPU都有完善的支持。 53 | 54 | 55 | - **版本/模型统一管理** 56 | 57 | AIGC-Gateway支持同时部署多种、多套、多版本的AIGC引擎。并且,可以对同一个版本的AIGC引擎进行简单便捷的批量管理。并且支持在不同版本的AIGC引擎进行数据的迁移与切换。 58 | 59 | 60 | - **成本核算与费用分摊** 61 | 62 | 在阿里云上,可以通过可视化大盘和API的方式,实现组户级、实例级使用成本的核算,便于内部的成本核算以及成本单价的估算。 63 | 64 | 65 | ## 下一步 66 | 67 | - [AIGC-Gateway Dashboard介绍](./docs/Dashboard介绍.md) 68 | - [安装并配置AIGC-Gateway](./docs/安装部署.md) 69 | - [如何部署多种AIGC引擎模版](./docs/模版管理.md) 70 | - [AIGC-Gateway的架构与原理](./docs/架构原理.md) 71 | - [常见问题与排查](./docs/常见问题.md) 72 | 73 | ## 上游项目 74 | 75 | - [OpenKruiseGame](https://github.com/openkruise/kruise-game) 76 | - [logto](https://github.com/logto-io/logto/) 77 | -------------------------------------------------------------------------------- /aigc-dashboard/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-prettier/skip-formatting' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 'latest' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /aigc-dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /aigc-dashboard/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /aigc-dashboard/README.md: -------------------------------------------------------------------------------- 1 | # aigc-dashboard 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Customize configuration 10 | 11 | See [Vite Configuration Reference](https://vitejs.dev/config/). 12 | 13 | ## Project Setup 14 | 15 | ```sh 16 | npm install 17 | ``` 18 | 19 | ### Compile and Hot-Reload for Development 20 | 21 | ```sh 22 | npm run dev 23 | ``` 24 | 25 | ### Compile and Minify for Production 26 | 27 | ```sh 28 | npm run build 29 | ``` 30 | 31 | ### Run Unit Tests with [Vitest](https://vitest.dev/) 32 | 33 | ```sh 34 | npm run test:unit 35 | ``` 36 | 37 | ### Lint with [ESLint](https://eslint.org/) 38 | 39 | ```sh 40 | npm run lint 41 | ``` 42 | -------------------------------------------------------------------------------- /aigc-dashboard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AIGC-Gateway 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /aigc-dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aigc-dashboard", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "test:unit": "vitest", 10 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", 11 | "format": "prettier --write src/" 12 | }, 13 | "dependencies": { 14 | "@fortawesome/fontawesome-free": "^6.4.0", 15 | "@mdi/font": "^7.2.96", 16 | "axios": "^1.4.0", 17 | "material-design-icons-iconfont": "^6.7.0", 18 | "pinia": "^2.0.35", 19 | "vue": "^3.2.47", 20 | "vue-axios": "^3.5.2", 21 | "vue-router": "^4.1.6", 22 | "vuetify": "^3.2.3" 23 | }, 24 | "devDependencies": { 25 | "@rushstack/eslint-patch": "^1.2.0", 26 | "@vitejs/plugin-vue": "^4.2.1", 27 | "@vue/eslint-config-prettier": "^7.1.0", 28 | "@vue/test-utils": "^2.3.2", 29 | "eslint": "^8.39.0", 30 | "eslint-plugin-vue": "^9.11.0", 31 | "jsdom": "^22.0.0", 32 | "prettier": "^2.8.8", 33 | "vite": "^4.3.5", 34 | "vitest": "^0.31.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /aigc-dashboard/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/aigc-dashboard/public/favicon.ico -------------------------------------------------------------------------------- /aigc-dashboard/src/App.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /aigc-dashboard/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /aigc-dashboard/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 18 | 19 | 21 | 23 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /aigc-dashboard/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; -------------------------------------------------------------------------------- /aigc-dashboard/src/components/engine.vue: -------------------------------------------------------------------------------- 1 | 165 | 166 | -------------------------------------------------------------------------------- /aigc-dashboard/src/components/engines.vue: -------------------------------------------------------------------------------- 1 | 3 | 25 | 26 | -------------------------------------------------------------------------------- /aigc-dashboard/src/main.js: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | import 'vuetify/styles' 3 | import {createVuetify} from 'vuetify' 4 | import * as components from 'vuetify/components' 5 | import * as directives from 'vuetify/directives' 6 | import 'material-design-icons-iconfont/dist/material-design-icons.css' 7 | import '@mdi/font/css/materialdesignicons.css' 8 | import '@fortawesome/fontawesome-free/css/all.css' 9 | import {createApp} from 'vue' 10 | import {createPinia} from 'pinia' 11 | 12 | import App from './App.vue' 13 | import router from './router' 14 | 15 | import axios from 'axios' 16 | import VueAxios from 'vue-axios' 17 | 18 | 19 | const vuetify = createVuetify({ 20 | components, 21 | directives, 22 | theme: { 23 | defaultTheme: 'dark' 24 | } 25 | }) 26 | 27 | const app = createApp(App) 28 | 29 | app.use(createPinia()) 30 | app.use(router) 31 | app.use(vuetify) 32 | app.use(VueAxios, axios) 33 | 34 | app.mount('#app') 35 | -------------------------------------------------------------------------------- /aigc-dashboard/src/router/index.js: -------------------------------------------------------------------------------- 1 | import {createRouter, createWebHashHistory} from 'vue-router' 2 | import WelcomeView from '../views/welcome.vue' 3 | import DashboardView from '../views/dashboard.vue' 4 | 5 | const router = createRouter({ 6 | history: createWebHashHistory(), 7 | routes: [ 8 | { 9 | path: '/', 10 | name: 'welcome', 11 | component: WelcomeView 12 | }, 13 | { 14 | path: '/dashboard', 15 | name: 'dashboard', 16 | // route level code-splitting 17 | // this generates a separate chunk (About.[hash].js) for this route 18 | // which is lazy-loaded when the route is visited. 19 | component: DashboardView, 20 | } 21 | ] 22 | }) 23 | 24 | 25 | router.beforeEach(async (to, from) => { 26 | if (!login && to.name != "welcome") { 27 | // redirect the user to the login page 28 | return {name: 'welcome'} 29 | } 30 | 31 | if (login && to.name == "welcome") { 32 | return {name: 'dashboard'} 33 | } 34 | }) 35 | 36 | export default router 37 | -------------------------------------------------------------------------------- /aigc-dashboard/src/stores/counter.js: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useCounterStore = defineStore('counter', () => { 5 | const count = ref(0) 6 | const doubleCount = computed(() => count.value * 2) 7 | function increment() { 8 | count.value++ 9 | } 10 | 11 | return { count, doubleCount, increment } 12 | }) 13 | -------------------------------------------------------------------------------- /aigc-dashboard/src/views/dashboard.vue: -------------------------------------------------------------------------------- 1 | 3 | 24 | 59 | -------------------------------------------------------------------------------- /aigc-dashboard/src/views/welcome.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 21 | 22 | 46 | -------------------------------------------------------------------------------- /aigc-dashboard/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | resolve: { 10 | alias: { 11 | '@': fileURLToPath(new URL('./src', import.meta.url)) 12 | } 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /aigc-dashboard/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { mergeConfig } from 'vite' 3 | import { configDefaults, defineConfig } from 'vitest/config' 4 | import viteConfig from './vite.config' 5 | 6 | export default mergeConfig( 7 | viteConfig, 8 | defineConfig({ 9 | test: { 10 | environment: 'jsdom', 11 | exclude: [...configDefaults.exclude, 'e2e/*'], 12 | root: fileURLToPath(new URL('./', import.meta.url)) 13 | } 14 | }) 15 | ) 16 | -------------------------------------------------------------------------------- /deploy/helm/aigc-gateway/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ -------------------------------------------------------------------------------- /deploy/helm/aigc-gateway/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: aigc-gateway 3 | description: Helm chart for aigc-gateway running on k8s 4 | version: 1.1.0 5 | appVersion: 1.1.0 6 | kubeVersion: ">= 1.16.0-0" 7 | -------------------------------------------------------------------------------- /deploy/helm/aigc-gateway/templates/aigc-gateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: aigc-gateway 5 | namespace: {{ .Values.installation.namespace }} 6 | labels: 7 | app: aigc-gateway 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: aigc-gateway 13 | template: 14 | metadata: 15 | labels: 16 | app: aigc-gateway 17 | spec: 18 | containers: 19 | - name: aigc-gateway 20 | image: {{ .Values.image.repository }}:{{ .Values.image.tag }} 21 | command: 22 | - "./aigc-gateway" 23 | env: 24 | - name: App_Id 25 | value: {{ .Values.appId }} 26 | - name: App_Secret 27 | value: {{ .Values.appSecret }} 28 | - name: M2M_Id 29 | value: {{ .Values.m2mId }} 30 | - name: M2M_Secret 31 | value: {{ .Values.m2mSecret }} 32 | - name: Redirect_Url 33 | value: https://{{ .Values.host }}/ 34 | - name: Endpoint 35 | value: {{ .Values.endpoint }} 36 | ports: 37 | - containerPort: 8090 38 | name: dashboard 39 | volumeMounts: 40 | - name: config-volume 41 | mountPath: "/etc/config" # 挂载点路径 42 | readOnly: true 43 | volumes: 44 | - name: config-volume 45 | configMap: 46 | name: aigc-gateway-config 47 | items: 48 | - key: config.json 49 | path: "config.json" # 映射为容器内的文件名 50 | serviceAccountName: aigc-gateway 51 | --- 52 | apiVersion: v1 53 | kind: ConfigMap 54 | metadata: 55 | name: aigc-gateway-config 56 | namespace: {{ .Values.installation.namespace }} 57 | data: 58 | config.json: | 59 | { 60 | "namespaces": ["default"], 61 | "gss_labels": {} 62 | } 63 | --- 64 | apiVersion: v1 65 | kind: Service 66 | metadata: 67 | name: aigc-gateway 68 | namespace: {{ .Values.installation.namespace }} 69 | labels: 70 | app: aigc-gateway 71 | spec: 72 | selector: 73 | app: aigc-gateway 74 | ports: 75 | - protocol: TCP 76 | name: dashboard 77 | port: 8090 78 | targetPort: 8090 79 | type: ClusterIP 80 | --- 81 | apiVersion: networking.k8s.io/v1 82 | kind: Ingress 83 | metadata: 84 | name: aigc-gateway 85 | namespace: {{ .Values.installation.namespace }} 86 | annotations: 87 | "nginx.ingress.kubernetes.io/ssl-redirect": "true" 88 | spec: 89 | ingressClassName: nginx 90 | tls: 91 | - hosts: 92 | - {{ .Values.host }} 93 | secretName: {{ .Values.secretName }} 94 | rules: 95 | - host: {{ .Values.host }} 96 | http: 97 | paths: 98 | - backend: 99 | service: 100 | name: aigc-gateway 101 | port: 102 | number: 8090 103 | pathType: ImplementationSpecific 104 | path: "/" 105 | --- 106 | apiVersion: v1 107 | kind: ServiceAccount 108 | metadata: 109 | name: aigc-gateway 110 | namespace: {{ .Values.installation.namespace }} 111 | --- 112 | apiVersion: rbac.authorization.k8s.io/v1 113 | kind: ClusterRole 114 | metadata: 115 | name: aigc-gateway 116 | rules: 117 | - apiGroups: 118 | - game.kruise.io 119 | resources: 120 | - gameservers 121 | verbs: 122 | - create 123 | - delete 124 | - get 125 | - list 126 | - patch 127 | - update 128 | - watch 129 | - apiGroups: 130 | - game.kruise.io 131 | resources: 132 | - gameservers/finalizers 133 | verbs: 134 | - update 135 | - apiGroups: 136 | - game.kruise.io 137 | resources: 138 | - gameservers/status 139 | verbs: 140 | - get 141 | - patch 142 | - update 143 | - apiGroups: 144 | - game.kruise.io 145 | resources: 146 | - gameserversets 147 | verbs: 148 | - create 149 | - delete 150 | - get 151 | - list 152 | - patch 153 | - update 154 | - watch 155 | - apiGroups: 156 | - game.kruise.io 157 | resources: 158 | - gameserversets/finalizers 159 | verbs: 160 | - update 161 | - apiGroups: 162 | - game.kruise.io 163 | resources: 164 | - gameserversets/status 165 | verbs: 166 | - get 167 | - patch 168 | - update 169 | - apiGroups: 170 | - "" 171 | resources: 172 | - persistentvolumeclaims 173 | verbs: 174 | - delete 175 | - get 176 | - apiGroups: 177 | - "" 178 | resources: 179 | - pods 180 | verbs: 181 | - get 182 | - delete 183 | --- 184 | apiVersion: rbac.authorization.k8s.io/v1 185 | kind: ClusterRoleBinding 186 | metadata: 187 | name: aigc-gateway 188 | roleRef: 189 | apiGroup: rbac.authorization.k8s.io 190 | kind: ClusterRole 191 | name: aigc-gateway 192 | subjects: 193 | - kind: ServiceAccount 194 | name: aigc-gateway 195 | namespace: {{ .Values.installation.namespace }} 196 | -------------------------------------------------------------------------------- /deploy/helm/aigc-gateway/values.yaml: -------------------------------------------------------------------------------- 1 | installation: 2 | namespace: aigc-gateway 3 | image: 4 | repository: registry-cn-hangzhou.ack.aliyuncs.com/acs/aigc-gateway 5 | tag: v1.3.0 6 | appId: "32qh5wp6c1q3jofy2myw" 7 | appSecret: "35mpn2dk62brjxaduy70" 8 | m2mId: "ezxyaol30ld99c7ns5ur" 9 | m2mSecret: "g7hly3nwq2omligpyh3k" 10 | host: "dashboard.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com" 11 | endpoint: "https://logto.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com/" 12 | secretName: "tls-logto" -------------------------------------------------------------------------------- /deploy/helm/logto/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ -------------------------------------------------------------------------------- /deploy/helm/logto/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: logto 3 | description: Helm chart for logto running on k8s 4 | version: 0.1.0 5 | appVersion: 0.1.0 6 | kubeVersion: ">= 1.16.0-0" 7 | -------------------------------------------------------------------------------- /deploy/helm/logto/templates/logto.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: logto 5 | namespace: {{ .Values.installation.namespace }} 6 | labels: 7 | app: logto 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: logto 13 | template: 14 | metadata: 15 | labels: 16 | app: logto 17 | spec: 18 | containers: 19 | - name: logto 20 | image: svhd/logto:1.4 # replace it with your exactly 21 | command: 22 | - "sh" 23 | - "-c" 24 | - "npm run cli db seed -- --swe && npm start" 25 | env: 26 | - name: TRUST_PROXY_HEADER 27 | value: "1" 28 | - name: DB_URL 29 | value: "postgres://postgres:p0stgr3s@postgres.{{ .Values.installation.namespace }}.svc.cluster.local:5432/logto" 30 | - name: ADMIN_ENDPOINT 31 | value: https://{{ .Values.adminEndpoint }} 32 | - name: ENDPOINT 33 | value: https://{{ .Values.endpoint }} 34 | ports: 35 | - containerPort: 3001 36 | name: signin 37 | - containerPort: 3002 38 | name: console 39 | --- 40 | apiVersion: v1 41 | kind: Service 42 | metadata: 43 | name: logto 44 | namespace: {{ .Values.installation.namespace }} 45 | labels: 46 | app: logto 47 | spec: 48 | selector: 49 | app: logto 50 | ports: 51 | - protocol: TCP 52 | name: signin 53 | port: 3001 54 | targetPort: 3001 55 | - protocol: TCP 56 | name: console 57 | port: 3002 58 | targetPort: 3002 59 | type: ClusterIP 60 | --- 61 | apiVersion: networking.k8s.io/v1 62 | kind: Ingress 63 | metadata: 64 | name: logto 65 | namespace: {{ .Values.installation.namespace }} 66 | annotations: 67 | "nginx.ingress.kubernetes.io/ssl-redirect": "true" 68 | spec: 69 | ingressClassName: nginx 70 | tls: 71 | - hosts: 72 | - {{ .Values.endpoint }} 73 | secretName: {{ .Values.secretName }} 74 | rules: 75 | - host: {{ .Values.endpoint }} 76 | http: 77 | paths: 78 | - backend: 79 | service: 80 | name: logto 81 | port: 82 | number: 3001 83 | pathType: ImplementationSpecific 84 | path: "/" 85 | --- 86 | apiVersion: networking.k8s.io/v1 87 | kind: Ingress 88 | metadata: 89 | name: logto-admin 90 | namespace: {{ .Values.installation.namespace }} 91 | annotations: 92 | "nginx.ingress.kubernetes.io/ssl-redirect": "true" 93 | spec: 94 | ingressClassName: nginx 95 | tls: 96 | - hosts: 97 | - {{ .Values.adminEndpoint }} 98 | secretName: {{ .Values.secretName }} 99 | rules: 100 | - host: {{ .Values.adminEndpoint }} 101 | http: 102 | paths: 103 | - backend: 104 | service: 105 | name: logto 106 | port: 107 | number: 3002 108 | pathType: ImplementationSpecific 109 | path: "/" -------------------------------------------------------------------------------- /deploy/helm/logto/templates/postgress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.installation.createNamespace }} 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: {{ .Values.installation.namespace }} 6 | {{- end }} 7 | --- 8 | apiVersion: apps/v1 9 | kind: StatefulSet 10 | metadata: 11 | name: postgres 12 | namespace: {{ .Values.installation.namespace }} 13 | spec: 14 | selector: 15 | matchLabels: 16 | app: postgres 17 | serviceName: "postgres" 18 | replicas: 1 19 | template: 20 | metadata: 21 | labels: 22 | app: postgres 23 | spec: 24 | containers: 25 | - name: postgres 26 | securityContext: 27 | runAsUser: 70 28 | runAsGroup: 70 29 | runAsNonRoot: true 30 | env: 31 | - name: POSTGRES_USER 32 | value: postgres 33 | - name: POSTGRES_PASSWORD 34 | value: p0stgr3s 35 | image: postgres:14-alpine 36 | ports: 37 | - containerPort: 5432 38 | name: postgres 39 | livenessProbe: 40 | exec: 41 | command: 42 | - "pg_isready" 43 | failureThreshold: 5 44 | periodSeconds: 10 45 | timeoutSeconds: 5 46 | --- 47 | 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: postgres 52 | namespace: {{ .Values.installation.namespace }} 53 | labels: 54 | app: postgres 55 | spec: 56 | ports: 57 | - port: 5432 58 | name: postgres 59 | clusterIP: None 60 | selector: 61 | app: postgres -------------------------------------------------------------------------------- /deploy/helm/logto/values.yaml: -------------------------------------------------------------------------------- 1 | adminEndpoint: "logto-admin.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com" 2 | endpoint: "logto.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com" 3 | secretName: "tls-logto" 4 | installation: 5 | namespace: aigc-gateway 6 | createNamespace: false 7 | -------------------------------------------------------------------------------- /deploy/yaml/aigc-gateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: aigc-gateway 5 | labels: 6 | app: aigc-gateway 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: aigc-gateway 12 | template: 13 | metadata: 14 | labels: 15 | app: aigc-gateway 16 | spec: 17 | containers: 18 | - name: aigc-gateway 19 | image: registry.cn-beijing.aliyuncs.com/acs/aigc-gateway:v0.1.0 # replace it with your exactly 20 | command: 21 | - "./aigc-gateway" 22 | env: 23 | - name: App_Id 24 | value: "550tqtqmoxfgc2efcs0hg" 25 | - name: App_Secret 26 | value: "os1dow3fzqrukv8ngvoat" 27 | - name: M2M_Id 28 | value: "" 29 | - name: M2M_Secret 30 | value: "" 31 | - name: Basic_Auth_Token 32 | value: "eHp3ZnB3cjR3cGhhdmVrcDY4ZGVnOjJjazF3YnY5cWdvbmw1ZjZtNWdjdw==" 33 | - name: Redirect_Url 34 | value: "https://dashboard.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com/" 35 | - name: Endpoint 36 | value: "https://logto.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com/" 37 | ports: 38 | - containerPort: 8090 39 | name: dashboard 40 | serviceAccountName: aigc-gateway 41 | --- 42 | apiVersion: v1 43 | kind: Service 44 | metadata: 45 | name: aigc-gateway 46 | labels: 47 | app: aigc-gateway 48 | spec: 49 | selector: 50 | app: aigc-gateway 51 | ports: 52 | - protocol: TCP 53 | name: dashboard 54 | port: 8090 55 | targetPort: 8090 56 | type: ClusterIP 57 | --- 58 | apiVersion: networking.k8s.io/v1 59 | kind: Ingress 60 | metadata: 61 | name: aigc-gateway 62 | annotations: 63 | "nginx.ingress.kubernetes.io/ssl-redirect": "true" 64 | spec: 65 | ingressClassName: nginx 66 | tls: 67 | - hosts: 68 | - "dashboard.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com" 69 | secretName: tls-logto 70 | rules: 71 | - host: "dashboard.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com" 72 | http: 73 | paths: 74 | - backend: 75 | service: 76 | name: aigc-gateway 77 | port: 78 | number: 8090 79 | pathType: ImplementationSpecific 80 | path: "/" 81 | --- 82 | apiVersion: v1 83 | kind: ServiceAccount 84 | metadata: 85 | name: aigc-gateway 86 | namespace: default 87 | --- 88 | apiVersion: rbac.authorization.k8s.io/v1 89 | kind: ClusterRole 90 | metadata: 91 | name: aigc-gateway 92 | rules: 93 | - apiGroups: 94 | - game.kruise.io 95 | resources: 96 | - gameservers 97 | verbs: 98 | - create 99 | - delete 100 | - get 101 | - list 102 | - patch 103 | - update 104 | - watch 105 | - apiGroups: 106 | - game.kruise.io 107 | resources: 108 | - gameservers/finalizers 109 | verbs: 110 | - update 111 | - apiGroups: 112 | - game.kruise.io 113 | resources: 114 | - gameservers/status 115 | verbs: 116 | - get 117 | - patch 118 | - update 119 | - apiGroups: 120 | - game.kruise.io 121 | resources: 122 | - gameserversets 123 | verbs: 124 | - create 125 | - delete 126 | - get 127 | - list 128 | - patch 129 | - update 130 | - watch 131 | - apiGroups: 132 | - game.kruise.io 133 | resources: 134 | - gameserversets/finalizers 135 | verbs: 136 | - update 137 | - apiGroups: 138 | - game.kruise.io 139 | resources: 140 | - gameserversets/status 141 | verbs: 142 | - get 143 | - patch 144 | - update 145 | --- 146 | apiVersion: rbac.authorization.k8s.io/v1 147 | kind: ClusterRoleBinding 148 | metadata: 149 | name: aigc-gateway 150 | roleRef: 151 | apiGroup: rbac.authorization.k8s.io 152 | kind: ClusterRole 153 | name: aigc-gateway 154 | subjects: 155 | - kind: ServiceAccount 156 | name: aigc-gateway 157 | namespace: default -------------------------------------------------------------------------------- /deploy/yaml/logto.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: logto 5 | labels: 6 | app: logto 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: logto 12 | template: 13 | metadata: 14 | labels: 15 | app: logto 16 | spec: 17 | containers: 18 | - name: logto 19 | image: svhd/logto:latest # replace it with your exactly 20 | command: 21 | - "sh" 22 | - "-c" 23 | - "npm run cli db seed -- --swe && npm start" 24 | env: 25 | - name: TRUST_PROXY_HEADER 26 | value: "1" 27 | - name: DB_URL 28 | value: "postgres://postgres:p0stgr3s@postgres.default.svc.cluster.local:5432/logto" 29 | - name: ADMIN_ENDPOINT 30 | value: "https://logto-admin.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com" 31 | - name: ENDPOINT 32 | value: "https://logto.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com" 33 | ports: 34 | - containerPort: 3001 35 | name: signin 36 | - containerPort: 3002 37 | name: console 38 | --- 39 | apiVersion: v1 40 | kind: Service 41 | metadata: 42 | name: logto 43 | labels: 44 | app: logto 45 | spec: 46 | selector: 47 | app: logto 48 | ports: 49 | - protocol: TCP 50 | name: signin 51 | port: 3001 52 | targetPort: 3001 53 | - protocol: TCP 54 | name: console 55 | port: 3002 56 | targetPort: 3002 57 | type: ClusterIP 58 | --- 59 | apiVersion: networking.k8s.io/v1 60 | kind: Ingress 61 | metadata: 62 | name: logto 63 | annotations: 64 | "nginx.ingress.kubernetes.io/ssl-redirect": "true" 65 | spec: 66 | ingressClassName: nginx 67 | tls: 68 | - hosts: 69 | - "logto.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com" 70 | secretName: tls-logto 71 | rules: 72 | - host: "logto.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com" 73 | http: 74 | paths: 75 | - backend: 76 | service: 77 | name: logto 78 | port: 79 | number: 3001 80 | pathType: ImplementationSpecific 81 | path: "/" 82 | --- 83 | apiVersion: networking.k8s.io/v1 84 | kind: Ingress 85 | metadata: 86 | name: logto-admin 87 | annotations: 88 | "nginx.ingress.kubernetes.io/ssl-redirect": "true" 89 | spec: 90 | ingressClassName: nginx 91 | tls: 92 | - hosts: 93 | - "logto-admin.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com" 94 | secretName: tls-logto 95 | rules: 96 | - host: "logto-admin.c5464a5f2c39341d3b3eda6e2dd37b55.cn-hangzhou.alicontainer.com" 97 | http: 98 | paths: 99 | - backend: 100 | service: 101 | name: logto 102 | port: 103 | number: 3002 104 | pathType: ImplementationSpecific 105 | path: "/" -------------------------------------------------------------------------------- /deploy/yaml/postgres.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: postgres 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: postgres 9 | serviceName: "postgres" 10 | replicas: 1 11 | template: 12 | metadata: 13 | labels: 14 | app: postgres 15 | spec: 16 | containers: 17 | - name: postgres 18 | securityContext: 19 | runAsUser: 70 20 | runAsGroup: 70 21 | runAsNonRoot: true 22 | env: 23 | - name: POSTGRES_USER 24 | value: postgres 25 | - name: POSTGRES_PASSWORD 26 | value: p0stgr3s 27 | image: postgres:14-alpine 28 | ports: 29 | - containerPort: 5432 30 | name: postgres 31 | livenessProbe: 32 | exec: 33 | command: 34 | - "pg_isready" 35 | failureThreshold: 5 36 | periodSeconds: 10 37 | timeoutSeconds: 5 38 | --- 39 | 40 | apiVersion: v1 41 | kind: Service 42 | metadata: 43 | name: postgres 44 | labels: 45 | app: postgres 46 | spec: 47 | ports: 48 | - port: 5432 49 | name: postgres 50 | clusterIP: None 51 | selector: 52 | app: postgres -------------------------------------------------------------------------------- /docs/Dashboard介绍.md: -------------------------------------------------------------------------------- 1 | 2 | ## 初始页面 3 | 4 | 访问AIGC-Gateway地址,进入初始页面。点击START TO USE,将进入用户个人控制台。 5 | 6 | ![Initial page](./images/dashboard-login.png) 7 | 8 | ## 用户认证页面 9 | 10 | 当用户未处于登录态,需要登录帐号进行身份认证。 11 | 12 | ![user login page](./images/user-login.png) 13 | 14 | ## 用户个人控制台 15 | 16 | ### 安装 17 | 18 | 新用户登录后不存在属于自己的AIGC实例,可以选择对应模版,安装所需实例。 19 | 20 | ![instance uninstalled page](./images/dashboard-uninstalled.png) 21 | 22 | ### 使用 23 | 24 | 当实例完成安装,用户可以点击VISIT访问对应实例 25 | 26 | ![instance installed page](./images/dashboard-installed.png) 27 | 28 | 本例中使用 Stable-Diffusion,点击VISIT后跳转至SD页面 29 | 30 | ![instance visit](./images/SD-dash.png) 31 | 32 | ### 停止与恢复 33 | 34 | 当用户使用完成,可以点击PAUSE释放实例。当实例完成释放,用户可以点击RECOVER来重载该实例,实例的持久化数据会再次加载进来,不会丢失。 35 | 36 | ![instance installed page](./images/dashboard-recover.png) 37 | 38 | 39 | ### 登出 40 | 41 | 用户完成使用,可以点击左边栏中Log Out,退出登录态 42 | 43 | ![logout](./images/logout.png) -------------------------------------------------------------------------------- /docs/en/README.md: -------------------------------------------------------------------------------- 1 | # AIGC-Gateway 2 | [中文](../../README.md) | English 3 | 4 | ## Overview of AIGC-Gateway 5 | 6 | In 2023, The trend of AIGC (Artificial Intelligence Generated Content) ignited a productivity revolution in the production of pan-entertainment content. AIGC paradigms such as AIGC graphics, AIGC audio, and AIGC video became the main scene for major companies to invest in and land production. Currently, the AIGC field is mainly divided into two major scenes: one is the scene for end customers, such as Midjourney or Hugging Face; the other is the scene for content producers, such as Stable Diffusion. The former is more focused on the service and commercialization of AIGC, while the latter is more focused on generating digital assets and improving efficiency within the enterprise. Most companies will use the second form to land AIGC capabilities within the enterprise. 7 | 8 | Compared to traditional internet-based businesses, the use and operation of AIGC engines have the following characteristics: 9 | 10 | - **Fragmented and Elastic Runtime** 11 | 12 | Usually, the usage time of AIGC engines is strongly related to the usage cycle of content producers. So far, the quality of AIGC engine-generated content requires manual evaluation and interactive adjustment. This will result in very fragmented usage time from the perspective of a single content producer, and the usage time interval will be very certain. Generally, the AIGC engine is only used for 4-6 hours on working days, and the effective usage time will be even shorter. There is usually no usage time on weekday evenings and weekends. 13 | 14 | 15 | - **Digital Asset Isolation and Security** 16 | 17 | For many industries and companies that are mainly content producers, the self-trained models in AIGC engines are digital core assets. Different content producers do not actively share models with each other. In addition, the content generated by different content producers needs to achieve access isolation, prevent tampering, and data isolation. 18 | 19 | 20 | - **Multiple Resource Types and Elasticity** 21 | 22 | AIGC engines strongly depend on the heterogeneous computing power of GPUs. However, there are many types of GPUs, and the cost and efficiency differences between different types of GPUs are very large. For example, in Stable Diffusion, the efficiency of generating images with A100 40G VRAM and A100 80G VRAM is about 1 second different, but the cost is more than twice as expensive for A100 80G VRAM. In addition, many companies that have been involved in AIGC for a long time will have quite a few consumer cards (such as NV3090) in their local data centers. When landing AIGC engines within the enterprise, it is necessary to consider how to utilize these resources and unify management and improve efficiency. 23 | 24 | 25 | - **Various Types and Versions of Engines** 26 | 27 | The iteration speed of AIGC engines is very fast, and there may be some incompatibilities between different versions. As a tool for generating digital assets, it is usually difficult to change the version once it is put into production to ensure the stable operation of the model. The versions that different content producers rely on may also differ significantly. Therefore, how to manage and upgrade multiple versions uniformly is also something that needs to be considered when landing AIGC within the enterprise. 28 | 29 | 30 | ## Features of AIGC-Gateway 31 | 32 | In order to solve the universality problem of landing AIGC engines within the enterprise, Alibaba Cloud and Xingzhe AI (Longyuan Games) have open-sourced the AIGC-Gateway project to reduce the difficulty and cost of landing AIGC within the enterprise, and truly achieve plug-and-play. 33 | 34 | ![architecture](../../docs/images/arch.png) 35 | 36 | The AIGC-Gateway has the following features: 37 | 38 | - **Dynamic Launch and On-Demand Release** 39 | 40 | AIGC-Gateway can launch any AIGC engine on demand and release the required GPU resources while preserving the data storage when exiting or leaving. When the same user accesses again, AIGC-Gateway can quickly launch new resources and mount the data storage belonging to this customer, achieving time-sharing reuse of GPU cards and reducing overall cost investment. 41 | 42 | 43 | - **Tenant Exclusive Access Isolation** 44 | 45 | The AIGC engine resources launched by AIGC-Gateway are exclusive to the tenant and data is isolated. In addition, access addresses are secured through OIDC for non-intrusive access control of any AIGC engine. Additionally, AIGC-Gateway integrates with the open-source authentication system Logto to achieve automatic single sign-on and authentication for enterprise internal account systems, simplifying the process. 46 | 47 | 48 | - **Unified GPU Resource Management** 49 | 50 | AIGC-Gateway is an open-source project that is cloud vendor independent and supports local deployment, hybrid cloud deployment, and multi-cloud deployment by enterprise IT engineers. It fully supports different types of GPU resources, such as local GPU, cloud single-card GPU, cloud multi-card GPU, and cloud-shared memory GPU. 51 | 52 | 53 | - **Version/Model Unified Management** 54 | 55 | AIGC-Gateway supports simultaneous deployment of multiple, multiple sets, and multiple versions of AIGC engines. It can also perform simple and convenient batch management for AIGC models of the same version and support data migration and switching between different versions of AIGC engines. 56 | 57 | 58 | - **Cost Accounting and Cost Allocation** 59 | 60 | On Alibaba Cloud, cost accounting for user and instance levels can be achieved through a visual dashboard and API, facilitating internal cost accounting and cost unit price estimation. 61 | 62 | 63 | ## What's Next 64 | 65 | - [Introduction of AIGC-Gateway Dashboard](../../docs/Dashboard介绍.md) 66 | - [Install & Config AIGC-Gateway](../../docs/安装部署.md) 67 | - [Deploy multi AIGC Engine Templates](../../docs/模版管理.md) 68 | - [Architecture & Principle of AIGC-Gateway](../../docs/架构原理.md) 69 | - [Q&A](../../docs/常见问题.md) 70 | 71 | ## Upstream Project 72 | 73 | - [OpenKruiseGame](https://github.com/openkruise/kruise-game) 74 | - [logto](https://github.com/logto-io/logto/) -------------------------------------------------------------------------------- /docs/images/Oops-wrong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/Oops-wrong.png -------------------------------------------------------------------------------- /docs/images/SD-dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/SD-dash.png -------------------------------------------------------------------------------- /docs/images/Traditional-Web-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/Traditional-Web-console.png -------------------------------------------------------------------------------- /docs/images/Traditional-Web-setting.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/Traditional-Web-setting.jpeg -------------------------------------------------------------------------------- /docs/images/admin-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/admin-login.png -------------------------------------------------------------------------------- /docs/images/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/arch.png -------------------------------------------------------------------------------- /docs/images/auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/auth.png -------------------------------------------------------------------------------- /docs/images/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/console.png -------------------------------------------------------------------------------- /docs/images/customdata-delete-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/customdata-delete-1.png -------------------------------------------------------------------------------- /docs/images/customdata-delete-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/customdata-delete-2.png -------------------------------------------------------------------------------- /docs/images/dashboard-installed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/dashboard-installed.png -------------------------------------------------------------------------------- /docs/images/dashboard-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/dashboard-login.png -------------------------------------------------------------------------------- /docs/images/dashboard-recover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/dashboard-recover.png -------------------------------------------------------------------------------- /docs/images/dashboard-uninstalled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/dashboard-uninstalled.png -------------------------------------------------------------------------------- /docs/images/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/logout.png -------------------------------------------------------------------------------- /docs/images/m2m-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/m2m-console.png -------------------------------------------------------------------------------- /docs/images/m2m-role-assign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/m2m-role-assign.png -------------------------------------------------------------------------------- /docs/images/m2m-role-setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/m2m-role-setting.png -------------------------------------------------------------------------------- /docs/images/m2m-setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/m2m-setting.png -------------------------------------------------------------------------------- /docs/images/quick-setup-realse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/quick-setup-realse.png -------------------------------------------------------------------------------- /docs/images/user-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CloudNativeGame/aigc-gateway/355383cb419bc08e945935a5b3a321695f00240c/docs/images/user-login.png -------------------------------------------------------------------------------- /docs/安装部署.md: -------------------------------------------------------------------------------- 1 | 本文介绍如何将AIGC-Gateway部署至Kubernetes集群 2 | 3 | AIGC-Gateway 包含两个组件 4 | - logto。用作用户身份认证,实现单点登录。 5 | - aigc-gateway。计算资源管理,同时维护用户与实例的映射关系。 6 | 7 | 完整地安装AIGC-Gateway需要经历以下步骤: 8 | 1. 生成logto证书,并在k8s集群中安装logto 9 | 2. 在logto admin控制台配置logto参数,并获取应用ID/secret等 10 | 3. 根据第二步中获取的信息,填写aigc-gateway相关参数,并安装 11 | 12 | ## 1. 安装logto 13 | 14 | 此步骤将会部署一个logto pod与一个postgress pod。logto用作用户鉴权,postgress用作logto对应数据库。建议生产环境采用独立的高可用数据库或云提供商数据库。 15 | 16 | ### 1.1 生成证书,并创建对应secret对象 17 | 18 | AIGC-Gateway 通过logto进行用户身份认证,默认通过 HTTPS 访问,因此需要提前准备证书,以下我们通过OpenSSL生成自定义证书用于测试,正式生产用途建议申请受信任的企业证书。 19 | 20 | ```bash 21 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com/O=c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com" 22 | 23 | kubectl create ns aigc-gateway 24 | kubectl -n aigc-gateway create secret tls tls-logto --key tls.key --cert tls.crt 25 | ``` 26 | 27 | ### 1.2 编辑./deploy/helm/logto/values.yaml 28 | 29 | ```yaml 30 | adminEndpoint: "logto-admin.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com" #logto admin控制台域名 31 | endpoint: "logto.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com" #logto域名 32 | secretName: "tls-logto" #第1.1步中生成的secret的名称 33 | installation: 34 | namespace: aigc-gateway #与第1.1步中生成的secret同命名空间 35 | createNamespace: false #如若存在该ns,则不需要创建新的 36 | ``` 37 | 38 | ### 1.3 安装 logto helm chart 39 | ```bash 40 | helm install logto ./deploy/helm/logto 41 | ``` 42 | 43 | ## 2. 配置logto参数 44 | 45 | ### 2.1 登录logto admin控制台 46 | 47 | 访问logto admin控制台,进行logto相关参数配置。首次登录时创建admin账户,生成管理员 48 | 49 | ![](./images/admin-login.png) 50 | 51 | ### 2.2 创建Traditional Web应用 52 | 输入用户名密码后,进入admin控制台,点击Application,点击Traditional Web,输入应用名称为aigc-gateway,创建应用。该配置页记录了AIGC-Gateway应用与logto交互的信息,其生成的ID与Secret将作为AIGC-Gateway的参数。 53 | 54 | ![](./images/Traditional-Web-console.png) 55 | 56 | 跳过应用指引,在配置页面输入aigc-gateway域名(例如 dashboard.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com)相关的两处URIs: 57 | 58 | - Redirect URIs:https://{aigc-gateway域名}/sign-in-callback 59 | - 例如:https://dashboard.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com/sign-in-callback 60 | - Post Sign-out URIs:https://{aigc-gateway域名}/ 61 | - 例如:https://dashboard.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com/ 62 | 63 | 点击保存更改 64 | 65 | ![](./images/Traditional-Web-setting.jpeg) 66 | 67 | ### 2.3 创建Machine-to-Machine应用 68 | Machine-to-Machine将赋予AIGC-Gateway管理登录用户与后端资源实例信息映射关系的权限。点击Application,点击Machine-to-Machine,输入自定义应用名称,点击创建。 69 | 70 | ![](./images/m2m-console.png) 71 | 72 | 在配置页面点击右下方Enable admin access按钮,点击保存更改(logto v1.4版本) 73 | 74 | ![](./images/m2m-setting.png) 75 | 76 | 在logto最新版(v1.14)中,m2m应用的admin权限需要通过设置role来实现,具体操作如下图所示。点击左侧导航栏Roles。创建role,点击show more options,填入对应信息。接下来将创建的role分配给m2m应用,完成权限绑定。 77 | 78 | ![](./images/m2m-role-setting.png) 79 | 80 | ![](./images/m2m-role-assign.png) 81 | 82 | 83 | ## 3. 安装AIGC-Gateway 84 | 85 | ### 3.1 编辑./deploy/helm/aigc-gateway/values.yaml 86 | 87 | ```yaml 88 | installation: 89 | namespace: aigc-gateway #建议与logto命名空间保持一致 90 | image: 91 | repository: registry-cn-hangzhou.ack.aliyuncs.com/acs/aigc-gateway 92 | tag: v1.3.0 93 | appId: "32qh5wp6c1q3jofy2myww" #在Traditional Web配置页中找到AppID并填入 94 | appSecret: "35mpn2dk62brjxaduy7x0" #在Traditional Web配置页中找到AppSecret并填入 95 | m2mId: "ezxyaol30ld99c7ns5u2r" #在Machine-to-Machine配置页中找到AppID并填入 96 | m2mSecret: "g7hly3nwq2omligpyh3ck" #在Machine-to-Machine配置页中找到AppSecret并填入 97 | host: "dashboard.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com" #输入AIGC-Gateway配置的域名,该域名也是用户的访问端点 98 | endpoint: "https://logto.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com/" #输入由logto域名组成的对应端点 99 | secretName: "tls-logto" #输入AIGC-Gateway对应的证书secret(本例中AIGC-Gateway证书与logto证书一致) 100 | ``` 101 | 102 | ### 3.2 安装 aigc-gateway helm chart 103 | ```bash 104 | helm install aigc-gateway ./deploy/helm/aigc-gateway 105 | ``` 106 | 107 | --- 108 | 109 | ## What's next 110 | 111 | - [模版管理](./模版管理.md) 112 | 113 | -------------------------------------------------------------------------------- /docs/常见问题.md: -------------------------------------------------------------------------------- 1 | ## 常见问题与处理 2 | 3 | ### 浏览器页面出现:post_logout_redirect_uri not registered 4 | 5 | 用户logout的时候报错: 6 | ```json 7 | {"message":"OIDC 内部错误: invalid_request","code":"oidc.provider_error","data":{"error_description":"post_logout_redirect_uri not registered"}} 8 | ``` 9 | 在logto admin控制台查看Tradition APP中设置的Post Sign-out Redirect URIs是否与集群中aigc-gatway的环境变量Redirect_Url一致。 10 | 通常情况下是因为,Post Sign-out Redirect URIs 的结尾缺少或多填写了 “/” 11 | 12 | ### 点击AIGC引擎的Install/Recover后出现转圈圈,刷新后无法访问 13 | 14 | 造成该情况出现有两个可能原因: 15 | a. 实例pod创建失败。可通过kebectl describe po {实例名称} -n {实例命名空间} 来查看创建失败原因。 16 | b. 实例访问网络创建失败。kebectl get gs {实例名称} -n {实例命名空间} -oyaml 找到network Status字段,查看网络是否Ready。 17 | 18 | ### 点击visit后无法访问 19 | 20 | - 首先,确定访问路径与登录用户是否匹配,该用户是否具有对应实例的访问权限。 21 | - 若匹配,kebectl get ingress {实例名称} -n {实例命名空间} 查看ingress是否存在且状态正常。查看对应annotation是否(如模版管理一节中示例所示)设置正确。 22 | - 若实例ingress正常,在浏览器端检查名为logto-session 的 cookie 对应domain是否是如下格式 23 | ``` 24 | .{aigc-gateway根域名} 25 | 26 | #若aigc-gateway域名为dashboard.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com 27 | #cookie对应domain应为 .c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com 28 | ``` 29 | 30 | ### 点击Start To Use进入失败,登录超时 31 | 32 | 例如浏览器点击后出现以下类似错误 33 | ``` 34 | Get "https://logto.cdadd9cd45d964c2694c15d4a4ad8fe37.ap-northeast-1.alicontainer.com/oidc/.well-known/openid-configuration": dial tcp 47.74.14.253:443: connect: connection timed out 35 | ``` 36 | 37 | - 问题表因:aigc-gateway 访问 logto失败,无法跳转登录页面 38 | - 可能根因:Kubernetes集群内访问集群LoadBalancer暴露的SLB地址不通。可尝试在aigc-gateway容器中访问nginx对应SLB地址确认问题。 39 | - 解决方法:面对阿里云ACK的使用者,可以通过升级Terway到最新版本解决。具体操作为:ACK集群控制台 -> 组件管理 -> 网络 -> 先升级terway-eniip -> 再升级terway-controlplane 40 | 41 | ### logto admin dashboard 出现 Oops! Something went wrong 42 | 43 | 例如下图所示情况 44 | 45 | ![img.png](./images/Oops-wrong.png) 46 | 47 | - 问题原因:使用自签证书时,浏览器还没有授予logto域名的访问权限。 48 | - 解决方法:手动访问logto域名,点击信任即可。 -------------------------------------------------------------------------------- /docs/架构原理.md: -------------------------------------------------------------------------------- 1 | ## AIGC-Gateway是如何实现快速拉起和释放的? 2 | 3 | ![instance uninstalled page](./images/quick-setup-realse.png) 4 | 5 | AIGC-Gateway是通过容器的方式拉起和释放AIGC实例的,在有镜像缓存和模型异步加载的情况下,可以做到秒级的启动。此外,AIGC引擎的管理是通过开源项目OpenKruiseGame来进行管理的,可以通过配置服务质量的方式配置自定义的下线策略,实现企业对用户登出行为的自定义。拓展阅读:https://openkruise.io/zh/kruisegame/user-manuals/autoscale/ 6 | 7 | ## AIGC-Gateway是如何如何实现用户访问控制的? 8 | 9 | ![instance uninstalled page](./images/auth.png) 10 | 11 | 默认AIGC-Gateway的网络是通过Ingress的方式进行透出的。AIGC-Gateway提供了增强的OAuth接口,支持在访问的时候认证登录态的同时,还可以校验资源和用户的归属。 12 | 13 | ## AIGC-Gateway是如何实现AIGC引擎管理的? 14 | 15 | 不同类型、不同版本、不同资源的AIGC引擎在AIGC-Gateway中是通过不同配置的Yaml模版来进行抽象的,增加、修改、变更一个AIGC引擎的配置,只需要修改对应的模版即可。AIGC-Gateway是通过Kubernetes中的接口反查获取的,无任何的内置和绑定,对开发者而言是完全开放的。 16 | 17 | ## AIGC-Gateway是如何实现GPU资源管理的? 18 | 19 | 不同的GPU资源,可以通过在负载中设置不同的Resource与调度策略的方式来实现,如果需要资源混合,例如:云下有3090的消费卡,云上有A100的数据中心卡,可以通过配置Kubernetes中的Affinity的方式实现优先使用云下资源,云下资源没有再使用云上资源。 20 | 21 | ## 如何配置自动拉起和自动释放的策略 22 | 23 | AIGC-Gateway可以通过配置探测脚本的方式标注用户离开的状态,例如:可以以无网络访问来判断用户的离开,并结合keda实现自动伸缩。拓展阅读:https://openkruise.io/zh/kruisegame/user-manuals/service-qualities 24 | 25 | ## 如何查看生成的图片与文件 26 | AIGC-Gateway可以通过在Sidecar中配置filebrowser的方式,实现web控制台的文件访问。 -------------------------------------------------------------------------------- /docs/模版管理.md: -------------------------------------------------------------------------------- 1 | AIGC-Gateway支持管理员自定义创建多种AIGC模版,模版初始部署在集群中时,集群不存在对应实例。管理员每部署一个模版,用户Dashboard视图中都会出现一个新的待安装的AIGC模型,当用户选择安装对应模版时,AIGC-Gateway会为该用户创建对应新的实例。 2 | 3 | ## 前置依赖 4 | 5 | 管理员需在Kubernetes集群中安装AIGC实例管理组件OKG(OpenKruiseGame),具体信息请参考:https://openkruise.io/zh/kruisegame/installation 6 | 7 | ```yaml 8 | $ helm repo add openkruise https://openkruise.github.io/charts/ 9 | 10 | $ helm repo update 11 | 12 | $ helm install kruise openkruise/kruise --version 1.4.0 13 | 14 | $ helm install kruise-game openkruise/kruise-game --version 0.4.1 15 | ``` 16 | 17 | ## 模版部署 18 | 19 | AIGC-Gateway中模版的概念等同于OKG中的GameServerSet对象,安装OKG后,管理员可在集群中部署多套AIGC示例模版(多组GameServerSet)。 20 | 21 | 以下是部署一个模版(GameServerSet)的示例。其中关键字段的说明如下: 22 | - spec.replicas 副本数目一定设置为0。因为模版下发后,不应存在任何实例,实例是由用户选择创建。 23 | - spec.gameServerTemplate.volumeClaimTemplates 用作创建属于用户个人的数据盘。该存储对象与实例生命周期解耦,也就是说,用户在释放计算实例后,个人数据并不丢失,实例恢复时数据重新加载。 24 | - spec.network 用于声明实例的访问网络。具体字段填写说明可参考示例中注释。 25 | 26 | ``` 27 | cat <区分。如实例0的域名为instances0...; 实例1的域名为instances1... 85 | #注意,该域名需要与aigc-gateway具有相同根域名,此例中为c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com 86 | value: instances-cpu.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com 87 | - name: PathType 88 | value: ImplementationSpecific 89 | - name: Path 90 | value: / 91 | - name: Annotation 92 | #用作实例访问鉴权,特定实例的访问权限只属于特定用户。下面的域名替换为AIGC-Gateway实际域名 93 | value: 'nginx.ingress.kubernetes.io/auth-url: https://dashboard.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com/auth' 94 | - name: Annotation 95 | #实例访问鉴权失败后进行登录认证。下面的域名替换为AIGC-Gateway实际域名 96 | value: 'nginx.ingress.kubernetes.io/auth-signin: https://dashboard.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com/sign-in' 97 | EOF 98 | ``` 99 | 100 | 以上模版为SD使用CPU的例子。通常,可以将不同的AIGC模型部署为不同的模版,使用不同GPU计算规格的模型部署成不同的模版,甚至可以将不同版本的模型部署成不同模版,AIGC-Gateway管理员可自行定义。 101 | 102 | 例如,部署单卡GPU的模版如下所示(以下 #... 代表省略的部分) 103 | 104 | ```yaml 105 | #... 106 | gameServerTemplate: 107 | spec: 108 | containers: 109 | - #... 110 | resources: 111 | limits: 112 | nvidia.com/gpu: 1 #申请一张GPU卡 113 | #... 114 | ``` 115 | 116 | 例如,部署共享GPU的模版如下所示,该模版生成的实例将使用3GiB显存的GPU 117 | 118 | ```yaml 119 | #... 120 | gameServerTemplate: 121 | spec: 122 | containers: 123 | - #... 124 | resources: 125 | limits: 126 | # 单位为GiB,该Pod总共申请了3 GiB显存。 127 | aliyun.com/gpu-mem: 3 # 设置GPU显存大小。 128 | #... 129 | ``` 130 | 131 | 例如,ECI-CPU模版如下所示 132 | 133 | ```yaml 134 | #... 135 | gameServerTemplate: 136 | metadata: 137 | labels: 138 | alibabacloud.com/eci: "true" 139 | spec: 140 | containers: 141 | - #... 142 | resources: 143 | limits: 144 | #生成实例将使用2核4G计算资源 145 | cpu: "2" 146 | memory: 4Gi 147 | #... 148 | ``` 149 | 150 | 例如,ECI-GPU模版如下所示 151 | 152 | ```yaml 153 | #... 154 | gameServerTemplate: 155 | metadata: 156 | labels: 157 | alibabacloud.com/eci: "true" 158 | annotations: 159 | k8s.aliyun.com/eci-use-specs: ecs.gn7i-c8g1.2xlarge #在此实例指定规格 160 | spec: 161 | containers: 162 | - #... 163 | resources: 164 | limits: 165 | nvidia.com/gpu: 1 #申请一张GPU卡 166 | #... 167 | ``` 168 | 169 | ## 模版更新 170 | 171 | 当平台决定更新模版镜像、规格等参数时,管理员可以操作对应GameServerSet对象: 172 | 173 | ```shell 174 | kubectl edit gss stable-diffusion-cpu -n default 175 | ``` 176 | 177 | 需要注意的是: 178 | 179 | - 避免更改`replicas`与`reserveGameServerIds`字段。 180 | - `gameServerTemplate`下的字段更新后,该模版下的实例将重启,会对正在使用的用户造成影响。【若存在无感升级的需求可以发布issue,我们将后续补充相关功能】 181 | 182 | ## 模版删除 183 | 184 | 当平台决定下线某版本模型时,可进行模版删除。 185 | 需要注意的是,删除模版后,对应的所有实例也将删除。 186 | 此外,管理员需要手动进行用户数据清理动作。 187 | 具体操作如下: 188 | 189 | 1. 在Kubernetes集群中删除GameServerSet对象,如删除上述部署在`default`命名空间的`stable-diffusion-cpu` 190 | 191 | ```shell 192 | kubectl delete gss stable-diffusion-cpu -n default 193 | ``` 194 | 195 | 2. 清理所有用户相关元数据 196 | 197 | 登录logto admin Dashboard,进入用户管理(User Management),依次遍历所有用户,找到Custom data,删除对应模版下的所有信息,点击保存。如图所示: 198 | 199 | ![customdata-delete-1](./images/customdata-delete-1.png) 200 | ![customdata-delete-2](./images/customdata-delete-2.png) 201 | 202 | 【若存在自动清理的需求可以发布issue,我们将后续补充相关功能】 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/CloudNativeGame/aigc-gateway 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gin-contrib/sessions v0.0.5 7 | github.com/gin-contrib/static v0.0.1 8 | github.com/gin-gonic/gin v1.9.0 9 | github.com/logto-io/go v1.0.1 10 | github.com/openkruise/kruise-game v0.3.0 11 | k8s.io/apimachinery v0.27.1 12 | k8s.io/client-go v0.27.1 13 | k8s.io/utils v0.0.0-20230209194617-a36077c30491 14 | sigs.k8s.io/controller-runtime v0.12.1 15 | ) 16 | 17 | require ( 18 | github.com/bytedance/sonic v1.8.0 // indirect 19 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 22 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 23 | github.com/gin-contrib/sse v0.1.0 // indirect 24 | github.com/go-logr/logr v1.2.3 // indirect 25 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 26 | github.com/go-openapi/jsonreference v0.20.1 // indirect 27 | github.com/go-openapi/swag v0.22.3 // indirect 28 | github.com/go-playground/locales v0.14.1 // indirect 29 | github.com/go-playground/universal-translator v0.18.1 // indirect 30 | github.com/go-playground/validator/v10 v10.11.2 // indirect 31 | github.com/goccy/go-json v0.10.0 // indirect 32 | github.com/gogo/protobuf v1.3.2 // indirect 33 | github.com/golang/protobuf v1.5.3 // indirect 34 | github.com/google/gnostic v0.5.7-v3refs // indirect 35 | github.com/google/go-cmp v0.5.9 // indirect 36 | github.com/google/gofuzz v1.1.0 // indirect 37 | github.com/google/uuid v1.3.0 // indirect 38 | github.com/gorilla/context v1.1.1 // indirect 39 | github.com/gorilla/securecookie v1.1.1 // indirect 40 | github.com/gorilla/sessions v1.2.1 // indirect 41 | github.com/imdario/mergo v0.3.12 // indirect 42 | github.com/josharian/intern v1.0.0 // indirect 43 | github.com/json-iterator/go v1.1.12 // indirect 44 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect 45 | github.com/leodido/go-urn v1.2.1 // indirect 46 | github.com/mailru/easyjson v0.7.7 // indirect 47 | github.com/mattn/go-isatty v0.0.17 // indirect 48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 49 | github.com/modern-go/reflect2 v1.0.2 // indirect 50 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 51 | github.com/openkruise/kruise-api v1.3.0 // indirect 52 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 53 | github.com/pkg/errors v0.9.1 // indirect 54 | github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect 55 | github.com/spf13/pflag v1.0.5 // indirect 56 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 57 | github.com/ugorji/go/codec v1.2.9 // indirect 58 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect 59 | golang.org/x/crypto v0.5.0 // indirect 60 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect 61 | golang.org/x/net v0.8.0 // indirect 62 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect 63 | golang.org/x/sys v0.6.0 // indirect 64 | golang.org/x/term v0.6.0 // indirect 65 | golang.org/x/text v0.8.0 // indirect 66 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 67 | google.golang.org/appengine v1.6.7 // indirect 68 | google.golang.org/protobuf v1.28.1 // indirect 69 | gopkg.in/inf.v0 v0.9.1 // indirect 70 | gopkg.in/square/go-jose.v2 v2.6.0 // indirect 71 | gopkg.in/yaml.v2 v2.4.0 // indirect 72 | gopkg.in/yaml.v3 v3.0.1 // indirect 73 | k8s.io/api v0.27.1 // indirect 74 | k8s.io/klog/v2 v2.90.1 // indirect 75 | k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect 76 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 77 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 78 | sigs.k8s.io/yaml v1.3.0 // indirect 79 | ) 80 | 81 | replace github.com/logto-io/go v1.0.1 => github.com/CloudNativeGame/logto-go v0.0.1 82 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 17 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 18 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 19 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 20 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 21 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 22 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 23 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 24 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 25 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 26 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 27 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 28 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 29 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 30 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 31 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 32 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 33 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 34 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 35 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 36 | github.com/CloudNativeGame/logto-go v0.0.1 h1:obbHUPFneigxB+Z02AP41YMjfE0u7P1L66XczPV8fFk= 37 | github.com/CloudNativeGame/logto-go v0.0.1/go.mod h1:kygmGSqVB8i9Dl6GOc8bPLGuPygaCID3ky5/UDOBljo= 38 | github.com/agiledragon/gomonkey/v2 v2.9.0 h1:PDiKKybR596O6FHW+RVSG0Z7uGCBNbmbUXh3uCNQ7Hc= 39 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 40 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 41 | github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= 42 | github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 43 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 44 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 45 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 46 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 47 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 48 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 49 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 50 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 51 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 52 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 53 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 54 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 56 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 58 | github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= 59 | github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 60 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 61 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 62 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 63 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 64 | github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= 65 | github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 66 | github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= 67 | github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE= 68 | github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY= 69 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 70 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 71 | github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U= 72 | github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs= 73 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 74 | github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= 75 | github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= 76 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 77 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 78 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 79 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 80 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 81 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 82 | github.com/go-logr/zapr v1.2.0 h1:n4JnPI1T3Qq1SFEi/F8rwLrZERp2bso19PJZDB9dayk= 83 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 84 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 85 | github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= 86 | github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 87 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 88 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 89 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 90 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 91 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 92 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 93 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 94 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 95 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 96 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 97 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 98 | github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= 99 | github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= 100 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= 101 | github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= 102 | github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 103 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 104 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 105 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 106 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 107 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 108 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 109 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 110 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 111 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 112 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 113 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 114 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 115 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 116 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 117 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 118 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 119 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 120 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 121 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 122 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 123 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 124 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 125 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 126 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 127 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 128 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 129 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 130 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 131 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 132 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 133 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 134 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 135 | github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= 136 | github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= 137 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 138 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 139 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 140 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 141 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 142 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 143 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 144 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 145 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 146 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 147 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 148 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 149 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 150 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 151 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 152 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 153 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 154 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 155 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 156 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 157 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 158 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 159 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= 160 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 161 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 162 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 163 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 164 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 165 | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 166 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 167 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 168 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 169 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 170 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 171 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 172 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 173 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 174 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 175 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 176 | github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= 177 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 178 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 179 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 180 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 181 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 182 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 183 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 184 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 185 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 186 | github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= 187 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 188 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 189 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 190 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 191 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 192 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 193 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 194 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 195 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 196 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 197 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 198 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 199 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 200 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 201 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 202 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 203 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 204 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= 205 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 206 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 207 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 208 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 209 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 210 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 211 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 212 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 213 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 214 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 215 | github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= 216 | github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= 217 | github.com/openkruise/kruise-api v1.3.0 h1:yfEy64uXgSuX/5RwePLbwUK/uX8RRM8fHJkccel5ZIQ= 218 | github.com/openkruise/kruise-api v1.3.0/go.mod h1:9ZX+ycdHKNzcA5ezAf35xOa2Mwfa2BYagWr0lKgi5dU= 219 | github.com/openkruise/kruise-game v0.3.0 h1:/bP5FFIoVF4zaegUEnPVoSARbj1VvxURMoV8WNcuXhM= 220 | github.com/openkruise/kruise-game v0.3.0/go.mod h1:cqcBfUyuOJFo2lBHUNY9Dvz1HOEa1pWsmWzDQCvrp9I= 221 | github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= 222 | github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 223 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 224 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 225 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 226 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 227 | github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= 228 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 229 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 230 | github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= 231 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= 232 | github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc= 233 | github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= 234 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 235 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 236 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 237 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 238 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 239 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 240 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 241 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 242 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 243 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 244 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 245 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 246 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 247 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 248 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 249 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 250 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 251 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 252 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 253 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 254 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 255 | github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= 256 | github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 257 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 258 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 259 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 260 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 261 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 262 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 263 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 264 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 265 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 266 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 267 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 268 | go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= 269 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= 270 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 271 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 272 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 273 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 274 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 275 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 276 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 277 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 278 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 279 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 280 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 281 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 282 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 283 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 284 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 285 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 286 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 287 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 288 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= 289 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 290 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 291 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 292 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 293 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 294 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 295 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 296 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 297 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 298 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 299 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 300 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 301 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 302 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 303 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 304 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 305 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 306 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 307 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 308 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 309 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 310 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 311 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 312 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 313 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 314 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 315 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 316 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 317 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 318 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 319 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 320 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 321 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 322 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 323 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 324 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 325 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 326 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 327 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 328 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 329 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 330 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 331 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 332 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 333 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 334 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 335 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 336 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 337 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 338 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 339 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 340 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 341 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 342 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 343 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 344 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 345 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= 346 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= 347 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 348 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 349 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 350 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 351 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 352 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 353 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 354 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 355 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 356 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 357 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 358 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 359 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 360 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 361 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 362 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 363 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 364 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 365 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 366 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 367 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 368 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 369 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 370 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 371 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 372 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 373 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 374 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 375 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 376 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 377 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 378 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 379 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 380 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 381 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 382 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 383 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 384 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 385 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 386 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 387 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 388 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 389 | golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= 390 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 391 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 392 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 393 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 394 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 395 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 396 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 397 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 398 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 399 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 400 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 401 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 402 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= 403 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 404 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 405 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 406 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 407 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 408 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 409 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 410 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 411 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 412 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 413 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 414 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 415 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 416 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 417 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 418 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 419 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 420 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 421 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 422 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 423 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 424 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 425 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 426 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 427 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 428 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 429 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 430 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 431 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 432 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 433 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 434 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 435 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 436 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 437 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 438 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 439 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 440 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 441 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 442 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 443 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 444 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 445 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 446 | golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= 447 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 448 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 449 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 450 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 451 | gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= 452 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 453 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 454 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 455 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 456 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 457 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 458 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 459 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 460 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 461 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 462 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 463 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 464 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 465 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 466 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 467 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 468 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 469 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 470 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 471 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 472 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 473 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 474 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 475 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 476 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 477 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 478 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 479 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 480 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 481 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 482 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 483 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 484 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 485 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 486 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 487 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 488 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 489 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 490 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 491 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 492 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 493 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 494 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 495 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 496 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 497 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 498 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 499 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 500 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 501 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 502 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 503 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 504 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 505 | google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 506 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 507 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 508 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 509 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 510 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 511 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 512 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 513 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 514 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 515 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 516 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 517 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 518 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 519 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 520 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 521 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 522 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 523 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 524 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 525 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 526 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 527 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 528 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 529 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 530 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 531 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 532 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 533 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 534 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 535 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 536 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 537 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 538 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 539 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 540 | gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= 541 | gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 542 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 543 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 544 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 545 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 546 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 547 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 548 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 549 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 550 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 551 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 552 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 553 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 554 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 555 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 556 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 557 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 558 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 559 | k8s.io/api v0.27.1 h1:Z6zUGQ1Vd10tJ+gHcNNNgkV5emCyW+v2XTmn+CLjSd0= 560 | k8s.io/api v0.27.1/go.mod h1:z5g/BpAiD+f6AArpqNjkY+cji8ueZDU/WV1jcj5Jk4E= 561 | k8s.io/apiextensions-apiserver v0.24.0 h1:JfgFqbA8gKJ/uDT++feAqk9jBIwNnL9YGdQvaI9DLtY= 562 | k8s.io/apimachinery v0.27.1 h1:EGuZiLI95UQQcClhanryclaQE6xjg1Bts6/L3cD7zyc= 563 | k8s.io/apimachinery v0.27.1/go.mod h1:5ikh59fK3AJ287GUvpUsryoMFtH9zj/ARfWCo3AyXTM= 564 | k8s.io/client-go v0.27.1 h1:oXsfhW/qncM1wDmWBIuDzRHNS2tLhK3BZv512Nc59W8= 565 | k8s.io/client-go v0.27.1/go.mod h1:f8LHMUkVb3b9N8bWturc+EDtVVVwZ7ueTVquFAJb2vA= 566 | k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= 567 | k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 568 | k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg= 569 | k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY= 570 | k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= 571 | k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 572 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 573 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 574 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 575 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 576 | sigs.k8s.io/controller-runtime v0.12.1 h1:4BJY01xe9zKQti8oRjj/NeHKRXthf1YkYJAgLONFFoI= 577 | sigs.k8s.io/controller-runtime v0.12.1/go.mod h1:BKhxlA4l7FPK4AQcsuL4X6vZeWnKDXez/vp1Y8dxTU0= 578 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 579 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 580 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= 581 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 582 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 583 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 584 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/CloudNativeGame/aigc-gateway/pkg/routers" 5 | "github.com/CloudNativeGame/aigc-gateway/pkg/utils" 6 | "github.com/gin-contrib/sessions" 7 | "github.com/gin-contrib/sessions/memstore" 8 | "github.com/gin-contrib/static" 9 | "github.com/gin-gonic/gin" 10 | "github.com/logto-io/go/client" 11 | "os" 12 | ) 13 | 14 | func main() { 15 | router := gin.Default() 16 | // load templates 17 | router.Delims("{[{", "}]}") 18 | router.LoadHTMLGlob("aigc-dashboard/dist/*.html") 19 | router.Use(static.Serve("/assets", static.LocalFile("aigc-dashboard/dist/assets", true))) 20 | 21 | endpoint := os.Getenv("Endpoint") 22 | logtoConfig := &client.LogtoConfig{ 23 | 24 | Endpoint: endpoint, 25 | AppId: os.Getenv("App_Id"), 26 | AppSecret: os.Getenv("App_Secret"), 27 | Scopes: []string{"email", "custom_data"}, 28 | } 29 | // We use memory-based session in this example 30 | store := memstore.NewStore([]byte("your session secret")) 31 | store.Options(sessions.Options{ 32 | Domain: utils.GetDomainFromEndpoint(endpoint), 33 | Path: "/", 34 | MaxAge: 604800, 35 | }) 36 | router.Use(sessions.Sessions("logto-session", store)) 37 | routers.RegisterSignRouters(router, logtoConfig) 38 | routers.RegisterResourceRouters(router, logtoConfig) 39 | 40 | router.Run(":8090") 41 | } 42 | -------------------------------------------------------------------------------- /pkg/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | // add informer policy to update resources. 4 | -------------------------------------------------------------------------------- /pkg/resources/error.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import "fmt" 4 | 5 | type ResourceErrorType string 6 | 7 | const ( 8 | ApiCallError ResourceErrorType = "ApiCallError" 9 | InternalError ResourceErrorType = "InternalError" 10 | ParameterError ResourceErrorType = "ParameterError" 11 | NotFoundError ResourceErrorType = "NotFoundError" 12 | ) 13 | 14 | type ErrorReasonType string 15 | 16 | const ( 17 | PauseReason = "Instance paused" 18 | NotExistReason = "Instance not existed" 19 | ResourceMetaNullReason = "ResourceMeta is Null" 20 | IdNullReason = "ID is Null" 21 | IdNotIntegerReason = "ID is not Integer" 22 | NameNullReason = "Name is Null" 23 | NamespaceNullReason = "Namespace is Null" 24 | ) 25 | 26 | type ResourceError interface { 27 | Error() string 28 | 29 | Reason() ErrorReasonType 30 | 31 | Type() ResourceErrorType 32 | } 33 | 34 | type ResourceErrorImplErrorImpl struct { 35 | errorType ResourceErrorType 36 | msg string 37 | reason ErrorReasonType 38 | } 39 | 40 | func (r ResourceErrorImplErrorImpl) Error() string { 41 | return r.msg 42 | } 43 | 44 | func (r ResourceErrorImplErrorImpl) Reason() ErrorReasonType { 45 | return r.reason 46 | } 47 | 48 | func (r ResourceErrorImplErrorImpl) Type() ResourceErrorType { 49 | return r.errorType 50 | } 51 | 52 | func IsResourceError(err error) bool { 53 | _, is := err.(ResourceErrorImplErrorImpl) 54 | return is 55 | } 56 | 57 | func IsNotFoundError(err error) bool { 58 | re, is := err.(ResourceErrorImplErrorImpl) 59 | if !is { 60 | return false 61 | } 62 | if re.Type() == NotFoundError { 63 | return true 64 | } 65 | return false 66 | } 67 | 68 | func IsApiCallError(err error) bool { 69 | re, is := err.(ResourceErrorImplErrorImpl) 70 | if !is { 71 | return false 72 | } 73 | if re.Type() == ApiCallError { 74 | return true 75 | } 76 | return false 77 | } 78 | 79 | func IsInternalError(err error) bool { 80 | re, is := err.(ResourceErrorImplErrorImpl) 81 | if !is { 82 | return false 83 | } 84 | if re.Type() == InternalError { 85 | return true 86 | } 87 | return false 88 | } 89 | 90 | func IsParameterError(err error) bool { 91 | re, is := err.(ResourceErrorImplErrorImpl) 92 | if !is { 93 | return false 94 | } 95 | if re.Type() == ParameterError { 96 | return true 97 | } 98 | return false 99 | } 100 | 101 | func GetErrorType(err error) ResourceErrorType { 102 | return err.(ResourceErrorImplErrorImpl).Type() 103 | } 104 | 105 | func GetErrorReason(err error) ErrorReasonType { 106 | return err.(ResourceErrorImplErrorImpl).Reason() 107 | } 108 | 109 | func NewResourceError(errorType ResourceErrorType, errorReason ErrorReasonType, msg string, args ...interface{}) ResourceError { 110 | return ResourceErrorImplErrorImpl{ 111 | errorType: errorType, 112 | reason: errorReason, 113 | msg: fmt.Sprintf(msg, args...), 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /pkg/resources/error_test.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestIsResourceError(t *testing.T) { 9 | tests := []struct { 10 | err error 11 | is bool 12 | }{ 13 | { 14 | err: NewResourceError("", "", ""), 15 | is: true, 16 | }, 17 | { 18 | err: fmt.Errorf("xxx"), 19 | is: false, 20 | }, 21 | } 22 | 23 | for caseI, test := range tests { 24 | actual := IsResourceError(test.err) 25 | expect := test.is 26 | if actual != expect { 27 | t.Errorf("case %d: whether test err is ResourceError should be %v", caseI, expect) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/resources/resource.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 4 | 5 | type Resource interface { 6 | v1.Object 7 | } 8 | 9 | type ResourceMeta struct { 10 | Name string 11 | Namespace string 12 | ID string 13 | Type string 14 | } 15 | -------------------------------------------------------------------------------- /pkg/resources/resource_manager.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "context" 5 | gamekruiseiov1alpha1 "github.com/openkruise/kruise-game/apis/v1alpha1" 6 | "github.com/openkruise/kruise-game/pkg/util" 7 | v1 "k8s.io/api/core/v1" 8 | "k8s.io/apimachinery/pkg/api/errors" 9 | "k8s.io/apimachinery/pkg/labels" 10 | "k8s.io/apimachinery/pkg/types" 11 | "k8s.io/client-go/kubernetes/scheme" 12 | "k8s.io/utils/pointer" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | "sigs.k8s.io/controller-runtime/pkg/client/config" 15 | "strconv" 16 | ) 17 | 18 | // ResourceManager conteroller 19 | type ResourceManager struct { 20 | client.Client 21 | } 22 | 23 | func (rm *ResourceManager) ListResources(namespaces []string, resourcesLabels map[string]string) ([]Resource, error) { 24 | var resources []Resource 25 | 26 | if len(namespaces) == 0 { 27 | gssList := &gamekruiseiov1alpha1.GameServerSetList{} 28 | err := rm.List(context.Background(), gssList, &client.ListOptions{ 29 | LabelSelector: labels.SelectorFromSet(resourcesLabels), 30 | }) 31 | if err != nil { 32 | return nil, NewResourceError(ApiCallError, "", err.Error()) 33 | } 34 | 35 | for _, gss := range gssList.Items { 36 | resources = append(resources, gss.DeepCopy()) 37 | } 38 | } 39 | 40 | for _, ns := range namespaces { 41 | listOptions := client.ListOptions{ 42 | Namespace: ns, 43 | LabelSelector: labels.SelectorFromSet(resourcesLabels), 44 | } 45 | gssList := &gamekruiseiov1alpha1.GameServerSetList{} 46 | err := rm.List(context.Background(), gssList, &listOptions) 47 | if err != nil { 48 | return nil, NewResourceError(ApiCallError, "", err.Error()) 49 | } 50 | for _, gss := range gssList.Items { 51 | resources = append(resources, gss.DeepCopy()) 52 | } 53 | } 54 | 55 | return resources, nil 56 | } 57 | 58 | func (rm *ResourceManager) GetResource(meta *ResourceMeta) (Resource, error) { 59 | if err := checkResourceMeta(meta, &metaNeed{ID: true, Name: true, Namespace: true}); err != nil { 60 | return nil, err 61 | } 62 | 63 | gs := &gamekruiseiov1alpha1.GameServer{} 64 | err := rm.Get(context.Background(), types.NamespacedName{ 65 | Name: meta.Name + "-" + meta.ID, 66 | Namespace: meta.Namespace, 67 | }, gs) 68 | if err != nil { 69 | if errors.IsNotFound(err) { 70 | gss := &gamekruiseiov1alpha1.GameServerSet{} 71 | err := rm.Get(context.Background(), types.NamespacedName{ 72 | Name: meta.Name, 73 | Namespace: meta.Namespace, 74 | }, gss) 75 | if err != nil { 76 | return nil, NewResourceError(ApiCallError, "", err.Error()) 77 | } 78 | idInt, _ := strconv.Atoi(meta.ID) 79 | if util.IsNumInList(idInt, gss.Spec.ReserveGameServerIds) { 80 | return nil, NewResourceError(NotFoundError, PauseReason, "") 81 | } else { 82 | return nil, NewResourceError(NotFoundError, NotExistReason, "") 83 | } 84 | } 85 | return nil, NewResourceError(ApiCallError, "", err.Error()) 86 | } 87 | 88 | return gs, nil 89 | } 90 | 91 | func (rm *ResourceManager) GetResourceEndpoint(meta *ResourceMeta) (string, error) { 92 | gs := &gamekruiseiov1alpha1.GameServer{} 93 | err := rm.Get(context.Background(), types.NamespacedName{ 94 | Name: meta.Name + "-" + meta.ID, 95 | Namespace: meta.Namespace, 96 | }, gs) 97 | if err != nil { 98 | if errors.IsNotFound(err) { 99 | return "", nil 100 | } 101 | return "", NewResourceError(ApiCallError, "", err.Error()) 102 | } 103 | if len(gs.Status.NetworkStatus.ExternalAddresses) == 0 { 104 | return "", nil 105 | } 106 | 107 | return gs.Status.NetworkStatus.ExternalAddresses[0].EndPoint, nil 108 | } 109 | 110 | func (rm *ResourceManager) CreateResource(meta *ResourceMeta) (*ResourceMeta, error) { 111 | if err := checkResourceMeta(meta, &metaNeed{Name: true, Namespace: true}); err != nil { 112 | return nil, err 113 | } 114 | 115 | // get GameServerSet 116 | gss := &gamekruiseiov1alpha1.GameServerSet{} 117 | err := rm.Get(context.Background(), types.NamespacedName{ 118 | Name: meta.Name, 119 | Namespace: meta.Namespace, 120 | }, gss) 121 | if err != nil { 122 | return nil, NewResourceError(ApiCallError, "", err.Error()) 123 | } 124 | 125 | newId := len(gss.Spec.ReserveGameServerIds) + int(*gss.Spec.Replicas) 126 | 127 | // update GameServerSet 128 | gss.Spec.Replicas = pointer.Int32(*gss.Spec.Replicas + 1) 129 | err = rm.Update(context.Background(), gss) 130 | if err != nil { 131 | return nil, NewResourceError(ApiCallError, "", err.Error()) 132 | } 133 | 134 | return &ResourceMeta{ 135 | Namespace: meta.Namespace, 136 | Name: meta.Name, 137 | ID: strconv.Itoa(newId), 138 | }, nil 139 | } 140 | 141 | func (rm *ResourceManager) PauseResource(meta *ResourceMeta) error { 142 | if err := checkResourceMeta(meta, &metaNeed{ID: true, Name: true, Namespace: true}); err != nil { 143 | return err 144 | } 145 | 146 | // get GameServerSet 147 | gss := &gamekruiseiov1alpha1.GameServerSet{} 148 | err := rm.Get(context.Background(), types.NamespacedName{ 149 | Name: meta.Name, 150 | Namespace: meta.Namespace, 151 | }, gss) 152 | if err != nil { 153 | return NewResourceError(ApiCallError, "", err.Error()) 154 | } 155 | 156 | idInt, _ := strconv.Atoi(meta.ID) 157 | // check if already paused 158 | if util.IsNumInList(idInt, gss.Spec.ReserveGameServerIds) { 159 | return NewResourceError(NotFoundError, PauseReason, "") 160 | } 161 | 162 | // update GameServerSet 163 | gss.Spec.Replicas = pointer.Int32(*gss.Spec.Replicas - 1) 164 | gss.Spec.ReserveGameServerIds = append(gss.Spec.ReserveGameServerIds, []int{idInt}...) 165 | err = rm.Update(context.Background(), gss) 166 | if err != nil { 167 | return err 168 | } 169 | 170 | return nil 171 | } 172 | 173 | func (rm *ResourceManager) RecoverResource(meta *ResourceMeta) (Resource, error) { 174 | if err := checkResourceMeta(meta, &metaNeed{ID: true, Name: true, Namespace: true}); err != nil { 175 | return nil, err 176 | } 177 | 178 | // get GameServer 179 | gs := &gamekruiseiov1alpha1.GameServer{} 180 | err := rm.Get(context.Background(), types.NamespacedName{ 181 | Name: meta.Name + "-" + meta.ID, 182 | Namespace: meta.Namespace, 183 | }, gs) 184 | if err != nil { 185 | if errors.IsNotFound(err) { 186 | // get GameServerSet 187 | gss := &gamekruiseiov1alpha1.GameServerSet{} 188 | err = rm.Get(context.Background(), types.NamespacedName{ 189 | Name: meta.Name, 190 | Namespace: meta.Namespace, 191 | }, gss) 192 | if err != nil { 193 | return nil, NewResourceError(ApiCallError, "", err.Error()) 194 | } 195 | 196 | idInt, _ := strconv.Atoi(meta.ID) 197 | if util.IsNumInList(idInt, gss.Spec.ReserveGameServerIds) { 198 | // update GameServerSet 199 | gss.Spec.Replicas = pointer.Int32(*gss.Spec.Replicas + 1) 200 | idInt, _ := strconv.Atoi(meta.ID) 201 | gss.Spec.ReserveGameServerIds = util.GetSliceInANotInB(gss.Spec.ReserveGameServerIds, []int{idInt}) 202 | err = rm.Update(context.Background(), gss) 203 | if err != nil { 204 | return nil, NewResourceError(ApiCallError, "", err.Error()) 205 | } 206 | return nil, NewResourceError(NotFoundError, PauseReason, "") 207 | } 208 | 209 | return nil, NewResourceError(NotFoundError, NotExistReason, "") 210 | } 211 | 212 | return nil, NewResourceError(ApiCallError, "", err.Error()) 213 | } 214 | 215 | return gs, nil 216 | } 217 | 218 | func (rm *ResourceManager) DeleteResource(meta *ResourceMeta) error { 219 | if err := checkResourceMeta(meta, &metaNeed{ID: true, Name: true, Namespace: true}); err != nil { 220 | return err 221 | } 222 | 223 | // get GameServerSet 224 | gss := &gamekruiseiov1alpha1.GameServerSet{} 225 | err := rm.Get(context.Background(), types.NamespacedName{ 226 | Name: meta.Name, 227 | Namespace: meta.Namespace, 228 | }, gss) 229 | if err != nil { 230 | return NewResourceError(ApiCallError, "", err.Error()) 231 | } 232 | 233 | idInt, _ := strconv.Atoi(meta.ID) 234 | // check if gs exist or not 235 | if !util.IsNumInList(idInt, gss.Spec.ReserveGameServerIds) { 236 | // update GameServerSet to delete gs 237 | gss.Spec.Replicas = pointer.Int32(*gss.Spec.Replicas - 1) 238 | gss.Spec.ReserveGameServerIds = append(gss.Spec.ReserveGameServerIds, []int{idInt}...) 239 | err = rm.Update(context.Background(), gss) 240 | if err != nil { 241 | return err 242 | } 243 | } 244 | 245 | // delete pvcs related to gss 246 | for _, vct := range gss.Spec.GameServerTemplate.VolumeClaimTemplates { 247 | pvc := &v1.PersistentVolumeClaim{} 248 | err = rm.Get(context.Background(), types.NamespacedName{ 249 | Name: vct.GetName() + "-" + meta.Name + "-" + meta.ID, 250 | Namespace: meta.Namespace, 251 | }, pvc) 252 | if err != nil { 253 | if errors.IsNotFound(err) { 254 | return nil 255 | } 256 | return NewResourceError(ApiCallError, "", err.Error()) 257 | } 258 | err = rm.Delete(context.Background(), pvc) 259 | if err != nil && !errors.IsNotFound(err) { 260 | return NewResourceError(ApiCallError, "", err.Error()) 261 | } 262 | } 263 | 264 | return nil 265 | } 266 | 267 | func (rm *ResourceManager) RestartResource(meta *ResourceMeta) error { 268 | if err := checkResourceMeta(meta, &metaNeed{ID: true, Name: true, Namespace: true}); err != nil { 269 | return err 270 | } 271 | 272 | // delete pod 273 | pod := &v1.Pod{} 274 | err := rm.Get(context.Background(), types.NamespacedName{ 275 | Name: meta.Name + "-" + meta.ID, 276 | Namespace: meta.Namespace, 277 | }, pod) 278 | if err != nil { 279 | if errors.IsNotFound(err) { 280 | return nil 281 | } 282 | return NewResourceError(ApiCallError, "", err.Error()) 283 | } 284 | err = rm.Delete(context.Background(), pod) 285 | if err != nil && !errors.IsNotFound(err) { 286 | return NewResourceError(ApiCallError, "", err.Error()) 287 | } 288 | 289 | return nil 290 | } 291 | 292 | type metaNeed struct { 293 | ID bool 294 | Name bool 295 | Namespace bool 296 | } 297 | 298 | func checkResourceMeta(meta *ResourceMeta, metaNeed *metaNeed) error { 299 | if meta == nil { 300 | return NewResourceError(ParameterError, ResourceMetaNullReason, "%s: %s", ParameterError, ResourceMetaNullReason) 301 | } 302 | 303 | if metaNeed.Namespace && meta.Namespace == "" { 304 | return NewResourceError(ParameterError, NamespaceNullReason, "%s: %s", ParameterError, NamespaceNullReason) 305 | } 306 | 307 | if metaNeed.Name && meta.Name == "" { 308 | return NewResourceError(ParameterError, NameNullReason, "%s: %s", ParameterError, NameNullReason) 309 | } 310 | 311 | if metaNeed.ID { 312 | if meta.ID == "" { 313 | return NewResourceError(ParameterError, IdNullReason, "%s: %s", ParameterError, IdNullReason) 314 | } 315 | _, err := strconv.Atoi(meta.ID) 316 | if err != nil { 317 | return NewResourceError(ParameterError, IdNotIntegerReason, "%s: %s", ParameterError, IdNotIntegerReason) 318 | } 319 | } 320 | 321 | return nil 322 | } 323 | 324 | var rm *ResourceManager 325 | 326 | func NewResourceManager() *ResourceManager { 327 | return rm 328 | } 329 | 330 | func init() { 331 | cfg := config.GetConfigOrDie() 332 | 333 | c, err := client.New(cfg, client.Options{}) 334 | if err != nil { 335 | panic(err) 336 | } 337 | rm = &ResourceManager{ 338 | Client: c, 339 | } 340 | gamekruiseiov1alpha1.AddToScheme(scheme.Scheme) 341 | } 342 | -------------------------------------------------------------------------------- /pkg/resources/resource_manager_test.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "context" 5 | gameKruiseV1alpha1 "github.com/openkruise/kruise-game/apis/v1alpha1" 6 | "github.com/openkruise/kruise-game/pkg/util" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/types" 10 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 11 | "k8s.io/utils/pointer" 12 | "reflect" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 15 | "testing" 16 | ) 17 | 18 | var ( 19 | schemeTest = runtime.NewScheme() 20 | ) 21 | 22 | func init() { 23 | utilruntime.Must(gameKruiseV1alpha1.AddToScheme(schemeTest)) 24 | } 25 | 26 | func TestResourceManager_checkResourceMeta(t *testing.T) { 27 | tests := []struct { 28 | meta *ResourceMeta 29 | metaNeed *metaNeed 30 | err error 31 | }{ 32 | { 33 | meta: &ResourceMeta{ 34 | Name: "gss-name", 35 | Namespace: "gss-ns", 36 | ID: "6", 37 | }, 38 | metaNeed: &metaNeed{ 39 | Name: true, 40 | Namespace: true, 41 | ID: true, 42 | }, 43 | err: nil, 44 | }, 45 | { 46 | meta: &ResourceMeta{ 47 | Name: "gss-name", 48 | Namespace: "gss-ns", 49 | ID: "f", 50 | }, 51 | metaNeed: &metaNeed{ 52 | Name: true, 53 | Namespace: true, 54 | ID: true, 55 | }, 56 | err: NewResourceError(ParameterError, IdNotIntegerReason, "%s: %s", ParameterError, IdNotIntegerReason), 57 | }, 58 | { 59 | meta: &ResourceMeta{ 60 | Name: "gss-name", 61 | Namespace: "gss-ns", 62 | }, 63 | metaNeed: &metaNeed{ 64 | Name: true, 65 | Namespace: true, 66 | }, 67 | err: nil, 68 | }, 69 | } 70 | 71 | for i, test := range tests { 72 | err := checkResourceMeta(test.meta, test.metaNeed) 73 | if !reflect.DeepEqual(test.err, err) { 74 | t.Errorf("case %d: expect err is: %s, but actual err is: %s", i, test.err.Error(), err.Error()) 75 | } 76 | } 77 | } 78 | 79 | func TestResourceManager_ListResources(t *testing.T) { 80 | tests := []struct { 81 | namespaces []string 82 | resourcesLabels map[string]string 83 | gssList []*gameKruiseV1alpha1.GameServerSet 84 | selectedNum []int 85 | }{ 86 | { 87 | gssList: []*gameKruiseV1alpha1.GameServerSet{ 88 | { 89 | ObjectMeta: metav1.ObjectMeta{ 90 | Namespace: "xxx", 91 | Name: "gss-0", 92 | ResourceVersion: "999", 93 | Labels: map[string]string{"types": "cv"}, 94 | }, 95 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 96 | Replicas: pointer.Int32(3), 97 | }, 98 | }, 99 | }, 100 | selectedNum: []int{0}, 101 | }, 102 | { 103 | namespaces: []string{"xx"}, 104 | gssList: []*gameKruiseV1alpha1.GameServerSet{ 105 | { 106 | ObjectMeta: metav1.ObjectMeta{ 107 | Namespace: "xxx", 108 | Name: "gss-0", 109 | ResourceVersion: "999", 110 | Labels: map[string]string{"types": "cv"}, 111 | }, 112 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 113 | Replicas: pointer.Int32(3), 114 | }, 115 | }, 116 | }, 117 | selectedNum: nil, 118 | }, 119 | { 120 | namespaces: []string{"xxx"}, 121 | resourcesLabels: map[string]string{"types": "cv"}, 122 | gssList: []*gameKruiseV1alpha1.GameServerSet{ 123 | { 124 | ObjectMeta: metav1.ObjectMeta{ 125 | Namespace: "xxx", 126 | Name: "gss-0", 127 | ResourceVersion: "999", 128 | Labels: map[string]string{"types": "cv"}, 129 | }, 130 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 131 | Replicas: pointer.Int32(3), 132 | }, 133 | }, 134 | { 135 | ObjectMeta: metav1.ObjectMeta{ 136 | Namespace: "xxx", 137 | Name: "gss-1", 138 | ResourceVersion: "999", 139 | Labels: map[string]string{"types": "nlp"}, 140 | }, 141 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 142 | Replicas: pointer.Int32(3), 143 | }, 144 | }, 145 | { 146 | ObjectMeta: metav1.ObjectMeta{ 147 | Namespace: "xxx", 148 | Name: "gss-2", 149 | ResourceVersion: "999", 150 | Labels: map[string]string{"types": "cv"}, 151 | }, 152 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 153 | Replicas: pointer.Int32(3), 154 | }, 155 | }, 156 | }, 157 | selectedNum: []int{0, 2}, 158 | }, 159 | { 160 | resourcesLabels: map[string]string{"types": "cv"}, 161 | gssList: []*gameKruiseV1alpha1.GameServerSet{ 162 | { 163 | ObjectMeta: metav1.ObjectMeta{ 164 | Namespace: "xxx", 165 | Name: "gss-0", 166 | ResourceVersion: "999", 167 | Labels: map[string]string{"types": "cv"}, 168 | }, 169 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 170 | Replicas: pointer.Int32(3), 171 | }, 172 | }, 173 | { 174 | ObjectMeta: metav1.ObjectMeta{ 175 | Namespace: "xx", 176 | Name: "gss-1", 177 | ResourceVersion: "999", 178 | Labels: map[string]string{"types": "nlp"}, 179 | }, 180 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 181 | Replicas: pointer.Int32(3), 182 | }, 183 | }, 184 | { 185 | ObjectMeta: metav1.ObjectMeta{ 186 | Namespace: "xx", 187 | Name: "gss-2", 188 | ResourceVersion: "999", 189 | Labels: map[string]string{"types": "cv"}, 190 | }, 191 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 192 | Replicas: pointer.Int32(3), 193 | }, 194 | }, 195 | }, 196 | selectedNum: []int{0, 2}, 197 | }, 198 | } 199 | 200 | for caseNum, test := range tests { 201 | var objs []client.Object 202 | for _, gss := range test.gssList { 203 | objs = append(objs, gss) 204 | } 205 | c := fake.NewClientBuilder().WithScheme(schemeTest).WithObjects(objs...).Build() 206 | rm := &ResourceManager{Client: c} 207 | actualResources, _ := rm.ListResources(test.namespaces, test.resourcesLabels) 208 | var actualNum []int 209 | for _, resource := range actualResources { 210 | num := util.GetIndexFromGsName(resource.GetName()) 211 | actualNum = append(actualNum, num) 212 | } 213 | if !util.IsSliceEqual(actualNum, test.selectedNum) { 214 | t.Errorf("case %d: expect resources: %v, but actual resources: %v", caseNum, test.selectedNum, actualNum) 215 | } 216 | } 217 | } 218 | 219 | func TestResourceManager_GetResource(t *testing.T) { 220 | tests := []struct { 221 | meta *ResourceMeta 222 | gs *gameKruiseV1alpha1.GameServer 223 | isError bool 224 | }{ 225 | { 226 | meta: &ResourceMeta{ 227 | Namespace: "xxx", 228 | Name: "xxx", 229 | ID: "1", 230 | }, 231 | gs: &gameKruiseV1alpha1.GameServer{ 232 | ObjectMeta: metav1.ObjectMeta{ 233 | Namespace: "xxx", 234 | Name: "xxx-0", 235 | Labels: map[string]string{ 236 | gameKruiseV1alpha1.GameServerOwnerGssKey: "xxx", 237 | }, 238 | }, 239 | }, 240 | isError: true, 241 | }, 242 | { 243 | meta: &ResourceMeta{ 244 | Namespace: "xxx", 245 | Name: "xxx", 246 | }, 247 | gs: &gameKruiseV1alpha1.GameServer{ 248 | ObjectMeta: metav1.ObjectMeta{ 249 | Namespace: "xxx", 250 | Name: "xxx-0", 251 | Labels: map[string]string{ 252 | gameKruiseV1alpha1.GameServerOwnerGssKey: "xxx", 253 | }, 254 | }, 255 | }, 256 | isError: true, 257 | }, 258 | { 259 | meta: &ResourceMeta{ 260 | Namespace: "xxx", 261 | Name: "xxx", 262 | ID: "0", 263 | }, 264 | gs: &gameKruiseV1alpha1.GameServer{ 265 | ObjectMeta: metav1.ObjectMeta{ 266 | Namespace: "xxx", 267 | Name: "xxx-0", 268 | Labels: map[string]string{ 269 | gameKruiseV1alpha1.GameServerOwnerGssKey: "xxx", 270 | }, 271 | }, 272 | }, 273 | isError: false, 274 | }, 275 | } 276 | 277 | for caseNum, test := range tests { 278 | objs := []client.Object{test.gs} 279 | c := fake.NewClientBuilder().WithScheme(schemeTest).WithObjects(objs...).Build() 280 | rm := &ResourceManager{Client: c} 281 | _, err := rm.GetResource(test.meta) 282 | if !reflect.DeepEqual(test.isError, err != nil) { 283 | t.Errorf("case %d: errs not equal", caseNum) 284 | } 285 | } 286 | } 287 | 288 | func TestResourceManager_CreateResource(t *testing.T) { 289 | tests := []struct { 290 | meta *ResourceMeta 291 | gssBefore *gameKruiseV1alpha1.GameServerSet 292 | newID string 293 | gssAfter *gameKruiseV1alpha1.GameServerSet 294 | }{ 295 | { 296 | meta: &ResourceMeta{ 297 | Namespace: "xxx", 298 | Name: "case-0", 299 | }, 300 | gssBefore: &gameKruiseV1alpha1.GameServerSet{ 301 | ObjectMeta: metav1.ObjectMeta{ 302 | Namespace: "xxx", 303 | Name: "case-0", 304 | ResourceVersion: "999", 305 | Annotations: map[string]string{gameKruiseV1alpha1.GameServerSetReserveIdsKey: "1"}, 306 | }, 307 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 308 | Replicas: pointer.Int32(3), 309 | ReserveGameServerIds: []int{1}, 310 | }, 311 | }, 312 | newID: "4", 313 | gssAfter: &gameKruiseV1alpha1.GameServerSet{ 314 | TypeMeta: metav1.TypeMeta{ 315 | APIVersion: "game.kruise.io/v1alpha1", 316 | Kind: "GameServerSet", 317 | }, 318 | ObjectMeta: metav1.ObjectMeta{ 319 | Namespace: "xxx", 320 | Name: "case-0", 321 | ResourceVersion: "1000", 322 | Annotations: map[string]string{gameKruiseV1alpha1.GameServerSetReserveIdsKey: "1"}, 323 | }, 324 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 325 | Replicas: pointer.Int32(4), 326 | ReserveGameServerIds: []int{1}, 327 | }, 328 | }, 329 | }, 330 | { 331 | meta: &ResourceMeta{ 332 | Namespace: "xxx", 333 | Name: "case-1", 334 | }, 335 | gssBefore: &gameKruiseV1alpha1.GameServerSet{ 336 | ObjectMeta: metav1.ObjectMeta{ 337 | Namespace: "xxx", 338 | Name: "case-1", 339 | ResourceVersion: "999", 340 | Annotations: map[string]string{gameKruiseV1alpha1.GameServerSetReserveIdsKey: "1,2,4"}, 341 | }, 342 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 343 | Replicas: pointer.Int32(7), 344 | ReserveGameServerIds: []int{1, 2, 4}, 345 | }, 346 | }, 347 | newID: "10", 348 | gssAfter: &gameKruiseV1alpha1.GameServerSet{ 349 | TypeMeta: metav1.TypeMeta{ 350 | APIVersion: "game.kruise.io/v1alpha1", 351 | Kind: "GameServerSet", 352 | }, 353 | ObjectMeta: metav1.ObjectMeta{ 354 | Namespace: "xxx", 355 | Name: "case-1", 356 | ResourceVersion: "1000", 357 | Annotations: map[string]string{gameKruiseV1alpha1.GameServerSetReserveIdsKey: "1,2,4"}, 358 | }, 359 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 360 | Replicas: pointer.Int32(8), 361 | ReserveGameServerIds: []int{1, 2, 4}, 362 | }, 363 | }, 364 | }, 365 | } 366 | 367 | for caseNum, test := range tests { 368 | objs := []client.Object{test.gssBefore} 369 | c := fake.NewClientBuilder().WithScheme(schemeTest).WithObjects(objs...).Build() 370 | rm := &ResourceManager{Client: c} 371 | actualMeta, err := rm.CreateResource(test.meta) 372 | if err != nil { 373 | t.Errorf("case %d: %s", caseNum, err.Error()) 374 | } 375 | if actualMeta.ID != test.newID { 376 | t.Errorf("case %d: expect return ID is %s, but get %s", caseNum, test.newID, actualMeta.ID) 377 | } 378 | actualGss := &gameKruiseV1alpha1.GameServerSet{} 379 | if err := rm.Get(context.Background(), types.NamespacedName{ 380 | Name: test.gssBefore.GetName(), 381 | Namespace: test.gssBefore.GetNamespace(), 382 | }, actualGss); err != nil { 383 | t.Error(err) 384 | } 385 | if !reflect.DeepEqual(test.gssAfter, actualGss) { 386 | t.Errorf("case %d: expect gss %v but got %v", caseNum, test.gssAfter, actualGss) 387 | } 388 | } 389 | } 390 | 391 | func TestResourceManager_PauseResource(t *testing.T) { 392 | tests := []struct { 393 | meta *ResourceMeta 394 | gssBefore *gameKruiseV1alpha1.GameServerSet 395 | gssAfter *gameKruiseV1alpha1.GameServerSet 396 | isError bool 397 | }{ 398 | { 399 | meta: &ResourceMeta{ 400 | Namespace: "xxx", 401 | Name: "case-0", 402 | ID: "1", 403 | }, 404 | gssBefore: &gameKruiseV1alpha1.GameServerSet{ 405 | ObjectMeta: metav1.ObjectMeta{ 406 | Namespace: "xxx", 407 | Name: "case-0", 408 | ResourceVersion: "999", 409 | }, 410 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 411 | Replicas: pointer.Int32(3), 412 | ReserveGameServerIds: []int{}, 413 | }, 414 | }, 415 | gssAfter: &gameKruiseV1alpha1.GameServerSet{ 416 | TypeMeta: metav1.TypeMeta{ 417 | APIVersion: "game.kruise.io/v1alpha1", 418 | Kind: "GameServerSet", 419 | }, 420 | ObjectMeta: metav1.ObjectMeta{ 421 | Namespace: "xxx", 422 | Name: "case-0", 423 | ResourceVersion: "1000", 424 | }, 425 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 426 | Replicas: pointer.Int32(2), 427 | ReserveGameServerIds: []int{1}, 428 | }, 429 | }, 430 | }, 431 | { 432 | meta: &ResourceMeta{ 433 | Namespace: "xxx", 434 | Name: "case-1", 435 | }, 436 | gssBefore: &gameKruiseV1alpha1.GameServerSet{ 437 | ObjectMeta: metav1.ObjectMeta{ 438 | Namespace: "xxx", 439 | Name: "case-1", 440 | ResourceVersion: "999", 441 | }, 442 | Spec: gameKruiseV1alpha1.GameServerSetSpec{ 443 | Replicas: pointer.Int32(3), 444 | ReserveGameServerIds: []int{}, 445 | }, 446 | }, 447 | isError: true, 448 | }, 449 | } 450 | 451 | for caseNum, test := range tests { 452 | objs := []client.Object{test.gssBefore} 453 | c := fake.NewClientBuilder().WithScheme(schemeTest).WithObjects(objs...).Build() 454 | rm := &ResourceManager{Client: c} 455 | err := rm.PauseResource(test.meta) 456 | 457 | if !reflect.DeepEqual(test.isError, err != nil) { 458 | t.Errorf("case %d: errs not equal", caseNum) 459 | continue 460 | } 461 | 462 | if !test.isError { 463 | actualGss := &gameKruiseV1alpha1.GameServerSet{} 464 | if err := rm.Get(context.Background(), types.NamespacedName{ 465 | Name: test.gssBefore.GetName(), 466 | Namespace: test.gssBefore.GetNamespace(), 467 | }, actualGss); err != nil { 468 | t.Error(err) 469 | } 470 | if !reflect.DeepEqual(test.gssAfter, actualGss) { 471 | t.Errorf("case %d: expect gss %v but got %v", caseNum, test.gssAfter, actualGss) 472 | } 473 | } 474 | } 475 | } 476 | -------------------------------------------------------------------------------- /pkg/routers/resource.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/CloudNativeGame/aigc-gateway/pkg/resources" 8 | mem "github.com/CloudNativeGame/aigc-gateway/pkg/session" 9 | "github.com/CloudNativeGame/aigc-gateway/pkg/user" 10 | "github.com/CloudNativeGame/aigc-gateway/pkg/utils" 11 | "github.com/gin-contrib/sessions" 12 | "github.com/gin-gonic/gin" 13 | "github.com/logto-io/go/client" 14 | "github.com/logto-io/go/core" 15 | ) 16 | 17 | func RegisterResourceRouters(router *gin.Engine, logtoConfig *client.LogtoConfig) { 18 | // Add a link to perform a sign-in request on the home page 19 | router.GET("/resources", func(ctx *gin.Context) { 20 | rm := resources.NewResourceManager() 21 | config, err := utils.ParseConfig("/etc/config/config.json") 22 | if err != nil { 23 | ctx.String(400, err.Error()) 24 | } 25 | 26 | resources, err := rm.ListResources(config.Namespaces, config.GssLabels) 27 | if err != nil { 28 | ctx.String(400, err.Error()) 29 | } 30 | ctx.JSON(200, resources) 31 | }) 32 | 33 | router.GET("/resource/:namespace/:name/:id", func(ctx *gin.Context) { 34 | name := ctx.Param("name") 35 | namespace := ctx.Param("namespace") 36 | id := ctx.Param("id") 37 | 38 | session := sessions.Default(ctx) 39 | logtoClient := client.NewLogtoClient( 40 | logtoConfig, 41 | &mem.SessionStorage{Session: session}, 42 | ) 43 | 44 | userInfo, err := logtoClient.FetchUserInfo() 45 | if err != nil { 46 | ctx.Error(err) 47 | 48 | return 49 | } 50 | cm := userInfo.CustomData 51 | key := fmt.Sprintf("%s-%s", namespace, name) 52 | 53 | value := cm[key] 54 | if value == nil { 55 | ctx.String(403, "have no privilege to get the instance with ID %d", id) 56 | return 57 | } 58 | 59 | rm := &resources.ResourceMeta{} 60 | valueBytes, err := interfaceToBytes(value) 61 | if err != nil { 62 | ctx.Error(err) 63 | 64 | return 65 | } 66 | err = json.Unmarshal(valueBytes, rm) 67 | if err != nil { 68 | ctx.Error(err) 69 | 70 | return 71 | } 72 | 73 | if id != rm.ID || name != rm.Name || namespace != rm.Namespace { 74 | ctx.String(403, "have no privilege to get the instance with ID %d", id) 75 | return 76 | } 77 | 78 | // add json wrapper 79 | resourceManager := resources.NewResourceManager() 80 | resource, err := resourceManager.GetResource(rm) 81 | 82 | if err != nil { 83 | if resources.GetErrorReason(err) == resources.PauseReason { 84 | ctx.Status(423) 85 | return 86 | } 87 | ctx.String(400, err.Error()) 88 | 89 | return 90 | } 91 | 92 | ctx.JSON(200, resource) 93 | }) 94 | 95 | // create a new one 96 | router.PUT("/resource/:namespace/:name", func(ctx *gin.Context) { 97 | name := ctx.Param("name") 98 | namespace := ctx.Param("namespace") 99 | 100 | session := sessions.Default(ctx) 101 | logtoClient := client.NewLogtoClient( 102 | logtoConfig, 103 | &mem.SessionStorage{Session: session}, 104 | ) 105 | 106 | userInfo, err := logtoClient.FetchUserInfo() 107 | 108 | if err != nil { 109 | ctx.Error(err) 110 | } 111 | 112 | cm := userInfo.CustomData 113 | 114 | if cm == nil { 115 | cm = make(map[string]interface{}) 116 | } 117 | 118 | key := fmt.Sprintf("%s-%s", namespace, name) 119 | 120 | // check if already installed 121 | if cm[key] != nil { 122 | ctx.String(400, "already installed") 123 | return 124 | } 125 | 126 | rm := &resources.ResourceMeta{ 127 | Name: name, 128 | Namespace: namespace, 129 | } 130 | 131 | // add json wrapper 132 | resourceManager := resources.NewResourceManager() 133 | meta, err := resourceManager.CreateResource(rm) 134 | if err != nil { 135 | ctx.Error(err) 136 | 137 | return 138 | } 139 | 140 | cm[key] = meta 141 | 142 | err = user.UpdateUserMetaData(userInfo.Sub, cm) 143 | if err != nil { 144 | ctx.Error(err) 145 | 146 | return 147 | } 148 | 149 | ctx.JSON(200, meta) 150 | 151 | }) 152 | 153 | router.POST("/resource/:namespace/:name/pause", func(ctx *gin.Context) { 154 | name := ctx.Param("name") 155 | namespace := ctx.Param("namespace") 156 | 157 | session := sessions.Default(ctx) 158 | logtoClient := client.NewLogtoClient( 159 | logtoConfig, 160 | &mem.SessionStorage{Session: session}, 161 | ) 162 | 163 | userInfo, err := logtoClient.FetchUserInfo() 164 | 165 | if err != nil { 166 | ctx.Error(err) 167 | 168 | return 169 | } 170 | 171 | cm := userInfo.CustomData 172 | 173 | key := fmt.Sprintf("%s-%s", namespace, name) 174 | value := cm[key] 175 | 176 | valueBytes, err := interfaceToBytes(value) 177 | 178 | if err != nil { 179 | ctx.Error(err) 180 | 181 | return 182 | } 183 | 184 | rm := &resources.ResourceMeta{} 185 | 186 | err = json.Unmarshal(valueBytes, rm) 187 | 188 | if err != nil { 189 | ctx.Error(err) 190 | 191 | return 192 | } 193 | 194 | // add json wrapper 195 | resourceManager := resources.NewResourceManager() 196 | err = resourceManager.PauseResource(rm) 197 | 198 | if err != nil { 199 | ctx.String(400, err.Error()) 200 | 201 | return 202 | } 203 | ctx.Status(200) 204 | }) 205 | 206 | router.POST("/resource/:namespace/:name/recover", func(ctx *gin.Context) { 207 | name := ctx.Param("name") 208 | namespace := ctx.Param("namespace") 209 | 210 | session := sessions.Default(ctx) 211 | logtoClient := client.NewLogtoClient( 212 | logtoConfig, 213 | &mem.SessionStorage{Session: session}, 214 | ) 215 | 216 | userInfo, err := logtoClient.FetchUserInfo() 217 | 218 | if err != nil { 219 | ctx.Error(err) 220 | 221 | return 222 | } 223 | 224 | cm := userInfo.CustomData 225 | 226 | key := fmt.Sprintf("%s-%s", namespace, name) 227 | value := cm[key] 228 | 229 | if value == nil { 230 | ctx.Status(200) 231 | return 232 | } 233 | 234 | valueBytes, err := interfaceToBytes(value) 235 | 236 | if err != nil { 237 | ctx.Error(err) 238 | 239 | return 240 | } 241 | 242 | rm := &resources.ResourceMeta{} 243 | 244 | json.Unmarshal(valueBytes, rm) 245 | 246 | // add json wrapper 247 | resourceManager := resources.NewResourceManager() 248 | _, err = resourceManager.RecoverResource(rm) 249 | 250 | if err != nil { 251 | ctx.String(400, err.Error()) 252 | 253 | return 254 | } 255 | ctx.Status(200) 256 | }) 257 | 258 | router.DELETE("/resource/:namespace/:name", func(ctx *gin.Context) { 259 | name := ctx.Param("name") 260 | namespace := ctx.Param("namespace") 261 | 262 | session := sessions.Default(ctx) 263 | logtoClient := client.NewLogtoClient( 264 | logtoConfig, 265 | &mem.SessionStorage{Session: session}, 266 | ) 267 | 268 | userInfo, err := logtoClient.FetchUserInfo() 269 | 270 | if err != nil { 271 | ctx.Error(err) 272 | return 273 | } 274 | 275 | cm := userInfo.CustomData 276 | 277 | key := fmt.Sprintf("%s-%s", namespace, name) 278 | value := cm[key] 279 | if value == nil { 280 | ctx.Status(400) 281 | return 282 | } 283 | valueBytes, err := interfaceToBytes(value) 284 | if err != nil { 285 | ctx.Error(err) 286 | return 287 | } 288 | 289 | rm := &resources.ResourceMeta{} 290 | err = json.Unmarshal(valueBytes, rm) 291 | if err != nil { 292 | ctx.Error(err) 293 | return 294 | } 295 | 296 | // add json wrapper 297 | resourceManager := resources.NewResourceManager() 298 | err = resourceManager.DeleteResource(rm) 299 | if err != nil { 300 | ctx.Error(err) 301 | } 302 | 303 | cm[key] = nil 304 | 305 | err = user.UpdateUserMetaData(userInfo.Sub, cm) 306 | if err != nil { 307 | ctx.Error(err) 308 | return 309 | } 310 | 311 | ctx.Status(200) 312 | return 313 | }) 314 | 315 | router.POST("/resource/:namespace/:name/restart", func(ctx *gin.Context) { 316 | name := ctx.Param("name") 317 | namespace := ctx.Param("namespace") 318 | 319 | session := sessions.Default(ctx) 320 | logtoClient := client.NewLogtoClient( 321 | logtoConfig, 322 | &mem.SessionStorage{Session: session}, 323 | ) 324 | 325 | userInfo, err := logtoClient.FetchUserInfo() 326 | 327 | if err != nil { 328 | ctx.Error(err) 329 | return 330 | } 331 | 332 | cm := userInfo.CustomData 333 | 334 | key := fmt.Sprintf("%s-%s", namespace, name) 335 | value := cm[key] 336 | if value == nil { 337 | ctx.Status(400) 338 | return 339 | } 340 | valueBytes, err := interfaceToBytes(value) 341 | if err != nil { 342 | ctx.Error(err) 343 | return 344 | } 345 | 346 | rm := &resources.ResourceMeta{} 347 | err = json.Unmarshal(valueBytes, rm) 348 | if err != nil { 349 | ctx.Error(err) 350 | return 351 | } 352 | 353 | // add json wrapper 354 | resourceManager := resources.NewResourceManager() 355 | err = resourceManager.RestartResource(rm) 356 | if err != nil { 357 | ctx.Error(err) 358 | return 359 | } 360 | 361 | ctx.Status(200) 362 | return 363 | }) 364 | } 365 | 366 | func interfaceToBytes(data interface{}) ([]byte, error) { 367 | bytes, err := json.Marshal(data) 368 | if err != nil { 369 | return nil, err 370 | } 371 | return bytes, nil 372 | } 373 | 374 | func getUserInfoFromSession(logtoConfig *client.LogtoConfig, ctx *gin.Context) (core.IdTokenClaims, error) { 375 | // Init LogtoClient 376 | session := sessions.Default(ctx) 377 | logtoClient := client.NewLogtoClient( 378 | logtoConfig, 379 | &mem.SessionStorage{Session: session}, 380 | ) 381 | 382 | tc, err := logtoClient.GetIdTokenClaims() 383 | 384 | if err != nil { 385 | return core.IdTokenClaims{}, err 386 | } 387 | 388 | return tc, nil 389 | } 390 | -------------------------------------------------------------------------------- /pkg/routers/sign.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/CloudNativeGame/aigc-gateway/pkg/resources" 6 | mem "github.com/CloudNativeGame/aigc-gateway/pkg/session" 7 | "github.com/gin-contrib/sessions" 8 | "github.com/gin-gonic/gin" 9 | "github.com/logto-io/go/client" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | var redirectURL = os.Getenv("Redirect_Url") 17 | 18 | func RegisterSignRouters(router *gin.Engine, logtoConfig *client.LogtoConfig) { 19 | // Add a link to perform a sign-in request on the home page 20 | router.GET("/", func(ctx *gin.Context) { 21 | // Init LogtoClient 22 | session := sessions.Default(ctx) 23 | logtoClient := client.NewLogtoClient( 24 | logtoConfig, 25 | &mem.SessionStorage{Session: session}, 26 | ) 27 | 28 | resp, err := logtoClient.FetchUserInfo() 29 | 30 | authState := false 31 | 32 | if err == nil { 33 | authState = true 34 | } 35 | 36 | ctx.HTML(http.StatusOK, "index.html", gin.H{ 37 | "title": "Main website", 38 | "authState": authState, 39 | "userInfo": resp, 40 | }) 41 | }) 42 | 43 | router.GET("/auth", func(ctx *gin.Context) { 44 | session := sessions.Default(ctx) 45 | logtoClient := client.NewLogtoClient( 46 | logtoConfig, 47 | &mem.SessionStorage{Session: session}, 48 | ) 49 | 50 | userInfo, err := logtoClient.FetchUserInfo() 51 | if err != nil { 52 | ctx.String(http.StatusUnauthorized, err.Error()) 53 | return 54 | } 55 | 56 | originUrl := ctx.GetHeader("X-Original-Url") 57 | 58 | resourceManager := resources.NewResourceManager() 59 | 60 | for _, info := range userInfo.CustomData { 61 | if info == nil { 62 | continue 63 | } 64 | valueBytes, err := interfaceToBytes(info) 65 | if err != nil { 66 | ctx.Error(err) 67 | } 68 | 69 | rm := &resources.ResourceMeta{} 70 | 71 | err = json.Unmarshal(valueBytes, rm) 72 | 73 | if err != nil { 74 | ctx.Error(err) 75 | } 76 | 77 | endpoint, err := resourceManager.GetResourceEndpoint(rm) 78 | if err != nil { 79 | ctx.String(http.StatusInternalServerError, err.Error()) 80 | } 81 | 82 | if endpoint != "" && strings.Contains(originUrl, endpoint) { 83 | ctx.String(http.StatusOK, "") 84 | return 85 | } 86 | } 87 | 88 | ctx.String(http.StatusUnauthorized, "User and Access Endpoint Mismatch") 89 | return 90 | }) 91 | 92 | // Add a route for handling sign-in requests 93 | router.GET("/sign-in", func(ctx *gin.Context) { 94 | session := sessions.Default(ctx) 95 | logtoClient := client.NewLogtoClient( 96 | logtoConfig, 97 | &mem.SessionStorage{Session: session}, 98 | ) 99 | 100 | signInUri, _ := url.JoinPath(redirectURL, "sign-in-callback") 101 | 102 | // The sign-in request is handled by Logto. 103 | // The user will be redirected to the Redirect URI on signed in. 104 | signInUri, err := logtoClient.SignIn(signInUri) 105 | if err != nil { 106 | ctx.String(http.StatusInternalServerError, err.Error()) 107 | return 108 | } 109 | 110 | // Redirect the user to the Logto sign-in page. 111 | ctx.Redirect(http.StatusTemporaryRedirect, signInUri) 112 | }) 113 | 114 | // Add a route for handling signing out requests 115 | router.GET("/sign-out", func(ctx *gin.Context) { 116 | session := sessions.Default(ctx) 117 | logtoClient := client.NewLogtoClient( 118 | logtoConfig, 119 | &mem.SessionStorage{Session: session}, 120 | ) 121 | 122 | // The sign-out request is handled by Logto. 123 | // The user will be redirected to the Post Sign-out Redirect URI on signed out. 124 | signOutUri, signOutErr := logtoClient.SignOut(redirectURL) 125 | 126 | if signOutErr != nil { 127 | ctx.String(http.StatusOK, signOutErr.Error()) 128 | return 129 | } 130 | 131 | ctx.Redirect(http.StatusTemporaryRedirect, signOutUri) 132 | }) 133 | 134 | // Add a route for handling sign-in callback requests 135 | router.GET("/sign-in-callback", func(ctx *gin.Context) { 136 | session := sessions.Default(ctx) 137 | logtoClient := client.NewLogtoClient( 138 | logtoConfig, 139 | &mem.SessionStorage{Session: session}, 140 | ) 141 | 142 | // The sign-in callback request is handled by Logto 143 | err := logtoClient.HandleSignInCallback(ctx.Request) 144 | if err != nil { 145 | ctx.String(http.StatusInternalServerError, err.Error()) 146 | return 147 | } 148 | 149 | // Jump to the page specified by the developer. 150 | // This example takes the user back to the home page. 151 | ctx.Redirect(http.StatusTemporaryRedirect, "/") 152 | }) 153 | } 154 | -------------------------------------------------------------------------------- /pkg/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "github.com/gin-contrib/sessions" 5 | ) 6 | 7 | type SessionStorage struct { 8 | Session sessions.Session 9 | } 10 | 11 | func (storage *SessionStorage) GetItem(key string) string { 12 | value := storage.Session.Get(key) 13 | if value == nil { 14 | return "" 15 | } 16 | return value.(string) 17 | } 18 | 19 | func (storage *SessionStorage) SetItem(key, value string) { 20 | storage.Session.Set(key, value) 21 | storage.Session.Save() 22 | } 23 | -------------------------------------------------------------------------------- /pkg/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | type CustomData map[string]interface{} 16 | 17 | type PatchBody struct { 18 | CustomData CustomData `json:"customData"` 19 | } 20 | 21 | type tokenAuth struct { 22 | AccessToken string `json:"access_token"` 23 | ExpiresIn int `json:"expires_in"` 24 | TokenType string `json:"token_type"` 25 | } 26 | 27 | type User struct { 28 | Id string `json:"id"` 29 | Username string `json:"username"` 30 | PrimaryEmail string `json:"primaryEmail"` 31 | PrimaryPhone string `json:"primaryPhone"` 32 | Name string `json:"name"` 33 | Avatar string `json:"avatar"` 34 | CustomData map[string]interface{} `json:"customData"` 35 | LastSignInAt int `json:"lastSignInAt"` 36 | CreatedAt int `json:"createdAt"` 37 | ApplicationId string `json:"applicationId"` 38 | IsSuspended bool `json:"isSuspended"` 39 | HasPassword bool `json:"hasPassword"` 40 | } 41 | 42 | var M2MID = os.Getenv("M2M_Id") 43 | var M2MSecret = os.Getenv("M2M_Secret") 44 | var Endpoint = os.Getenv("Endpoint") 45 | 46 | func UpdateUserMetaData(userId string, customData map[string]interface{}) error { 47 | 48 | c := http.Client{ 49 | Transport: &http.Transport{ 50 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 51 | }, 52 | } 53 | form := url.Values{} 54 | form.Add("grant_type", "client_credentials") 55 | form.Add("resource", "https://default.logto.app/api") 56 | form.Add("scope", "all") 57 | 58 | tokenRequest, err := http.NewRequest(http.MethodPost, Endpoint+"oidc/token", strings.NewReader(form.Encode())) 59 | 60 | if err != nil { 61 | return err 62 | } 63 | tokenRequest.SetBasicAuth(M2MID, M2MSecret) 64 | tokenRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded") 65 | resp, err := c.Do(tokenRequest) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | defer resp.Body.Close() 71 | 72 | b, _ := ioutil.ReadAll(resp.Body) 73 | 74 | au := &tokenAuth{} 75 | err = json.Unmarshal(b, au) 76 | 77 | if err != nil { 78 | return err 79 | } 80 | 81 | patchBody := &PatchBody{} 82 | patchBody.CustomData = customData 83 | 84 | customDataBytes, err := json.Marshal(patchBody) 85 | 86 | patchRequest, err := http.NewRequest(http.MethodPatch, fmt.Sprintf(Endpoint+"api/users/%s/custom-data", userId), bytes.NewBuffer(customDataBytes)) 87 | if err != nil { 88 | return err 89 | } 90 | patchRequest.Header.Set("Authorization", "Bearer "+au.AccessToken) 91 | patchRequest.Header.Set("Content-Type", "application/json") 92 | 93 | patchResponse, err := c.Do(patchRequest) 94 | defer patchResponse.Body.Close() 95 | if err != nil { 96 | return err 97 | } 98 | return nil 99 | } 100 | 101 | func GetUserProfile(userId string) (*User, error) { 102 | c := http.Client{} 103 | form := url.Values{} 104 | form.Add("grant_type", "client_credentials") 105 | form.Add("resource", "https://default.logto.app/api") 106 | form.Add("scope", "all") 107 | 108 | tokenRequest, err := http.NewRequest(http.MethodPost, Endpoint+"oidc/token", strings.NewReader(form.Encode())) 109 | if err != nil { 110 | return nil, err 111 | } 112 | tokenRequest.SetBasicAuth(M2MID, M2MSecret) 113 | tokenRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded") 114 | resp, err := c.Do(tokenRequest) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | defer resp.Body.Close() 120 | 121 | b, _ := ioutil.ReadAll(resp.Body) 122 | 123 | au := &tokenAuth{} 124 | err = json.Unmarshal(b, au) 125 | 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | userResp, err := http.Get(Endpoint + "api/users/" + userId) 131 | 132 | defer userResp.Body.Close() 133 | 134 | if err != nil { 135 | return nil, err 136 | } 137 | userBytes, err := ioutil.ReadAll(resp.Body) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | u := &User{} 143 | 144 | err = json.Unmarshal(userBytes, u) 145 | 146 | return u, err 147 | 148 | } 149 | -------------------------------------------------------------------------------- /pkg/utils/config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | type AigcGatewayConfig struct { 11 | Namespaces []string `json:"namespaces"` 12 | GssLabels map[string]string `json:"gss_labels"` 13 | } 14 | 15 | func ParseConfig(name string) (*AigcGatewayConfig, error) { 16 | // 打开 JSON 文件 17 | file, err := os.Open(name) 18 | if err != nil { 19 | fmt.Printf("Error opening file: %s\n", err) 20 | return nil, err 21 | } 22 | defer file.Close() 23 | 24 | // 读取文件内容 25 | bytes, err := ioutil.ReadAll(file) 26 | if err != nil { 27 | fmt.Printf("Error reading file: %s\n", err) 28 | return nil, err 29 | } 30 | 31 | // 将 JSON 数据解析到 Config 结构体中 32 | config := &AigcGatewayConfig{} 33 | if err := json.Unmarshal(bytes, config); err != nil { 34 | fmt.Printf("Error parsing JSON: %s\n", err) 35 | return nil, err 36 | } 37 | 38 | // 输出解析结果 39 | fmt.Printf("Namespaces: %s\n", config.Namespaces) 40 | fmt.Printf("GSS Labels: %+v\n", config.GssLabels) 41 | return config, nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/utils/config_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestParseConfig(t *testing.T) { 6 | ParseConfig("./test.config") 7 | } 8 | -------------------------------------------------------------------------------- /pkg/utils/string.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "strings" 4 | 5 | func GetDomainFromEndpoint(endpoint string) string { 6 | var domain string 7 | tail := 0 8 | index := strings.Index(endpoint, ".") 9 | if endpoint[len(endpoint)-1] == '/' { 10 | tail = 1 11 | } 12 | if index != -1 { 13 | domain = endpoint[index : len(endpoint)-tail] 14 | } 15 | return domain 16 | } 17 | -------------------------------------------------------------------------------- /pkg/utils/string_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestGetDomainFromEndpoint(t *testing.T) { 6 | tests := []struct { 7 | endpoint string 8 | domain string 9 | }{ 10 | { 11 | endpoint: "https://logto.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com/", 12 | domain: ".c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com", 13 | }, 14 | { 15 | endpoint: "https://logto.c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com", 16 | domain: ".c5464a5f2c39341d3b3eda6e2dd37b505.cn-hangzhou.alicontainer.com", 17 | }, 18 | } 19 | 20 | for _, test := range tests { 21 | actual := GetDomainFromEndpoint(test.endpoint) 22 | expect := test.domain 23 | if actual != expect { 24 | t.Errorf("expect domain: %s, but actual got %v", expect, actual) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/utils/test.config: -------------------------------------------------------------------------------- 1 | { 2 | "namespaces": ["xxx"], 3 | "gss_labels": { 4 | "xxx": "xxx" 5 | } 6 | } --------------------------------------------------------------------------------