!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

* 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:
戴明辰
2024-09-30 06:48:01 +00:00
parent fada46777a
commit 4b6f1b9cb5
76 changed files with 2714 additions and 248 deletions

View File

@@ -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:

View File

@@ -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:

View 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
#

View File

@@ -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/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")

26
go.mod
View File

@@ -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

503
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -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')"`

View 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
}

View 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
}

View 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,
}
}

View 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
}

View File

@@ -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"`
}

View File

@@ -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"}
)

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -0,0 +1,11 @@
package dto
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type ListDevcontainersOptions struct {
metav1.ListOptions
Namespace string `json:"namespace"`
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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,
)
}

View File

@@ -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)
}

View 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",
}

View 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
}

View File

@@ -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
}

View File

@@ -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"`
}

View 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")
}
}

View File

@@ -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

View File

@@ -217,6 +217,7 @@ func LoadSettings() {
loadMimeTypeMapFrom(CfgProvider)
loadFederationFrom(CfgProvider)
loadWechatSettingsFrom(CfgProvider)
loadDevstarDevcontainerFrom(CfgProvider)
}
// LoadSettingsForInstall initializes the settings for install

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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 != "" {

View File

@@ -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.

View File

@@ -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=此仓库已存档,您不能在此合并请求添加评论。

View 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)
}

View 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)
}

View 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)
}

View File

@@ -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 {

View File

@@ -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 条件,注意仓库的所有者可能与当前操作用户不一致!

View File

@@ -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"
)
/*

View File

@@ -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
}

View File

@@ -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 失败",
}

View File

@@ -8,8 +8,8 @@ var RespPendingQrNotScanned = ResultType{
Msg: "用户未扫描微信公众号带参数二维码",
}
// RespFailedIllegalParam 二维码凭证Ticket参数无效
var RespFailedIllegalParam = ResultType{
// RespFailedIllegalWechatQrTicket 二维码凭证Ticket参数无效
var RespFailedIllegalWechatQrTicket = ResultType{
Code: 10002,
Msg: "提交的微信公众号带参数二维码凭证Ticket参数无效",
}

View File

@@ -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"

View 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
}

View File

@@ -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")
}

View 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)
}

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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写入权限

View File

@@ -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
}

View File

@@ -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),
}
}
}

View File

@@ -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,"+

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View 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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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 层查询数据库
}

View File

@@ -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 层查询数据库
}

View File

@@ -0,0 +1,6 @@
package options
type OpenDevcontainerAppDispatcherOptions struct {
Name string `json:"name"`
Wait bool `json:"wait"`
}

View File

@@ -0,0 +1,6 @@
package vo
// OpenDevcontainerAbstractAgentVO 封装查询 DevContainer 实时信息,与具体 agent 无关,返回前端
type OpenDevcontainerAbstractAgentVO struct {
NodePortAssigned uint16
}

View 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)
}

View File

@@ -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"

View File

@@ -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>