diff --git a/README_ZH.md b/README_ZH.md index a3ff927d5b..4b2f2bd168 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -56,6 +56,10 @@ ENABLED = true AGENT = docker TIMEOUT_SECONDS = 120 HOST = 127.0.0.1 + +[devstar.ssh_key_pair] +KEY_SIZE = <写入希望生成的SSH密钥长度,比如 4096 ,默认值 2048> + ``` 正式部署单机版 @@ -146,6 +150,9 @@ data: HOST = NAMESPACE = + devstar.ssh_key_pair: | + KEY_SIZE = <写入希望生成的SSH密钥长度,比如 4096 ,默认值 2048> + ``` 对于 `Secrets` 类型,参考: diff --git a/models/devstar_devcontainer/devstar_devcontainer.go b/models/devstar_devcontainer/devstar_devcontainer.go index 5687f9b326..5085b798ba 100644 --- a/models/devstar_devcontainer/devstar_devcontainer.go +++ b/models/devstar_devcontainer/devstar_devcontainer.go @@ -15,7 +15,6 @@ type DevstarDevcontainer struct { 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')"` DevcontainerUsername string `xorm:"VARCHAR(32) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_username' comment('SSH Username')"` - DevcontainerPassword string `xorm:"VARCHAR(32) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_password' comment('SSH Password')"` 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表主键')"` UserId int64 `xorm:"BIGINT NOT NULL 'user_id' comment('user表主键')"` diff --git a/modules/devstar_devcontainer/k8s_agent/CreateDevcontainer.go b/modules/devstar_devcontainer/k8s_agent/CreateDevcontainer.go index a9fc59a5dc..07b98cea4e 100644 --- a/modules/devstar_devcontainer/k8s_agent/CreateDevcontainer.go +++ b/modules/devstar_devcontainer/k8s_agent/CreateDevcontainer.go @@ -35,9 +35,10 @@ func CreateDevcontainer(ctx *context.Context, client dynamic_client.Interface, o }, Spec: devcontainer_api_v1.DevcontainerAppSpec{ StatefulSet: devcontainer_api_v1.StatefulSetSpec{ - Image: opts.Image, - Command: opts.CommandList, - ContainerPort: opts.ContainerPort, + Image: opts.Image, + Command: opts.CommandList, + ContainerPort: opts.ContainerPort, + SSHPublicKeyList: opts.SSHPublicKeyList, }, }, } diff --git a/modules/devstar_devcontainer/k8s_agent/api/v1/devcontainerapp_types.go b/modules/devstar_devcontainer/k8s_agent/api/v1/devcontainerapp_types.go index 609465a331..bae3a24c53 100644 --- a/modules/devstar_devcontainer/k8s_agent/api/v1/devcontainerapp_types.go +++ b/modules/devstar_devcontainer/k8s_agent/api/v1/devcontainerapp_types.go @@ -65,6 +65,10 @@ type StatefulSetSpec struct { Image string `json:"image"` Command []string `json:"command"` + // +kubebuilder:validation:MinItems=1 + // 至少包含一个 SSH Public Key 才能通过校验规则 + SSHPublicKeyList []string `json:"sshPublicKeyList"` + // +kubebuilder:validation:Minimum=1 // +optional ContainerPort uint16 `json:"containerPort,omitempty"` diff --git a/modules/devstar_devcontainer/k8s_agent/api/v1/zz_generated.deepcopy.go b/modules/devstar_devcontainer/k8s_agent/api/v1/zz_generated.deepcopy.go index 89ca8ce3a6..3c94a77596 100644 --- a/modules/devstar_devcontainer/k8s_agent/api/v1/zz_generated.deepcopy.go +++ b/modules/devstar_devcontainer/k8s_agent/api/v1/zz_generated.deepcopy.go @@ -168,6 +168,11 @@ func (in *StatefulSetSpec) DeepCopyInto(out *StatefulSetSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.SSHPublicKeyList != nil { + in, out := &in.SSHPublicKeyList, &out.SSHPublicKeyList + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatefulSetSpec. diff --git a/modules/devstar_devcontainer/k8s_agent/dto/CreateDevcontainerOptions.go b/modules/devstar_devcontainer/k8s_agent/dto/CreateDevcontainerOptions.go index 60c66557ae..7b10a79871 100644 --- a/modules/devstar_devcontainer/k8s_agent/dto/CreateDevcontainerOptions.go +++ b/modules/devstar_devcontainer/k8s_agent/dto/CreateDevcontainerOptions.go @@ -8,10 +8,11 @@ import ( 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"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Image string `json:"image"` + CommandList []string `json:"command"` + ContainerPort uint16 `json:"containerPort"` + ServicePort uint16 `json:"servicePort"` + SSHPublicKeyList []string `json:"sshPublicKeyList"` } diff --git a/modules/setting/devstar_devcontainer.go b/modules/setting/devstar_devcontainer.go index d90324a4c3..584c3fc639 100644 --- a/modules/setting/devstar_devcontainer.go +++ b/modules/setting/devstar_devcontainer.go @@ -17,12 +17,16 @@ var validDevcontainerAgentSet = map[string]struct{}{ var Devstar = struct { Devcontainer DevcontainerType `ini:"devstar.devcontainer"` + SSHKeypair SSHKeyPairType `ini:"devstar.ssh_key_pair"` }{ Devcontainer: DevcontainerType{ Enabled: false, Namespace: "default", TimeoutSeconds: 900, // 最长等待 DevContainer 就绪时间(阻塞式),默认15分钟,可被 app.ini 指定值覆盖 }, + SSHKeypair: SSHKeyPairType{ + KeySize: 2048, + }, } type DevcontainerType struct { @@ -33,10 +37,12 @@ type DevcontainerType struct { TimeoutSeconds int64 } -// loadDevstarDevcontainerFrom 从 ini 配置文件中读取 DevStar DevContainer 配置信息,并进行检查,若数据无效,则自动禁用 DevContainer -func loadDevstarDevcontainerFrom(rootCfg ConfigProvider) { +type SSHKeyPairType struct { + KeySize int +} - mustMapSetting(rootCfg, "devstar", &Devstar) +// validateDevstarDevcontainerSettings 检查从 ini 配置文件中读取 DevStar DevContainer 配置信息,若数据无效,则自动禁用 DevContainer +func validateDevstarDevcontainerSettings() { // 检查 Host 是否为空,若为空,则自动将 DevContainer 设置为禁用 if len(Devstar.Devcontainer.Host) == 0 { @@ -56,3 +62,17 @@ func loadDevstarDevcontainerFrom(rootCfg ConfigProvider) { log.Info("DevStar DevContainer Service Enabled") } } + +// validateDevstarSSHKeyPairSettings 检查从 ini 配置文件中读取 DevStar SSH Key Pair 配置信息 +func validateDevstarSSHKeyPairSettings() { + if Devstar.SSHKeypair.KeySize < 1024 { + Devstar.SSHKeypair.KeySize = 1024 + } +} + +func loadDevstarFrom(rootCfg ConfigProvider) { + mustMapSetting(rootCfg, "devstar", &Devstar) + + validateDevstarDevcontainerSettings() + validateDevstarSSHKeyPairSettings() +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index d6150f697e..e440a6c3e0 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -217,7 +217,7 @@ func LoadSettings() { loadMimeTypeMapFrom(CfgProvider) loadFederationFrom(CfgProvider) loadWechatSettingsFrom(CfgProvider) - loadDevstarDevcontainerFrom(CfgProvider) + loadDevstarFrom(CfgProvider) } // LoadSettingsForInstall initializes the settings for install diff --git a/routers/api/devcontainer/create_repo_devcontainer.go b/routers/api/devcontainer/create_repo_devcontainer.go index 3e95a6b2f3..24ae7eba0e 100644 --- a/routers/api/devcontainer/create_repo_devcontainer.go +++ b/routers/api/devcontainer/create_repo_devcontainer.go @@ -5,7 +5,7 @@ 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_dto "code.gitea.io/gitea/services/devstar_devcontainer/dto" + devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options" "code.gitea.io/gitea/services/forms" "strconv" ) @@ -15,6 +15,7 @@ import ( // POST /api/devcontainer // 请求体参数: // -- repoId: 需要为哪个仓库创建 DevContainer +// -- sshPublicKeyList: 列表,填入用户希望临时使用的SSH会话加密公钥 // 注意:必须携带 用户登录凭证 func CreateRepoDevcontainer(ctx *gitea_web_context.Context) { @@ -25,14 +26,13 @@ func CreateRepoDevcontainer(ctx *gitea_web_context.Context) { } // 2. 检查表单校验规则是否失败 - hasError, _ := ctx.Data["HasError"].(bool) - if hasError { + if ctx.HasError() { // POST Binding 表单正则表达式校验失败,返回 API 错误信息 failedToValidateFormData := &Result.ResultType{ Code: Result.RespFailedIllegalParams.Code, Msg: Result.RespFailedIllegalParams.Msg, Data: map[string]string{ - "ErrorMsg": ctx.Data["ErrorMsg"].(string), + "ErrorMsg": ctx.GetErrMsg(), }, } failedToValidateFormData.RespondJson2HttpResponseWriter(ctx.Resp) @@ -47,17 +47,19 @@ func CreateRepoDevcontainer(ctx *gitea_web_context.Context) { Code: Result.RespFailedIllegalParams.Code, Msg: Result.RespFailedIllegalParams.Msg, Data: map[string]string{ - "ErrorMsg": err.Error(), + // fix nullptr deference of `err.Error()` when repoId == 0 + "ErrorMsg": "repoId 必须是正数", }, } failedToParseRepoId.RespondJson2HttpResponseWriter(ctx.Resp) return } - // 3. 调用 API Service 层创建 DevContainer - opts := &devcontainer_service_dto.CreateDevcontainerDTO{ - Actor: ctx.Doer, - RepoId: repoId, + // 4. 调用 API Service 层创建 DevContainer + opts := &devcontainer_service_options.CreateDevcontainerOptions{ + Actor: ctx.Doer, + RepoId: repoId, + SSHPublicKeyList: form.SSHPublicKeyList, } err = devcontainer_api_service.CreateDevcontainerAPIService(ctx, opts) if err != nil { diff --git a/routers/api/devcontainer/vo/ListRepoDevcontainerVO.go b/routers/api/devcontainer/vo/ListRepoDevcontainerVO.go index 170f3fc124..989a03b702 100644 --- a/routers/api/devcontainer/vo/ListRepoDevcontainerVO.go +++ b/routers/api/devcontainer/vo/ListRepoDevcontainerVO.go @@ -11,7 +11,6 @@ type RepoDevContainerVO struct { DevContainerName string `json:"devContainerName" xorm:"devcontainer_name"` DevContainerHost string `json:"devContainerHost" xorm:"devcontainer_host"` 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"` @@ -30,3 +29,10 @@ type RepoDevcontainerOptions struct { Actor *user_model.User Repository *repo_model.Repository } + +// CreateRepoDevcontainerOptions 创建 DevContainer 必要字段 +type CreateRepoDevcontainerOptions struct { + Actor *user_model.User + Repository *repo_model.Repository + SSHPublicKeyList []string +} diff --git a/routers/api/devstar_ssh/key_pair/GenerateNewSSHSessionKeyPair.go b/routers/api/devstar_ssh/key_pair/GenerateNewSSHSessionKeyPair.go new file mode 100644 index 0000000000..fe62358fcb --- /dev/null +++ b/routers/api/devstar_ssh/key_pair/GenerateNewSSHSessionKeyPair.go @@ -0,0 +1,48 @@ +package key_pair + +import ( + "code.gitea.io/gitea/modules/setting" + Result "code.gitea.io/gitea/routers/entity" + gitea_web_context "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/devstar_ssh_key_pair/api_service" + "strconv" +) + +// GenerateNewSSHSessionKeyPair 创建临时会话 SSH 密钥对 +// GET /api/devstar_ssh/key_pair/new_temp +func GenerateNewSSHSessionKeyPair(ctx *gitea_web_context.Context) { + + // 1. 节约服务器资源:仅限已登录用户使用生成临时 SSH 会话密钥对 + if ctx == nil || ctx.Doer == nil { + Result.RespUnauthorizedFailure.RespondJson2HttpResponseWriter(ctx.Resp) + return + } + + // 2. 调用 API Service 层,生成 SSH 私钥、公钥、公钥指纹 + err, vo := api_service.GenerateNewRSASSHSessionKeyPair() + if err != nil { + resp := Result.ResultType{ + Code: Result.RespSSHKeyPairGenFailed.Code, + Msg: Result.RespSSHKeyPairGenFailed.Msg, + Data: map[string]string{ + "ErrorMsg": err.Error(), + "KeySize": strconv.Itoa(setting.Devstar.SSHKeypair.KeySize), + }, + } + resp.RespondJson2HttpResponseWriter(ctx.Resp) + return + } + + resp := Result.ResultType{ + Code: Result.RespSuccess.Code, + Msg: Result.RespSuccess.Msg, + Data: map[string]string{ + "publicKeyPem": vo.PublicKeyPEM, + "privateKeyPem": vo.PrivateKeyPEM, + "publicKeyFingerprint": vo.PublicKeyFingerprint, + "KeySize": vo.KeySize, + }, + } + resp.RespondJson2HttpResponseWriter(ctx.Resp) + return +} diff --git a/routers/entity/devcontainer_result_constants.go b/routers/entity/devcontainer_result_constants.go index b8ca058590..027e976520 100644 --- a/routers/entity/devcontainer_result_constants.go +++ b/routers/entity/devcontainer_result_constants.go @@ -2,12 +2,6 @@ package entity // 错误码 110xx 表示 devContainer 相关错误信息 -// RespUnauthorizedFailure 获取 devContainer 信息失败:用户未授权 -var RespUnauthorizedFailure = ResultType{ - Code: 11001, - Msg: "您未授权,无法查看 devContainer 信息", -} - // RespFailedIllegalParams 仓库ID参数无效 var RespFailedIllegalParams = ResultType{ Code: 11002, diff --git a/routers/entity/result.go b/routers/entity/result.go index f450198bb9..1d7b6f9a40 100644 --- a/routers/entity/result.go +++ b/routers/entity/result.go @@ -33,3 +33,9 @@ var RespSuccess = ResultType{ Code: 0, Msg: "操作成功", } + +// RespUnauthorizedFailure 获取 devContainer 信息失败:用户未授权 +var RespUnauthorizedFailure = ResultType{ + Code: 00001, + Msg: "未登录,禁止访问", +} diff --git a/routers/entity/ssh_key_pair_result_constants.go b/routers/entity/ssh_key_pair_result_constants.go new file mode 100644 index 0000000000..6a12914e48 --- /dev/null +++ b/routers/entity/ssh_key_pair_result_constants.go @@ -0,0 +1,9 @@ +package entity + +// 错误码 120xx 表示 SSH Key Pair 相关错误信息 + +// RespSSHKeyPairGenFailed 生成 SSH 密钥对失败 +var RespSSHKeyPairGenFailed = ResultType{ + Code: 12001, + Msg: "生成 SSH 密钥对失败", +} diff --git a/routers/web/devcontainer/CreateRepoDevContainer.go b/routers/web/devcontainer/CreateRepoDevContainer.go index 929e44445e..bce7779bea 100644 --- a/routers/web/devcontainer/CreateRepoDevContainer.go +++ b/routers/web/devcontainer/CreateRepoDevContainer.go @@ -10,7 +10,7 @@ import ( // CreateRepoDevContainer 创建仓库 Dev Container func CreateRepoDevContainer(ctx *gitea_web_context.Context) { if !isUserDevcontainerAlreadyInRepository(ctx) { - opts := &DevcontainersVO.RepoDevcontainerOptions{ + opts := &DevcontainersVO.CreateRepoDevcontainerOptions{ Actor: ctx.Doer, Repository: ctx.Repo.Repository, } diff --git a/routers/web/devcontainer/DeleteRepoDevContainerForCurrentActor.go b/routers/web/devcontainer/DeleteRepoDevContainerForCurrentActor.go index 26411bf87b..94d2b1f772 100644 --- a/routers/web/devcontainer/DeleteRepoDevContainerForCurrentActor.go +++ b/routers/web/devcontainer/DeleteRepoDevContainerForCurrentActor.go @@ -17,7 +17,7 @@ func DeleteRepoDevContainerForCurrentActor(ctx *gitea_web_context.Context) { } err := DevcontainersService.DeleteRepoDevcontainer(ctx, opts) if err != nil { - log.Warn("failed to create devContainer with option{%v}: %v", opts, err.Error()) + log.Warn("failed to delete 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)) diff --git a/routers/web/web.go b/routers/web/web.go index b6793831ce..f043e710c6 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -5,6 +5,7 @@ package web import ( devcontainer_api "code.gitea.io/gitea/routers/api/devcontainer" + devstar_ssh_key_pair_api "code.gitea.io/gitea/routers/api/devstar_ssh/key_pair" devcontainer_web "code.gitea.io/gitea/routers/web/devcontainer" gocontext "context" "net/http" @@ -523,6 +524,10 @@ func registerRoutes(m *web.Router) { m.Get("/user", devcontainer_api.ListUserDevcontainers) }) // ***** END: DevContainer ***** + m.Group("/api/devstar_ssh", func() { + // 创建SSH会话 临时密钥对 + m.Get("/key_pair/new_temp", devstar_ssh_key_pair_api.GenerateNewSSHSessionKeyPair) + }) // ***** START: User ***** // "user/login" doesn't need signOut, then logged-in users can still access this route for redirection purposes by "/user/login?redirec_to=..." diff --git a/services/devstar_devcontainer/RepoDevcontainerAbstractAgentService.go b/services/devstar_devcontainer/RepoDevcontainerAbstractAgentService.go index 38812a5c56..0aab866f8f 100644 --- a/services/devstar_devcontainer/RepoDevcontainerAbstractAgentService.go +++ b/services/devstar_devcontainer/RepoDevcontainerAbstractAgentService.go @@ -6,6 +6,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo" + devcontainer_service_dto "code.gitea.io/gitea/services/devstar_devcontainer/dto" devcontainer_k8s_agent_service "code.gitea.io/gitea/services/devstar_devcontainer/k8s_agent" "context" "fmt" @@ -37,7 +38,6 @@ func GetRepoDevcontainerDetails(ctx context.Context, opts *DevcontainersVO.RepoD 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, devstar_devcontainer.repo_id AS repo_id, repository.name AS repo_name, @@ -59,7 +59,6 @@ func GetRepoDevcontainerDetails(ctx context.Context, opts *DevcontainersVO.RepoD "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,"+ "devstar_devcontainer.repo_id AS repo_id,"+ "repository.name AS repo_name,"+ @@ -87,18 +86,20 @@ func GetRepoDevcontainerDetails(ctx context.Context, opts *DevcontainersVO.RepoD - 当前用户拥有 repo code写入权限 - 数据库此前不存在 该用户在该repo创建的 Dev Container */ -func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevcontainerOptions) error { +func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.CreateRepoDevcontainerOptions) error { username := opts.Actor.Name repoName := opts.Repository.Name - newDevcontainer := &devstar_devcontainer_models.DevstarDevcontainer{ - Name: getSanitizedDevcontainerName(username, repoName), - 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, + + newDevcontainer := &devcontainer_service_dto.CreateDevcontainerDTO{ + DevstarDevcontainer: devstar_devcontainer_models.DevstarDevcontainer{ + Name: getSanitizedDevcontainerName(username, repoName), + DevcontainerHost: setting.Devstar.Devcontainer.Host, + DevcontainerUsername: "root", + DevcontainerWorkDir: "/data", + RepoId: opts.Repository.ID, + UserId: opts.Actor.ID, + }, + SSHPublicKeyList: opts.SSHPublicKeyList, } // 在数据库事务中创建 Dev Container 分配资源,出错时自动回滚相对应数据库字段,保证数据一致 @@ -115,7 +116,7 @@ func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevco // 2. 根据分配的 NodePort 更新数据库字段 rowsAffect, err := db.GetEngine(ctx). Table("devstar_devcontainer"). - Insert(newDevcontainer) + Insert(newDevcontainer.DevstarDevcontainer) if err != nil { return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{ Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName), @@ -247,7 +248,7 @@ func purgeDevcontainersResource(ctx *context.Context, devcontainersList *[]devst } // claimDevcontainerResource 分发创建 DevContainer 任务到配置文件指定的执行器 -func claimDevcontainerResource(ctx *context.Context, newDevContainer *devstar_devcontainer_models.DevstarDevcontainer) error { +func claimDevcontainerResource(ctx *context.Context, newDevContainer *devcontainer_service_dto.CreateDevcontainerDTO) error { // 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束 if setting.Devstar.Devcontainer.Enabled == false { return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{ diff --git a/services/devstar_devcontainer/UserDevcontainerService.go b/services/devstar_devcontainer/UserDevcontainerService.go index 0051948288..85a769843d 100644 --- a/services/devstar_devcontainer/UserDevcontainerService.go +++ b/services/devstar_devcontainer/UserDevcontainerService.go @@ -88,7 +88,6 @@ func GetUserDevcontainersList(ctx context.Context, opts *vo.SearchUserDevcontain devstar_devcontainer.name AS devcontainer_name, devstar_devcontainer.devcontainer_host AS devcontainer_host, devstar_devcontainer.devcontainer_username AS devcontainer_username, - devstar_devcontainer.devcontainer_password AS devcontainer_password, devstar_devcontainer.devcontainer_work_dir AS devcontainer_work_dir, devstar_devcontainer.repo_id AS repo_id, repository.name AS repo_name, @@ -109,7 +108,6 @@ func GetUserDevcontainersList(ctx context.Context, opts *vo.SearchUserDevcontain "devstar_devcontainer.name AS devcontainer_name,"+ "devstar_devcontainer.devcontainer_host AS devcontainer_host,"+ "devstar_devcontainer.devcontainer_username AS devcontainer_username,"+ - "devstar_devcontainer.devcontainer_password AS devcontainer_password,"+ "devstar_devcontainer.devcontainer_work_dir AS devcontainer_work_dir,"+ "devstar_devcontainer.repo_id AS repo_id,"+ "repository.name AS repo_name,"+ diff --git a/services/devstar_devcontainer/api_services/CreateDevcontainerAPIService.go b/services/devstar_devcontainer/api_services/CreateDevcontainerAPIService.go index 8544a26cf5..2022610f6e 100644 --- a/services/devstar_devcontainer/api_services/CreateDevcontainerAPIService.go +++ b/services/devstar_devcontainer/api_services/CreateDevcontainerAPIService.go @@ -6,13 +6,14 @@ import ( 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" + devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options" "context" + "fmt" ) // CreateDevcontainerAPIService API专用创建 DevContainer Service -func CreateDevcontainerAPIService(ctx *gitea_web_context.Context, opts *devcontainer_service_dto.CreateDevcontainerDTO) error { +func CreateDevcontainerAPIService(ctx *gitea_web_context.Context, opts *devcontainer_service_options.CreateDevcontainerOptions) error { // 0. 检查用户传入参数 if ctx == nil || opts == nil || opts.Actor == nil || opts.RepoId <= 0 { return devcontainer_service_errors.ErrIllegalParams{ @@ -44,10 +45,36 @@ func CreateDevcontainerAPIService(ctx *gitea_web_context.Context, opts *devconta } } - // 1.3 调用 k8s Agent 创建 DevContainer - optsCreateDevcontainer := &DevcontainersVO.RepoDevcontainerOptions{ - Actor: opts.Actor, - Repository: repositoryInDB, + // 1.3 查询数据库,收集用户 SSH 公钥,合并用户临时填入SSH公钥(若用户合计 SSH公钥个数为0,拒绝创建DevContainer) + /** + SELECT content FROM public_key where owner_id = #{opts.Actor.ID} + */ + var userSSHPublicKeyList []string + err = db.GetEngine(ctx). + Table("public_key"). + Select("content"). + Where("owner_id = ?", opts.Actor.ID). + Find(&userSSHPublicKeyList) + if err != nil { + return devcontainer_service_errors.ErrOperateDevcontainer{ + Action: fmt.Sprintf("query SSH Public Key List for User %s", opts.Actor.Name), + Message: err.Error(), + } + } + userSSHPublicKeyList = append(userSSHPublicKeyList, opts.SSHPublicKeyList...) + if len(userSSHPublicKeyList) <= 0 { + // API没提供临时SSH公钥,用户后台也没有永久SSH公钥,直接结束并回滚事务 + return devcontainer_service_errors.ErrOperateDevcontainer{ + Action: "Check SSH Public Key List", + Message: "禁止创建无法连通的DevContainer:用户未提供 SSH 公钥,请先使用API临时创建SSH密钥对、或在Web端手动添加SSH公钥", + } + } + + // 1.4 调用 DevContainer Service 创建 DevContainer + optsCreateDevcontainer := &DevcontainersVO.CreateRepoDevcontainerOptions{ + Actor: opts.Actor, + Repository: repositoryInDB, + SSHPublicKeyList: userSSHPublicKeyList, } return DevcontainersService.CreateRepoDevcontainer(ctx, optsCreateDevcontainer) }) diff --git a/services/devstar_devcontainer/dto/CreateDevcontainerDTO.go b/services/devstar_devcontainer/dto/CreateDevcontainerDTO.go index 7fde187173..31b65a5022 100644 --- a/services/devstar_devcontainer/dto/CreateDevcontainerDTO.go +++ b/services/devstar_devcontainer/dto/CreateDevcontainerDTO.go @@ -1,9 +1,10 @@ package dto -import user_model "code.gitea.io/gitea/models/user" +import ( + devstar_devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer" +) -// CreateDevcontainerDTO 封装 API 创建 DevContainer 数据,即 router 层 POST /api/devcontainer 向下传递的数据结构 type CreateDevcontainerDTO struct { - Actor *user_model.User - RepoId int64 + devstar_devcontainer_models.DevstarDevcontainer + SSHPublicKeyList []string } diff --git a/services/devstar_devcontainer/k8s_agent/AssignDevcontainerCreation2K8sOperator.go b/services/devstar_devcontainer/k8s_agent/AssignDevcontainerCreation2K8sOperator.go index e846b76068..f28a61c33b 100644 --- a/services/devstar_devcontainer/k8s_agent/AssignDevcontainerCreation2K8sOperator.go +++ b/services/devstar_devcontainer/k8s_agent/AssignDevcontainerCreation2K8sOperator.go @@ -1,9 +1,9 @@ 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" + devcontainer_service_dto "code.gitea.io/gitea/services/devstar_devcontainer/dto" "context" apimachinery_meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,7 +17,7 @@ import ( // AssignDevcontainerCreation2K8sOperator 将 DevContainer 资源创建任务派遣至 k8s Operator,同时根据结果更新 NodePort // // 注意:本方法仍然在数据库事务中,因此不适合执行长时间操作,故需要后期异步判断 DevContainer 是否就绪 -func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContainer *devstar_devcontainer_models.DevstarDevcontainer) error { +func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContainer *devcontainer_service_dto.CreateDevcontainerDTO) error { // 1. 获取 Dynamic Client client, err := devcontainer_k8s_agent_module.GetKubernetesClient(ctx) @@ -36,10 +36,11 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine 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", + "apt-get update && apt-get install -y openssh-server && service ssh start && apt-get clean && tail -f /dev/null", }, - ContainerPort: 22, - ServicePort: 22, + ContainerPort: 22, + ServicePort: 22, + SSHPublicKeyList: newDevContainer.SSHPublicKeyList, } // 2. 创建成功,取回集群中的 DevContainer diff --git a/services/devstar_devcontainer/options/CreateDevcontainerOptions.go b/services/devstar_devcontainer/options/CreateDevcontainerOptions.go new file mode 100644 index 0000000000..40f3a7b5a0 --- /dev/null +++ b/services/devstar_devcontainer/options/CreateDevcontainerOptions.go @@ -0,0 +1,10 @@ +package options + +import user_model "code.gitea.io/gitea/models/user" + +// CreateDevcontainerOptions 封装 API 创建 DevContainer 数据,即 router 层 POST /api/devcontainer 向下传递的数据结构 +type CreateDevcontainerOptions struct { + Actor *user_model.User + RepoId int64 + SSHPublicKeyList []string +} diff --git a/services/devstar_ssh_key_pair/api_service/GenerateNewRSASSHSessionKeyPair.go b/services/devstar_ssh_key_pair/api_service/GenerateNewRSASSHSessionKeyPair.go new file mode 100644 index 0000000000..1f7caa04d9 --- /dev/null +++ b/services/devstar_ssh_key_pair/api_service/GenerateNewRSASSHSessionKeyPair.go @@ -0,0 +1,78 @@ +package api_service + +import ( + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/devstar_ssh_key_pair/errors" + "code.gitea.io/gitea/services/devstar_ssh_key_pair/vo" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "golang.org/x/crypto/ssh" + "strconv" +) + +// GenerateNewRSASSHSessionKeyPair 生成 RSA SSH 密钥对 +func GenerateNewRSASSHSessionKeyPair() (error, *vo.GenerateNewRSASSHSessionKeyPairVO) { + + // 1. 生成 SSH 密钥对 (算法 RSA,长度 setting.Devstar.SSHKeypair.KeySize) + privateKey, err := rsa.GenerateKey(rand.Reader, setting.Devstar.SSHKeypair.KeySize) + if err != nil { + return err, nil + } + + // 2. 获取 Private Key PEM + privateKeyDer := x509.MarshalPKCS1PrivateKey(privateKey) + privateKeyBlock := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: privateKeyDer, + } + privateKeyPem := pem.EncodeToMemory(&privateKeyBlock) + if privateKeyPem == nil { + return errors.ErrGenerateNewRSASSHSessionKeyPair{ + Action: "Encode Private Key to memory", + Message: "private key PEM encoded to be nil", + }, nil + } + privateKeyPemStr := string(privateKeyPem) + + // 3. 获取 Public Key PEM + publicKey := privateKey.PublicKey + publicKeyDer, err := x509.MarshalPKIXPublicKey(&publicKey) + if err != nil { + return errors.ErrGenerateNewRSASSHSessionKeyPair{ + Action: "Marshal PKIX Public Key", + Message: err.Error(), + }, nil + } + publicKeyBlock := pem.Block{ + Type: "PUBLIC KEY", + Headers: nil, + Bytes: publicKeyDer, + } + publicKeyPem := pem.EncodeToMemory(&publicKeyBlock) + if publicKeyPem == nil { + return errors.ErrGenerateNewRSASSHSessionKeyPair{ + Action: "Encode Public Key to memory", + Message: "public key PEM encoded to be nil", + }, nil + } + publicKeyPemStr := string(publicKeyPem) + + // 3. 计算 SSH Public Key Fingerprint,用于 ~/.ssh/authorized_keys + sshPublicKey, err := ssh.NewPublicKey(&publicKey) + if err != nil { + return errors.ErrGenerateNewRSASSHSessionKeyPair{ + Action: "Calculate SSH Public Key Finerprint", + Message: err.Error(), + }, nil + } + sshPublicKeyfingerprintSHA256Str := string(ssh.MarshalAuthorizedKey(sshPublicKey)) + return nil, &vo.GenerateNewRSASSHSessionKeyPairVO{ + PublicKeyPEM: publicKeyPemStr, + PrivateKeyPEM: privateKeyPemStr, + PublicKeyFingerprint: sshPublicKeyfingerprintSHA256Str, + KeySize: strconv.Itoa(setting.Devstar.SSHKeypair.KeySize), + } +} diff --git a/services/devstar_ssh_key_pair/errors/ErrGenerateNewRSASSHSessionKeyPair.go b/services/devstar_ssh_key_pair/errors/ErrGenerateNewRSASSHSessionKeyPair.go new file mode 100644 index 0000000000..f4ddf38fcf --- /dev/null +++ b/services/devstar_ssh_key_pair/errors/ErrGenerateNewRSASSHSessionKeyPair.go @@ -0,0 +1,12 @@ +package errors + +import "fmt" + +type ErrGenerateNewRSASSHSessionKeyPair struct { + Action string + Message string +} + +func (err ErrGenerateNewRSASSHSessionKeyPair) Error() string { + return fmt.Sprintf("Failed to %s for SSH Key: %v", err.Action, err.Message) +} diff --git a/services/devstar_ssh_key_pair/vo/GenerateNewRSASSHSessionKeyPairVO.go b/services/devstar_ssh_key_pair/vo/GenerateNewRSASSHSessionKeyPairVO.go new file mode 100644 index 0000000000..49c23338de --- /dev/null +++ b/services/devstar_ssh_key_pair/vo/GenerateNewRSASSHSessionKeyPairVO.go @@ -0,0 +1,8 @@ +package vo + +type GenerateNewRSASSHSessionKeyPairVO struct { + PublicKeyPEM string + PrivateKeyPEM string + PublicKeyFingerprint string + KeySize string +} diff --git a/services/forms/devcontainer_form.go b/services/forms/devcontainer_form.go index 95d30e5b21..a9d8d6e53e 100644 --- a/services/forms/devcontainer_form.go +++ b/services/forms/devcontainer_form.go @@ -9,7 +9,8 @@ import ( // CreateRepoDevcontainerForm 用户使用 API 创建仓库 DevContainer POST 表单数据绑定与校验规则 type CreateRepoDevcontainerForm struct { - RepoId string `json:"repoId" binding:"Required;MinSize(1);MaxSize(19);PositiveBase10IntegerNumberRule"` + RepoId string `json:"repoId" binding:"Required;MinSize(1);MaxSize(19);PositiveBase10IntegerNumberRule"` + SSHPublicKeyList []string `json:"sshPublicKeyList"` } // Validate 用户使用 API 创建仓库 DevContainer POST 表单数据校验器 diff --git a/services/repository/repository.go b/services/repository/repository.go index 0acc977dfa..e66c9f228c 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -72,10 +72,7 @@ func DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_mod deleteDevcontainerOptions := &DevcontainersVO.RepoDevcontainerOptions{ Repository: repo, } - if err := DevcontainersService.DeleteRepoDevcontainer(ctx, deleteDevcontainerOptions); err != nil { - return err - } - + _ = DevcontainersService.DeleteRepoDevcontainer(ctx, deleteDevcontainerOptions) return packages_model.UnlinkRepositoryFromAllPackages(ctx, repo.ID) }