!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:
戴明辰
2024-10-13 12:07:28 +00:00
repo.diff.parent ddd4109dbc
repo.diff.commit 6bb8bba3aa
repo.diff.stats_desc%!(EXTRA int=28, int=309, int=68)

repo.diff.view_file

@@ -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` 类型,参考:

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

@@ -2,12 +2,6 @@ package entity
// 错误码 110xx 表示 devContainer 相关错误信息
// RespUnauthorizedFailure 获取 devContainer 信息失败:用户未授权
var RespUnauthorizedFailure = ResultType{
Code: 11001,
Msg: "您未授权,无法查看 devContainer 信息",
}
// RespFailedIllegalParams 仓库ID参数无效
var RespFailedIllegalParams = ResultType{
Code: 11002,

repo.diff.view_file

@@ -33,3 +33,9 @@ var RespSuccess = ResultType{
Code: 0,
Msg: "操作成功",
}
// RespUnauthorizedFailure 获取 devContainer 信息失败:用户未授权
var RespUnauthorizedFailure = ResultType{
Code: 00001,
Msg: "未登录,禁止访问",
}

repo.diff.view_file

@@ -0,0 +1,9 @@
package entity
// 错误码 120xx 表示 SSH Key Pair 相关错误信息
// RespSSHKeyPairGenFailed 生成 SSH 密钥对失败
var RespSSHKeyPairGenFailed = ResultType{
Code: 12001,
Msg: "生成 SSH 密钥对失败",
}

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

@@ -0,0 +1,8 @@
package vo
type GenerateNewRSASSHSessionKeyPairVO struct {
PublicKeyPEM string
PrivateKeyPEM string
PublicKeyFingerprint string
KeySize string
}

repo.diff.view_file

@@ -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 表单数据校验器

repo.diff.view_file

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