!74 vscode插件相关功能
Some checks failed
DevStar Studio CI Pipeline - dev branch / build-and-push-x86-64-docker-image (push) Failing after 17m34s
Some checks failed
DevStar Studio CI Pipeline - dev branch / build-and-push-x86-64-docker-image (push) Failing after 17m34s
Merge pull request !74 from Levi Yan/feature-vscode-home
This commit is contained in:
@@ -13,7 +13,7 @@ type Devcontainer struct {
|
||||
Id int64 `xorm:"BIGINT pk NOT NULL autoincr 'id' comment('主键,devContainerId')"`
|
||||
Name string `xorm:"VARCHAR(64) charset=utf8mb4 collate=utf8mb4_bin UNIQUE NOT NULL 'name' comment('devContainer名称,自动生成')"`
|
||||
DevcontainerHost string `xorm:"VARCHAR(256) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_host' comment('SSH Host')"`
|
||||
DevcontainerPort uint16 `xorm:"SMALLINT UNSIGNED NOT NULL 'devcontainer_port' comment('SSH Port')"`
|
||||
DevcontainerStatus uint16 `xorm:"SMALLINT UNSIGNED NOT NULL 'devcontainer_status' comment('SSH Status')"`
|
||||
DevcontainerUsername string `xorm:"VARCHAR(32) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_username' comment('SSH Username')"`
|
||||
DevcontainerWorkDir string `xorm:"VARCHAR(256) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_work_dir' comment('SSH 工作路径,典型值 ~/${project_name},256字节以内')"`
|
||||
RepoId int64 `xorm:"BIGINT NOT NULL 'repo_id' comment('repository表主键')"`
|
||||
|
||||
@@ -1020,8 +1020,8 @@ visibility.private_tooltip=仅对您已加入的组织的成员可见。
|
||||
|
||||
[repo]
|
||||
dev_container = 开发容器
|
||||
dev_container_empty = 您还没有该仓库的开发容器配置
|
||||
dev_container_invalid_config_prompt = 开发容器配置无效:请上传有效的 devcontainer.json 至默认分支,且确保仓库未处于存档状态
|
||||
dev_container_empty = 本仓库没有开发容器配置
|
||||
dev_container_invalid_config_prompt = 开发容器配置无效:需要上传有效的 devcontainer.json 至默认分支,且确保仓库未处于存档状态
|
||||
dev_container_control.update = 保存开发容器
|
||||
dev_container_control.create = 创建开发容器
|
||||
dev_container_control.creation_success_for_user = 用户 '%s' 已成功创建开发容器
|
||||
|
||||
@@ -6,7 +6,7 @@ NAME=DevStar-Studio
|
||||
IMAGE_REGISTRY_USER=mengning997
|
||||
IMAGE_NAME=devstar-studio
|
||||
VERSION=latest # DevStar Studio的默认版本为最新版本
|
||||
PORT=8080 # 设置端口默认值为 8080
|
||||
PORT=80 # 设置端口默认值为 80
|
||||
SSH_PORT=2222 # 设置ssh默认端口号2222
|
||||
DATA_DIR=${HOME}/devstar_data
|
||||
APP_INI=${DATA_DIR}/app.ini
|
||||
@@ -151,7 +151,7 @@ function usage {
|
||||
success "DevStar usage help:"
|
||||
success " help, -h, --help, Help information"
|
||||
success " start Start DevStar Studio"
|
||||
success " --port=<arg> Specify the port number (default port is 8080)"
|
||||
success " --port=<arg> Specify the port number (default port is 80)"
|
||||
success " --ssh-port=<arg> Specify the ssh-port number (default ssh-port is 2222)"
|
||||
success " --version=<arg> Specify the DevStar Studio Image Version (default verson is latest)"
|
||||
success " --image=<arg> Specify the DevStar Studio Image example: devstar-studio:latest "
|
||||
|
||||
@@ -3,6 +3,7 @@ package devcontainer
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
web_module "code.gitea.io/gitea/modules/web"
|
||||
Result "code.gitea.io/gitea/routers/entity"
|
||||
@@ -58,6 +59,20 @@ func CreateRepoDevcontainer(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// 4. 调用 API Service 层创建 DevContainer
|
||||
repo, err := repo.GetRepositoryByID(ctx, repoId)
|
||||
if err != nil {
|
||||
errCreateDevcontainer := Result.ResultType{
|
||||
Code: Result.RespFailedCreateDevcontainer.Code,
|
||||
Msg: Result.RespFailedCreateDevcontainer.Msg,
|
||||
Data: map[string]string{
|
||||
"ErrorMsg": "repo not found",
|
||||
},
|
||||
}
|
||||
errCreateDevcontainer.RespondJson2HttpResponseWriter(ctx.Resp)
|
||||
return
|
||||
}
|
||||
devcontainer_service.CreateDevcontainerJSON(ctx, repo, ctx.Doer)
|
||||
|
||||
opts := &devcontainer_service.CreateDevcontainerOptions{
|
||||
Actor: ctx.Doer,
|
||||
RepoId: repoId,
|
||||
|
||||
@@ -48,6 +48,16 @@ func GetRepoDevContainerDetails(ctx *context.Context) {
|
||||
ctx.Data["InitializedContainer"] = false
|
||||
}
|
||||
}
|
||||
ctx.Data["isAdmin"] = false
|
||||
if ctx.Doer.IsAdmin {
|
||||
ctx.Data["isAdmin"] = true
|
||||
ctx.Data["canRead"] = true
|
||||
} else {
|
||||
canRead, _ := devcontainer_service.CanCreateDevcontainer(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID)
|
||||
ctx.Data["canRead"] = canRead
|
||||
isAdmin, _ := devcontainer_service.IsOwner(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID)
|
||||
ctx.Data["isAdmin"] = isAdmin
|
||||
}
|
||||
|
||||
//ctx.Repo.RepoLink == ctx.Repo.Repository.Link()
|
||||
devContainerMetadata, err := devcontainer_service.GetRepoDevcontainerDetails(ctx, opts)
|
||||
@@ -58,12 +68,41 @@ func GetRepoDevContainerDetails(ctx *context.Context) {
|
||||
if hasDevContainer {
|
||||
ctx.Data["DevContainer"] = devContainerMetadata
|
||||
}
|
||||
ctx.Data["PullImage"] = false
|
||||
ctx.Data["CreateContainer"] = false
|
||||
ctx.Data["Init"] = false
|
||||
ctx.Data["Running"] = false
|
||||
ctx.Data["RestartOrStop"] = false
|
||||
ctx.Data["Save"] = false
|
||||
switch devContainerMetadata.DevContainerStatus {
|
||||
case 1:
|
||||
ctx.Data["PullImage"] = true
|
||||
case 2:
|
||||
ctx.Data["CreateContainer"] = true
|
||||
case 3:
|
||||
ctx.Data["Init"] = true
|
||||
case 4:
|
||||
ctx.Data["Running"] = true
|
||||
case 5:
|
||||
ctx.Data["Restart"] = true
|
||||
case 6:
|
||||
ctx.Data["Stop"] = true
|
||||
case 7:
|
||||
ctx.Data["Save"] = true
|
||||
default:
|
||||
log.Info("unknown status")
|
||||
}
|
||||
|
||||
// 2. 检查当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json
|
||||
isValidRepoDevcontainerJson := isValidRepoDevcontainerJsonFile(ctx)
|
||||
hasDockerfile := isValidRepoDevcontainerDockerfile(ctx)
|
||||
if !hasDevContainer && !isValidRepoDevcontainerJson {
|
||||
ctx.Flash.Error(ctx.Tr("repo.dev_container_invalid_config_prompt"), true)
|
||||
}
|
||||
ctx.Data["HasDockerfile"] = false
|
||||
if hasDockerfile {
|
||||
ctx.Data["HasDockerfile"] = true
|
||||
}
|
||||
// 从devcontainer.json文件提取image字段解析成仓库地址、命名空间、镜像名
|
||||
devcontainerJson, err := devcontainer_service.GetDevcontainerJsonModel(ctx, ctx.Repo.Repository)
|
||||
if err == nil {
|
||||
@@ -112,7 +151,11 @@ func GetRepoDevContainerDetails(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func CreateRepoDevContainerConfiguration(ctx *context.Context) {
|
||||
devcontainer_service.CreateDevcontainerJSON(ctx)
|
||||
if !ctx.Doer.IsAdmin {
|
||||
ctx.Flash.Error("permisson denied", true)
|
||||
return
|
||||
}
|
||||
devcontainer_service.CreateDevcontainerJSON(ctx, ctx.Repo.Repository, ctx.Doer)
|
||||
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/dev-container"))
|
||||
}
|
||||
func ParseImageName(imageName string) (registry, namespace, repo, tag string) {
|
||||
@@ -173,6 +216,28 @@ func isValidRepoDevcontainerJsonFile(ctx *context.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// 辅助判断当前仓库的当前分支是否存在有效的 /.devcontainer/Dockerfile
|
||||
func isValidRepoDevcontainerDockerfile(ctx *context.Context) bool {
|
||||
|
||||
// 1. 仓库非空,且非 Archived 状态
|
||||
if ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsArchived {
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. 当前分支的目录 .devcontainer 下存在 devcontainer.json 文件
|
||||
dockerfilePath, err := devcontainer_service.GetDockerfilePath(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
dockerfileExists, err := ctx.Repo.FileExists(".devcontainer/"+dockerfilePath, ctx.Repo.BranchName)
|
||||
if err != nil || !dockerfileExists {
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. TODO: DevContainer 格式正确
|
||||
return true
|
||||
}
|
||||
|
||||
// 辅助判断当前用户在当前仓库是否已有 Dev Container
|
||||
func isUserDevcontainerAlreadyInRepository(ctx *context.Context) bool {
|
||||
|
||||
@@ -186,7 +251,10 @@ func isUserDevcontainerAlreadyInRepository(ctx *context.Context) bool {
|
||||
}
|
||||
|
||||
func UpdateRepoDevContainerForCurrentActor(ctx *context.Context) {
|
||||
|
||||
if !ctx.Doer.IsAdmin {
|
||||
ctx.Flash.Error("permisson denied", true)
|
||||
return
|
||||
}
|
||||
opt := &devcontainer_service.RepoDevcontainerOptions{
|
||||
Actor: ctx.Doer,
|
||||
Repository: ctx.Repo.Repository,
|
||||
@@ -224,7 +292,6 @@ func UpdateRepoDevContainerForCurrentActor(ctx *context.Context) {
|
||||
|
||||
// 删除仓库 当前用户 Dev Container
|
||||
func DeleteRepoDevContainerForCurrentActor(ctx *context.Context) {
|
||||
|
||||
if isUserDevcontainerAlreadyInRepository(ctx) {
|
||||
opts := &devcontainer_service.RepoDevcontainerOptions{
|
||||
Actor: ctx.Doer,
|
||||
@@ -315,6 +382,7 @@ func RestartContainer(ctx *context.Context) {
|
||||
if err != nil {
|
||||
ctx.Flash.Error("fail to restart container")
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]string{})
|
||||
}
|
||||
func StopContainer(ctx *context.Context) {
|
||||
@@ -328,5 +396,6 @@ func StopContainer(ctx *context.Context) {
|
||||
if err != nil {
|
||||
ctx.Flash.Error("fail to stop container")
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]string{})
|
||||
}
|
||||
|
||||
@@ -1353,7 +1353,7 @@ func registerRoutes(m *web.Router) {
|
||||
m.Group("/{username}/{reponame}/dev-container", func() { // repo Dev Container
|
||||
m.Group("", func() {
|
||||
m.Combo("").Get(devcontainer_web.GetRepoDevContainerDetails)
|
||||
}, reqSignIn, context.RepoAssignment, reqRepoCodeReader, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived(), repo.MustBeEditable)
|
||||
}, context.RepoRef())
|
||||
|
||||
m.Get("/createConfiguration", devcontainer_web.CreateRepoDevContainerConfiguration)
|
||||
m.Get("/create", devcontainer_web.CreateRepoDevContainer, context.RepoMustNotBeArchived()) // 仓库状态非 Archived 才可以创建 DevContainer
|
||||
@@ -1367,7 +1367,7 @@ func registerRoutes(m *web.Router) {
|
||||
// 1. 已登录
|
||||
// 2. repo 信息已加载到 Gitea Web Context (否则无法判定当前repo是否有写入Code权限,从而返回无权访问错误码 HTTP 404)
|
||||
// 3. 具有code写入权限
|
||||
reqSignIn, context.RepoAssignment, reqRepoCodeWriter,
|
||||
reqSignIn, context.RepoAssignment, reqRepoCodeReader,
|
||||
)
|
||||
|
||||
m.Group("/{username}/{reponame}", func() { // repo tags
|
||||
|
||||
@@ -15,15 +15,17 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
devcontainer_model "code.gitea.io/gitea/models/devcontainer"
|
||||
devcontainer_models_errors "code.gitea.io/gitea/models/devcontainer/errors"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/docker"
|
||||
devcontainer_k8s_agent_module "code.gitea.io/gitea/modules/k8s"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
devcontainer_service_errors "code.gitea.io/gitea/services/devcontainer/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"code.gitea.io/gitea/services/devstar_ssh_key_pair/api_service"
|
||||
"github.com/google/uuid"
|
||||
@@ -128,7 +130,7 @@ func GetRepoDevcontainerDetails(ctx context.Context, opts *RepoDevcontainerOptio
|
||||
"devcontainer.id AS devcontainer_id,"+
|
||||
"devcontainer.name AS devcontainer_name,"+
|
||||
"devcontainer.devcontainer_host AS devcontainer_host,"+
|
||||
"devcontainer.devcontainer_port AS devcontainer_port,"+
|
||||
"devcontainer.devcontainer_status AS devcontainer_status,"+
|
||||
"devcontainer.devcontainer_username AS devcontainer_username,"+
|
||||
"devcontainer.devcontainer_work_dir AS devcontainer_work_dir,"+
|
||||
"devcontainer.repo_id AS repo_id,"+
|
||||
@@ -191,6 +193,7 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt
|
||||
DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(),
|
||||
DevcontainerUsername: "root",
|
||||
DevcontainerWorkDir: "/data/workspace",
|
||||
DevcontainerStatus: 0,
|
||||
RepoId: opts.Repository.ID,
|
||||
UserId: opts.Actor.ID,
|
||||
CreatedUnix: unixTimestamp,
|
||||
@@ -200,7 +203,20 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt
|
||||
Image: devContainerJson.Image,
|
||||
GitRepositoryURL: strings.TrimSuffix(setting.AppURL, "/") + opts.Repository.Link(),
|
||||
}
|
||||
|
||||
rowsAffect, err := db.GetEngine(ctx).
|
||||
Table("devcontainer").
|
||||
Insert(newDevcontainer.Devcontainer)
|
||||
if err != nil {
|
||||
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
||||
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
|
||||
Message: err.Error(),
|
||||
}
|
||||
} else if rowsAffect == 0 {
|
||||
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
||||
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
|
||||
Message: "expected 1 row to be inserted, but got 0",
|
||||
}
|
||||
}
|
||||
// 在数据库事务中创建 Dev Container 分配资源,出错时自动回滚相对应数据库字段,保证数据一致
|
||||
dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
@@ -246,21 +262,6 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
// 2. 根据分配的 NodePort 更新数据库字段
|
||||
rowsAffect, err := db.GetEngine(ctx).
|
||||
Table("devcontainer").
|
||||
Insert(newDevcontainer.Devcontainer)
|
||||
if err != nil {
|
||||
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
||||
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
|
||||
Message: err.Error(),
|
||||
}
|
||||
} else if rowsAffect == 0 {
|
||||
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
||||
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
|
||||
Message: "expected 1 row to be inserted, but got 0",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -436,7 +437,8 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContai
|
||||
|
||||
}
|
||||
return "://mengning.devstar/" +
|
||||
"openProject?host=" + devcontainer.DevContainerHost +
|
||||
"openProject?host=" + devcontainer.RepoName +
|
||||
"&hostname=" + devcontainer.DevContainerHost +
|
||||
"&port=" + port +
|
||||
"&username=" + devcontainer.DevContainerUsername +
|
||||
"&path=" + devcontainer.DevContainerWorkDir +
|
||||
@@ -733,3 +735,40 @@ func StopDevcontainer(gitea_ctx context.Context, opts *RepoDevContainer) error {
|
||||
}
|
||||
|
||||
}
|
||||
func CanCreateDevcontainer(gitea_ctx context.Context, repoID, userID int64) (bool, error) {
|
||||
e := db.GetEngine(gitea_ctx)
|
||||
|
||||
teamMember, err := e.Table("team_user").
|
||||
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
|
||||
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
|
||||
Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
|
||||
repoID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypeCode).
|
||||
And("team_user.uid = ?", userID).Exist()
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
if teamMember {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return repo_model.IsCollaborator(gitea_ctx, repoID, userID)
|
||||
|
||||
}
|
||||
func IsOwner(gitea_ctx context.Context, repoID, userID int64) (bool, error) {
|
||||
e := db.GetEngine(gitea_ctx)
|
||||
|
||||
teamMember, err := e.Table("team_user").
|
||||
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
|
||||
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
|
||||
Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode = ? ",
|
||||
repoID, perm.AccessModeAdmin).
|
||||
And("team_user.uid = ?", userID).Exist()
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
if teamMember {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return e.Get(&repo_model.Collaboration{RepoID: repoID, UserID: userID, Mode: 3})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
devcontainer_model "code.gitea.io/gitea/models/devcontainer"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -33,7 +34,7 @@ type DevStarJSON struct {
|
||||
RunArgs []string
|
||||
}
|
||||
|
||||
func CreateDevcontainerJSON(ctx *gitea_context.Context) {
|
||||
func CreateDevcontainerJSON(ctx *gitea_context.Context, repo *repo.Repository, doer *user.User) {
|
||||
jsonString := `{
|
||||
"image":"mcr.microsoft.com/devcontainers/base:dev-ubuntu-20.04",
|
||||
"forwardPorts": [
|
||||
@@ -55,7 +56,7 @@ func CreateDevcontainerJSON(ctx *gitea_context.Context) {
|
||||
"8888:8888"
|
||||
]
|
||||
}`
|
||||
_, err := files_service.ChangeRepoFiles(db.DefaultContext, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
||||
_, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, doer, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
@@ -210,8 +211,16 @@ func GetFileContentByPath(ctx context.Context, repo *repo.Repository, path strin
|
||||
func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (string, error) {
|
||||
return GetFileContentByPath(ctx, repo, ".devcontainer/devcontainer.json")
|
||||
}
|
||||
|
||||
func GetDockerfileContent(ctx context.Context, repo *repo.Repository) (string, error) {
|
||||
dockerfilePath, err := GetDockerfilePath(ctx, repo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return GetFileContentByPath(ctx, repo, ".devcontainer/"+dockerfilePath)
|
||||
}
|
||||
|
||||
func GetDockerfilePath(ctx context.Context, repo *repo.Repository) (string, error) {
|
||||
devcontainerJSONContent, err := GetDevcontainerJsonString(ctx, repo)
|
||||
var devContainerJson *devcontainer_model.DevContainerJSON
|
||||
if err != nil {
|
||||
@@ -230,10 +239,11 @@ func GetDockerfileContent(ctx context.Context, repo *repo.Repository) (string, e
|
||||
log.Error("Failed to unmarshal .devcontainer/devcontainer.json: %v", err)
|
||||
return "", err
|
||||
}
|
||||
if devContainerJson.Build.Dockerfile == "" {
|
||||
return "", nil
|
||||
if devContainerJson.Build == nil || devContainerJson.Build.Dockerfile == "" {
|
||||
return "", fmt.Errorf("devcontainer.json error")
|
||||
}
|
||||
return GetFileContentByPath(ctx, repo, ".devcontainer/"+devContainerJson.Build.Dockerfile)
|
||||
log.Info("%vsdadasdsa", devContainerJson.Build.Dockerfile)
|
||||
return devContainerJson.Build.Dockerfile, nil
|
||||
}
|
||||
|
||||
// 移除 JSON 文件中的注释
|
||||
|
||||
@@ -18,12 +18,11 @@ type RepoDevContainer struct {
|
||||
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" xorm:"repo_link"`
|
||||
RepoDescription string `json:"repoDescription,omitempty" xorm:"repo_description"`
|
||||
|
||||
// 实时查询获取,不再从数据库获取
|
||||
DevContainerPort uint16 `json:"devContainerPort,omitempty"`
|
||||
RepoOwnerName string `json:"repo_owner_name" xorm:"repo_owner_name"`
|
||||
RepoLink string `json:"repo_link" xorm:"repo_link"`
|
||||
RepoDescription string `json:"repoDescription,omitempty" xorm:"repo_description"`
|
||||
DevContainerPort uint16 `json:"devContainerPort,omitempty"`
|
||||
DevContainerStatus uint16 `json:"devContainerStatus,omitempty" xorm:"devcontainer_status"`
|
||||
}
|
||||
|
||||
// RepoDevcontainerOptions 仓库 Dev Container 条件,注意仓库的所有者可能与当前操作用户不一致!
|
||||
@@ -75,6 +74,7 @@ type CreateDevcontainerOptions struct {
|
||||
type OpenDevcontainerAppDispatcherOptions struct {
|
||||
Name string `json:"name"`
|
||||
Wait bool `json:"wait"`
|
||||
Status uint16
|
||||
Port uint16
|
||||
UserPublicKey string
|
||||
RepoID int64
|
||||
@@ -111,4 +111,5 @@ type CreateDevcontainerDTO struct {
|
||||
GitRepositoryURL string
|
||||
Image string
|
||||
DockerfileContent string
|
||||
DevcontainerPort uint16
|
||||
}
|
||||
|
||||
@@ -173,18 +173,14 @@ func SaveDevcontainer(ctx *gitea_web_context.Context, opts *UpdateDevcontainerOp
|
||||
return fmt.Errorf("创建docker client失败 %v", err)
|
||||
}
|
||||
defer cli.Close()
|
||||
if opts.SaveMethod == "Container" {
|
||||
// 获取容器ID
|
||||
containerID, err := docker_module.GetContainerID(cli, opts.DevContainerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取容器ID失败 %v", err)
|
||||
}
|
||||
// 提交容器
|
||||
_, err = cli.ContainerCommit(ctx, containerID, types.ContainerCommitOptions{Reference: imageRef})
|
||||
if err != nil {
|
||||
return fmt.Errorf("提交容器失败 %v", err)
|
||||
}
|
||||
} else if opts.SaveMethod == "DockerFile" {
|
||||
dbEngine := db.GetEngine(*ctx)
|
||||
_, err = dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", opts.Actor.ID, opts.Repository.ID).
|
||||
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 6})
|
||||
if err != nil {
|
||||
log.Info("err %v", err)
|
||||
}
|
||||
if opts.SaveMethod == "on" {
|
||||
// 创建构建上下文(包含Dockerfile的tar包)
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
@@ -218,6 +214,18 @@ func SaveDevcontainer(ctx *gitea_web_context.Context, opts *UpdateDevcontainerOp
|
||||
log.Info(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
} else {
|
||||
// 获取容器ID
|
||||
containerID, err := docker_module.GetContainerID(cli, opts.DevContainerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取容器ID失败 %v", err)
|
||||
}
|
||||
// 提交容器
|
||||
_, err = cli.ContainerCommit(ctx, containerID, types.ContainerCommitOptions{Reference: imageRef})
|
||||
if err != nil {
|
||||
return fmt.Errorf("提交容器失败 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 推送到仓库
|
||||
@@ -232,13 +240,26 @@ func SaveDevcontainer(ctx *gitea_web_context.Context, opts *UpdateDevcontainerOp
|
||||
// 使用正则表达式查找并替换 image 字段的值
|
||||
newJSONStr := re.ReplaceAllString(devcontainerJson, `"image": "`+imageRef+`"`)
|
||||
|
||||
_, err = dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", opts.Actor.ID, opts.Repository.ID).
|
||||
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 4})
|
||||
if err != nil {
|
||||
log.Info("err %v", err)
|
||||
}
|
||||
return UpdateDevcontainerJSON(ctx, newJSONStr)
|
||||
}
|
||||
|
||||
func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, dockerHost string, opts *docker_module.CreateDevcontainerOptions) error {
|
||||
var stdoutScanner, stderrScanner *bufio.Scanner
|
||||
// 创建扫描器来读取输出
|
||||
dbEngine := db.GetEngine(*ctx)
|
||||
|
||||
_, err := dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
|
||||
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 1})
|
||||
if err != nil {
|
||||
log.Info("err %v", err)
|
||||
}
|
||||
if opts.DockerfileContent != "" {
|
||||
|
||||
// 创建构建上下文(包含Dockerfile的tar包)
|
||||
@@ -295,7 +316,6 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d
|
||||
stderrScanner = bufio.NewScanner(stderr)
|
||||
}
|
||||
|
||||
dbEngine := db.GetEngine(*ctx)
|
||||
var pullImageOutput = devcontainer_models.DevcontainerOutput{
|
||||
Output: "",
|
||||
ListId: 0,
|
||||
@@ -308,7 +328,8 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d
|
||||
log.Info("Failed to insert record: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err := dbEngine.Table("devcontainer_output").
|
||||
|
||||
_, err = dbEngine.Table("devcontainer_output").
|
||||
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
|
||||
Get(&pullImageOutput)
|
||||
if err != nil {
|
||||
@@ -401,7 +422,13 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d
|
||||
Update(&devcontainer_models.DevcontainerOutput{Status: "success"})
|
||||
|
||||
// 创建并启动容器
|
||||
output, err := docker_module.CreateAndStartContainer(cli, opts)
|
||||
_, err := dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
|
||||
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 2})
|
||||
if err != nil {
|
||||
log.Info("err %v", err)
|
||||
}
|
||||
output, err = docker_module.CreateAndStartContainer(cli, opts)
|
||||
if err != nil {
|
||||
log.Info("创建或启动容器失败: %v", err)
|
||||
}
|
||||
@@ -423,7 +450,12 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d
|
||||
log.Info("Error storing output for command %v: %v\n", opts.CommandList[2], err)
|
||||
}
|
||||
// 创建 exec 实例
|
||||
|
||||
_, err = dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
|
||||
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 3})
|
||||
if err != nil {
|
||||
log.Info("err %v", err)
|
||||
}
|
||||
var buffer string = ""
|
||||
var state int = 2
|
||||
for index, cmd := range opts.PostCreateCommand {
|
||||
@@ -465,6 +497,12 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d
|
||||
if err != nil {
|
||||
log.Info("Error storing output for command pull image: %v\n", err)
|
||||
}
|
||||
_, err = dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
|
||||
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 4})
|
||||
if err != nil {
|
||||
log.Info("err %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
@@ -482,6 +520,13 @@ func DockerRestartContainer(gitea_ctx *gitea_web_context.Context, opts *RepoDevC
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取容器ID失败 %v", err)
|
||||
}
|
||||
dbEngine := db.GetEngine(ctx)
|
||||
_, err = dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
|
||||
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 5})
|
||||
if err != nil {
|
||||
log.Info("err %v", err)
|
||||
}
|
||||
timeout := 10 // 超时时间(秒)
|
||||
err = cli.ContainerRestart(context.Background(), containerID, container.StopOptions{
|
||||
Timeout: &timeout,
|
||||
@@ -499,7 +544,7 @@ func DockerRestartContainer(gitea_ctx *gitea_web_context.Context, opts *RepoDevC
|
||||
cmd := []string{"/home/devcontainer_restart.sh"}
|
||||
postCreateCommand := append(cmd, devContainerJson.PostCreateCommand...)
|
||||
// 创建 exec 实例
|
||||
dbEngine := db.GetEngine(ctx)
|
||||
|
||||
var buffer string = ""
|
||||
var state int = 2
|
||||
_, err = dbEngine.Table("devcontainer_output").
|
||||
@@ -572,6 +617,13 @@ func DockerRestartContainer(gitea_ctx *gitea_web_context.Context, opts *RepoDevC
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
|
||||
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 4})
|
||||
log.Info("DockerRestartContainerDockerRestartContainerDockerRestartContainer")
|
||||
if err != nil {
|
||||
log.Info("err %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func DockerStopContainer(ctx *context.Context, opts *RepoDevContainer) error {
|
||||
@@ -586,6 +638,13 @@ func DockerStopContainer(ctx *context.Context, opts *RepoDevContainer) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取容器ID失败 %v", err)
|
||||
}
|
||||
dbEngine := db.GetEngine(*ctx)
|
||||
_, err = dbEngine.Table("devcontainer").
|
||||
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
|
||||
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 6})
|
||||
if err != nil {
|
||||
log.Info("err %v", err)
|
||||
}
|
||||
timeout := 10 // 超时时间(秒)
|
||||
err = cli.ContainerStop(context.Background(), containerID, container.StopOptions{
|
||||
Timeout: &timeout,
|
||||
|
||||
1097
templates/devstar-home-vscode-js.tmpl
Normal file
1097
templates/devstar-home-vscode-js.tmpl
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,13 +8,17 @@
|
||||
<!-- 开始:Dev Container 正文内容 - 左侧主展示区 -->
|
||||
<div class="issue-content-left">
|
||||
{{if not .HasValidDevContainerJSON}}
|
||||
|
||||
<div class="empty-placeholder">
|
||||
{{svg "octicon-container" 48}}
|
||||
<h2>{{ctx.Locale.Tr "repo.dev_container_empty"}}</h2>
|
||||
{{if .isAdmin}}
|
||||
<form method="get" action="{{.CreateDevcontainerSettingUrl}}" class="ui edit form">
|
||||
<button class="ui primary button" type="submit">Create</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
|
||||
<div class="ui container">
|
||||
@@ -49,9 +53,15 @@
|
||||
|
||||
{{if .HasDevContainer}}
|
||||
<div class="item"><a class="delete-button flex-text-inline" data-modal="#delete-repo-devcontainer-of-user-modal" href="#" data-url="{{.Repository.Link}}/dev-container/delete">{{svg "octicon-trash" 14}}{{ctx.Locale.Tr "repo.dev_container_control.delete"}}</a></div>
|
||||
{{if .isAdmin}}
|
||||
|
||||
{{if .Running}}
|
||||
<div class="item"><a class="delete-button flex-text-inline" style="color:black;" data-modal-id="updatemodal" href="#">{{svg "octicon-database"}}{{ctx.Locale.Tr "repo.dev_container_control.update"}}</a></div>
|
||||
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if .InitializedContainer}}
|
||||
{{if not .Restart}}
|
||||
{{if not .Stop}}
|
||||
<div class="item"><button id="restartButton" class="flex-text-inline" style="color:black;" onclick="handleClick(event, '{{.Repository.Link}}/dev-container/restart', document.getElementById('stopButton'))" >{{svg "octicon-terminal" 14 "tw-mr-2"}} Restart Dev Container</button></div>
|
||||
<div class="item"><button id="stopButton" class="flex-text-inline" style="color:black;" onclick="handleClick(event, '{{.Repository.Link}}/dev-container/stop', document.getElementById('restartButton'))" >{{svg "octicon-terminal" 14 "tw-mr-2"}} Stop Dev Container</button></div>
|
||||
|
||||
@@ -60,10 +70,16 @@
|
||||
<div class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.CursorUrl}}'">{{svg "octicon-code" 14}}open with Cursor</a ></div>
|
||||
<div class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.WindsurfUrl}}'">{{svg "octicon-code" 14}}open with Windsurf</a ></div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{else if .HasValidDevContainerJSON}}
|
||||
<div class="item">
|
||||
{{if not .isCreatingDevcontainer}}
|
||||
{{if .canRead}}
|
||||
<a class="button flex-text-inline" href="{{.Repository.Link}}/dev-container/create">{{svg "octicon-terminal" 14 "tw-mr-2"}} Create Dev Container</a>
|
||||
{{else}}
|
||||
<div class="button flex-text-inline" disabled>Permission Denied</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="button flex-text-inline" disabled>{{svg "octicon-terminal" 14 "tw-mr-2"}} Running</div>
|
||||
{{end}}
|
||||
@@ -138,15 +154,14 @@
|
||||
<div class="content">
|
||||
<form class="ui form tw-max-w-2xl tw-m-auto" id="updateForm" onsubmit="submitForm(event)">
|
||||
<div class="inline field">
|
||||
<label>SaveMethod</label>
|
||||
<div class="ui selection owner dropdown">
|
||||
<input type="hidden" id="SaveMethod" name="SaveMethod" value="{{.SaveMethod}}">
|
||||
<div class="default text">Container</div>
|
||||
<div class="menu">
|
||||
{{range .SaveMethods}}
|
||||
<div class="item" data-value="{{.}}">{{.}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="ui checkbox">
|
||||
{{if not .HasDockerfile}}
|
||||
<input type="checkbox" id="SaveMethod" name="SaveMethod" disabled>
|
||||
{{else}}
|
||||
<input type="checkbox" id="SaveMethod" name="SaveMethod" value="on">
|
||||
{{end}}
|
||||
<label for="SaveMethod">Build From Dockerfile</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="required field ">
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
{{end}}
|
||||
|
||||
<!-- 定义tab: DevContainer List -->
|
||||
{{if .Permission.CanWrite ctx.Consts.RepoUnitTypeCode}}
|
||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
|
||||
<a class="{{if .PageIsRepoDevcontainerDetails}}active {{end}}item" href="{{.RepoLink}}/dev-container">
|
||||
{{svg "octicon-container"}} {{ctx.Locale.Tr "repo.dev_container"}}
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user