!6 k8s Agent for DevStar DevContainer
All checks were successful
DevStar Studio CI Pipeline - dev branch / build-and-push-x86-64-docker-image (push) Successful in 21m49s
All checks were successful
DevStar Studio CI Pipeline - dev branch / build-and-push-x86-64-docker-image (push) Successful in 21m49s
* DELETE /api/devcontainer?repoId=${repoId} 删除 DevContainer
* refactor
* GET /api/devcontainer?repoId=${repoId}&wait=true 阻塞式等待打开就绪的 DevContainer
* POST /api/devcontainer 创建 DevContainer
* refactored the code
* Updated context usage with cancel function
* 预留接口,适配单机版 DevStar DevContainer
* bugFix: context canceled while deleting k8s CRD DevcontainerApp
* 用户界面删除 k8s CRD DevContainer
* 用户界面创建 DevContainer 并更新 NodePort
* 完成用户界面创建 DevContainer
* transplant test code into DevStar Studio
* refactored API router to /routers/api
* 更改 DevContainer Doc
* 更改 DevContainer namespace
* 特殊仓库重定向
* [Doc] 更新 Kubernetes 部署 DevStar Studio 文档说明,特别是 namespace 管理
* [Doc] 更新 CI脚本说明
* Revert "optimized CI workflow"
* optimized CI workflow
* fix typo
* [feature test]: 测试 Pod 内使用 Kubernetes Operator 功能
* [Optimization] error msg for archived repo
* [Optimization]: display detailed err msg on creating devContainer for …
This commit is contained in:
@@ -6,10 +6,16 @@
|
||||
# Add variables of Remote Git Repository Panel:
|
||||
# - ${{ vars.DOCKER_REGISTRY_ADDRESS }}: the address for Docker Registry
|
||||
# - ${{ vars.DOCKER_REPOSITORY_ARTIFACT}}: the artifact $name:$version, e.g., `devstar/devstar-studio:latest-rootless`
|
||||
# - ${{ vars.K8S_NAMESPACE }}: the namespace defined in Helm Chart
|
||||
# - ${{ vars.K8S_DEPLOYMENT_NAME}}: the Deployment to rolled out restart after pushing artifact to Docker Registry
|
||||
|
||||
# Note: the actual artifact name for `master` branch: ${{ vars.DOCKER_REPOSITORY_ARTIFACT}}-build-${{ gitea.sha }}
|
||||
# Artifact命名规则: ${{ vars.DOCKER_REPOSITORY_ARTIFACT}}-build-${{ gitea.sha }}, e.g., www.devstar.cn/devstar/devstar-studio:rootless-build-0047d315a3f73cca0c18c641d24b0347456618d5
|
||||
# DOCKER_REPOSITORY_ARTIFACT = www.devstar.cn/devstar/devstar-studio:rootless,表示 Artifact 名称
|
||||
# build 表示分支类别:正式发行分支
|
||||
# ${{ gitea.sha }} 表示触发 CI Workflow 的 commit SHA
|
||||
|
||||
# 特别注意:如果使用 devstar.cn 作为 Docker Registry,需要设置 Gitea Actions Runner 变量 DOCKER_REGISTRY_ADDRESS = www.devstar.cn,若没有www前缀会导致上传失败(Ingress重定向规则导致,暂时无解)
|
||||
# 上传 Artifact 必须加上 www前缀,而拉取时不需要加 www 前缀,例如:
|
||||
# 上传前 artifact 名称: docker push www.devstar.cn/devstar/devstar-studio:rootless-build-0047d315a3f73cca0c18c641d24b0347456618d5
|
||||
# 拉取 artifact 只需要: docker pull devstar.cn/devstar/devstar-studio:rootless-build-0047d315a3f73cca0c18c641d24b0347456618d5
|
||||
|
||||
name: DevStar Studio CI Pipeline - master branch
|
||||
on:
|
||||
|
||||
@@ -6,10 +6,16 @@
|
||||
# Add variables of Remote Git Repository Panel:
|
||||
# - ${{ vars.DOCKER_REGISTRY_ADDRESS }}: the address for Docker Registry
|
||||
# - ${{ vars.DOCKER_REPOSITORY_ARTIFACT}}: the artifact $name:$version, e.g., `devstar/devstar-studio:rootless`
|
||||
# - ${{ vars.K8S_NAMESPACE }}: the namespace defined in Helm Chart
|
||||
# - ${{ vars.K8S_DEPLOYMENT_NAME}}: the Deployment to rolled out restart after pushing artifact to Docker Registry
|
||||
|
||||
# Note: the actual artifact name for `dev` branch: ${{ vars.DOCKER_REPOSITORY_ARTIFACT}}-dev-${{ gitea.sha }}
|
||||
# Artifact命名规则: ${{ vars.DOCKER_REPOSITORY_ARTIFACT}}-dev-${{ gitea.sha }}, e.g., www.devstar.cn/devstar/devstar-studio:rootless-dev-0047d315a3f73cca0c18c641d24b0347456618d5
|
||||
# DOCKER_REPOSITORY_ARTIFACT = www.devstar.cn/devstar/devstar-studio:rootless,表示 Artifact 名称
|
||||
# dev 表示分支类别:临时开发分支
|
||||
# ${{ gitea.sha }} 表示触发 CI Workflow 的 commit SHA
|
||||
|
||||
# 特别注意:如果使用 devstar.cn 作为 Docker Registry,需要设置 Gitea Actions Runner 变量 DOCKER_REGISTRY_ADDRESS = www.devstar.cn,若没有www前缀会导致上传失败(Ingress重定向规则导致,暂时无解)
|
||||
# 上传 Artifact 必须加上 www前缀,而拉取时不需要加 www 前缀,例如:
|
||||
# 上传前 artifact 名称: docker push www.devstar.cn/devstar/devstar-studio:rootless-dev-0047d315a3f73cca0c18c641d24b0347456618d5
|
||||
# 拉取 artifact 只需要: docker pull devstar.cn/devstar/devstar-studio:rootless-dev-0047d315a3f73cca0c18c641d24b0347456618d5
|
||||
|
||||
name: DevStar Studio CI Pipeline - dev branch
|
||||
on:
|
||||
|
||||
66
.gitea/workflows/devstar-studio-feature-k8s-operator-ci.yaml
Normal file
66
.gitea/workflows/devstar-studio-feature-k8s-operator-ci.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
#
|
||||
# Add secrets of Remote Git Repository Panel:
|
||||
# - ${{ secrets.DOCKER_REGISTRY_USERNAME }}: username of Docker Registry
|
||||
# - ${{ secrets.DOCKER_REGISTRY_PASSWORD }}: password corresponding to the Docker Registry username
|
||||
|
||||
# Add variables of Remote Git Repository Panel:
|
||||
# - ${{ vars.DOCKER_REGISTRY_ADDRESS }}: the address for Docker Registry
|
||||
# - ${{ vars.DOCKER_REPOSITORY_ARTIFACT}}: the artifact $name:$version, e.g., `devstar/devstar-studio:rootless`
|
||||
|
||||
# Artifact命名规则: ${{ vars.DOCKER_REPOSITORY_ARTIFACT}}-feature-${{ gitea.sha }}, e.g., www.devstar.cn/devstar/devstar-studio:rootless-feature-0047d315a3f73cca0c18c641d24b0347456618d5
|
||||
# DOCKER_REPOSITORY_ARTIFACT = www.devstar.cn/devstar/devstar-studio:rootless,表示 Artifact 名称
|
||||
# feature 表示分支类别:临时测试分支
|
||||
# ${{ gitea.sha }} 表示触发 CI Workflow 的 commit SHA
|
||||
|
||||
# 特别注意:如果使用 devstar.cn 作为 Docker Registry,需要设置 Gitea Actions Runner 变量 DOCKER_REGISTRY_ADDRESS = www.devstar.cn,若没有www前缀会导致上传失败(Ingress重定向规则导致,暂时无解)
|
||||
# 上传 Artifact 必须加上 www前缀,而拉取时不需要加 www 前缀,例如:
|
||||
# 上传前 artifact 名称: docker push www.devstar.cn/devstar/devstar-studio:rootless-feature-0047d315a3f73cca0c18c641d24b0347456618d5
|
||||
# 拉取 artifact 只需要: docker pull devstar.cn/devstar/devstar-studio:rootless-feature-0047d315a3f73cca0c18c641d24b0347456618d5
|
||||
|
||||
# k8s Operator 功能临时测试 CI脚本(正式发行版不应该有此文件)
|
||||
|
||||
name: DevStar Studio CI Pipeline - branch feature-k8s-operator-agent
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- feature-k8s-operator-agent
|
||||
|
||||
jobs:
|
||||
build-and-push-x86-64-docker-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🔍 Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: feature-k8s-operator-agent
|
||||
- name: 🔧 Test Codes and Build an Artifact
|
||||
run: |
|
||||
echo "Prepare to build repository code ${{ gitea.repository }}:${{ gitea.ref }}."
|
||||
make docker
|
||||
- name: 🚀 Push Artifact to Docker Registry
|
||||
run: |
|
||||
docker tag devstar-studio:latest ${{ vars.DOCKER_REGISTRY_ADDRESS }}/${{ vars.DOCKER_REPOSITORY_ARTIFACT}}-feature-${{ gitea.sha }}
|
||||
echo "${{ secrets.DOCKER_REGISTRY_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_REGISTRY_USERNAME }} ${{ vars.DOCKER_REGISTRY_ADDRESS }} --password-stdin
|
||||
docker push ${{ vars.DOCKER_REGISTRY_ADDRESS }}/${{ vars.DOCKER_REPOSITORY_ARTIFACT}}-feature-${{ gitea.sha }}
|
||||
- name: 🍏 Job Status Report
|
||||
run: |
|
||||
echo "🍏 This job's status is ${{ job.status }}."
|
||||
echo "Output Artifact: ${{ vars.DOCKER_REGISTRY_ADDRESS }}/${{ vars.DOCKER_REPOSITORY_ARTIFACT}}-feature-${{ gitea.sha }}"
|
||||
|
||||
|
||||
#
|
||||
# P.S.:
|
||||
################################################################################
|
||||
# 1. How to config runner:
|
||||
# $ docker run \
|
||||
# --name gitea-act-runner-repo-devstar-studio \
|
||||
# -d \
|
||||
# -e GITEA_INSTANCE_URL=https://www.devstar.cn \
|
||||
# -e GITEA_RUNNER_REGISTRATION_TOKEN=${YOUR_GITEA_RUNNER_REGISTRATION_TOKEN} \
|
||||
# -v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# gitea/act_runner:latest
|
||||
#
|
||||
# 2. To clean the docker cache:
|
||||
# $ docker builder prune --force
|
||||
# $ if [ "$(docker volume ls -qf dangling=true)" ]; then docker volume rm $(docker volume ls -qf dangling=true); fi
|
||||
#
|
||||
159
README_ZH.md
159
README_ZH.md
@@ -2,10 +2,10 @@
|
||||
|
||||
DevStar Studio 是 Gitea 发行版
|
||||
|
||||
## 快速开始
|
||||
## 1. 快速开始
|
||||
编译、打包成为镜像:代码目录执行 `make docker` 命令
|
||||
|
||||
### 单机版部署
|
||||
### 1.1 单机版部署
|
||||
|
||||
使用docker-compose部署DevStar Studio需要参考官方网站:
|
||||
https://docs.gitea.com/zh-cn/next/installation/install-with-docker-rootless
|
||||
@@ -50,6 +50,11 @@ CONTENT_SECURITY_POLICY = default-src 'self' data: 'unsafe-inline' https://mp.we
|
||||
[ui.admin]
|
||||
;; Dev Container 分页参数(每页展示 DevContainer 个数),若未指定,默认值 50
|
||||
DEV_CONTAINERS_PAGING_NUM = 50
|
||||
|
||||
[devstar.devcontainer]
|
||||
enabled = true
|
||||
agent = docker
|
||||
host = 127.0.0.1
|
||||
```
|
||||
|
||||
正式部署单机版
|
||||
@@ -69,7 +74,157 @@ docker run \
|
||||
devstar-studio:latest
|
||||
```
|
||||
|
||||
### 1.2 Kubernetes 集群部署
|
||||
|
||||
DevStar Studio 在 Kubernetes集群中部署需要使用 Helm Charts,可参考 [官方示例](https://gitea.com/gitea/helm-chart) 。
|
||||
|
||||
推荐将 DevStar Studio 与 Dev Container 进行基于 namespace 的资源 **隔离**:
|
||||
*由于 k8s RBAC 限制,建议将 DevStar Studio 的主应用程序,数据库和缓存 与 Dev Container 部署在同一个 namespace 下*
|
||||
|
||||
- `devstar-studio-ns`:
|
||||
- **指定方式**:部署 Helm Charts 时候执行
|
||||
```bash
|
||||
helm install devstar-studio . --namespace devstar-studio-ns --create-namespace
|
||||
```
|
||||
- **引用方式**:`{{ .Release.Namespace }}`,例如:
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: devstar-studio-gitea-repository-app-ini-configmap
|
||||
namespace: {{ .Release.Namespace }}
|
||||
data:
|
||||
repository: |
|
||||
MAX_CREATION_LIMIT = 0
|
||||
```
|
||||
|
||||
|
||||
#### 1.2.1 client-go 版本适配 Kubernetes 集群
|
||||
|
||||
**注意**:部署在 k8s 之上要根据 Kubernetes 版本调整 `k8s.io/client-go` 版本。例如部署在 **Kubernetes 1.23.10** 之上,需要执行下列指令调整 go packages:
|
||||
```bash
|
||||
# 移除旧版本 client-go 的信息
|
||||
go get k8s.io/client-go@none
|
||||
|
||||
# 安装适配于 Kubernetes 1.23.10 版本的 client-go
|
||||
go get k8s.io/client-go@kubernetes-1.23.10
|
||||
```
|
||||
|
||||
#### 1.2.2 应用配置 `app.ini` 信息挂载
|
||||
|
||||
在 Helm Charts 目录下创建文件夹 `templates/devstar-studio-app.ini/`,用于存储 Gitea 配置信息,请参考 Docker 版 `ini` 配置文件字段,使用 `ConfigMaps` 或者 `Secrets` 类型资源描述。
|
||||
|
||||
对于 `ConfigMaps` 类型,参考:
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: devstar-studio-gitea-app-ini-configmap
|
||||
namespace: {{ .Release.Namespace }}
|
||||
data:
|
||||
database: |
|
||||
CHARSET_COLLATION = utf8mb4_bin
|
||||
|
||||
wechat: |
|
||||
WECHAT_OFFICIAL_ACCOUNT_TEMP_QR_EXPIRE_SECONDS=60
|
||||
WECHAT_OFFICIAL_ACCOUNT_APP_ID=<微信公众号APPID>
|
||||
WECHAT_OFFICIAL_ACCOUNT_APP_SECRET=<微信公众号SECRET>
|
||||
WECHAT_OFFICIAL_ACCOUNT_MESSAGE_TOKEN = <微信公众号自定义Token>
|
||||
WECHAT_OFFICIAL_ACCOUNT_MESSAGE_AES_KEY = <微信公众号AES加密密钥>
|
||||
|
||||
cors: |
|
||||
CONTENT_SECURITY_POLICY = default-src 'self' data: 'unsafe-inline' https://mp.weixin.qq.com; img-src * data:
|
||||
|
||||
ui.admin: |
|
||||
DEV_CONTAINERS_PAGING_NUM = 50
|
||||
|
||||
devstar.devcontainer: |
|
||||
enabled = true
|
||||
agent = k8s
|
||||
namespace = devstar-studio-ns
|
||||
host = <k8s 暴露访问域名或IP,比如 devcontainer.devstar.cn >
|
||||
```
|
||||
|
||||
对于 `Secrets` 类型,参考:
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: devstar-studio-gitea-app-ini-secrets
|
||||
namespace: {{ .Release.Namespace }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
wechat: |
|
||||
WECHAT_OFFICIAL_ACCOUNT_TEMP_QR_EXPIRE_SECONDS=60
|
||||
WECHAT_OFFICIAL_ACCOUNT_APP_ID=<微信公众号APPID>
|
||||
WECHAT_OFFICIAL_ACCOUNT_APP_SECRET=<微信公众号SECRET>
|
||||
WECHAT_OFFICIAL_ACCOUNT_MESSAGE_TOKEN = <微信公众号自定义Token>
|
||||
WECHAT_OFFICIAL_ACCOUNT_MESSAGE_AES_KEY = <微信公众号AES加密密钥>
|
||||
```
|
||||
|
||||
为了使 Gitea启动时,能将配置信息同步到 `app.ini` 文件中,需要在 Helm Charts `/values.yaml` 中的 `additionalConfigSources` 中挂载创建的 `ConfigMaps` 与 `Secrets` 资源,示例格式:
|
||||
|
||||
```yaml
|
||||
## @param gitea.additionalConfigSources Additional configuration from secret or configmap
|
||||
additionalConfigSources:
|
||||
- secret:
|
||||
secretName: devstar-studio-gitea-app-ini-secrets
|
||||
- configMap:
|
||||
name: devstar-studio-gitea-app-ini-configmap
|
||||
```
|
||||
|
||||
#### 1.2.3 Kubernetes RBAC 配置
|
||||
|
||||
为了能够在运行时创建 Dev Container,需要配置**基于角色的访问控制**(Role-Based Access Control, RBAC)。
|
||||
|
||||
由于 DevStar Studio 运行后将自动创建 ServiceAccount `default`:
|
||||
|
||||
```bash
|
||||
kubectl get serviceaccounts -n devstar-studio-ns
|
||||
# NAME SECRETS AGE
|
||||
# default 1 44d
|
||||
# devstar-studio-postgresql 1 42d
|
||||
```
|
||||
|
||||
因此,只需要创建关联该 ServiceAccount 的 RoleBinding,在 Helm Charts 目录下 创建文件夹 `templates/devstar-devcontainer-rbac/`
|
||||
|
||||
1. 创建 Role
|
||||
```yaml
|
||||
kind: Role
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
metadata:
|
||||
name: read-devcontainer-pods
|
||||
namespace: {{ .Values.devstar.devcontainer.namespace }}
|
||||
rules:
|
||||
- apiGroups: [""] # meaning: the core API Group
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
```
|
||||
|
||||
2. 创建 RoleBiding
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: read-secrets
|
||||
namespace: {{ .Values.devstar.devcontainer.namespace }}
|
||||
roleRef:
|
||||
kind: Role
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
name: admin
|
||||
namespace:
|
||||
subjects:
|
||||
- apiGroup: rbac.authorization.k8s.io
|
||||
kind: User
|
||||
name: admin
|
||||
```
|
||||
|
||||
**注意**: RBAC 应当对 Gitea 透明,即不得将 RBAC 信息挂载到 `/values.yaml` 中如 1.2.2 小节所示的 `additionalConfigSources` 字段
|
||||
|
||||
|
||||
|
||||
--------------------
|
||||
## Gitea
|
||||
|
||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
||||
|
||||
26
go.mod
26
go.mod
@@ -51,7 +51,7 @@ require (
|
||||
github.com/go-ldap/ldap/v3 v3.4.6
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/go-swagger/go-swagger v0.31.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.11.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.8.1
|
||||
github.com/go-webauthn/webauthn v0.10.2
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
||||
@@ -121,6 +121,8 @@ require (
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.23.10
|
||||
k8s.io/apimachinery v0.23.10
|
||||
mvdan.cc/xurls/v2 v2.5.0
|
||||
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
|
||||
xorm.io/builder v0.3.13
|
||||
@@ -130,9 +132,19 @@ require (
|
||||
require (
|
||||
github.com/ArtisanCloud/PowerSocialite/v3 v3.0.7 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/gomodule/redigo v1.8.4 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
golang.org/x/term v0.21.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
k8s.io/client-go v0.23.10
|
||||
k8s.io/klog/v2 v2.30.0 // indirect
|
||||
k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect
|
||||
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -142,8 +154,6 @@ require (
|
||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
|
||||
github.com/ArtisanCloud/PowerWeChat/v3 v3.2.25
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 // indirect
|
||||
github.com/ClickHouse/ch-go v0.61.5 // indirect
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.25.0 // indirect
|
||||
github.com/DataDog/zstd v1.5.5 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
@@ -196,8 +206,6 @@ require (
|
||||
github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||
github.com/go-enry/go-oniguruma v1.2.1 // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||
github.com/go-openapi/errors v0.22.0 // indirect
|
||||
@@ -257,7 +265,6 @@ require (
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/paulmach/orb v0.11.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
@@ -272,7 +279,6 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
@@ -298,8 +304,6 @@ require (
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
go.etcd.io/bbolt v1.3.10 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.27.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
// DevstarDevcontainer devContainer 关联 代码仓库 和 用户
|
||||
//
|
||||
// TODO 移除 port 信息,需要前端主动连接时候实时获取最新分配结果,可以考虑给用户API提供选项:是否阻塞等待
|
||||
//
|
||||
// 遵循gonic规则映射数据库表 `devstar_devcontainer`,各字段注解见 https://xorm.io/docs/chapter-02/4.columns/
|
||||
type DevstarDevcontainer struct {
|
||||
Id int64 `xorm:"BIGINT pk NOT NULL autoincr 'id' comment('主键,devContainerId')"`
|
||||
|
||||
126
modules/devstar_devcontainer/k8s_agent/CreateDevcontainer.go
Normal file
126
modules/devstar_devcontainer/k8s_agent/CreateDevcontainer.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package k8s_agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
devcontainer_api_v1 "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/api/v1"
|
||||
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
|
||||
devcontainer_k8s_agent_modules_errors "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/errors"
|
||||
apimachinery_apis_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
apimachinery_apis_v1_unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
apimachinery_watch "k8s.io/apimachinery/pkg/watch"
|
||||
dynamic_client "k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
// CreateDevcontainer 创建开发容器
|
||||
func CreateDevcontainer(ctx *context.Context, client dynamic_client.Interface, opts *devcontainer_dto.CreateDevcontainerOptions) (*devcontainer_api_v1.DevcontainerApp, error) {
|
||||
|
||||
if ctx == nil || opts == nil {
|
||||
return nil, devcontainer_k8s_agent_modules_errors.ErrIllegalDevcontainerParameters{
|
||||
FieldList: []string{"ctx", "opts"},
|
||||
Message: "cannot be nil",
|
||||
}
|
||||
}
|
||||
|
||||
devcontainerApp := &devcontainer_api_v1.DevcontainerApp{
|
||||
TypeMeta: apimachinery_apis_v1.TypeMeta{
|
||||
Kind: "DevcontainerApp",
|
||||
APIVersion: "devcontainer.devstar.cn/v1",
|
||||
},
|
||||
ObjectMeta: apimachinery_apis_v1.ObjectMeta{
|
||||
Name: opts.Name,
|
||||
Namespace: opts.Namespace,
|
||||
},
|
||||
Spec: devcontainer_api_v1.DevcontainerAppSpec{
|
||||
StatefulSet: devcontainer_api_v1.StatefulSetSpec{
|
||||
Image: opts.Image,
|
||||
Command: opts.CommandList,
|
||||
ContainerPort: opts.ContainerPort,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(devcontainerApp)
|
||||
if err != nil {
|
||||
return nil, devcontainer_k8s_agent_modules_errors.ErrOperateDevcontainer{
|
||||
Action: "Marshal JSON",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
unstructuredObj := &apimachinery_apis_v1_unstructured.Unstructured{}
|
||||
_, _, err = apimachinery_apis_v1_unstructured.UnstructuredJSONScheme.Decode(jsonData, &groupVersionKind, unstructuredObj)
|
||||
if err != nil {
|
||||
return nil, devcontainer_k8s_agent_modules_errors.ErrOperateDevcontainer{
|
||||
Action: "build unstructured obj",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 DevContainer Status.nodePortAssigned 信息
|
||||
_, err = client.Resource(groupVersionResource).Namespace(opts.Namespace).Create(*ctx, unstructuredObj, opts.CreateOptions)
|
||||
if err != nil {
|
||||
return nil, devcontainer_k8s_agent_modules_errors.ErrOperateDevcontainer{
|
||||
Action: "create DevContainer via Dynamic Client",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 watcher 监听 DevContainer Status.nodePortAssigned 信息
|
||||
watcherTimeoutSeconds := int64(3)
|
||||
watcher, err := client.Resource(groupVersionResource).Namespace(opts.Namespace).Watch(*ctx, apimachinery_apis_v1.ListOptions{
|
||||
FieldSelector: fmt.Sprintf("metadata.name=%s", opts.Name),
|
||||
//Watch: false,
|
||||
TimeoutSeconds: &watcherTimeoutSeconds,
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, devcontainer_k8s_agent_modules_errors.ErrOperateDevcontainer{
|
||||
Action: "register watcher of DevContainer NodePort",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
defer watcher.Stop()
|
||||
|
||||
//log.Info(" ===== 开始监听 DevContainer NodePort 分配信息 =====")
|
||||
var nodePortAssigned int64
|
||||
for event := range watcher.ResultChan() {
|
||||
switch event.Type {
|
||||
case apimachinery_watch.Modified:
|
||||
if devcontainerUnstructured, ok := event.Object.(*apimachinery_apis_v1_unstructured.Unstructured); ok {
|
||||
|
||||
statusDevcontainer, ok, err := apimachinery_apis_v1_unstructured.NestedMap(devcontainerUnstructured.Object, "status")
|
||||
if err == nil && ok {
|
||||
nodePortAssigned = statusDevcontainer["nodePortAssigned"].(int64)
|
||||
if 30000 <= nodePortAssigned && nodePortAssigned <= 32767 {
|
||||
devcontainerApp.Status.NodePortAssigned = uint16(nodePortAssigned)
|
||||
//log.Info("DevContainer NodePort Status 更新完成,最新 NodePort = %v", nodePortAssigned)
|
||||
//break
|
||||
// 收到 NodePort Service MODIFIED 消息后,更新 NodePort,直接返回,不再处理后续 Event (否则必须超时3秒才得到 NodePort)
|
||||
return devcontainerApp, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//log.Info(" ===== 结束监听 DevContainer NodePort 分配信息 =====")
|
||||
|
||||
/*
|
||||
// 目前需要更新的字段只有 devcontainerInCluster.Status.NodePortAssigned
|
||||
// 如果后续有其他较多的需要更新的字段,可以考虑重新查询,目前,暂时只考虑 直接更新内存缓存对象,然后返回即可
|
||||
// 避免对 k8s API Server 造成访问压力
|
||||
//response, err := client.Resource(groupVersionResource).Namespace(opts.Namespace).Get(*ctx, opts.Name, apimachinery_apis_v1.GetOptions{})
|
||||
//jsonData, err = response.MarshalJSON()
|
||||
//devcontainerInCluster := &devcontainer_api_v1.DevcontainerApp{}
|
||||
//err = json.Unmarshal(jsonData, devcontainerInCluster)
|
||||
//if err != nil {
|
||||
// return nil, devcontainer_errors.ErrOperateDevcontainer{
|
||||
// Action: "parse response result of in-cluster DevContainer",
|
||||
// Message: err.Error(),
|
||||
// }
|
||||
//}
|
||||
*/
|
||||
return devcontainerApp, nil
|
||||
}
|
||||
29
modules/devstar_devcontainer/k8s_agent/DeleteDevcontainer.go
Normal file
29
modules/devstar_devcontainer/k8s_agent/DeleteDevcontainer.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package k8s_agent
|
||||
|
||||
import (
|
||||
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
|
||||
devcontainer_errors "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/errors"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"context"
|
||||
"fmt"
|
||||
dynamic_client "k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
func DeleteDevcontainer(ctx *context.Context, client dynamic_client.Interface, opts *devcontainer_dto.DeleteDevcontainerOptions) error {
|
||||
if ctx == nil || opts == nil || len(opts.Namespace) == 0 || len(opts.Name) == 0 {
|
||||
return devcontainer_errors.ErrIllegalDevcontainerParameters{
|
||||
FieldList: []string{"ctx", "opts", "opts.Name", "opts.Namespace"},
|
||||
Message: "cannot be nil",
|
||||
}
|
||||
}
|
||||
|
||||
err := client.Resource(groupVersionResource).Namespace(opts.Namespace).Delete(*ctx, opts.Name, opts.DeleteOptions)
|
||||
if err != nil {
|
||||
log.Warn("Failed to delete DevcontainerApp '%s' in namespace '%s': %s", opts.Name, opts.Namespace, err.Error())
|
||||
return devcontainer_errors.ErrOperateDevcontainer{
|
||||
Action: fmt.Sprintf("delete devcontainer '%s' in namespace '%s'", opts.Name, opts.Namespace),
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
157
modules/devstar_devcontainer/k8s_agent/GetDevcontainer.go
Normal file
157
modules/devstar_devcontainer/k8s_agent/GetDevcontainer.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package k8s_agent
|
||||
|
||||
import (
|
||||
devcontainer_api_v1 "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/api/v1"
|
||||
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
|
||||
devcontainer_errors "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/errors"
|
||||
devcontainer_module_utils "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/utils"
|
||||
devcontainer_agent_module_vo "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/vo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"context"
|
||||
"fmt"
|
||||
apimachinery_api_metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
apimachinery_apis_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
apimachinery_apis_v1_unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
apimachinery_runtime_utils "k8s.io/apimachinery/pkg/runtime"
|
||||
apimachinery_watch "k8s.io/apimachinery/pkg/watch"
|
||||
dynamic_client "k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
func GetDevcontainer(ctx *context.Context, client dynamic_client.Interface, opts *devcontainer_dto.GetDevcontainerOptions) (*devcontainer_api_v1.DevcontainerApp, error) {
|
||||
|
||||
// 0. 检查参数
|
||||
if ctx == nil || opts == nil || len(opts.Namespace) == 0 || len(opts.Name) == 0 {
|
||||
return nil, devcontainer_errors.ErrIllegalDevcontainerParameters{
|
||||
FieldList: []string{"ctx", "opts", "opts.Name", "opts.Namespace"},
|
||||
Message: "cannot be nil",
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 获取 k8s CRD 资源 DevcontainerApp
|
||||
devcontainerUnstructured, err := client.Resource(groupVersionResource).Namespace(opts.Namespace).Get(*ctx, opts.Name, opts.GetOptions)
|
||||
if err != nil {
|
||||
return nil, devcontainer_errors.ErrOperateDevcontainer{
|
||||
Action: "Get DevcontainerApp thru k8s API Server",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 解析 DevcontainerApp Status 域,装填 VO
|
||||
devcontainerApp := &devcontainer_api_v1.DevcontainerApp{}
|
||||
err = apimachinery_runtime_utils.DefaultUnstructuredConverter.FromUnstructured(devcontainerUnstructured.Object, &devcontainerApp)
|
||||
if err != nil {
|
||||
return nil, devcontainer_errors.ErrOperateDevcontainer{
|
||||
Action: "Convert k8s API Server unstructured response into DevcontainerApp",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查 Devcontainer 是否就绪
|
||||
if !devcontainer_module_utils.IsK8sDevcontainerStatusReady(&devcontainerApp.Status) {
|
||||
// 3.1 检查 Wait 参数,若用户不需要阻塞式等待,直接返回 “DevContainer 未就绪” 错误
|
||||
if opts.Wait == false {
|
||||
return nil, devcontainer_errors.ErrK8sDevcontainerNotReady{
|
||||
Name: opts.Name,
|
||||
Namespace: opts.Namespace,
|
||||
Wait: opts.Wait,
|
||||
}
|
||||
}
|
||||
|
||||
// 3.2 执行阻塞式等待
|
||||
devcontainerStatusVO, err := waitUntilDevcontainerReadyWithTimeout(ctx, client, opts)
|
||||
if err != nil {
|
||||
return nil, devcontainer_errors.ErrOperateDevcontainer{
|
||||
Action: "wait for k8s DevContainer to be ready",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
devcontainerApp.Status.Ready = devcontainerStatusVO.Ready
|
||||
devcontainerApp.Status.NodePortAssigned = devcontainerStatusVO.NodePortAssigned
|
||||
}
|
||||
|
||||
// 4. 将就绪的 DevContainer Status VO 返回
|
||||
return devcontainerApp, nil
|
||||
}
|
||||
|
||||
// waitUntilDevcontainerReadyWithTimeout 辅助方法:在超时时间内阻塞等待 DevContainer 就绪
|
||||
func waitUntilDevcontainerReadyWithTimeout(ctx *context.Context, client dynamic_client.Interface, opts *devcontainer_dto.GetDevcontainerOptions) (*devcontainer_agent_module_vo.DevcontainerStatusK8sAgentVO, error) {
|
||||
|
||||
// 0. 检查参数
|
||||
if ctx == nil || client == nil || opts == nil || len(opts.Name) == 0 || len(opts.Namespace) == 0 {
|
||||
return nil, devcontainer_errors.ErrIllegalDevcontainerParameters{
|
||||
FieldList: []string{"ctx", "client", "opts", "opts.Name", "opts.Namespace"},
|
||||
Message: "could not be nil",
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 注册 watcher 监听 DevContainer Status 变化
|
||||
watcherTimeoutSeconds := setting.Devstar.Devcontainer.TimeoutSeconds
|
||||
watcher, err := client.Resource(groupVersionResource).Namespace(opts.Namespace).Watch(*ctx, apimachinery_apis_v1.ListOptions{
|
||||
FieldSelector: fmt.Sprintf("metadata.name=%s", opts.Name),
|
||||
Watch: true,
|
||||
TimeoutSeconds: &watcherTimeoutSeconds,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, devcontainer_errors.ErrOperateDevcontainer{
|
||||
Action: "register watcher of DevContainer Readiness",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
defer watcher.Stop()
|
||||
|
||||
// 2. 当 DevContainer Watcher 事件处理
|
||||
devcontainerStatusVO := &devcontainer_agent_module_vo.DevcontainerStatusK8sAgentVO{}
|
||||
for event := range watcher.ResultChan() {
|
||||
switch event.Type {
|
||||
case apimachinery_watch.Added:
|
||||
// 2.1 监听 DevcontainerApp ADDED 事件,直接 fallthrough 到 MODIFIED 事件合并处理
|
||||
fallthrough
|
||||
case apimachinery_watch.Modified:
|
||||
// 2.2 监听 DevcontainerApp MODIFIED 事件
|
||||
if devcontainerUnstructured, ok := event.Object.(*apimachinery_apis_v1_unstructured.Unstructured); ok {
|
||||
// 2.2.1 解析 status 域
|
||||
statusDevcontainer, ok, err := apimachinery_apis_v1_unstructured.NestedMap(devcontainerUnstructured.Object, "status")
|
||||
if err == nil && ok {
|
||||
devcontainerCurrentStatus := &devcontainer_api_v1.DevcontainerAppStatus{
|
||||
Ready: statusDevcontainer["ready"].(bool),
|
||||
NodePortAssigned: uint16(statusDevcontainer["nodePortAssigned"].(int64)),
|
||||
}
|
||||
// 2.2.2 当 Status 达到就绪状态后,返回
|
||||
if devcontainer_module_utils.IsK8sDevcontainerStatusReady(devcontainerCurrentStatus) {
|
||||
return devcontainerStatusVO, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
case apimachinery_watch.Error:
|
||||
// 2.3 监听 DevcontainerApp ERROR 事件,返回报错信息
|
||||
apimachineryApiMetav1Status, ok := event.Object.(*apimachinery_api_metav1.Status)
|
||||
if !ok {
|
||||
return nil, devcontainer_errors.ErrOperateDevcontainer{
|
||||
Action: fmt.Sprintf("wait for Devcontainer '%s' in namespace '%s' to be ready", opts.Name, opts.Namespace),
|
||||
Message: fmt.Sprintf("An error occurred in k8s CRD DevcontainerApp Watcher: \n"+
|
||||
" Code: %v (status = %v)\n"+
|
||||
"Message: %v\n"+
|
||||
" Reason: %v\n"+
|
||||
"Details: %v",
|
||||
apimachineryApiMetav1Status.Code, apimachineryApiMetav1Status.Status,
|
||||
apimachineryApiMetav1Status.Message,
|
||||
apimachineryApiMetav1Status.Reason,
|
||||
apimachineryApiMetav1Status.Details),
|
||||
}
|
||||
}
|
||||
case apimachinery_watch.Deleted:
|
||||
// 2.4 监听 DevcontainerApp DELETED 事件,返回报错信息
|
||||
return nil, devcontainer_errors.ErrOperateDevcontainer{
|
||||
Action: fmt.Sprintf("Open DevContainer '%s' in namespace '%s'", opts.Name, opts.Namespace),
|
||||
Message: fmt.Sprintf("'%s' of Kind DevcontainerApp has been Deleted", opts.Name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. k8s CRD DevcontainerApp Watcher 超时关闭处理:直接返回超时错误
|
||||
return nil, devcontainer_errors.ErrOpenDevcontainerTimeout{
|
||||
Name: opts.Name,
|
||||
Namespace: opts.Namespace,
|
||||
TimeoutSeconds: setting.Devstar.Devcontainer.TimeoutSeconds,
|
||||
}
|
||||
}
|
||||
48
modules/devstar_devcontainer/k8s_agent/ListDevcontainers.go
Normal file
48
modules/devstar_devcontainer/k8s_agent/ListDevcontainers.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package k8s_agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
dynamic_client "k8s.io/client-go/dynamic"
|
||||
|
||||
devcontainer_api_v1 "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/api/v1"
|
||||
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
|
||||
devcontainer_errors "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/errors"
|
||||
)
|
||||
|
||||
// ListDevcontainers 根据条件列举 DevContainer
|
||||
func ListDevcontainers(ctx *context.Context, client dynamic_client.Interface, opts *devcontainer_dto.ListDevcontainersOptions) (*devcontainer_api_v1.DevcontainerAppList, error) {
|
||||
|
||||
if ctx == nil || opts == nil || len(opts.Namespace) == 0 {
|
||||
return nil, devcontainer_errors.ErrIllegalDevcontainerParameters{
|
||||
FieldList: []string{"ctx", "namespace"},
|
||||
Message: "cannot be empty",
|
||||
}
|
||||
}
|
||||
|
||||
list, err := client.Resource(groupVersionResource).Namespace(opts.Namespace).List(*ctx, opts.ListOptions)
|
||||
if err != nil {
|
||||
return nil, devcontainer_errors.ErrOperateDevcontainer{
|
||||
Action: fmt.Sprintf("List Devcontainer in namespace '%s'", opts.Namespace),
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
// JSON 反序列化为 DevcontainerAppList
|
||||
jsonData, err := list.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, devcontainer_errors.ErrOperateDevcontainer{
|
||||
Action: "verify JSON data of Devcontainer List",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
devcontainerList := &devcontainer_api_v1.DevcontainerAppList{}
|
||||
if err := json.Unmarshal(jsonData, devcontainerList); err != nil {
|
||||
return nil, devcontainer_errors.ErrOperateDevcontainer{
|
||||
Action: "deserialize Devcontainer List data",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
return devcontainerList, nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright 2024.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
|
||||
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
|
||||
|
||||
// DevcontainerAppSpec defines the desired state of DevcontainerApp
|
||||
type DevcontainerAppSpec struct {
|
||||
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
|
||||
StatefulSet StatefulSetSpec `json:"statefulset"`
|
||||
// +optional
|
||||
Service ServiceSpec `json:"service"`
|
||||
|
||||
// +kubebuilder:validation:Minimum=0
|
||||
|
||||
// Optional deadline in seconds for starting the job if it misses scheduled
|
||||
// time for any reason. Missed jobs executions will be counted as failed ones.
|
||||
// +optional
|
||||
StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"`
|
||||
|
||||
// This flag tells the controller to suspend subsequent executions, it does
|
||||
// not apply to already started executions. Defaults to false.
|
||||
// +optional
|
||||
Suspend *bool `json:"suspend,omitempty"`
|
||||
|
||||
// +kubebuilder:validation:Minimum=0
|
||||
|
||||
// The number of successful finished jobs to retain.
|
||||
// This is a pointer to distinguish between explicit zero and not specified.
|
||||
// +optional
|
||||
SuccessfulJobsHistoryLimit *int32 `json:"successfulJobsHistoryLimit,omitempty"`
|
||||
|
||||
// +kubebuilder:validation:Minimum=0
|
||||
|
||||
// The number of failed finished jobs to retain.
|
||||
// This is a pointer to distinguish between explicit zero and not specified.
|
||||
// +optional
|
||||
FailedJobsHistoryLimit *int32 `json:"failedJobsHistoryLimit,omitempty"`
|
||||
}
|
||||
|
||||
// StatefulSetSpec specifies StatefulSet for DevContainer
|
||||
type StatefulSetSpec struct {
|
||||
Image string `json:"image"`
|
||||
Command []string `json:"command"`
|
||||
|
||||
// +kubebuilder:validation:Minimum=1
|
||||
// +optional
|
||||
ContainerPort uint16 `json:"containerPort,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceSpec specifies Service for DevContainer
|
||||
type ServiceSpec struct {
|
||||
// +kubebuilder:validation:Minimum=30000
|
||||
// +kubebuilder:validation:Maximum=32767
|
||||
// +optional
|
||||
NodePort uint16 `json:"nodePort,omitempty"`
|
||||
|
||||
// +kubebuilder:validation:Minimum=1
|
||||
// +optional
|
||||
ServicePort uint16 `json:"servicePort,omitempty"`
|
||||
}
|
||||
|
||||
// DevcontainerAppStatus defines the observed state of DevcontainerApp
|
||||
type DevcontainerAppStatus struct {
|
||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
|
||||
// A list of pointers to currently running jobs.
|
||||
// +optional
|
||||
Active []corev1.ObjectReference `json:"active,omitempty"`
|
||||
|
||||
// Information when was the last time the job was successfully scheduled.
|
||||
// +optional
|
||||
LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"`
|
||||
|
||||
// NodePortAssigned 存储 DevcontainerApp CRD调度后集群分配的 NodePort
|
||||
// +optional
|
||||
NodePortAssigned uint16 `json:"nodePortAssigned"`
|
||||
|
||||
// Ready 标识 DevcontainerApp 管理的 Pod 的 Readiness Probe 是否达到就绪状态
|
||||
// +optional
|
||||
Ready bool `json:"ready"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
|
||||
// DevcontainerApp is the Schema for the devcontainerapps API
|
||||
type DevcontainerApp struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec DevcontainerAppSpec `json:"spec,omitempty"`
|
||||
Status DevcontainerAppStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// DevcontainerAppList contains a list of DevcontainerApp
|
||||
type DevcontainerAppList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []DevcontainerApp `json:"items"`
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2024.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package v1 contains API Schema definitions for the devcontainer v1 API group
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=devcontainer.devstar.cn
|
||||
package v1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
// GroupVersion is group version used to register these objects
|
||||
GroupVersion = schema.GroupVersion{Group: "devcontainer.devstar.cn", Version: "v1"}
|
||||
)
|
||||
@@ -0,0 +1,181 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2024.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DevcontainerApp) DeepCopyInto(out *DevcontainerApp) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevcontainerApp.
|
||||
func (in *DevcontainerApp) DeepCopy() *DevcontainerApp {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DevcontainerApp)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *DevcontainerApp) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DevcontainerAppList) DeepCopyInto(out *DevcontainerAppList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]DevcontainerApp, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevcontainerAppList.
|
||||
func (in *DevcontainerAppList) DeepCopy() *DevcontainerAppList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DevcontainerAppList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *DevcontainerAppList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DevcontainerAppSpec) DeepCopyInto(out *DevcontainerAppSpec) {
|
||||
*out = *in
|
||||
in.StatefulSet.DeepCopyInto(&out.StatefulSet)
|
||||
out.Service = in.Service
|
||||
if in.StartingDeadlineSeconds != nil {
|
||||
in, out := &in.StartingDeadlineSeconds, &out.StartingDeadlineSeconds
|
||||
*out = new(int64)
|
||||
**out = **in
|
||||
}
|
||||
if in.Suspend != nil {
|
||||
in, out := &in.Suspend, &out.Suspend
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.SuccessfulJobsHistoryLimit != nil {
|
||||
in, out := &in.SuccessfulJobsHistoryLimit, &out.SuccessfulJobsHistoryLimit
|
||||
*out = new(int32)
|
||||
**out = **in
|
||||
}
|
||||
if in.FailedJobsHistoryLimit != nil {
|
||||
in, out := &in.FailedJobsHistoryLimit, &out.FailedJobsHistoryLimit
|
||||
*out = new(int32)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevcontainerAppSpec.
|
||||
func (in *DevcontainerAppSpec) DeepCopy() *DevcontainerAppSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DevcontainerAppSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DevcontainerAppStatus) DeepCopyInto(out *DevcontainerAppStatus) {
|
||||
*out = *in
|
||||
if in.Active != nil {
|
||||
in, out := &in.Active, &out.Active
|
||||
*out = make([]corev1.ObjectReference, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.LastScheduleTime != nil {
|
||||
in, out := &in.LastScheduleTime, &out.LastScheduleTime
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DevcontainerAppStatus.
|
||||
func (in *DevcontainerAppStatus) DeepCopy() *DevcontainerAppStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DevcontainerAppStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec.
|
||||
func (in *ServiceSpec) DeepCopy() *ServiceSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ServiceSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *StatefulSetSpec) DeepCopyInto(out *StatefulSetSpec) {
|
||||
*out = *in
|
||||
if in.Command != nil {
|
||||
in, out := &in.Command, &out.Command
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatefulSetSpec.
|
||||
func (in *StatefulSetSpec) DeepCopy() *StatefulSetSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(StatefulSetSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// CreateDevcontainerOptions 定义创建开发容器选项
|
||||
type CreateDevcontainerOptions struct {
|
||||
metav1.CreateOptions
|
||||
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Image string `json:"image"`
|
||||
CommandList []string `json:"command"`
|
||||
ContainerPort uint16 `json:"containerPort"`
|
||||
ServicePort uint16 `json:"servicePort"`
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type DeleteDevcontainerOptions struct {
|
||||
metav1.DeleteOptions
|
||||
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dto
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type GetDevcontainerOptions struct {
|
||||
metav1.GetOptions
|
||||
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Wait bool `json:"wait"`
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type ListDevcontainersOptions struct {
|
||||
metav1.ListOptions
|
||||
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ErrIllegalDevcontainerParameters struct {
|
||||
FieldList []string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (err ErrIllegalDevcontainerParameters) Error() string {
|
||||
return fmt.Sprintf("Illegal DevContainer parameters detected: %v (%s)", err.FieldList, err.Message)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ErrK8sDevcontainerNotReady struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Wait bool
|
||||
}
|
||||
|
||||
func (err ErrK8sDevcontainerNotReady) Error() string {
|
||||
return fmt.Sprintf("Failed to open k8s Devcontainer '%s' in namespace '%s': DevContainer Not Ready (Wait = %v)",
|
||||
err.Name, err.Namespace, err.Wait)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrOpenDevcontainerTimeout 阻塞式等待 DevContainer 超时
|
||||
type ErrOpenDevcontainerTimeout struct {
|
||||
Name string
|
||||
Namespace string
|
||||
TimeoutSeconds int64
|
||||
}
|
||||
|
||||
func (err ErrOpenDevcontainerTimeout) Error() string {
|
||||
return fmt.Sprintf("Failed to open DevContainer '%s' in namespace '%s': waiting timeout limit of %d seconds has been exceeded.",
|
||||
err.Name, err.Namespace, err.TimeoutSeconds,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ErrOperateDevcontainer struct {
|
||||
Action string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (err ErrOperateDevcontainer) Error() string {
|
||||
return fmt.Sprintf("Failed to %v in DevStar DevContainer DB: %v", err.Action, err.Message)
|
||||
}
|
||||
19
modules/devstar_devcontainer/k8s_agent/gvr.go
Normal file
19
modules/devstar_devcontainer/k8s_agent/gvr.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package k8s_agent
|
||||
|
||||
import (
|
||||
devcontainer_api_v1 "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/api/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// groupVersionResource 用于描述 CRD,供 dynamic Client 交互使用
|
||||
var groupVersionResource = schema.GroupVersionResource{
|
||||
Group: devcontainer_api_v1.GroupVersion.Group,
|
||||
Version: devcontainer_api_v1.GroupVersion.Version,
|
||||
Resource: "devcontainerapps",
|
||||
}
|
||||
|
||||
var groupVersionKind = schema.GroupVersionKind{
|
||||
Group: devcontainer_api_v1.GroupVersion.Group,
|
||||
Version: devcontainer_api_v1.GroupVersion.Version,
|
||||
Kind: "DevcontainerApp",
|
||||
}
|
||||
34
modules/devstar_devcontainer/k8s_agent/init.go
Normal file
34
modules/devstar_devcontainer/k8s_agent/init.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package k8s_agent
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"context"
|
||||
|
||||
dynamicclient "k8s.io/client-go/dynamic"
|
||||
clientgorest "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientgocmdtools "k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// GetKubernetesClient
|
||||
func GetKubernetesClient(ctx *context.Context) (dynamicclient.Interface, error) {
|
||||
// 1.0 尝试从集群外获取 kubectl 配置信息
|
||||
config, err := clientcmd.BuildConfigFromFlags("", clientgocmdtools.RecommendedHomeFile)
|
||||
if err != nil {
|
||||
// 1.1 集群外尝试失败,改从集群内获取 kubectl 配置信息
|
||||
log.Warn("Failed to obtain Kubernetes config outside of cluster: " + clientgocmdtools.RecommendedHomeFile)
|
||||
config, err = clientgorest.InClusterConfig()
|
||||
if err != nil {
|
||||
log.Error("Failed to obtain Kubernetes config both inside/outside of cluster, the DevContainer is Disabled")
|
||||
setting.Devstar.Devcontainer.Enabled = false
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// 2. 根据 k8s 配置信息构建 ClientSet
|
||||
dynamicClient, err := dynamicclient.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dynamicClient, err
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
devcontainer_api_v1 "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/api/v1"
|
||||
)
|
||||
|
||||
// IsK8sDevcontainerStatusReady 工具类方法,判断给定的 DevcontainerApp.Status 是否达到就绪状态
|
||||
// 1. DevcontainerApp.Status.Ready == true
|
||||
// 2. DevcontainerApp.Status.NodePortAssigned 介于闭区间 [30000, 32767]
|
||||
func IsK8sDevcontainerStatusReady(devcontainerAppStatus *devcontainer_api_v1.DevcontainerAppStatus) bool {
|
||||
return devcontainerAppStatus != nil &&
|
||||
devcontainerAppStatus.Ready &&
|
||||
devcontainerAppStatus.NodePortAssigned >= 30000 &&
|
||||
devcontainerAppStatus.NodePortAssigned <= 32767
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package vo
|
||||
|
||||
type DevcontainerStatusK8sAgentVO struct {
|
||||
// CRD Controller 向 DevcontainerApp.Status.NodePortAssigned 写入了最新的 NodePort 端口值,当且仅当 Service 被调度且分配了最新的 NodePort
|
||||
NodePortAssigned uint16 `json:"nodePortAssigned"`
|
||||
|
||||
// CRD Controller 向 DevcontainerApp.Status.Ready 写入了 true,当且仅当 StatefulSet 控制下的 Pod 中的 Readiness Probe 返回 true
|
||||
Ready bool `json:"ready"`
|
||||
}
|
||||
58
modules/setting/devstar_devcontainer.go
Normal file
58
modules/setting/devstar_devcontainer.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
const (
|
||||
DEVCONTAINER_AGENT_NAME_K8S string = "k8s"
|
||||
DEVCONTAINER_AGENT_NAME_DOCKER string = "docker"
|
||||
)
|
||||
|
||||
// package 内部私有变量,是一个 Set 结构,标识目前系统所有支持的 DevContainer Agent 类型
|
||||
var validDevcontainerAgentSet = map[string]struct{}{
|
||||
DEVCONTAINER_AGENT_NAME_K8S: {},
|
||||
DEVCONTAINER_AGENT_NAME_DOCKER: {},
|
||||
}
|
||||
|
||||
var Devstar = struct {
|
||||
Devcontainer DevcontainerType `ini:"devstar.devcontainer"`
|
||||
}{
|
||||
Devcontainer: DevcontainerType{
|
||||
Enabled: false,
|
||||
Namespace: "default",
|
||||
TimeoutSeconds: 900, // 最长等待 DevContainer 就绪时间(阻塞式),默认15分钟,可被 app.ini 指定值覆盖
|
||||
},
|
||||
}
|
||||
|
||||
type DevcontainerType struct {
|
||||
Enabled bool `ini:"enabled"`
|
||||
Host string `ini:"host"`
|
||||
Agent string `ini:"agent"`
|
||||
Namespace string `ini:"namespace"`
|
||||
TimeoutSeconds int64 `ini:"timeout_seconds"`
|
||||
}
|
||||
|
||||
// loadDevstarDevcontainerFrom 从 ini 配置文件中读取 DevStar DevContainer 配置信息,并进行检查,若数据无效,则自动禁用 DevContainer
|
||||
func loadDevstarDevcontainerFrom(rootCfg ConfigProvider) {
|
||||
|
||||
mustMapSetting(rootCfg, "devstar", &Devstar)
|
||||
|
||||
// 检查 Host 是否为空,若为空,则自动将 DevContainer 设置为禁用
|
||||
if len(Devstar.Devcontainer.Host) == 0 {
|
||||
log.Warn("INVALID config 'host' for DevStar DevContainer")
|
||||
Devstar.Devcontainer.Enabled = false
|
||||
}
|
||||
|
||||
// 检查用户输入的 DevContainer Agent 是否存在支持列表,若不支持,则将 DevContainer 设置为禁用
|
||||
if _, exists := validDevcontainerAgentSet[Devstar.Devcontainer.Agent]; !exists {
|
||||
log.Warn("INVALID config 'agent' for DevStar DevContainer")
|
||||
Devstar.Devcontainer.Enabled = false
|
||||
}
|
||||
|
||||
if Devstar.Devcontainer.Enabled == false {
|
||||
log.Warn("DevStar DevContainer Service Disabled")
|
||||
} else {
|
||||
log.Info("DevStar DevContainer Service Enabled")
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,9 @@ const (
|
||||
LandingPageExplore LandingPage = "/explore"
|
||||
LandingPageOrganizations LandingPage = "/explore/organizations"
|
||||
LandingPageLogin LandingPage = "/user/login/wechat/official-account"
|
||||
|
||||
// LandingPageDevstarDevcontainerRepoLink 是特殊仓库,表示 DevcontainerApp k8s CRD 脚手架工程: devstar.cn/DevcontainerApp
|
||||
LandingPageDevstarDevcontainerRepoLink LandingPage = "/devstar/devstar-devcontainer-kubebuilder-scaffold"
|
||||
)
|
||||
|
||||
// Server settings
|
||||
|
||||
@@ -217,6 +217,7 @@ func LoadSettings() {
|
||||
loadMimeTypeMapFrom(CfgProvider)
|
||||
loadFederationFrom(CfgProvider)
|
||||
loadWechatSettingsFrom(CfgProvider)
|
||||
loadDevstarDevcontainerFrom(CfgProvider)
|
||||
}
|
||||
|
||||
// LoadSettingsForInstall initializes the settings for install
|
||||
|
||||
@@ -26,6 +26,9 @@ const (
|
||||
ErrUsername = "UsernameError"
|
||||
// ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
|
||||
ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
|
||||
|
||||
// ErrUserIdOrRepoId 如果 int64 十进制正数规则校验失败(User ID 或者 Repo ID),返回 ErrUserIdOrRepoId
|
||||
ErrUserIdOrRepoId = "UserIdOrRepoIdError"
|
||||
)
|
||||
|
||||
// AddBindingRules adds additional binding rules
|
||||
@@ -38,6 +41,9 @@ func AddBindingRules() {
|
||||
addGlobOrRegexPatternRule()
|
||||
addUsernamePatternRule()
|
||||
addValidGroupTeamMapRule()
|
||||
|
||||
// 下面是 DevStar Studio 定制化规则:校验是否是有效的 int64 十进制正数字符串(UserID 或者 RepoID)
|
||||
addUserIdRepoIdPatternRule()
|
||||
}
|
||||
|
||||
func addGitRefNameBindingRule() {
|
||||
@@ -168,6 +174,23 @@ func addUsernamePatternRule() {
|
||||
})
|
||||
}
|
||||
|
||||
// 新增 用户ID 或者 Repo ID (十进制int64正数部分字符串校验规则)
|
||||
func addUserIdRepoIdPatternRule() {
|
||||
binding.AddRule(&binding.Rule{
|
||||
IsMatch: func(rule string) bool {
|
||||
return rule == "PositiveBase10IntegerNumberRule"
|
||||
},
|
||||
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
|
||||
str := fmt.Sprintf("%v", val)
|
||||
if !IsValidUserIdOrRepoId(str) {
|
||||
errs.Add([]string{name}, ErrUserIdOrRepoId, "invalid User ID or Repo ID")
|
||||
return false, errs
|
||||
}
|
||||
return true, errs
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func addValidGroupTeamMapRule() {
|
||||
binding.AddRule(&binding.Rule{
|
||||
IsMatch: func(rule string) bool {
|
||||
|
||||
@@ -119,6 +119,9 @@ func IsValidExternalTrackerURLFormat(uri string) bool {
|
||||
var (
|
||||
validUsernamePattern = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`)
|
||||
invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) // No consecutive or trailing non-alphanumeric chars
|
||||
|
||||
// DevStar Studio 定制规则:判断是否是有效的 int64 ID 十进制正数字符串(UserID 或者 Repo ID)
|
||||
validBase10PositiveNumberPattern = regexp.MustCompile(`^\d+$`)
|
||||
)
|
||||
|
||||
// IsValidUsername checks if username is valid
|
||||
@@ -127,3 +130,8 @@ func IsValidUsername(name string) bool {
|
||||
// but it's easier to use positive and negative checks.
|
||||
return validUsernamePattern.MatchString(name) && !invalidUsernamePattern.MatchString(name)
|
||||
}
|
||||
|
||||
// IsValidUserIdOrRepoId 判断是否是有效的 用户ID 或者 Repo ID (十进制int64正数部分字符串校验规则)
|
||||
func IsValidUserIdOrRepoId(idStr string) bool {
|
||||
return validBase10PositiveNumberPattern.MatchString(idStr)
|
||||
}
|
||||
|
||||
@@ -138,6 +138,9 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
|
||||
data["ErrorMsg"] = trName + l.TrString("form.username_error")
|
||||
case validation.ErrInvalidGroupTeamMap:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
|
||||
case validation.ErrUserIdOrRepoId:
|
||||
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
|
||||
|
||||
default:
|
||||
msg := errs[0].Classification
|
||||
if msg != "" && errs[0].Message != "" {
|
||||
|
||||
@@ -630,6 +630,8 @@ target_branch_not_exist = Target branch does not exist.
|
||||
|
||||
admin_cannot_delete_self = You cannot delete yourself when you are an admin. Please remove your admin privileges first.
|
||||
|
||||
invalid_user_id_or_repo_id = The User ID or Repository ID you provided is invalid. Please check again.
|
||||
|
||||
[user]
|
||||
change_avatar = Change your avatar…
|
||||
joined_on = Joined on %s
|
||||
@@ -1014,7 +1016,7 @@ visibility.private_tooltip = Visible only to members of organizations you have j
|
||||
[repo]
|
||||
dev_container = Dev Container
|
||||
dev_container_empty = Oops, it looks like there is no Dev Container in this repository.
|
||||
dev_container_lack_of_config_prompt = Unable to create Dev Container: Please upload 'devcontainer.json' file to the default branch of the Repository.
|
||||
dev_container_invalid_config_prompt = Invalid Dev Container Configuration: Please upload a valid 'devcontainer.json' file to the default branch, and ensure that this repository is NOT archived.
|
||||
dev_container_control.create = Create Dev Container
|
||||
dev_container_control.creation_success_for_user = The Dev Container has been created successfully for user '%s'.
|
||||
dev_container_control.creation_failed_for_user = Failed to create the Dev Container for user '%s'.
|
||||
@@ -1148,8 +1150,8 @@ template.issue_labels = Issue Labels
|
||||
template.one_item = Must select at least one template item
|
||||
template.invalid = Must select a template repository
|
||||
|
||||
archive.title = This repo is archived. You can view files and clone it, but cannot push or open issues or pull requests.
|
||||
archive.title_date = This repository has been archived on %s. You can view files and clone it, but cannot push or open issues or pull requests.
|
||||
archive.title = This repo is archived. You can view files and clone it, but cannot push or create Dev Container or open issues or open pull requests.
|
||||
archive.title_date = This repository has been archived on %s. You can view files and clone it, but cannot push or create Dev Container or open issues or open pull requests.
|
||||
archive.issue.nocomment = This repo is archived. You cannot comment on issues.
|
||||
archive.pull.nocomment = This repo is archived. You cannot comment on pull requests.
|
||||
|
||||
|
||||
@@ -629,6 +629,8 @@ target_branch_not_exist=目标分支不存在。
|
||||
|
||||
admin_cannot_delete_self=当您是管理员时,您不能删除自己。请先移除您的管理员权限
|
||||
|
||||
invalid_user_id_or_repo_id = 用户ID 或 仓库ID 输入有误,请重新检查
|
||||
|
||||
[user]
|
||||
change_avatar=修改头像
|
||||
joined_on=加入于 %s
|
||||
@@ -1013,7 +1015,7 @@ visibility.private_tooltip=仅对您已加入的组织的成员可见。
|
||||
[repo]
|
||||
dev_container = 开发容器
|
||||
dev_container_empty = 您还没有该仓库的开发容器
|
||||
dev_container_lack_of_config_prompt = 无法创建开发容器:请上传 devcontainer.json 至当前仓库默认分支
|
||||
dev_container_invalid_config_prompt = 开发容器配置无效:请上传有效的 devcontainer.json 至默认分支,且确保仓库未处于存档状态
|
||||
dev_container_control.create = 创建开发容器
|
||||
dev_container_control.creation_success_for_user = 用户 '%s' 已成功创建开发容器
|
||||
dev_container_control.creation_failed_for_user = 用户 '%s' 开发容器创建失败
|
||||
@@ -1148,8 +1150,8 @@ template.issue_labels=工单标签
|
||||
template.one_item=必须至少选择一个模板项
|
||||
template.invalid=必须选择一个模板仓库
|
||||
|
||||
archive.title=该仓库已被归档。您可以查看文件和克隆它,但不能推送、创建工单或合并请求。
|
||||
archive.title_date=该仓库已于 %s 归档。您可以查看文件或克隆它,但不能推送、创建工单或合并请求。
|
||||
archive.title=该仓库已被归档。您可以查看文件和克隆它,但不能推送、创建开发容器、创建工单或合并请求。
|
||||
archive.title_date=该仓库已于 %s 归档。您可以查看文件或克隆它,但不能推送、创建开发容器、创建工单或合并请求。
|
||||
archive.issue.nocomment=此仓库已存档,您不能在此工单添加评论。
|
||||
archive.pull.nocomment=此仓库已存档,您不能在此合并请求添加评论。
|
||||
|
||||
|
||||
77
routers/api/devcontainer/create_repo_devcontainer.go
Normal file
77
routers/api/devcontainer/create_repo_devcontainer.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package devcontainer
|
||||
|
||||
import (
|
||||
gitea_web_module "code.gitea.io/gitea/modules/web"
|
||||
Result "code.gitea.io/gitea/routers/entity"
|
||||
gitea_web_context "code.gitea.io/gitea/services/context"
|
||||
devcontainer_api_service "code.gitea.io/gitea/services/devstar_devcontainer/api_services"
|
||||
devcontainer_service_dto "code.gitea.io/gitea/services/devstar_devcontainer/dto"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// CreateRepoDevcontainer 创建 某用户在某仓库的 DevContainer
|
||||
//
|
||||
// POST /api/devcontainer
|
||||
// 请求体参数:
|
||||
// -- repoId: 需要为哪个仓库创建 DevContainer
|
||||
// 注意:必须携带 用户登录凭证
|
||||
func CreateRepoDevcontainer(ctx *gitea_web_context.Context) {
|
||||
|
||||
// 1. 检查用户登录状态,若未登录则返回未授权错误
|
||||
if ctx == nil || ctx.Doer == nil {
|
||||
Result.RespUnauthorizedFailure.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 检查表单校验规则是否失败
|
||||
hasError, _ := ctx.Data["HasError"].(bool)
|
||||
if hasError {
|
||||
// POST Binding 表单正则表达式校验失败,返回 API 错误信息
|
||||
failedToValidateFormData := &Result.ResultType{
|
||||
Code: Result.RespFailedIllegalParams.Code,
|
||||
Msg: Result.RespFailedIllegalParams.Msg,
|
||||
Data: map[string]string{
|
||||
"ErrorMsg": ctx.Data["ErrorMsg"].(string),
|
||||
},
|
||||
}
|
||||
failedToValidateFormData.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 解析 repoId
|
||||
form := gitea_web_module.GetForm(ctx).(*forms.CreateRepoDevcontainerForm)
|
||||
repoId, err := strconv.ParseInt(form.RepoId, 10, 64)
|
||||
if err != nil || repoId <= 0 {
|
||||
failedToParseRepoId := Result.ResultType{
|
||||
Code: Result.RespFailedIllegalParams.Code,
|
||||
Msg: Result.RespFailedIllegalParams.Msg,
|
||||
Data: map[string]string{
|
||||
"ErrorMsg": err.Error(),
|
||||
},
|
||||
}
|
||||
failedToParseRepoId.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 调用 API Service 层创建 DevContainer
|
||||
opts := &devcontainer_service_dto.CreateDevcontainerDTO{
|
||||
Actor: ctx.Doer,
|
||||
RepoId: repoId,
|
||||
}
|
||||
err = devcontainer_api_service.CreateDevcontainerAPIService(ctx, opts)
|
||||
if err != nil {
|
||||
errCreateDevcontainer := Result.ResultType{
|
||||
Code: Result.RespFailedCreateDevcontainer.Code,
|
||||
Msg: Result.RespFailedCreateDevcontainer.Msg,
|
||||
Data: map[string]string{
|
||||
"ErrorMsg": err.Error(),
|
||||
},
|
||||
}
|
||||
errCreateDevcontainer.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 创建 DevContainer 成功,直接返回
|
||||
Result.RespSuccess.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
}
|
||||
52
routers/api/devcontainer/delete_repo_devcontainer.go
Normal file
52
routers/api/devcontainer/delete_repo_devcontainer.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package devcontainer
|
||||
|
||||
import (
|
||||
Result "code.gitea.io/gitea/routers/entity"
|
||||
gitea_web_context "code.gitea.io/gitea/services/context"
|
||||
devcontainer_api_service "code.gitea.io/gitea/services/devstar_devcontainer/api_services"
|
||||
devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// DeleteRepoDevcontainer 删除某仓库的 DevContainer
|
||||
//
|
||||
// DELETE /api/devcontainer
|
||||
// 请求体参数:
|
||||
// -- repoId: 需要为哪个仓库创建 DevContainer
|
||||
// 注意:必须携带 用户登录凭证
|
||||
func DeleteRepoDevcontainer(ctx *gitea_web_context.Context) {
|
||||
// 1. 检查用户登录状态,若未登录则返回未授权错误
|
||||
if ctx == nil || ctx.Doer == nil {
|
||||
Result.RespUnauthorizedFailure.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 取得参数 repoId
|
||||
repoIdStr := ctx.FormString("repoId")
|
||||
repoId, err := strconv.ParseInt(repoIdStr, 10, 64)
|
||||
if err != nil || repoId <= 0 {
|
||||
Result.RespFailedIllegalParams.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 调用 API Service 层,删除 DevContainer
|
||||
opts := &devcontainer_service_options.AbstractDeleteDevcontainerOptions{
|
||||
Actor: ctx.Doer,
|
||||
RepoId: repoId,
|
||||
}
|
||||
err = devcontainer_api_service.DeleteDevcontainerAPIService(ctx, opts)
|
||||
if err != nil {
|
||||
failureDeleteDevcontainer := Result.ResultType{
|
||||
Code: Result.RespFailedDeleteDevcontainer.Code,
|
||||
Msg: Result.RespFailedDeleteDevcontainer.Msg,
|
||||
Data: map[string]any{
|
||||
"ErrorMsg": err.Error(),
|
||||
},
|
||||
}
|
||||
failureDeleteDevcontainer.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 删除成功,返回提示信息
|
||||
Result.RespSuccess.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
}
|
||||
60
routers/api/devcontainer/get_repo_devcontainer.go
Normal file
60
routers/api/devcontainer/get_repo_devcontainer.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package devcontainer
|
||||
|
||||
import (
|
||||
Result "code.gitea.io/gitea/routers/entity"
|
||||
gitea_web_context "code.gitea.io/gitea/services/context"
|
||||
devcontainer_api_service "code.gitea.io/gitea/services/devstar_devcontainer/api_services"
|
||||
"code.gitea.io/gitea/services/devstar_devcontainer/options"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GetDevcontainer 查找某用户在某仓库的 DevContainer
|
||||
//
|
||||
// GET /api/devcontainer
|
||||
// 请求体参数:
|
||||
// -- repoId: 需要为哪个仓库创建 DevContainer
|
||||
// -- wait: 是否等待 DevContainer 就绪(默认为 false 直接返回“未就绪”,否则阻塞等待)
|
||||
// 注意:必须携带 用户登录凭证
|
||||
func GetDevcontainer(ctx *gitea_web_context.Context) {
|
||||
// 1. 检查用户登录状态,若未登录则返回未授权错误
|
||||
if ctx == nil || ctx.Doer == nil {
|
||||
Result.RespUnauthorizedFailure.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 取得参数
|
||||
wait := ctx.FormBool("wait")
|
||||
repoIdStr := ctx.FormString("repoId")
|
||||
repoId, err := strconv.ParseInt(repoIdStr, 10, 64)
|
||||
if err != nil || repoId <= 0 {
|
||||
Result.RespFailedIllegalParams.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 准备调用 API Service 层,获取 DevContainer 信息
|
||||
optsAbstractOpenDevcontainer := &options.AbstractOpenDevcontainerOptions{
|
||||
Wait: wait,
|
||||
RepoId: repoId,
|
||||
Actor: ctx.Doer,
|
||||
}
|
||||
repoDevcontainerVO, err := devcontainer_api_service.OpenDevcontainerAPIService(ctx, optsAbstractOpenDevcontainer)
|
||||
if err != nil {
|
||||
failureGetDevcontainer := Result.ResultType{
|
||||
Code: Result.RespFailedOpenDevcontainer.Code,
|
||||
Msg: Result.RespFailedOpenDevcontainer.Msg,
|
||||
Data: map[string]any{
|
||||
"ErrorMsg": err.Error(),
|
||||
},
|
||||
}
|
||||
failureGetDevcontainer.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 封装返回成功信息
|
||||
successGetDevContainer := Result.ResultType{
|
||||
Code: Result.RespSuccess.Code,
|
||||
Msg: Result.RespSuccess.Msg,
|
||||
Data: repoDevcontainerVO,
|
||||
}
|
||||
successGetDevContainer.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
}
|
||||
@@ -3,19 +3,19 @@ package devcontainer
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
|
||||
Result "code.gitea.io/gitea/routers/entity"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/web/devcontainer/vo"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
gitea_web_context "code.gitea.io/gitea/services/context"
|
||||
devstar_devcontainer_service "code.gitea.io/gitea/services/devstar_devcontainer"
|
||||
)
|
||||
|
||||
// ListUserDevcontainers 枚举已登录用户所有的 devContainers,但目前测试阶段只返回 mock数据
|
||||
// ListUserDevcontainers 枚举已登录用户所有的 DevContainers
|
||||
//
|
||||
// GET /api/devcontainer/user
|
||||
// 请求输入参数:
|
||||
// - page: 当前第几页(默认第1页),从1开始计数
|
||||
// - pageSize: 每页记录数(默认值 setting.UI.Admin.DevContainersPagingNum)
|
||||
func ListUserDevcontainers(ctx *context.Context) {
|
||||
func ListUserDevcontainers(ctx *gitea_web_context.Context) {
|
||||
|
||||
// 1. 检查用户登录状态,若未登录则返回未授权错误
|
||||
if ctx.Doer == nil {
|
||||
@@ -10,16 +10,19 @@ type RepoDevContainerVO struct {
|
||||
DevContainerId int64 `json:"devContainerId" xorm:"devcontainer_id"`
|
||||
DevContainerName string `json:"devContainerName" xorm:"devcontainer_name"`
|
||||
DevContainerHost string `json:"devContainerHost" xorm:"devcontainer_host"`
|
||||
DevContainerPort uint16 `json:"devContainerPort" xorm:"devcontainer_port"`
|
||||
DevContainerUsername string `json:"devContainerUsername" xorm:"devcontainer_username"`
|
||||
DevContainerPassword string `json:"devContainerPassword" xorm:"devcontainer_password"`
|
||||
DevContainerWorkDir string `json:"devContainerWorkDir" xorm:"devcontainer_work_dir"`
|
||||
|
||||
RepoId int64 `json:"repoId" xorm:"repo_id"`
|
||||
RepoName string `json:"repoName" xorm:"repo_name"`
|
||||
//RepoOwnerID int64 `json:"repo_owner_id" xorm:"repo_owner_id"`
|
||||
RepoOwnerName string `json:"repo_owner_name" xorm:"repo_owner_name"`
|
||||
RepoLink string `json:"repo_link,omitempty"` // 无对应 SQL结果字段,需查表结束后调用 Repo.Link() 手动写入
|
||||
RepoLink string `json:"repo_link" xorm:"repo_link"`
|
||||
RepoDescription string `json:"repoDescription,omitempty" xorm:"repo_description"`
|
||||
|
||||
// 实时查询获取,不再从数据库获取
|
||||
DevContainerPort uint16 `json:"devContainerPort,omitempty"`
|
||||
}
|
||||
|
||||
// RepoDevcontainerOptions 仓库 Dev Container 条件,注意仓库的所有者可能与当前操作用户不一致!
|
||||
@@ -2,7 +2,7 @@ package wechat
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
wechat_official_account_routers "code.gitea.io/gitea/routers/web/wechat/official_account"
|
||||
wechat_official_account_routers "code.gitea.io/gitea/routers/api/wechat/official_account"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -30,7 +30,7 @@ func QrCheckCodeStatus(responseWriter http.ResponseWriter, request *http.Request
|
||||
// 从请求中提取 ticket 参数
|
||||
ticket := request.URL.Query().Get("ticket")
|
||||
if ticket == "" {
|
||||
Result.RespFailedIllegalParam.RespondJson2HttpResponseWriter(responseWriter)
|
||||
Result.RespFailedIllegalWechatQrTicket.RespondJson2HttpResponseWriter(responseWriter)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,3 +7,27 @@ var RespUnauthorizedFailure = ResultType{
|
||||
Code: 11001,
|
||||
Msg: "您未授权,无法查看 devContainer 信息",
|
||||
}
|
||||
|
||||
// RespFailedIllegalParams 仓库ID参数无效
|
||||
var RespFailedIllegalParams = ResultType{
|
||||
Code: 11002,
|
||||
Msg: "无效参数",
|
||||
}
|
||||
|
||||
// RespFailedCreateDevcontainer 创建 DevContainer 失败
|
||||
var RespFailedCreateDevcontainer = ResultType{
|
||||
Code: 11003,
|
||||
Msg: "创建 DevContainer 失败",
|
||||
}
|
||||
|
||||
// RespFailedOpenDevcontainer 打开 DevContainer 失败
|
||||
var RespFailedOpenDevcontainer = ResultType{
|
||||
Code: 11004,
|
||||
Msg: "打开 DevContainer 失败",
|
||||
}
|
||||
|
||||
// RespFailedDeleteDevcontainer 删除 DevContainer 失败
|
||||
var RespFailedDeleteDevcontainer = ResultType{
|
||||
Code: 11005,
|
||||
Msg: "删除 DevContainer 失败",
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ var RespPendingQrNotScanned = ResultType{
|
||||
Msg: "用户未扫描微信公众号带参数二维码",
|
||||
}
|
||||
|
||||
// RespFailedIllegalParam 二维码凭证Ticket参数无效
|
||||
var RespFailedIllegalParam = ResultType{
|
||||
// RespFailedIllegalWechatQrTicket 二维码凭证Ticket参数无效
|
||||
var RespFailedIllegalWechatQrTicket = ResultType{
|
||||
Code: 10002,
|
||||
Msg: "提交的微信公众号带参数二维码凭证Ticket参数无效",
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
wechat_routers "code.gitea.io/gitea/routers/api/wechat"
|
||||
"context"
|
||||
"net/http"
|
||||
"reflect"
|
||||
@@ -34,7 +35,6 @@ import (
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/routers/private"
|
||||
web_routers "code.gitea.io/gitea/routers/web"
|
||||
wechat_routers "code.gitea.io/gitea/routers/web/wechat"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
|
||||
57
routers/web/devcontainer/CreateRepoDevContainer.go
Normal file
57
routers/web/devcontainer/CreateRepoDevContainer.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package devcontainer
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
|
||||
gitea_web_context "code.gitea.io/gitea/services/context"
|
||||
DevcontainersService "code.gitea.io/gitea/services/devstar_devcontainer"
|
||||
)
|
||||
|
||||
// CreateRepoDevContainer 创建仓库 Dev Container
|
||||
func CreateRepoDevContainer(ctx *gitea_web_context.Context) {
|
||||
if !isUserDevcontainerAlreadyInRepository(ctx) {
|
||||
opts := &DevcontainersVO.RepoDevcontainerOptions{
|
||||
Actor: ctx.Doer,
|
||||
Repository: ctx.Repo.Repository,
|
||||
}
|
||||
err := DevcontainersService.CreateRepoDevcontainer(ctx, opts)
|
||||
if err != nil {
|
||||
log.Error("failed to create devContainer with option{%v}: %v", opts, err.Error())
|
||||
ctx.Flash.Error(ctx.Tr("repo.dev_container_control.creation_failed_for_user", ctx.Doer.Name))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.dev_container_control.creation_success_for_user", ctx.Doer.Name))
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/dev-container")
|
||||
}
|
||||
|
||||
// isValidRepoDevcontainerJsonFile 辅助判断当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json
|
||||
func isValidRepoDevcontainerJsonFile(ctx *gitea_web_context.Context) bool {
|
||||
|
||||
// 1. 仓库非空,且非 Archived 状态
|
||||
if ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsArchived {
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. 当前分支的目录 .devcontainer 下存在 devcontainer.json 文件
|
||||
fileDevcontainerJsonExists, err := ctx.Repo.FileExists(".devcontainer/devcontainer.json", ctx.Repo.BranchName)
|
||||
if err != nil || !fileDevcontainerJsonExists {
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. TODO: DevContainer 格式正确
|
||||
return true
|
||||
}
|
||||
|
||||
// isUserDevcontainerAlreadyInRepository 辅助判断当前用户在当前仓库是否已有 Dev Container
|
||||
func isUserDevcontainerAlreadyInRepository(ctx *gitea_web_context.Context) bool {
|
||||
|
||||
opts := &DevcontainersVO.RepoDevcontainerOptions{
|
||||
Actor: ctx.Doer,
|
||||
Repository: ctx.Repo.Repository,
|
||||
}
|
||||
|
||||
devcontainerDetails, _ := DevcontainersService.GetRepoDevcontainerDetails(ctx, opts)
|
||||
return devcontainerDetails.DevContainerId > 0
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package devcontainer
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
|
||||
gitea_web_context "code.gitea.io/gitea/services/context"
|
||||
DevcontainersService "code.gitea.io/gitea/services/devstar_devcontainer"
|
||||
)
|
||||
|
||||
// DeleteRepoDevContainerForCurrentActor 删除仓库 当前用户 Dev Container
|
||||
func DeleteRepoDevContainerForCurrentActor(ctx *gitea_web_context.Context) {
|
||||
|
||||
if isUserDevcontainerAlreadyInRepository(ctx) {
|
||||
opts := &DevcontainersVO.RepoDevcontainerOptions{
|
||||
Actor: ctx.Doer,
|
||||
Repository: ctx.Repo.Repository,
|
||||
}
|
||||
err := DevcontainersService.DeleteRepoDevcontainer(ctx, opts)
|
||||
if err != nil {
|
||||
log.Warn("failed to create devContainer with option{%v}: %v", opts, err.Error())
|
||||
ctx.Flash.Error(ctx.Tr("repo.dev_container_control.deletion_failed_for_user", ctx.Doer.Name))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.dev_container_control.deletion_success_for_user", ctx.Doer.Name))
|
||||
}
|
||||
}
|
||||
ctx.JSONRedirect(ctx.Repo.RepoLink + "/dev-container")
|
||||
}
|
||||
44
routers/web/devcontainer/GetRepoDevContainerDetails.go
Normal file
44
routers/web/devcontainer/GetRepoDevContainerDetails.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package devcontainer
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
|
||||
gitea_web_context "code.gitea.io/gitea/services/context"
|
||||
DevcontainersService "code.gitea.io/gitea/services/devstar_devcontainer"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
tplGetRepoDevcontainerDetail base.TplName = "repo/devcontainer/details"
|
||||
)
|
||||
|
||||
// GetRepoDevContainerDetails 获取仓库 Dev Container 详细信息
|
||||
func GetRepoDevContainerDetails(ctx *gitea_web_context.Context) {
|
||||
|
||||
// 1. 查询当前 Repo 已有的 Dev Container 信息
|
||||
opts := &DevcontainersVO.RepoDevcontainerOptions{
|
||||
Actor: ctx.Doer,
|
||||
Repository: ctx.Repo.Repository,
|
||||
}
|
||||
//ctx.Repo.RepoLink == ctx.Repo.Repository.Link()
|
||||
devContainerMetadata, err := DevcontainersService.GetRepoDevcontainerDetails(ctx, opts)
|
||||
hasDevContainer := err == nil && devContainerMetadata.DevContainerId > 0
|
||||
ctx.Data["HasDevContainer"] = hasDevContainer
|
||||
if hasDevContainer {
|
||||
ctx.Data["DevContainer"] = devContainerMetadata
|
||||
}
|
||||
|
||||
// 2. 检查当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json
|
||||
isValidRepoDevcontainerJson := isValidRepoDevcontainerJsonFile(ctx)
|
||||
if !hasDevContainer && !isValidRepoDevcontainerJson {
|
||||
ctx.Flash.Error(ctx.Tr("repo.dev_container_invalid_config_prompt"), true)
|
||||
}
|
||||
|
||||
// 3. 携带数据渲染页面,返回
|
||||
ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container")
|
||||
ctx.Data["PageIsRepoDevcontainerDetails"] = true
|
||||
ctx.Data["HasValidDevContainerJSON"] = isValidRepoDevcontainerJson
|
||||
ctx.Data["Repository"] = ctx.Repo.Repository
|
||||
ctx.Data["ContextUser"] = ctx.Doer
|
||||
ctx.HTML(http.StatusOK, tplGetRepoDevcontainerDetail)
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package devcontainer
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/web/devcontainer/vo"
|
||||
gitea_web_context "code.gitea.io/gitea/services/context"
|
||||
DevcontainersService "code.gitea.io/gitea/services/devstar_devcontainer"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
tplGetRepoDevcontainerDetail base.TplName = "repo/devcontainer/details"
|
||||
)
|
||||
|
||||
// GetRepoDevContainerDetails 获取仓库 Dev Container 详细信息
|
||||
func GetRepoDevContainerDetails(ctx *gitea_web_context.Context) {
|
||||
|
||||
// 1. 查询当前 Repo 已有的 Dev Container 信息
|
||||
opts := &DevcontainersVO.RepoDevcontainerOptions{
|
||||
Actor: ctx.Doer,
|
||||
Repository: ctx.Repo.Repository,
|
||||
}
|
||||
//ctx.Repo.RepoLink == ctx.Repo.Repository.Link()
|
||||
devContainerMetadata, err := DevcontainersService.GetRepoDevcontainerDetails(ctx, opts)
|
||||
hasDevContainer := err == nil && devContainerMetadata.DevContainerId > 0
|
||||
ctx.Data["HasDevContainer"] = hasDevContainer
|
||||
if hasDevContainer {
|
||||
ctx.Data["DevContainer"] = devContainerMetadata
|
||||
}
|
||||
|
||||
// 2. 检查当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json
|
||||
isValidRepoDevcontainerJson := isValidRepoDevcontainerJsonFile(ctx)
|
||||
if !hasDevContainer && !isValidRepoDevcontainerJson {
|
||||
ctx.Flash.Error(ctx.Tr("repo.dev_container_lack_of_config_prompt"), true)
|
||||
}
|
||||
|
||||
// 3. 携带数据渲染页面,返回
|
||||
ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container")
|
||||
ctx.Data["PageIsRepoDevcontainerDetails"] = true
|
||||
ctx.Data["HasValidDevContainerJSON"] = isValidRepoDevcontainerJson
|
||||
ctx.Data["Repository"] = ctx.Repo.Repository
|
||||
ctx.Data["ContextUser"] = ctx.Doer
|
||||
ctx.HTML(http.StatusOK, tplGetRepoDevcontainerDetail)
|
||||
}
|
||||
|
||||
// CreateRepoDevContainer 创建仓库 Dev Container
|
||||
func CreateRepoDevContainer(ctx *gitea_web_context.Context) {
|
||||
if !isUserDevcontainerAlreadyInRepository(ctx) {
|
||||
opts := &DevcontainersVO.RepoDevcontainerOptions{
|
||||
Actor: ctx.Doer,
|
||||
Repository: ctx.Repo.Repository,
|
||||
}
|
||||
err := DevcontainersService.CreateRepoDevcontainer(ctx, opts)
|
||||
if err != nil {
|
||||
log.Warn("failed to create devContainer with option{%v}: %v", opts, err.Error())
|
||||
ctx.Flash.Error(ctx.Tr("repo.dev_container_control.creation_failed_for_user", ctx.Doer.Name))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.dev_container_control.creation_success_for_user", ctx.Doer.Name))
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/dev-container")
|
||||
}
|
||||
|
||||
// DeleteRepoDevContainerForCurrentActor 删除仓库 当前用户 Dev Container
|
||||
func DeleteRepoDevContainerForCurrentActor(ctx *gitea_web_context.Context) {
|
||||
|
||||
if isUserDevcontainerAlreadyInRepository(ctx) {
|
||||
opts := &DevcontainersVO.RepoDevcontainerOptions{
|
||||
Actor: ctx.Doer,
|
||||
Repository: ctx.Repo.Repository,
|
||||
}
|
||||
err := DevcontainersService.DeleteRepoDevcontainer(ctx, opts)
|
||||
if err != nil {
|
||||
log.Warn("failed to create devContainer with option{%v}: %v", opts, err.Error())
|
||||
ctx.Flash.Error(ctx.Tr("repo.dev_container_control.deletion_failed_for_user", ctx.Doer.Name))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.dev_container_control.deletion_success_for_user", ctx.Doer.Name))
|
||||
}
|
||||
}
|
||||
ctx.JSONRedirect(ctx.Repo.RepoLink + "/dev-container")
|
||||
}
|
||||
|
||||
// isValidRepoDevcontainerJsonFile 辅助判断当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json
|
||||
func isValidRepoDevcontainerJsonFile(ctx *gitea_web_context.Context) bool {
|
||||
|
||||
// 1. 仓库非空,且非 Archived 状态
|
||||
if ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsArchived {
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. 当前分支的目录 .devcontainer 下存在 devcontainer.json 文件
|
||||
fileDevcontainerJsonExists, err := ctx.Repo.FileExists(".devcontainer/devcontainer.json", ctx.Repo.BranchName)
|
||||
if err != nil || !fileDevcontainerJsonExists {
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. TODO: DevContainer 格式正确
|
||||
return true
|
||||
}
|
||||
|
||||
// isUserDevcontainerAlreadyInRepository 辅助判断当前用户在当前仓库是否已有 Dev Container
|
||||
func isUserDevcontainerAlreadyInRepository(ctx *gitea_web_context.Context) bool {
|
||||
|
||||
opts := &DevcontainersVO.RepoDevcontainerOptions{
|
||||
Actor: ctx.Doer,
|
||||
Repository: ctx.Repo.Repository,
|
||||
}
|
||||
|
||||
devcontainerDetails, _ := DevcontainersService.GetRepoDevcontainerDetails(ctx, opts)
|
||||
return devcontainerDetails.DevContainerId > 0
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/web/devcontainer/vo"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
devstar_devcontainer_service "code.gitea.io/gitea/services/devstar_devcontainer"
|
||||
"net/http"
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/routers/web/devcontainer"
|
||||
devcontainer_api "code.gitea.io/gitea/routers/api/devcontainer"
|
||||
devcontainer_web "code.gitea.io/gitea/routers/web/devcontainer"
|
||||
gocontext "context"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -505,7 +506,21 @@ func registerRoutes(m *web.Router) {
|
||||
|
||||
// ***** START: DevContainer *****
|
||||
m.Group("/api/devcontainer", func() {
|
||||
m.Get("/user", devcontainer.ListUserDevcontainers)
|
||||
// 获取 某用户在某仓库中的 DevContainer 细节(包括SSH连接信息),默认不会等待 (wait = false)
|
||||
// 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true // 无需传入 userId,直接从 token 中提取
|
||||
m.Get("", devcontainer_api.GetDevcontainer)
|
||||
|
||||
// 抽象方法,创建 某用户在某仓库中的 DevContainer
|
||||
// 请求方式: POST /api/devcontainer
|
||||
// 请求体数据: { repoId: REPO_ID }
|
||||
m.Post("", web.Bind(forms.CreateRepoDevcontainerForm{}), devcontainer_api.CreateRepoDevcontainer)
|
||||
|
||||
// 删除某用户在某仓库中的 DevContainer
|
||||
// 请求方法: DELETE /api/devcontainer?repoId=${repoId}
|
||||
m.Delete("", devcontainer_api.DeleteRepoDevcontainer)
|
||||
|
||||
// 列举某用户已创建的所有 DevContainer
|
||||
m.Get("/user", devcontainer_api.ListUserDevcontainers)
|
||||
})
|
||||
// ***** END: DevContainer *****
|
||||
|
||||
@@ -1318,11 +1333,11 @@ func registerRoutes(m *web.Router) {
|
||||
// end "/{username}/{reponame}": repo code
|
||||
|
||||
m.Group("/{username}/{reponame}/dev-container", func() { // repo Dev Container
|
||||
m.Get("", devcontainer.GetRepoDevContainerDetails)
|
||||
m.Get("/create", devcontainer.CreateRepoDevContainer)
|
||||
m.Post("/delete", devcontainer.DeleteRepoDevContainerForCurrentActor)
|
||||
m.Get("", devcontainer_web.GetRepoDevContainerDetails)
|
||||
m.Get("/create", devcontainer_web.CreateRepoDevContainer, context.RepoMustNotBeArchived()) // 仓库状态非 Archived 才可以创建 DevContainer
|
||||
m.Post("/delete", devcontainer_web.DeleteRepoDevContainerForCurrentActor)
|
||||
},
|
||||
// 进入 Dev Container 编辑页面需要符合条件: // 更新tmpl!!! // TODO
|
||||
// 进入 Dev Container 编辑页面需要符合条件:
|
||||
// 1. 已登录
|
||||
// 2. repo 信息已加载到 Gitea Web Context (否则无法判定当前repo是否有写入Code权限,从而返回无权访问错误码 HTTP 404)
|
||||
// 3. 具有code写入权限
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package devstar_devcontainer
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
gitea_web_context "code.gitea.io/gitea/services/context"
|
||||
devcontainer_service_errors "code.gitea.io/gitea/services/devstar_devcontainer/errors"
|
||||
k8s_agent_services "code.gitea.io/gitea/services/devstar_devcontainer/k8s_agent"
|
||||
devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
|
||||
devcontainer_service_vo "code.gitea.io/gitea/services/devstar_devcontainer/vo"
|
||||
)
|
||||
|
||||
// OpenDevcontainerService 获取 DevContainer 连接信息,抽象方法,适配多种 DevContainer Agent
|
||||
func OpenDevcontainerService(ctx *gitea_web_context.Context, opts *devcontainer_service_options.OpenDevcontainerAppDispatcherOptions) (*devcontainer_service_vo.OpenDevcontainerAbstractAgentVO, error) {
|
||||
|
||||
// 0. 检查参数
|
||||
if ctx == nil || opts == nil || len(opts.Name) == 0 {
|
||||
return nil, devcontainer_service_errors.ErrIllegalParams{
|
||||
FieldNameList: []string{"ctx", "opts.Name"},
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 检查 DevContainer 功能是否开启
|
||||
if setting.Devstar.Devcontainer.Enabled == false {
|
||||
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
|
||||
Action: "check availability of DevStar DevContainer",
|
||||
Message: "DevContainer is turned off globally",
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 根据 DevContainer Agent 类型分发任务
|
||||
apiRequestContext := ctx.Req.Context()
|
||||
openDevcontainerAbstractAgentVO := &devcontainer_service_vo.OpenDevcontainerAbstractAgentVO{}
|
||||
switch setting.Devstar.Devcontainer.Agent {
|
||||
case setting.DEVCONTAINER_AGENT_NAME_K8S:
|
||||
devcontainerApp, err := k8s_agent_services.AssignDevcontainerGetting2K8sOperator(&apiRequestContext, opts)
|
||||
if err != nil {
|
||||
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
|
||||
Action: "Open DevContainer in k8s",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
openDevcontainerAbstractAgentVO.NodePortAssigned = devcontainerApp.Status.NodePortAssigned
|
||||
|
||||
default:
|
||||
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
|
||||
Action: "Open DevContainer",
|
||||
Message: "No Valid DevContainer Agent Found",
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 封装返回结果
|
||||
return openDevcontainerAbstractAgentVO, nil
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
devstar_devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/web/devcontainer/vo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
|
||||
devcontainer_k8s_agent_service "code.gitea.io/gitea/services/devstar_devcontainer/k8s_agent"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
@@ -86,27 +88,34 @@ func GetRepoDevcontainerDetails(ctx context.Context, opts *DevcontainersVO.RepoD
|
||||
- 数据库此前不存在 该用户在该repo创建的 Dev Container
|
||||
*/
|
||||
func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevcontainerOptions) error {
|
||||
// TODO: 调用 k8s client 创建
|
||||
// 目前只是 mock数据
|
||||
username := opts.Actor.Name
|
||||
repoName := opts.Repository.Name
|
||||
newDevcontainer := &devstar_devcontainer_models.DevstarDevcontainer{
|
||||
Name: getSanitizedDevcontainerName(username, repoName),
|
||||
DevcontainerHost: "127.0.0.1",
|
||||
DevcontainerPort: 10086,
|
||||
DevcontainerUsername: "mock-username",
|
||||
DevcontainerPassword: "mock-password",
|
||||
DevcontainerWorkDir: "~/workspace/",
|
||||
DevcontainerHost: setting.Devstar.Devcontainer.Host,
|
||||
// DevcontainerPort: 10086, // NodePort 交由 k8s Operator 调度后更新
|
||||
DevcontainerUsername: "username", // TODO: 填入用户名
|
||||
DevcontainerPassword: "password", // TODO: 生成随机一次性密码
|
||||
DevcontainerWorkDir: "~",
|
||||
RepoId: opts.Repository.ID,
|
||||
UserId: opts.Actor.ID,
|
||||
}
|
||||
|
||||
// 在数据库事务中创建 Dev Container 分配资源,出错时自动回滚相对应数据库字段,保证数据一致
|
||||
dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
// 1. 调用 k8s Operator Agent,创建 DevContainer 资源,同时更新k8s调度器分配的 NodePort
|
||||
err = claimDevcontainerResource(&ctx, newDevcontainer)
|
||||
if err != nil {
|
||||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||||
Action: fmt.Sprintf("claim resource for Dev Container %v", newDevcontainer),
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
// 2. 根据分配的 NodePort 更新数据库字段
|
||||
rowsAffect, err := db.GetEngine(ctx).
|
||||
Table("devstar_devcontainer").
|
||||
Insert(newDevcontainer)
|
||||
|
||||
if err != nil {
|
||||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||||
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
|
||||
@@ -118,14 +127,6 @@ func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevco
|
||||
Message: "expected 1 row to be inserted, but got 0",
|
||||
}
|
||||
}
|
||||
|
||||
err = claimDevcontainerResource(newDevcontainer)
|
||||
if err != nil {
|
||||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||||
Action: fmt.Sprintf("Failed to claim resource for Dev Container devstar_devcontainer.DevstarDevcontainer %v", newDevcontainer),
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -166,7 +167,15 @@ func DeleteRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevco
|
||||
}
|
||||
}
|
||||
|
||||
// 2.2 条件删除: user_id 和/或 repo_id
|
||||
// 2.2 空列表,直接结束事务(由于前一个操作只是查询,所以回滚事务不会导致数据不一致问题)
|
||||
if len(devcontainersList) == 0 {
|
||||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||||
Action: fmt.Sprintf("find devcontainer(s) with condition '%v'", sqlDevcontainerCondition),
|
||||
Message: "No DevContainer found",
|
||||
}
|
||||
}
|
||||
|
||||
// 2.3 条件删除: user_id 和/或 repo_id
|
||||
_, err = db.GetEngine(ctx).
|
||||
Table("devstar_devcontainer").
|
||||
Where(sqlDevcontainerCondition).
|
||||
@@ -187,7 +196,12 @@ func DeleteRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevco
|
||||
|
||||
// 3. 后台启动一个goroutine慢慢回收 Dev Container 资源 (如果回收失败,将会产生孤儿 Dev Container,只能管理员手动识别、删除)
|
||||
go func() {
|
||||
_ = purgeDevcontainersResource(devcontainersList)
|
||||
// 注意:由于执行删除 k8s 资源 与 数据库交互和Web页面更新是异步的,因此在 goroutine 中必须重新创建 context,否则报错:
|
||||
// Delete "https://192.168.49.2:8443/apis/devcontainer.devstar.cn/v1/...": context canceled
|
||||
isolatedContextToPurgeK8sResource, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
|
||||
_ = purgeDevcontainersResource(&isolatedContextToPurgeK8sResource, &devcontainersList)
|
||||
}()
|
||||
|
||||
return dbTransactionErr
|
||||
@@ -211,16 +225,47 @@ func getSanitizedDevcontainerName(username, repoName string) string {
|
||||
return fmt.Sprintf("%s-%s-%s", sanitizedUsername, sanitizedRepoName, uuidStr)
|
||||
}
|
||||
|
||||
// purgeDevcontainersResource 辅助函数,用于goroutine后台执行,回收DevContainer资源 // TODO
|
||||
func purgeDevcontainersResource(devcontainersList []devstar_devcontainer_models.DevstarDevcontainer) error {
|
||||
for _, devContainer := range devcontainersList {
|
||||
log.Info("[Goroutine purgeDevcontainersResource]: 正在装模做样地回收资源: devstar_devcontainer_models.DevstarDevcontainer %v", devContainer)
|
||||
}
|
||||
// purgeDevcontainersResource 辅助函数,用于goroutine后台执行,回收DevContainer资源
|
||||
func purgeDevcontainersResource(ctx *context.Context, devcontainersList *[]devstar_devcontainer_models.DevstarDevcontainer) error {
|
||||
// 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束,不会真正执行删除操作
|
||||
if setting.Devstar.Devcontainer.Enabled == false {
|
||||
// 如果用户设置禁用 DevContainer,无法删除资源,会直接忽略,而数据库相关记录会继续清空、不会发生回滚
|
||||
log.Warn("Orphan DevContainers in namespace `%s` left undeleted: %v", setting.Devstar.Devcontainer.Namespace, devcontainersList)
|
||||
return nil
|
||||
}
|
||||
// 2. 根据配置文件中指定的 DevContainer Agent 派遣创建任务
|
||||
switch setting.Devstar.Devcontainer.Agent {
|
||||
case setting.DEVCONTAINER_AGENT_NAME_K8S:
|
||||
return devcontainer_k8s_agent_service.AssignDevcontainerDeletion2K8sOperator(ctx, devcontainersList)
|
||||
default:
|
||||
// 未知 Agent,直接报错
|
||||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||||
Action: "dispatch DevContainer deletion",
|
||||
Message: fmt.Sprintf("unknown DevContainer agent `%s`, please check your config files", setting.Devstar.Devcontainer.Agent),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// claimDevcontainerResource
|
||||
func claimDevcontainerResource(newDevContainer *devstar_devcontainer_models.DevstarDevcontainer) error {
|
||||
log.Info("[claimDevcontainerResource]: 正在装模做样地分配资源: devstar_devcontainer_models.DevstarDevcontainer %v", newDevContainer)
|
||||
return nil
|
||||
// claimDevcontainerResource 分发创建 DevContainer 任务到配置文件指定的执行器
|
||||
func claimDevcontainerResource(ctx *context.Context, newDevContainer *devstar_devcontainer_models.DevstarDevcontainer) error {
|
||||
// 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束
|
||||
if setting.Devstar.Devcontainer.Enabled == false {
|
||||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||||
Action: "Check for DevContainer functionality switch",
|
||||
Message: "DevContainer is disabled globally, please check your configuration files",
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 根据配置文件中指定的 DevContainer Agent 派遣创建任务
|
||||
switch setting.Devstar.Devcontainer.Agent {
|
||||
case setting.DEVCONTAINER_AGENT_NAME_K8S:
|
||||
// k8s Operator
|
||||
return devcontainer_k8s_agent_service.AssignDevcontainerCreation2K8sOperator(ctx, newDevContainer)
|
||||
default:
|
||||
// 未知 Agent,直接报错
|
||||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||||
Action: "dispatch DevContainer creation",
|
||||
Message: fmt.Sprintf("unknown DevContainer agent `%s`, please check your config files", setting.Devstar.Devcontainer.Agent),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,22 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
devstar_devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/web/devcontainer/vo"
|
||||
"code.gitea.io/gitea/routers/api/devcontainer/vo"
|
||||
"context"
|
||||
"fmt"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// GetUserDevcontainersList 根据 userId 查询名下 devContainer 列表
|
||||
func GetUserDevcontainersList(ctx context.Context, opts *DevcontainersVO.SearchUserDevcontainerListItemVoOptions) (DevcontainersVO.ListUserDevcontainersVO, error) {
|
||||
func GetUserDevcontainersList(ctx context.Context, opts *vo.SearchUserDevcontainerListItemVoOptions) (vo.ListUserDevcontainersVO, error) {
|
||||
|
||||
// 0. 构造异常返回时的空数据
|
||||
var resultDevContainerListVO = DevcontainersVO.ListUserDevcontainersVO{
|
||||
var resultDevContainerListVO = vo.ListUserDevcontainersVO{
|
||||
Page: 0,
|
||||
PageSize: setting.UI.Admin.DevContainersPagingNum,
|
||||
PageTotalNum: 0,
|
||||
ItemTotalNum: 0,
|
||||
DevContainers: []DevcontainersVO.RepoDevContainerVO{},
|
||||
DevContainers: []vo.RepoDevContainerVO{},
|
||||
}
|
||||
|
||||
// 1. 查询参数预处理
|
||||
@@ -81,13 +81,12 @@ func GetUserDevcontainersList(ctx context.Context, opts *DevcontainersVO.SearchU
|
||||
}
|
||||
|
||||
// 2.3 数据库带条件分页查询
|
||||
resultDevContainerListVO.DevContainers = make([]DevcontainersVO.RepoDevContainerVO, 0, opts.PaginationOptions.PageSize)
|
||||
resultDevContainerListVO.DevContainers = make([]vo.RepoDevContainerVO, 0, opts.PaginationOptions.PageSize)
|
||||
/*
|
||||
SELECT
|
||||
devstar_devcontainer.id AS devcontainer_id,
|
||||
devstar_devcontainer.name AS devcontainer_name,
|
||||
devstar_devcontainer.devcontainer_host AS devcontainer_host,
|
||||
devstar_devcontainer.devcontainer_port AS devcontainer_port,
|
||||
devstar_devcontainer.devcontainer_username AS devcontainer_username,
|
||||
devstar_devcontainer.devcontainer_password AS devcontainer_password,
|
||||
devstar_devcontainer.devcontainer_work_dir AS devcontainer_work_dir,
|
||||
@@ -109,7 +108,6 @@ func GetUserDevcontainersList(ctx context.Context, opts *DevcontainersVO.SearchU
|
||||
"devstar_devcontainer.id AS devcontainer_id,"+
|
||||
"devstar_devcontainer.name AS devcontainer_name,"+
|
||||
"devstar_devcontainer.devcontainer_host AS devcontainer_host,"+
|
||||
"devstar_devcontainer.devcontainer_port AS devcontainer_port,"+
|
||||
"devstar_devcontainer.devcontainer_username AS devcontainer_username,"+
|
||||
"devstar_devcontainer.devcontainer_password AS devcontainer_password,"+
|
||||
"devstar_devcontainer.devcontainer_work_dir AS devcontainer_work_dir,"+
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package api_services
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
|
||||
gitea_web_context "code.gitea.io/gitea/services/context"
|
||||
DevcontainersService "code.gitea.io/gitea/services/devstar_devcontainer"
|
||||
devcontainer_service_dto "code.gitea.io/gitea/services/devstar_devcontainer/dto"
|
||||
devcontainer_service_errors "code.gitea.io/gitea/services/devstar_devcontainer/errors"
|
||||
"context"
|
||||
)
|
||||
|
||||
// CreateDevcontainerAPIService API专用创建 DevContainer Service
|
||||
func CreateDevcontainerAPIService(ctx *gitea_web_context.Context, opts *devcontainer_service_dto.CreateDevcontainerDTO) error {
|
||||
// 0. 检查用户传入参数
|
||||
if ctx == nil || opts == nil || opts.Actor == nil || opts.RepoId <= 0 {
|
||||
return devcontainer_service_errors.ErrIllegalParams{
|
||||
FieldNameList: []string{"ctx", "opts", "opts.Actor", "opts.RepoId"},
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 开启事务
|
||||
errTxn := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
|
||||
// 1.1 调用 model层,查询数据库,将 repoId 变换为 Repository 对象
|
||||
repositoryInDB, err := repo.GetRepositoryByID(ctx, opts.RepoId)
|
||||
if err != nil || repositoryInDB == nil {
|
||||
return devcontainer_service_errors.ErrIllegalParams{
|
||||
FieldNameList: []string{"opts.RepoId"},
|
||||
}
|
||||
}
|
||||
|
||||
// 1.2 检查该用户在该仓库 是否已经创建过 DevContainer
|
||||
optsRepoDevcontainer := &DevcontainersVO.RepoDevcontainerOptions{
|
||||
Actor: opts.Actor,
|
||||
Repository: repositoryInDB,
|
||||
}
|
||||
devcontainerDetails, err := DevcontainersService.GetRepoDevcontainerDetails(ctx, optsRepoDevcontainer)
|
||||
if err != nil || devcontainerDetails.DevContainerId > 0 {
|
||||
return devcontainer_service_errors.ErrDevcontainerAlreadyCreated{
|
||||
Actor: opts.Actor,
|
||||
Repository: repositoryInDB,
|
||||
}
|
||||
}
|
||||
|
||||
// 1.3 调用 k8s Agent 创建 DevContainer
|
||||
optsCreateDevcontainer := &DevcontainersVO.RepoDevcontainerOptions{
|
||||
Actor: opts.Actor,
|
||||
Repository: repositoryInDB,
|
||||
}
|
||||
return DevcontainersService.CreateRepoDevcontainer(ctx, optsCreateDevcontainer)
|
||||
})
|
||||
|
||||
return errTxn
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package api_services
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
|
||||
gitea_web_context "code.gitea.io/gitea/services/context"
|
||||
DevcontainersService "code.gitea.io/gitea/services/devstar_devcontainer"
|
||||
devcontainer_service_errors "code.gitea.io/gitea/services/devstar_devcontainer/errors"
|
||||
devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
|
||||
"context"
|
||||
)
|
||||
|
||||
// DeleteDevcontainerAPIService API 专用删除 DevContainer Service
|
||||
func DeleteDevcontainerAPIService(ctx *gitea_web_context.Context, opts *devcontainer_service_options.AbstractDeleteDevcontainerOptions) error {
|
||||
// 0. 检查用户传入参数
|
||||
if ctx == nil || opts == nil || opts.Actor == nil || opts.RepoId <= 0 {
|
||||
return devcontainer_service_errors.ErrIllegalParams{
|
||||
FieldNameList: []string{"ctx", "opts", "opts.Actor", "opts.RepoId"},
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 开启数据库事务,查询 某用户在某仓库的 DevContainer
|
||||
errTxn := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
// 1.1 调用 model层,查询数据库,将 repoId 变换为 Repository 对象
|
||||
repositoryInDB, err := repo.GetRepositoryByID(ctx, opts.RepoId)
|
||||
if err != nil || repositoryInDB == nil {
|
||||
return devcontainer_service_errors.ErrIllegalParams{
|
||||
FieldNameList: []string{"opts.RepoId"},
|
||||
}
|
||||
}
|
||||
|
||||
// 1.2 直接尝试删除 DevContainer,若报错,则说明不存在 DevContainer
|
||||
optsRepoDevcontainer := &DevcontainersVO.RepoDevcontainerOptions{
|
||||
Actor: opts.Actor,
|
||||
Repository: repositoryInDB,
|
||||
}
|
||||
err = DevcontainersService.DeleteRepoDevcontainer(ctx, optsRepoDevcontainer)
|
||||
if err != nil {
|
||||
return devcontainer_service_errors.ErrDevcontainerNotFound{
|
||||
Actor: opts.Actor,
|
||||
Repository: repositoryInDB,
|
||||
}
|
||||
}
|
||||
|
||||
// 1.3 查询 DevContainer 成功,结束数据库事务
|
||||
return nil
|
||||
})
|
||||
|
||||
// 2. 返回删除结果
|
||||
return errTxn
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package api_services
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
|
||||
gitea_web_context "code.gitea.io/gitea/services/context"
|
||||
DevcontainersService "code.gitea.io/gitea/services/devstar_devcontainer"
|
||||
devcontainer_service_errors "code.gitea.io/gitea/services/devstar_devcontainer/errors"
|
||||
devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
|
||||
"context"
|
||||
)
|
||||
|
||||
// OpenDevcontainerAPIService API 专用获取 DevContainer Service
|
||||
func OpenDevcontainerAPIService(ctx *gitea_web_context.Context, opts *devcontainer_service_options.AbstractOpenDevcontainerOptions) (*DevcontainersVO.RepoDevContainerVO, error) {
|
||||
// 0. 检查用户传入参数
|
||||
if ctx == nil || opts == nil || opts.Actor == nil || opts.RepoId <= 0 {
|
||||
return nil, devcontainer_service_errors.ErrIllegalParams{
|
||||
FieldNameList: []string{"ctx", "opts", "opts.Actor", "opts.RepoId"},
|
||||
}
|
||||
}
|
||||
|
||||
var devcontainerDetails DevcontainersVO.RepoDevContainerVO
|
||||
|
||||
// 1. 开启数据库事务,查询 某用户在某仓库的 DevContainer
|
||||
errTxn := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
// 1.1 调用 model层,查询数据库,将 repoId 变换为 Repository 对象
|
||||
repositoryInDB, err := repo.GetRepositoryByID(ctx, opts.RepoId)
|
||||
if err != nil || repositoryInDB == nil {
|
||||
return devcontainer_service_errors.ErrIllegalParams{
|
||||
FieldNameList: []string{"opts.RepoId"},
|
||||
}
|
||||
}
|
||||
|
||||
// 1.2 检查该用户在该仓库 是否已经创建过 DevContainer
|
||||
optsRepoDevcontainer := &DevcontainersVO.RepoDevcontainerOptions{
|
||||
Actor: opts.Actor,
|
||||
Repository: repositoryInDB,
|
||||
}
|
||||
devcontainerDetails, err = DevcontainersService.GetRepoDevcontainerDetails(ctx, optsRepoDevcontainer)
|
||||
if err != nil || devcontainerDetails.DevContainerId <= 0 {
|
||||
return devcontainer_service_errors.ErrDevcontainerNotFound{
|
||||
Actor: opts.Actor,
|
||||
Repository: repositoryInDB,
|
||||
}
|
||||
}
|
||||
|
||||
// 1.3 查询 DevContainer 成功,结束数据库事务
|
||||
return nil
|
||||
})
|
||||
if errTxn != nil {
|
||||
return nil, errTxn
|
||||
}
|
||||
|
||||
// 2. 调用抽象层获取 DevContainer 最新状态(需要根据用户传入的 wait 参数决定是否要阻塞等待 DevContainer 就绪)
|
||||
optsOpenDevcontainer := &devcontainer_service_options.OpenDevcontainerAppDispatcherOptions{
|
||||
Name: devcontainerDetails.DevContainerName,
|
||||
Wait: opts.Wait,
|
||||
}
|
||||
openDevcontainerAbstractAgentVO, err := DevcontainersService.OpenDevcontainerService(ctx, optsOpenDevcontainer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 更新VO,合并成为真正的 SSH连接信息
|
||||
devcontainerDetails.DevContainerPort = openDevcontainerAbstractAgentVO.NodePortAssigned
|
||||
return &devcontainerDetails, nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package dto
|
||||
|
||||
import user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
// CreateDevcontainerDTO 封装 API 创建 DevContainer 数据,即 router 层 POST /api/devcontainer 向下传递的数据结构
|
||||
type CreateDevcontainerDTO struct {
|
||||
Actor *user_model.User
|
||||
RepoId int64
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ErrDevcontainerAlreadyCreated struct {
|
||||
Actor *user.User
|
||||
Repository *repo.Repository
|
||||
}
|
||||
|
||||
func (err ErrDevcontainerAlreadyCreated) Error() string {
|
||||
return fmt.Sprintf("Failed to create DevContainer in repo '%s'(repoId = %d) by user '%s'(userId = %d), since it already exists.",
|
||||
err.Repository.Name, err.Repository.ID, err.Actor.Name, err.Actor.ID)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ErrDevcontainerNotFound struct {
|
||||
Actor *user.User
|
||||
Repository *repo.Repository
|
||||
}
|
||||
|
||||
func (err ErrDevcontainerNotFound) Error() string {
|
||||
return fmt.Sprintf("DevContainer NOT found in repo '%s'(repoId = %d) of user '%s'(userId = %d)",
|
||||
err.Repository.Name, err.Repository.ID, err.Actor.Name, err.Actor.ID)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ErrOperateDevcontainer struct {
|
||||
Action string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (err ErrOperateDevcontainer) Error() string {
|
||||
return fmt.Sprintf("Failed to %s in DevContainer Service: %s", err.Action, err.Message)
|
||||
}
|
||||
11
services/devstar_devcontainer/errors/ErrIllegalParams.go
Normal file
11
services/devstar_devcontainer/errors/ErrIllegalParams.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ErrIllegalParams struct {
|
||||
FieldNameList []string
|
||||
}
|
||||
|
||||
func (err ErrIllegalParams) Error() string {
|
||||
return fmt.Sprintf("Illegal Params: %v", err.FieldNameList)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package k8s_agent
|
||||
|
||||
import (
|
||||
devstar_devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer"
|
||||
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"context"
|
||||
apimachinery_meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
devcontainer_k8s_agent_module "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent"
|
||||
)
|
||||
|
||||
// 补充笔记: modules/ 与 services/ 两个目录中的 k8s Agent 区别是什么?
|
||||
// - modules/ 与 k8s API Server 交互密切相关
|
||||
// - services/ 进行了封装,简化用户界面使用
|
||||
|
||||
// AssignDevcontainerCreation2K8sOperator 将 DevContainer 资源创建任务派遣至 k8s Operator,同时根据结果更新 NodePort
|
||||
//
|
||||
// 注意:本方法仍然在数据库事务中,因此不适合执行长时间操作,故需要后期异步判断 DevContainer 是否就绪
|
||||
func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContainer *devstar_devcontainer_models.DevstarDevcontainer) error {
|
||||
|
||||
// 1. 获取 Dynamic Client
|
||||
client, err := devcontainer_k8s_agent_module.GetKubernetesClient(ctx)
|
||||
if err != nil {
|
||||
// 层层返回错误,结束数据库事务
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 调用 modules 层 k8s Agent,执行创建资源
|
||||
opts := &devcontainer_dto.CreateDevcontainerOptions{
|
||||
CreateOptions: apimachinery_meta_v1.CreateOptions{},
|
||||
Name: newDevContainer.Name,
|
||||
Namespace: setting.Devstar.Devcontainer.Namespace,
|
||||
// TODO: 后期根据 .devcontainer/devcontainer.json 或默认设置选项,指定 devContainer image 和 初始化命令
|
||||
Image: "mcr.microsoft.com/devcontainers/base:dev-ubuntu-20.04",
|
||||
CommandList: []string{
|
||||
"/bin/bash",
|
||||
"-c",
|
||||
"echo 'root:root' | chpasswd && useradd -m -s /bin/bash username && echo 'username:password' | chpasswd && usermod -aG sudo username && apt-get update && apt-get install -y openssh-server && service ssh start && apt-get clean && tail -f /dev/null",
|
||||
},
|
||||
ContainerPort: 22,
|
||||
ServicePort: 22,
|
||||
}
|
||||
|
||||
// 2. 创建成功,取回集群中的 DevContainer
|
||||
devcontainerInCluster, err := devcontainer_k8s_agent_module.CreateDevcontainer(ctx, client, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 将分配的 NodePort Service 写回 newDevcontainer,供写入数据库进行下一步操作
|
||||
newDevContainer.DevcontainerPort = devcontainerInCluster.Status.NodePortAssigned
|
||||
|
||||
// 4. 层层返回 nil,自动提交数据库事务,完成 DevContainer 创建
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package k8s_agent
|
||||
|
||||
import (
|
||||
devstar_devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer"
|
||||
devcontainer_k8s_agent_module "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent"
|
||||
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
|
||||
devcontainer_errors "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/errors"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"context"
|
||||
"fmt"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// 补充笔记: modules/ 与 services/ 两个目录中的 k8s Agent 区别是什么?
|
||||
// - modules/ 与 k8s API Server 交互密切相关
|
||||
// - services/ 进行了封装,简化用户界面使用
|
||||
|
||||
func AssignDevcontainerDeletion2K8sOperator(ctx *context.Context, devcontainersList *[]devstar_devcontainer_models.DevstarDevcontainer) error {
|
||||
|
||||
// 1. 获取 Dynamic Client
|
||||
client, err := devcontainer_k8s_agent_module.GetKubernetesClient(ctx)
|
||||
if err != nil {
|
||||
// 层层返回错误,结束数据库事务
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 调用 modules 层 k8s Agent,执行删除资源
|
||||
opts := &devcontainer_dto.DeleteDevcontainerOptions{
|
||||
DeleteOptions: metav1.DeleteOptions{},
|
||||
Namespace: setting.Devstar.Devcontainer.Namespace,
|
||||
}
|
||||
if devcontainersList == nil || len(*devcontainersList) == 0 {
|
||||
return devcontainer_errors.ErrOperateDevcontainer{
|
||||
Action: fmt.Sprintf("Delete DevContainer in namespace '%s'", opts.Namespace),
|
||||
Message: "The DevContainer List is empty",
|
||||
}
|
||||
}
|
||||
// 遍历列表删除 DevContainer,如果删除出错,交由 module 层打印日志,交由管理员手动处理
|
||||
for _, devcontainer := range *devcontainersList {
|
||||
opts.Name = devcontainer.Name
|
||||
_ = devcontainer_k8s_agent_module.DeleteDevcontainer(ctx, client, opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package k8s_agent
|
||||
|
||||
import (
|
||||
devcontainer_k8s_agent_module "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent"
|
||||
devcontainer_api_v1 "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/api/v1"
|
||||
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/devstar_devcontainer/errors"
|
||||
devcontainer_k8s_agent_errors "code.gitea.io/gitea/services/devstar_devcontainer/k8s_agent/errors"
|
||||
devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
|
||||
"context"
|
||||
"fmt"
|
||||
apimachinery_api_metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// AssignDevcontainerGetting2K8sOperator 获取 k8s CRD 资源 DevcontainerApp 最新状态(需要根据用户传入的 wait 参数决定是否要阻塞等待 DevContainer 就绪)
|
||||
func AssignDevcontainerGetting2K8sOperator(ctx *context.Context, opts *devcontainer_service_options.OpenDevcontainerAppDispatcherOptions) (*devcontainer_api_v1.DevcontainerApp, error) {
|
||||
|
||||
// 0. 检查参数
|
||||
if ctx == nil || opts == nil || len(opts.Name) == 0 {
|
||||
return nil, devcontainer_k8s_agent_errors.ErrIllegalK8sAgentParams{
|
||||
FieldNameList: []string{"ctx", "opts", "opts.Name"},
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 获取 Dynamic Client
|
||||
client, err := devcontainer_k8s_agent_module.GetKubernetesClient(ctx)
|
||||
if err != nil {
|
||||
// 层层返回错误,结束数据库事务
|
||||
return nil, errors.ErrOperateDevcontainer{
|
||||
Action: "Connect to k8s API Server",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 调用 modules 层 k8s Agent 获取 k8s CRD 资源 DevcontainerApp
|
||||
optsGetDevcontainer := &devcontainer_dto.GetDevcontainerOptions{
|
||||
GetOptions: apimachinery_api_metav1.GetOptions{},
|
||||
Name: opts.Name,
|
||||
Namespace: setting.Devstar.Devcontainer.Namespace,
|
||||
Wait: opts.Wait,
|
||||
}
|
||||
devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(ctx, client, optsGetDevcontainer)
|
||||
if err != nil {
|
||||
return nil, errors.ErrOperateDevcontainer{
|
||||
Action: fmt.Sprintf("Open Devcontainer '%s' (wait=%v)", opts.Name, opts.Wait),
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 成功获取最新的 DevcontainerApp,返回
|
||||
return devcontainerApp, nil
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ErrDeleteDevcontainers struct {
|
||||
NameList []string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
func (err ErrDeleteDevcontainers) Error() string {
|
||||
return fmt.Sprintf("Failed to delete DevContainer(s) in namespace '%s': %v", err.Namespace, err.NameList)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ErrIllegalK8sAgentParams struct {
|
||||
FieldNameList []string
|
||||
}
|
||||
|
||||
func (err ErrIllegalK8sAgentParams) Error() string {
|
||||
return fmt.Sprintf("Illegal Params: %v", err.FieldNameList)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
type AbstractDeleteDevcontainerOptions struct {
|
||||
Actor *user_model.User // 当前操作用户实体
|
||||
RepoId int64 // 仓库ID,用于 API Service 层查询数据库
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package options
|
||||
|
||||
import user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
// AbstractOpenDevcontainerOptions 封装 API 获取 DevContainer 数据,即 router 层 GET /api/devcontainer 向下传递的数据结构
|
||||
type AbstractOpenDevcontainerOptions struct {
|
||||
Wait bool // 标记用户在API调用时候是否希望阻塞等待 DevContainer 就绪
|
||||
Actor *user_model.User // 当前操作用户实体
|
||||
RepoId int64 // 仓库ID,用于 API Service 层查询数据库
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package options
|
||||
|
||||
type OpenDevcontainerAppDispatcherOptions struct {
|
||||
Name string `json:"name"`
|
||||
Wait bool `json:"wait"`
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package vo
|
||||
|
||||
// OpenDevcontainerAbstractAgentVO 封装查询 DevContainer 实时信息,与具体 agent 无关,返回前端
|
||||
type OpenDevcontainerAbstractAgentVO struct {
|
||||
NodePortAssigned uint16
|
||||
}
|
||||
19
services/forms/devcontainer_form.go
Normal file
19
services/forms/devcontainer_form.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.com/go-chi/binding"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CreateRepoDevcontainerForm 用户使用 API 创建仓库 DevContainer POST 表单数据绑定与校验规则
|
||||
type CreateRepoDevcontainerForm struct {
|
||||
RepoId string `json:"repoId" binding:"Required;MinSize(1);MaxSize(19);PositiveBase10IntegerNumberRule"`
|
||||
}
|
||||
|
||||
// Validate 用户使用 API 创建仓库 DevContainer POST 表单数据校验器
|
||||
func (f *CreateRepoDevcontainerForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/web/devcontainer/vo"
|
||||
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
|
||||
DevcontainersService "code.gitea.io/gitea/services/devstar_devcontainer"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
@@ -33,14 +33,6 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Repo Name</td>
|
||||
<td>{{.DevContainer.RepoName}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Repo Description</td>
|
||||
<td>{{.DevContainer.RepoDescription}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DevContainer Name</td>
|
||||
<td>{{.DevContainer.DevContainerName}}</td>
|
||||
@@ -49,24 +41,25 @@
|
||||
<td>Host</td>
|
||||
<td>{{.DevContainer.DevContainerHost}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Port</td>
|
||||
<td>{{.DevContainer.DevContainerPort}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Username</td>
|
||||
<td>{{.DevContainer.DevContainerUsername}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password</td>
|
||||
<td>{{.DevContainer.DevContainerPassword}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Work Dir</td>
|
||||
<td>{{.DevContainer.DevContainerWorkDir}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Repo Name</td>
|
||||
<td>{{.DevContainer.RepoName}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Repo Description</td>
|
||||
<td>{{.DevContainer.RepoDescription}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>*See GET /api/devcontainer with param 'repoId' and 'wait'</p>
|
||||
{{end}}
|
||||
</div>
|
||||
<!-- 结束:Dev Container 正文内容 - 左侧主展示区 -->
|
||||
@@ -87,7 +80,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
{{if not .HasValidDevContainerJSON}}
|
||||
<div class="item">{{svg "octicon-alert" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.dev_container_lack_of_config_prompt"}} </div>
|
||||
<div class="item">{{svg "octicon-alert" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.dev_container_invalid_config_prompt"}} </div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user