!8 Added SSH Public Key Login For DevContainer
* [Feature] Added DevContainer Public Key Login, and deprecated SSH Password login * [Improvement] Anti-spam * GET /api/devstar_ssh/key_pair/new_temp: SSH Keypair Gen * fix HTTP 500 error while deleting Repo * updated DevContainer WorkDIR * updated warn msg
This commit is contained in:
@@ -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 = <k8s 暴露访问域名或IP,比如 devcontainer.devstar.cn >
|
||||
NAMESPACE = <k8s DevStar Studio 部署 namespace,比如 devstar-studio-ns >
|
||||
|
||||
devstar.ssh_key_pair: |
|
||||
KEY_SIZE = <写入希望生成的SSH密钥长度,比如 4096 ,默认值 2048>
|
||||
|
||||
```
|
||||
|
||||
对于 `Secrets` 类型,参考:
|
||||
|
||||
@@ -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表主键')"`
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ func LoadSettings() {
|
||||
loadMimeTypeMapFrom(CfgProvider)
|
||||
loadFederationFrom(CfgProvider)
|
||||
loadWechatSettingsFrom(CfgProvider)
|
||||
loadDevstarDevcontainerFrom(CfgProvider)
|
||||
loadDevstarFrom(CfgProvider)
|
||||
}
|
||||
|
||||
// LoadSettingsForInstall initializes the settings for install
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -2,12 +2,6 @@ package entity
|
||||
|
||||
// 错误码 110xx 表示 devContainer 相关错误信息
|
||||
|
||||
// RespUnauthorizedFailure 获取 devContainer 信息失败:用户未授权
|
||||
var RespUnauthorizedFailure = ResultType{
|
||||
Code: 11001,
|
||||
Msg: "您未授权,无法查看 devContainer 信息",
|
||||
}
|
||||
|
||||
// RespFailedIllegalParams 仓库ID参数无效
|
||||
var RespFailedIllegalParams = ResultType{
|
||||
Code: 11002,
|
||||
|
||||
@@ -33,3 +33,9 @@ var RespSuccess = ResultType{
|
||||
Code: 0,
|
||||
Msg: "操作成功",
|
||||
}
|
||||
|
||||
// RespUnauthorizedFailure 获取 devContainer 信息失败:用户未授权
|
||||
var RespUnauthorizedFailure = ResultType{
|
||||
Code: 00001,
|
||||
Msg: "未登录,禁止访问",
|
||||
}
|
||||
|
||||
9
routers/entity/ssh_key_pair_result_constants.go
Normal file
9
routers/entity/ssh_key_pair_result_constants.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package entity
|
||||
|
||||
// 错误码 120xx 表示 SSH Key Pair 相关错误信息
|
||||
|
||||
// RespSSHKeyPairGenFailed 生成 SSH 密钥对失败
|
||||
var RespSSHKeyPairGenFailed = ResultType{
|
||||
Code: 12001,
|
||||
Msg: "生成 SSH 密钥对失败",
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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=..."
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,"+
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package vo
|
||||
|
||||
type GenerateNewRSASSHSessionKeyPairVO struct {
|
||||
PublicKeyPEM string
|
||||
PrivateKeyPEM string
|
||||
PublicKeyFingerprint string
|
||||
KeySize string
|
||||
}
|
||||
@@ -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 表单数据校验器
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user