diff --git a/docs/content/development/api-devstar.zh-cn.md b/docs/content/development/api-devstar.zh-cn.md index a909c36b80..44d27c5293 100644 --- a/docs/content/development/api-devstar.zh-cn.md +++ b/docs/content/development/api-devstar.zh-cn.md @@ -45,7 +45,7 @@ GET /api/devstar_ssh/key_pair/new_temp?_=${CurrentTimestamp} "data": { "KeySize": "2048", "privateKeyPem": "-----BEGIN RSA PRIVATE KEY-----\n......\n-----END RSA PRIVATE KEY-----\n", - "publicKeyFingerprint": "ssh-rsa AAAAB3N......\n", + "publicKeySsh": "ssh-rsa AAAAB3N......\n", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n......\n-----END PUBLIC KEY-----\n" } } @@ -59,7 +59,7 @@ GET /api/devstar_ssh/key_pair/new_temp?_=${CurrentTimestamp} - `KeySize`: 当前系统使用的密钥长度,默认值 2048,可在 app.ini 文件中的 `devstar.ssh_key_pair.KEY_SIZE` 指定大于 2048 位的密钥长度 - `privateKeyPem`: PEM 格式的 RSA 私钥,可保存于客户端目录 `~/.ssh/id_rsa` 用于 SSH 登录使用的 RSA 私钥 - `publicKeyPem`: PEM 格式的 RSA 公钥 - - `publicKeyFingerprint`: SSH 格式的 RSA 公钥,可保存于服务器目录 `~/.ssh/authorized_keys` 用于 SSH 登录使用的 RSA 公钥 + - `publicKeySsh`: SSH 格式的 RSA 公钥,可保存于服务器目录 `~/.ssh/authorized_keys` 用于 SSH 登录使用的 RSA 公钥 **响应格式(操作失败:未登录)**: diff --git a/modules/setting/devstar_devcontainer.go b/modules/setting/devstar_devcontainer.go index 199aa30e53..dd46408e7f 100644 --- a/modules/setting/devstar_devcontainer.go +++ b/modules/setting/devstar_devcontainer.go @@ -33,6 +33,14 @@ var Devstar = struct { Enabled: false, Namespace: "default", TimeoutSeconds: 900, // 最长等待 DevContainer 就绪时间(阻塞式),默认15分钟,可被 app.ini 指定值覆盖 + + // 获取 .devcontainer/devcontainer.json 默认分支名称 + DefaultGitBranchName: "main", + + // 若 .devcontainer/devcontainer.json 默认分支未指定 DevContainer Image (`image`),则使用如下默认值 + // 注: 该镜像必须含有 OpenSSH 服务器程序,否则需要手动制作 + // 手动制作方式参考: https://gitee.com/devstar/devcontainer-tweak + DefaultDevcontainerImageName: "devstar.cn/public/base-ssh-devcontainer:ubuntu-20.04-20241014", }, SSHKeypair: SSHKeyPairType{ KeySize: 2048, @@ -48,6 +56,9 @@ type DevcontainerType struct { Agent string Namespace string TimeoutSeconds int64 + + DefaultGitBranchName string + DefaultDevcontainerImageName string } type SSHKeyPairType struct { @@ -86,6 +97,18 @@ func validateDevstarDevcontainerSettings() { Devstar.Devcontainer.Enabled = false } + // 检查默认分支名称设置 + if len(Devstar.Devcontainer.DefaultGitBranchName) == 0 { + log.Warn("INVALID config 'DefaultGitBranchName' for DevStar DevContainer") + Devstar.Devcontainer.Enabled = false + } + + // 检查默认 DevContainer Image + if len(Devstar.Devcontainer.DefaultDevcontainerImageName) == 0 { + log.Warn("INVALID config 'DefaultGitBranchNameDefaultDevcontainerImageName' for DevStar DevContainer") + Devstar.Devcontainer.Enabled = false + } + if Devstar.Devcontainer.Enabled == false { log.Warn("DevStar DevContainer Service Disabled") } else { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 8b82bf1c8d..dcae066efc 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -777,7 +777,7 @@ change_phone_success= Phone updated successfully. wechat_qr_login = WeChat QR change_wechat= Update WeChat Account wechat_official_account_qr_prompt=Scan QR with WeChat, and follow the Official Account. -wechat_official_account_qr_expired=WeChat QR expired. +wechat_official_account_qr_expired=WeChat QR expired. Please Click here to refresh QR Code. wechat_official_account_bind_confirm=Are you sure to bind to WeChat Official Account '%s'? wechat_official_account_update_success= Wechat account updated successfully. diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index bfb293e056..1d9b1a47d1 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -776,7 +776,7 @@ change_phone_success=手机号成功更新 wechat_qr_login = 微信二维码 change_wechat=更新微信 wechat_official_account_qr_prompt=使用微信扫描二维码,关注公众号 -wechat_official_account_qr_expired=微信二维码已过期 +wechat_official_account_qr_expired=微信二维码已过期, 请点击刷新 wechat_official_account_bind_confirm=你确定要将当前用户绑定到微信 '%s'? wechat_official_account_update_success=微信成功更新 diff --git a/services/devstar_devcontainer/RepoDevcontainerAbstractAgentService.go b/services/devstar_devcontainer/RepoDevcontainerAbstractAgentService.go index 0392001004..d89953854a 100644 --- a/services/devstar_devcontainer/RepoDevcontainerAbstractAgentService.go +++ b/services/devstar_devcontainer/RepoDevcontainerAbstractAgentService.go @@ -1,20 +1,24 @@ package devstar_devcontainer import ( + "context" + "encoding/json" + "fmt" + "regexp" + "strings" + "time" + "code.gitea.io/gitea/models/db" devstar_devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer" + repo_model "code.gitea.io/gitea/models/repo" + git_module "code.gitea.io/gitea/modules/git" "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_service_errors "code.gitea.io/gitea/services/devstar_devcontainer/errors" devcontainer_k8s_agent_service "code.gitea.io/gitea/services/devstar_devcontainer/k8s_agent" - "context" - "fmt" "github.com/google/uuid" - "regexp" - "strings" - "time" "xorm.io/builder" ) @@ -106,6 +110,7 @@ func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.CreateRep CreatedUnix: unixTimestamp, UpdatedUnix: unixTimestamp, }, + Image: GetDefaultDevcontainerImageFromRepoDevcontainerJSON(ctx, opts.Repository), GitRepositoryURL: strings.TrimSuffix(setting.AppURL, "/") + opts.Repository.Link(), } @@ -262,7 +267,7 @@ func getSanitizedDevcontainerName(username, repoName string) string { // purgeDevcontainersResource 辅助函数,用于goroutine后台执行,回收DevContainer资源 func purgeDevcontainersResource(ctx *context.Context, devcontainersList *[]devstar_devcontainer_models.DevstarDevcontainer) error { // 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束,不会真正执行删除操作 - if setting.Devstar.Devcontainer.Enabled == false { + if !setting.Devstar.Devcontainer.Enabled { // 如果用户设置禁用 DevContainer,无法删除资源,会直接忽略,而数据库相关记录会继续清空、不会发生回滚 log.Warn("Orphan DevContainers in namespace `%s` left undeleted: %v", setting.Devstar.Devcontainer.Namespace, devcontainersList) return nil @@ -283,7 +288,7 @@ func purgeDevcontainersResource(ctx *context.Context, devcontainersList *[]devst // claimDevcontainerResource 分发创建 DevContainer 任务到配置文件指定的执行器 func claimDevcontainerResource(ctx *context.Context, newDevContainer *devcontainer_service_dto.CreateDevcontainerDTO) error { // 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束 - if setting.Devstar.Devcontainer.Enabled == false { + if !setting.Devstar.Devcontainer.Enabled { return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{ Action: "Check for DevContainer functionality switch", Message: "DevContainer is disabled globally, please check your configuration files", @@ -303,3 +308,52 @@ func claimDevcontainerResource(ctx *context.Context, newDevContainer *devcontain } } } + +// GetDefaultDevcontainerImageFromRepoDevcontainerJSON 从 .devcontainer/devcontainer.json 中获取 devContainer image +func GetDefaultDevcontainerImageFromRepoDevcontainerJSON(ctx context.Context, repo *repo_model.Repository) string { + + // 1. 获取默认分支名 + branchName := repo.DefaultBranch + if len(branchName) == 0 { + branchName = setting.Devstar.Devcontainer.DefaultGitBranchName + } + + // 2. 打开默认分支 + gitRepoEntity, err := git_module.OpenRepository(ctx, repo.RepoPath()) + if err != nil { + log.Error("Failed to open repository %s: %v", repo.RepoPath(), err) + return setting.Devstar.Devcontainer.DefaultDevcontainerImageName + } + defer func(gitRepoEntity *git_module.Repository) { + _ = gitRepoEntity.Close() + }(gitRepoEntity) + + // 3. 获取分支名称 + commit, err := gitRepoEntity.GetBranchCommit(branchName) + if err != nil { + return setting.Devstar.Devcontainer.DefaultDevcontainerImageName + } + + // 4. 读取 .devcontainer/devcontainer.json 文件 + const maxDevcontainerJSONSize = 10 * 1024 * 1024 // 设置最大允许的文件大小 1MB + devcontainerJSONContent, err := commit.GetFileContent(".devcontainer/devcontainer.json", maxDevcontainerJSONSize) + if err != nil { + log.Error("Failed to get .devcontainer/devcontainer.json file: %v", err) + return setting.Devstar.Devcontainer.DefaultDevcontainerImageName + } + + // 5. 解析 JSON + var devcontainerJSON map[string]interface{} + err = json.Unmarshal([]byte(devcontainerJSONContent), &devcontainerJSON) + if err != nil { + log.Error("Failed to unmarshal .devcontainer/devcontainer.json: %v", err) + return setting.Devstar.Devcontainer.DefaultDevcontainerImageName + } + + // 6. 解析并返回 + devcontainerImage, ok := devcontainerJSON["image"].(string) + if !ok || len(devcontainerImage) == 0 { + return setting.Devstar.Devcontainer.DefaultDevcontainerImageName + } + return devcontainerImage +} diff --git a/services/devstar_devcontainer/dto/CreateDevcontainerDTO.go b/services/devstar_devcontainer/dto/CreateDevcontainerDTO.go index 2e5db15f48..f86c62853d 100644 --- a/services/devstar_devcontainer/dto/CreateDevcontainerDTO.go +++ b/services/devstar_devcontainer/dto/CreateDevcontainerDTO.go @@ -8,4 +8,5 @@ type CreateDevcontainerDTO struct { devstar_devcontainer_models.DevstarDevcontainer SSHPublicKeyList []string GitRepositoryURL string + Image string } diff --git a/services/devstar_devcontainer/k8s_agent/AssignDevcontainerCreation2K8sOperator.go b/services/devstar_devcontainer/k8s_agent/AssignDevcontainerCreation2K8sOperator.go index cd09acc077..76a294cb69 100644 --- a/services/devstar_devcontainer/k8s_agent/AssignDevcontainerCreation2K8sOperator.go +++ b/services/devstar_devcontainer/k8s_agent/AssignDevcontainerCreation2K8sOperator.go @@ -1,10 +1,11 @@ package k8s_agent import ( + "context" + 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" devcontainer_k8s_agent_module "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent" @@ -31,12 +32,29 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine CreateOptions: apimachinery_meta_v1.CreateOptions{}, Name: newDevContainer.Name, Namespace: setting.Devstar.Devcontainer.Namespace, - // TODO: 后期根据 .devcontainer/devcontainer.json 或默认设置选项,指定 devContainer image 和 初始化命令 - Image: "devstar.cn/public/base-ssh-devcontainer:ubuntu-20.04-20241014", + Image: newDevContainer.Image, + /** + * 配置 Kubernetes 主容器启动命令注意事项: + * 1. 确保 Image 中已安装 OpenSSH Server + * 2. 容器启动后必须拉起 OpenSSH 后台服务 + * 3. 请勿使用 sleep infinity 或者 tail -f /dev/null 等无限等待命令,否则将造成大量僵尸()进程: + * $ ps aux | grep "" # 列举僵尸进程 + * USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND + * pollina+ 2336 0.0 0.0 0 0 ? Z 17:22 0:00 [sshd] + * pollina+ 10986 0.0 0.0 0 0 ? Z 16:12 0:00 [sshd] + * pollina+ 24722 0.0 0.0 0 0 ? Z 18:36 0:00 [sshd] + * pollina+ 26773 0.0 0.0 0 0 ? Z 18:37 0:00 [sshd] + * $ ubuntu@node2:~$ ps o ppid 2336 10986 24722 26773 # 查询僵尸进程父进程PID + * PPID + * 21826 + * $ ps aux | grep # 列举僵尸进程父进程详情 + * USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND + * root 21826 0.0 0.0 2520 408 ? Ss 18:36 0:00 sleep infinity + */ CommandList: []string{ "/bin/bash", "-c", - "service ssh restart && sleep infinity", + "service ssh restart", }, ContainerPort: 22, ServicePort: 22, diff --git a/templates/user/auth/signin_wechat_qr_inner.tmpl b/templates/user/auth/signin_wechat_qr_inner.tmpl index 71dbd80623..5eb6b5f1a3 100644 --- a/templates/user/auth/signin_wechat_qr_inner.tmpl +++ b/templates/user/auth/signin_wechat_qr_inner.tmpl @@ -17,7 +17,7 @@
- Wechat Official Accout QR Code Ticket {{.wechatQrTicket}} + Wechat Official Accout QR Code Ticket {{.wechatQrTicket}}
@@ -26,17 +26,44 @@ @@ -56,12 +83,15 @@ document.addEventListener('DOMContentLoaded', () => { /* 停止轮询 */ isQrTicketWaitingPolling = false; - /* 创建新的pre标签并插入DOM树中二维码图片下方,提示用户微信二维码已经过期 */ - const qrImageElement = document.getElementById('idWechatQr') - qrImageElement.classList.add('expire-mask'); - const qrExpirationMessage = document.createElement('pre'); - qrImageElement.insertAdjacentElement('afterend', qrExpirationMessage); - qrExpirationMessage.textContent = {{ctx.Locale.Tr "settings.wechat_official_account_qr_expired"}}; + /* 创建遮罩层 */ + const qrContainer = document.querySelector('.wechat-qr-container'); + const expireMask = document.createElement('div'); + expireMask.className = 'expire-mask'; + expireMask.innerHTML = ` +
{{ctx.Locale.Tr "settings.wechat_official_account_qr_expired"}}
+ `; + expireMask.addEventListener('click', () => window.location.reload()); + qrContainer.appendChild(expireMask); } }, timeoutQrTicketPolling