!46 [DIP-2][DevContainer] 添加 WebSSH 和 保存镜像功能、更新了DevContainer相关的配置

* 合并devcontainer web相关的源文件,简化目录结构
* devcontainer、ssh_key_pair和devcontainer.cloud
* fixed bug:创建容器时Host为localhost时创建失败的问题
* 删除了死代码,更新了一些命名(主要是去掉devstar字符串)
* 常量名全大写
* devcontainer HOST改为用户设置的域名或IP
* 安装时如没有配置devcontainer则默认设置为docker方式
* 直接使用kubernetes和docker简化代码提高可读性
* 去除services/devstar_devcontainer文件夹名中不必要的devstar字符串
* 去除services/devstar_devcontainer文件夹名中不必要的devstar字符串
* 文件名中去掉不必要的devstar字符串
* 变量名中删除不必要的Devstar字符串
* Merge branch 'dev' into feature-websshAndUpdateImage
* change pages style
* change Structure
* fix bug
* websshAndUpdateImage
This commit is contained in:
xinitx
2025-01-07 01:25:54 +00:00
repo.diff.committed_by 孟宁
repo.diff.parent 171bc80cd7
repo.diff.commit e6d1dbb381
repo.diff.stats_desc%!(EXTRA int=61, int=1004, int=642)

repo.diff.view_file

@@ -55,17 +55,17 @@ data:
ui.admin: |
DEV_CONTAINERS_PAGING_NUM = 50
devstar.devcontainer: |
devcontainer: |
ENABLED = true
AGENT = k8s
TIMEOUT_SECONDS = 120
HOST = <k8s 暴露访问域名或IP比如 devcontainer.devstar.cn >
NAMESPACE = <k8s DevStar Studio 部署 namespace比如 devstar-studio-ns >
devstar.ssh_key_pair: |
ssh_key_pair: |
KEY_SIZE = <写入希望生成的SSH密钥长度比如 4096 ,默认值 2048>
devstar.cloud: |
devcontainer.cloud: |
ENABLED = true
PROVIDER = tencent
@@ -87,7 +87,7 @@ stringData:
WECHAT_OFFICIAL_ACCOUNT_MESSAGE_TOKEN = <微信公众号自定义Token>
WECHAT_OFFICIAL_ACCOUNT_MESSAGE_AES_KEY = <微信公众号AES加密密钥>
devstar.cloud.tencent: |
devcontainer.cloud.tencent: |
ENDPOINT = <API访问端点名称例如 vpc.tencentcloudapi.com>
REGION = <区域代码,例如 ap-shanghai>
NAT_GATEWAY_ID = <腾讯云控制台使用的 NAT网关 ID>
@@ -258,7 +258,7 @@ subjects:
> config, err = clientgorest.InClusterConfig()
> if err != nil {
> log.Error("Failed to obtain Kubernetes config both inside/outside of cluster, the DevContainer is Disabled")
> setting.Devstar.Devcontainer.Enabled = false
> setting.Devcontainer.Enabled = false
> return nil, err
> }
> }

repo.diff.view_file

@@ -1,13 +1,14 @@
package k8s_agent
import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/devstar_cloud_provider"
"code.gitea.io/gitea/services/devstar_devcontainer/errors"
"context"
"encoding/json"
"fmt"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/devcontainer/errors"
"code.gitea.io/gitea/services/devstar_cloud_provider"
devcontainer_api_v1 "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/api/v1"
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
devcontainer_k8s_agent_modules_errors "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/errors"
@@ -138,7 +139,7 @@ func CreateDevcontainer(ctx *context.Context, client dynamic_client.Interface, o
// test-mockrepo1-6c5369588f8911e-0 0/1 Init:CrashLoopBackOff 1 (5s ago) 7s
// 需要删除刚刚创建的 k8s CRD然后返回 DevContainer 初始化失败
optsDeleteInitFailed := &devcontainer_dto.DeleteDevcontainerOptions{
Namespace: setting.Devstar.Devcontainer.Namespace,
Namespace: setting.Devcontainer.Namespace,
Name: devcontainerApp.Name,
}
_ = DeleteDevcontainer(ctx, client, optsDeleteInitFailed)

repo.diff.view_file

@@ -1,14 +1,15 @@
package k8s_agent
import (
"context"
"fmt"
devcontainer_api_v1 "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/api/v1"
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
devcontainer_errors "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/errors"
devcontainer_module_utils "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/utils"
devcontainer_agent_module_vo "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/vo"
"code.gitea.io/gitea/modules/setting"
"context"
"fmt"
apimachinery_api_metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apimachinery_apis_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apimachinery_apis_v1_unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -85,7 +86,7 @@ func waitUntilDevcontainerReadyWithTimeout(ctx *context.Context, client dynamic_
}
// 1. 注册 watcher 监听 DevContainer Status 变化
watcherTimeoutSeconds := setting.Devstar.Devcontainer.TimeoutSeconds
watcherTimeoutSeconds := setting.Devcontainer.TimeoutSeconds
watcher, err := client.Resource(groupVersionResource).Namespace(opts.Namespace).Watch(*ctx, apimachinery_apis_v1.ListOptions{
FieldSelector: fmt.Sprintf("metadata.name=%s", opts.Name),
Watch: true,
@@ -154,6 +155,6 @@ func waitUntilDevcontainerReadyWithTimeout(ctx *context.Context, client dynamic_
return nil, devcontainer_errors.ErrOpenDevcontainerTimeout{
Name: opts.Name,
Namespace: opts.Namespace,
TimeoutSeconds: setting.Devstar.Devcontainer.TimeoutSeconds,
TimeoutSeconds: setting.Devcontainer.TimeoutSeconds,
}
}

repo.diff.view_file

@@ -1,9 +1,10 @@
package k8s_agent
import (
"context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"context"
dynamicclient "k8s.io/client-go/dynamic"
clientgorest "k8s.io/client-go/rest"
@@ -21,7 +22,7 @@ func GetKubernetesClient(ctx *context.Context) (dynamicclient.Interface, error)
config, err = clientgorest.InClusterConfig()
if err != nil {
log.Error("Failed to obtain Kubernetes config both inside/outside of cluster, the DevContainer is Disabled")
setting.Devstar.Devcontainer.Enabled = false
setting.Devcontainer.Enabled = false
return nil, err
}
}

repo.diff.view_file

@@ -1,4 +1,4 @@
package docker_agent
package docker
import (
"context"
@@ -13,7 +13,7 @@ import (
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
devcontainer_service_dto "code.gitea.io/gitea/services/devstar_devcontainer/dto"
devcontainer_service_dto "code.gitea.io/gitea/services/devcontainer/dto"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
@@ -46,9 +46,9 @@ func AssignDevcontainerCreationDockerOperator(ctx *context.Context, newDevContai
CreateOptions: apimachinery_meta_v1.CreateOptions{},
Image: newDevContainer.Image,
CommandList: []string{
"sh",
"sh",
"-c",
"rm -f /etc/ssh/ssh_host_*; ssh-keygen -A ; service ssh start ; while true; do sleep 60; done",
"rm -f /etc/ssh/ssh_host_*; ssh-keygen -A ; service ssh start ; cd ~/ttyd/build; ./ttyd -W bash;",
},
SSHPublicKeyList: newDevContainer.SSHPublicKeyList,
GitRepositoryURL: newDevContainer.GitRepositoryURL,
@@ -107,17 +107,26 @@ func createAndStartContainer(cli *client.Client, opts *devcontainer_dto.CreateDe
Tty: true,
OpenStdin: true,
ExposedPorts: nat.PortSet{
"22/tcp": struct{}{},
"22/tcp": struct{}{},
"7681/tcp": struct{}{},
},
}
host := setting.Devcontainer.Host
if host == "localhost" {
host = "127.0.0.1" // 或使用宿主机的真实 IP 地址
}
// 设置容器主机配置
hostConfig := &container.HostConfig{
// PortBindings: nat.PortMap{
// "22/tcp": []nat.PortBinding{{HostIP: "0.0.0.0", HostPort: strconv.Itoa(int(opts.ServicePort))}}, // 将容器的22端口映射到宿主机的8080端口
// },
ExtraHosts: []string{"host.docker.internal:" + setting.Devstar.Devcontainer.Host},
ExtraHosts: []string{"host.docker.internal:" + host},
PublishAllPorts: true,
RestartPolicy: container.RestartPolicy{
Name: "always",
},
}
// 创建容器
@@ -175,6 +184,11 @@ func createAndStartContainer(cli *client.Client, opts *devcontainer_dto.CreateDe
if err != nil {
return "", fmt.Errorf("执行命令失败:%v", err)
}
// 启动执行实例
err = cli.ContainerExecStart(context.Background(), ex.ID, types.ExecStartCheck{})
if err != nil {
return "", fmt.Errorf("Failed to start exec instance:%v", err)
}
output, err := ioutil.ReadAll(execResp.Reader)
log.Info("Command output:\n%s\n", output)
// 获取容器详细信息

repo.diff.view_file

@@ -1,4 +1,4 @@
package docker_agent
package docker
import (
devstar_devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer"

repo.diff.view_file

@@ -0,0 +1,82 @@
package docker
import (
"context"
"fmt"
"io/ioutil"
"strconv"
"code.gitea.io/gitea/modules/log"
devcontainer_service_errors "code.gitea.io/gitea/services/devcontainer/errors"
devcontainer_service_options "code.gitea.io/gitea/services/devcontainer/options"
"github.com/docker/docker/api/types"
)
func AssignDevcontainerGettingDockerOperator(ctx *context.Context, opts *devcontainer_service_options.OpenDevcontainerAppDispatcherOptions) (uint16, error) {
// 1. 创建docker client
cli, err := CreateDockerClient(ctx)
if err != nil {
return 0, err
}
if cli != nil {
defer cli.Close()
}
// 获取容器详细信息
containerJSON, err := cli.ContainerInspect(context.Background(), opts.Name)
if err != nil {
return 0, err
}
// 获取端口映射信息
portBindings := containerJSON.NetworkSettings.Ports
for containerPort, bindings := range portBindings {
for _, binding := range bindings {
log.Info("Container Port %s is mapped to Host Port %s on IP %s\n", containerPort, binding.HostPort, binding.HostIP)
if containerPort.Port() == "22" {
v, err := strconv.ParseUint(binding.HostPort, 10, 16)
if err != nil {
return 0, err
}
// 执行命令在容器内添加公钥
execConfig := types.ExecConfig{
Cmd: []string{"sh", "-c", fmt.Sprintf("echo '%s' >> ~/.ssh/authorized_keys", opts.UserPublicKey)},
AttachStdout: true,
AttachStderr: true,
}
// 创建执行实例
execID, err := cli.ContainerExecCreate(context.Background(), containerJSON.ID, execConfig)
if err != nil {
log.Info("Failed to create exec instance:", err)
return 0, err
}
// 附加到执行实例
attachConfig := types.ExecStartCheck{}
resp, err := cli.ContainerExecAttach(context.Background(), execID.ID, attachConfig)
if err != nil {
log.Info("Failed to attach to exec instance:", err)
return 0, err
}
defer resp.Close()
// 启动执行实例
err = cli.ContainerExecStart(context.Background(), execID.ID, attachConfig)
if err != nil {
log.Info("Failed to start exec instance:", err)
return 0, err
}
output, err := ioutil.ReadAll(resp.Reader)
log.Info("Command output:\n%s\n", output)
return uint16(v), nil
}
}
}
return 0, devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Open DevContainer in docker",
Message: "cannot find SSH containerPort 22 for DevContainer " + opts.Name,
}
}
func addTemporaryPublicKey() {
}

repo.diff.view_file

@@ -0,0 +1,97 @@
package docker
import (
"bytes"
"context"
"net/http"
"os/exec"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
gitea_web_context "code.gitea.io/gitea/services/context"
devcontainer_service_options "code.gitea.io/gitea/services/devcontainer/options"
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/docker/docker/api/types"
)
func AssignDevcontainerUpdateDockerOperator(ctx *gitea_web_context.Context, opts *devcontainer_service_options.UpdateDevcontainerOptions) {
// 1. 创建docker client
reqctx := ctx.Req.Context()
cli, err := CreateDockerClient(&reqctx)
defer cli.Close()
if err != nil {
ctx.JSON(500, map[string]string{
"message": "error CreateDockerClient"})
return
}
dockerSocketPath, err := GetDockerSocketPath()
if err != nil {
ctx.JSON(500, map[string]string{
"message": "error GetDockerSocketPath"})
return
}
script := "docker " + "-H " + dockerSocketPath + " login -u " + opts.RepositoryUsername + " -p " + opts.PassWord + " " + opts.RepositoryAddress + " "
log.Info(string(script))
cmd := exec.Command("sh", "-c", script)
output, err := cmd.CombinedOutput()
log.Info(string(output))
if err != nil {
log.Info("error RegistryLogin:", err)
ctx.JSON(500, map[string]string{
"message": "error RegistryLogin"})
return
}
// 获取容器详细信息
containerJSON, err := cli.ContainerInspect(context.Background(), opts.DevContainerName)
if err != nil {
log.Info("error ContainerInspect:", err)
ctx.JSON(500, map[string]string{
"message": "error ContainerInspect"})
return
}
imageRef := opts.RepositoryAddress + "/" + opts.RepositoryUsername + "/" + opts.ImageName
commitResp, err := cli.ContainerCommit(ctx, containerJSON.ID, types.ContainerCommitOptions{Reference: imageRef})
log.Info(commitResp.ID)
if err != nil {
log.Info("error ContainerCommit:", err)
ctx.JSON(500, map[string]string{
"message": "error ContainerCommit"})
return
}
// 推送到仓库
script = "docker " + "-H " + dockerSocketPath + " push " + imageRef
log.Info(string(script))
cmd = exec.Command("sh", "-c", script)
output, err = cmd.CombinedOutput()
log.Info(string(output))
if err != nil {
log.Info("error docker push:", err)
ctx.JSON(500, map[string]string{
"message": "error docker push",
})
return
}
jsonString := `{"image":"` + imageRef + `"}`
resp, err := files_service.ChangeRepoFiles(db.DefaultContext, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "update",
TreePath: ".devcontainer/devcontainer.json",
ContentReader: bytes.NewReader([]byte(jsonString)),
},
},
OldBranch: ctx.Repo.Repository.DefaultBranch,
Message: "Update container",
})
log.Info(resp.Commit.URL)
if err != nil {
log.Info("error ChangeRepoFiles:", err)
ctx.JSON(500, map[string]string{
"message": "error ChangeRepoFiles"})
return
}
ctx.JSON(http.StatusOK, map[string]string{
"redirect": ctx.Repo.RepoLink + "/dev-container",
"message": "success"})
}

repo.diff.view_file

@@ -1,14 +1,16 @@
package docker_agent
package docker
import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"context"
"fmt"
"github.com/docker/docker/client"
"os"
"path/filepath"
"strconv"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/docker/docker/client"
)
func CreateDockerClient(ctx *context.Context) (*client.Client, error) {
@@ -30,8 +32,8 @@ func CreateDockerClient(ctx *context.Context) (*client.Client, error) {
Docker环境路径优先级: 配置文件环境变量
*/
func GetDockerSocketPath() (string, error) {
if setting.Devstar.Devcontainer.DockerHost != "" {
return setting.Devstar.Devcontainer.DockerHost, nil
if setting.Devcontainer.DockerHost != "" {
return setting.Devcontainer.DockerHost, nil
}
socket, found := os.LookupEnv("DOCKER_HOST")
if found {
@@ -66,3 +68,32 @@ func CheckIfDockerRunning(ctx context.Context, configDockerHost string) (*client
}
return cli, nil
}
func GetSSHPort(ctx *context.Context, name string) uint16 {
// 1. 创建docker client
cli, err := CreateDockerClient(ctx)
defer cli.Close()
if err != nil {
return 0
}
// 获取容器详细信息
containerJSON, err := cli.ContainerInspect(context.Background(), name)
if err != nil {
return 0
}
// 获取端口映射信息
portBindings := containerJSON.NetworkSettings.Ports
for containerPort, bindings := range portBindings {
for _, binding := range bindings {
log.Info("Container Port %s is mapped to Host Port %s on IP %s\n", containerPort, binding.HostPort, binding.HostIP)
if containerPort.Port() == "7681" {
v, err := strconv.ParseUint(binding.HostPort, 10, 16)
if err != nil {
return 0
}
return uint16(v)
}
}
}
return 0
}

repo.diff.view_file

@@ -0,0 +1,177 @@
package setting
import (
"code.gitea.io/gitea/modules/log"
)
const (
KUBERNETES = "kubernetes" // 支持 "k8s" 和 "kubernetes"
DOCKER = "docker"
)
// 检查用户输入的 DevContainer Agent 是否有效
func isValidAgent(agent string) bool {
return agent == "k8s" || agent == KUBERNETES || agent == DOCKER
}
const (
CLOUD_PROVIDER_TENCENT = "tencent"
DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX = "DevContainer: "
)
// validCloudProviderSet 私有 Set 结构,标识目前系统所有支持的 Cloud Provider 类型
var validCloudProviderSet = map[string]struct{}{
CLOUD_PROVIDER_TENCENT: {},
}
type DevcontainerType struct {
Enabled bool
Host string
Agent string
Namespace string
TimeoutSeconds int64
DefaultGitBranchName string
DefaultDevcontainerImageName string
DockerHost string
}
type SSHKeyPairType struct {
KeySize int
}
type CloudType struct {
Enabled bool
Provider string
Tencent CloudProviderTencentType `ini:"devcontainer.cloud.tencent"`
}
type CloudProviderTencentType struct {
Endpoint string
Region string
NatGatewayId string
PublicIpAddress string
PrivateIpAddress string
IpProtocol string
SecretId string
SecretKey string
}
var Devcontainer = DevcontainerType{
Enabled: false,
Namespace: "default",
TimeoutSeconds: 900, // Max wait time for DevContainer to be ready (blocking), default is 15 minutes, can be overridden by app.ini
DefaultGitBranchName: "main", // Default branch name for .devcontainer/devcontainer.json
DefaultDevcontainerImageName: "devstar.cn/public/base-ssh-devcontainer:ubuntu-20.04-20241014", // Default image if not specified
}
var SSHKeypair = SSHKeyPairType{
KeySize: 2048, // Size of the SSH key
}
var Cloud = CloudType{
Enabled: false, // Cloud feature toggle
}
// validateDevcontainerSettings 检查从 ini 配置文件中读取 DevStar DevContainer 配置信息,若数据无效,则自动禁用 DevContainer
func validateDevcontainerSettings() {
// 检查 Host 是否为空,若为空,则自动将 DevContainer 设置为禁用
if len(Devcontainer.Host) == 0 {
log.Warn("INVALID config 'host' for DevStar DevContainer")
Devcontainer.Enabled = false
}
// 检查用户输入的 DevContainer Agent 是否存在支持列表,若不支持,则将 DevContainer 设置为禁用
if !isValidAgent(Devcontainer.Agent) {
log.Warn("Invalid config 'agent' for DevStar DevContainer")
Devcontainer.Enabled = false
}
// 检查默认分支名称设置
if len(Devcontainer.DefaultGitBranchName) == 0 {
log.Warn("INVALID config 'DefaultGitBranchName' for DevStar DevContainer")
Devcontainer.Enabled = false
}
// 检查默认 DevContainer Image
if len(Devcontainer.DefaultDevcontainerImageName) == 0 {
log.Warn("INVALID config 'DefaultGitBranchNameDefaultDevcontainerImageName' for DevStar DevContainer")
Devcontainer.Enabled = false
}
if Devcontainer.Enabled == false {
log.Warn("DevStar DevContainer Service Disabled")
} else {
log.Info("DevStar DevContainer Service Enabled")
}
}
// validateSSHKeyPairSettings 检查从 ini 配置文件中读取 DevStar SSH Key Pair 配置信息
func validateSSHKeyPairSettings() {
if SSHKeypair.KeySize < 1024 {
SSHKeypair.KeySize = 1024
}
}
// validateDevcontainerCloudSettings 检查从 ini 配置文件中读取 DevStar Cloud 配置信息
func validateDevcontainerCloudSettings() {
switch Cloud.Provider {
case CLOUD_PROVIDER_TENCENT:
// 腾讯云配置检查
if len(Cloud.Tencent.NatGatewayId) < 4 {
log.Warn("INVALID NAT Gateway ID '%v' for DevStar Cloud Provider Tencent", Cloud.Tencent.NatGatewayId)
Cloud.Enabled = false
}
if Cloud.Tencent.IpProtocol != "TCP" && Cloud.Tencent.IpProtocol != "UDP" {
log.Warn("INVALID IP Protocol '%v' for DevStar Cloud Provider Tencent", Cloud.Tencent.IpProtocol)
Cloud.Enabled = false
}
if len(Cloud.Tencent.Region) < 3 || len(Cloud.Tencent.Endpoint) == 0 {
log.Warn("INVALID (Region, Endpoint) pair ('%v', '%v') for DevStar Cloud Provider Tencent",
Cloud.Tencent.Region, Cloud.Tencent.Endpoint)
Cloud.Enabled = false
}
if len(Cloud.Tencent.PrivateIpAddress) == 0 || len(Cloud.Tencent.PublicIpAddress) == 0 {
log.Warn("INVALID (PublicIpAddress, PrivateIpAddress) pair ('%v', '%v') for DevStar Cloud Provider Tencent",
Cloud.Tencent.PublicIpAddress, Cloud.Tencent.PrivateIpAddress)
Cloud.Enabled = false
}
if len(Cloud.Tencent.SecretId) == 0 || len(Cloud.Tencent.SecretKey) == 0 {
log.Warn("INVALID (SecretId, SecretKey) pair for DevStar Cloud Provider Tencent")
Cloud.Enabled = false
}
default:
// 无效 Cloud Provider 名称
log.Warn("INVALID config '%v' for DevStar Cloud", Cloud.Provider)
Cloud.Enabled = false
}
if Cloud.Enabled == false {
log.Warn("DevStar Cloud Provider Service Disabled")
} else {
log.Info("DevStar Cloud Provider '%v' Enabled", Cloud.Provider)
}
}
func loadDevcontainerFrom(rootCfg ConfigProvider) {
mustMapSetting(rootCfg, "devcontainer", &Devcontainer)
validateDevcontainerSettings()
mustMapSetting(rootCfg, "ssh_key_pair", &SSHKeypair)
validateSSHKeyPairSettings()
if Devcontainer.Agent == "k8s" || Devcontainer.Agent == KUBERNETES {
mustMapSetting(rootCfg, "devcontainer.cloud", &Cloud)
validateDevcontainerCloudSettings()
}
}

repo.diff.view_file

@@ -1,181 +0,0 @@
package setting
import (
"code.gitea.io/gitea/modules/log"
)
const (
DEVCONTAINER_AGENT_NAME_K8S string = "k8s"
DEVCONTAINER_AGENT_NAME_DOCKER string = "docker"
)
// package 内部私有变量,是一个 Set 结构,标识目前系统所有支持的 DevContainer Agent 类型
var validDevcontainerAgentSet = map[string]struct{}{
DEVCONTAINER_AGENT_NAME_K8S: {},
DEVCONTAINER_AGENT_NAME_DOCKER: {},
}
const (
CLOUD_PROVIDER_TENCENT = "tencent"
DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX = "DevContainer: "
)
// validCloudProviderSet 私有 Set 结构,标识目前系统所有支持的 Cloud Provider 类型
var validCloudProviderSet = map[string]struct{}{
CLOUD_PROVIDER_TENCENT: {},
}
var Devstar = struct {
Devcontainer DevcontainerType `ini:"devstar.devcontainer"`
SSHKeypair SSHKeyPairType `ini:"devstar.ssh_key_pair"`
Cloud CloudType `ini:"devstar.cloud"`
}{
Devcontainer: DevcontainerType{
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,
},
Cloud: CloudType{
Enabled: false,
},
}
type DevcontainerType struct {
Enabled bool
Host string
Agent string
Namespace string
TimeoutSeconds int64
DefaultGitBranchName string
DefaultDevcontainerImageName string
DockerHost string
}
type SSHKeyPairType struct {
KeySize int
}
type CloudType struct {
Enabled bool
Provider string
Tencent CloudProviderTencentType `ini:"devstar.cloud.tencent"`
}
type CloudProviderTencentType struct {
Endpoint string
Region string
NatGatewayId string
PublicIpAddress string
PrivateIpAddress string
IpProtocol string
SecretId string
SecretKey string
}
// validateDevstarDevcontainerSettings 检查从 ini 配置文件中读取 DevStar DevContainer 配置信息,若数据无效,则自动禁用 DevContainer
func validateDevstarDevcontainerSettings() {
// 检查 Host 是否为空,若为空,则自动将 DevContainer 设置为禁用
if len(Devstar.Devcontainer.Host) == 0 {
log.Warn("INVALID config 'host' for DevStar DevContainer")
Devstar.Devcontainer.Enabled = false
}
// 检查用户输入的 DevContainer Agent 是否存在支持列表,若不支持,则将 DevContainer 设置为禁用
if _, exists := validDevcontainerAgentSet[Devstar.Devcontainer.Agent]; !exists {
log.Warn("INVALID config 'agent' for DevStar DevContainer")
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 {
log.Info("DevStar DevContainer Service Enabled")
}
}
// validateDevstarSSHKeyPairSettings 检查从 ini 配置文件中读取 DevStar SSH Key Pair 配置信息
func validateDevstarSSHKeyPairSettings() {
if Devstar.SSHKeypair.KeySize < 1024 {
Devstar.SSHKeypair.KeySize = 1024
}
}
// validateDevstarCloudSettings 检查从 ini 配置文件中读取 DevStar Cloud 配置信息
func validateDevstarCloudSettings() {
switch Devstar.Cloud.Provider {
case CLOUD_PROVIDER_TENCENT:
// 腾讯云配置检查
if len(Devstar.Cloud.Tencent.NatGatewayId) < 4 {
log.Warn("INVALID NAT Gateway ID '%v' for DevStar Cloud Provider Tencent", Devstar.Cloud.Tencent.NatGatewayId)
Devstar.Cloud.Enabled = false
}
if Devstar.Cloud.Tencent.IpProtocol != "TCP" && Devstar.Cloud.Tencent.IpProtocol != "UDP" {
log.Warn("INVALID IP Protocol '%v' for DevStar Cloud Provider Tencent", Devstar.Cloud.Tencent.IpProtocol)
Devstar.Cloud.Enabled = false
}
if len(Devstar.Cloud.Tencent.Region) < 3 || len(Devstar.Cloud.Tencent.Endpoint) == 0 {
log.Warn("INVALID (Region, Endpoint) pair ('%v', '%v') for DevStar Cloud Provider Tencent",
Devstar.Cloud.Tencent.Region, Devstar.Cloud.Tencent.Endpoint)
Devstar.Cloud.Enabled = false
}
if len(Devstar.Cloud.Tencent.PrivateIpAddress) == 0 || len(Devstar.Cloud.Tencent.PublicIpAddress) == 0 {
log.Warn("INVALID (PublicIpAddress, PrivateIpAddress) pair ('%v', '%v') for DevStar Cloud Provider Tencent",
Devstar.Cloud.Tencent.PublicIpAddress, Devstar.Cloud.Tencent.PrivateIpAddress)
Devstar.Cloud.Enabled = false
}
if len(Devstar.Cloud.Tencent.SecretId) == 0 || len(Devstar.Cloud.Tencent.SecretKey) == 0 {
log.Warn("INVALID (SecretId, SecretKey) pair for DevStar Cloud Provider Tencent")
Devstar.Cloud.Enabled = false
}
default:
// 无效 Cloud Provider 名称
log.Warn("INVALID config '%v' for DevStar Cloud", Devstar.Cloud.Provider)
Devstar.Cloud.Enabled = false
}
if Devstar.Cloud.Enabled == false {
log.Warn("DevStar Cloud Provider Service Disabled")
} else {
log.Info("DevStar Cloud Provider '%v' Enabled", Devstar.Cloud.Provider)
}
}
func loadDevstarFrom(rootCfg ConfigProvider) {
mustMapSetting(rootCfg, "devstar", &Devstar)
validateDevstarDevcontainerSettings()
validateDevstarSSHKeyPairSettings()
validateDevstarCloudSettings()
}

repo.diff.view_file

@@ -217,7 +217,7 @@ func LoadSettings() {
loadMimeTypeMapFrom(CfgProvider)
loadFederationFrom(CfgProvider)
loadWechatSettingsFrom(CfgProvider)
loadDevstarFrom(CfgProvider)
loadDevcontainerFrom(CfgProvider)
}
// LoadSettingsForInstall initializes the settings for install
@@ -226,6 +226,7 @@ func LoadSettingsForInstall() {
loadServiceFrom(CfgProvider)
loadMailerFrom(CfgProvider)
loadWechatSettingsFrom(CfgProvider)
loadDevcontainerFrom(CfgProvider)
}
var configuredPaths = make(map[string]string)

repo.diff.view_file

@@ -1024,6 +1024,7 @@ visibility.private_tooltip = Visible only to members of organizations you have j
dev_container = Dev Container
dev_container_empty = Oops, it looks like there is no Dev Container in this repository.
dev_container_invalid_config_prompt = Invalid Dev Container Configuration: Please upload a valid 'devcontainer.json' file to the default branch, and ensure that this repository is NOT archived.
dev_container_control.update = Save Dev Container
dev_container_control.create = Create Dev Container
dev_container_control.creation_success_for_user = The Dev Container has been created successfully for user '%s'.
dev_container_control.creation_failed_for_user = Failed to create the Dev Container for user '%s'.

repo.diff.view_file

@@ -1022,6 +1022,7 @@ visibility.private_tooltip=仅对您已加入的组织的成员可见。
dev_container = 开发容器
dev_container_empty = 您还没有该仓库的开发容器
dev_container_invalid_config_prompt = 开发容器配置无效:请上传有效的 devcontainer.json 至默认分支,且确保仓库未处于存档状态
dev_container_control.update = 保存开发容器
dev_container_control.create = 创建开发容器
dev_container_control.creation_success_for_user = 用户 '%s' 已成功创建开发容器
dev_container_control.creation_failed_for_user = 用户 '%s' 开发容器创建失败

repo.diff.view_file

@@ -1,13 +1,14 @@
package devcontainer
import (
"strconv"
gitea_web_module "code.gitea.io/gitea/modules/web"
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_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
devcontainer_api_service "code.gitea.io/gitea/services/devcontainer/api_services"
devcontainer_service_options "code.gitea.io/gitea/services/devcontainer/options"
"code.gitea.io/gitea/services/forms"
"strconv"
)
// CreateRepoDevcontainer 创建 某用户在某仓库的 DevContainer

repo.diff.view_file

@@ -1,11 +1,12 @@
package devcontainer
import (
"strconv"
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_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
"strconv"
devcontainer_api_service "code.gitea.io/gitea/services/devcontainer/api_services"
devcontainer_service_options "code.gitea.io/gitea/services/devcontainer/options"
)
// DeleteRepoDevcontainer 删除某仓库的 DevContainer

repo.diff.view_file

@@ -1,11 +1,12 @@
package devcontainer
import (
"strconv"
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"
"code.gitea.io/gitea/services/devstar_devcontainer/options"
"strconv"
devcontainer_api_service "code.gitea.io/gitea/services/devcontainer/api_services"
"code.gitea.io/gitea/services/devcontainer/options"
)
// GetDevcontainer 查找某用户在某仓库的 DevContainer
@@ -14,6 +15,7 @@ import (
// 请求体参数:
// -- repoId: 需要为哪个仓库创建 DevContainer
// -- wait: 是否等待 DevContainer 就绪(默认为 false 直接返回“未就绪”,否则阻塞等待)
// -- UserPublicKey
// 注意:必须携带 用户登录凭证
func GetDevcontainer(ctx *gitea_web_context.Context) {
// 1. 检查用户登录状态,若未登录则返回未授权错误
@@ -25,6 +27,7 @@ func GetDevcontainer(ctx *gitea_web_context.Context) {
// 2. 取得参数
wait := ctx.FormBool("wait")
repoIdStr := ctx.FormString("repoId")
UserPublicKey := ctx.FormString("UserPublicKey")
repoId, err := strconv.ParseInt(repoIdStr, 10, 64)
if err != nil || repoId <= 0 {
Result.RespFailedIllegalParams.RespondJson2HttpResponseWriter(ctx.Resp)
@@ -33,9 +36,10 @@ func GetDevcontainer(ctx *gitea_web_context.Context) {
// 3. 准备调用 API Service 层,获取 DevContainer 信息
optsAbstractOpenDevcontainer := &options.AbstractOpenDevcontainerOptions{
Wait: wait,
RepoId: repoId,
Actor: ctx.Doer,
Wait: wait,
RepoId: repoId,
Actor: ctx.Doer,
UserPublicKey: UserPublicKey,
}
repoDevcontainerVO, err := devcontainer_api_service.OpenDevcontainerAPIService(ctx, optsAbstractOpenDevcontainer)
if err != nil {

repo.diff.view_file

@@ -6,7 +6,7 @@ import (
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
Result "code.gitea.io/gitea/routers/entity"
gitea_web_context "code.gitea.io/gitea/services/context"
devstar_devcontainer_service "code.gitea.io/gitea/services/devstar_devcontainer"
devstar_devcontainer_service "code.gitea.io/gitea/services/devcontainer"
)
// ListUserDevcontainers 枚举已登录用户所有的 DevContainers

repo.diff.view_file

@@ -0,0 +1,8 @@
package vo
type UpdateInfo struct {
ImageName string `json:"ImageName"`
PassWord string `json:"RepositoryPassword"`
RepositoryAddress string `json:"RepositoryAddress"`
RepositoryUsername string `json:"RepositoryUsername"`
}

repo.diff.view_file

@@ -1,11 +1,12 @@
package key_pair
import (
"strconv"
"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 密钥对
@@ -26,7 +27,7 @@ func GenerateNewSSHSessionKeyPair(ctx *gitea_web_context.Context) {
Msg: Result.RespSSHKeyPairGenFailed.Msg,
Data: map[string]string{
"ErrorMsg": err.Error(),
"KeySize": strconv.Itoa(setting.Devstar.SSHKeypair.KeySize),
"KeySize": strconv.Itoa(setting.SSHKeypair.KeySize),
},
}
resp.RespondJson2HttpResponseWriter(ctx.Resp)

repo.diff.view_file

@@ -420,6 +420,13 @@ func SubmitInstall(ctx *context.Context) {
cfg.Section("server").Key("LFS_START_SERVER").SetValue("false")
}
if !setting.Devcontainer.Enabled {
cfg.Section("devcontainer").Key("ENABLED").SetValue("true")
cfg.Section("devcontainer").Key("AGENT").SetValue("docker")
cfg.Section("devcontainer").Key("HOST").SetValue(form.Domain)
cfg.Section("devcontainer").Key("TIMEOUT_SECONDS").SetValue("120")
}
if len(strings.TrimSpace(form.SMTPAddr)) > 0 {
if _, err := mail.ParseAddress(form.SMTPFrom); err != nil {
ctx.RenderWithErr(ctx.Tr("install.smtp_from_invalid"), tplInstall, &form)

repo.diff.view_file

@@ -1,57 +0,0 @@
package devcontainer
import (
"code.gitea.io/gitea/modules/log"
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"
)
// CreateRepoDevContainer 创建仓库 Dev Container
func CreateRepoDevContainer(ctx *gitea_web_context.Context) {
if !isUserDevcontainerAlreadyInRepository(ctx) {
opts := &DevcontainersVO.CreateRepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
err := DevcontainersService.CreateRepoDevcontainer(ctx, opts)
if err != nil {
log.Error("failed to create devContainer with option{%v}: %v", opts, err.Error())
ctx.Flash.Error(ctx.Tr("repo.dev_container_control.creation_failed_for_user", ctx.Doer.Name))
} else {
ctx.Flash.Success(ctx.Tr("repo.dev_container_control.creation_success_for_user", ctx.Doer.Name))
}
}
ctx.Redirect(ctx.Repo.RepoLink + "/dev-container")
}
// isValidRepoDevcontainerJsonFile 辅助判断当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json
func isValidRepoDevcontainerJsonFile(ctx *gitea_web_context.Context) bool {
// 1. 仓库非空,且非 Archived 状态
if ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsArchived {
return false
}
// 2. 当前分支的目录 .devcontainer 下存在 devcontainer.json 文件
fileDevcontainerJsonExists, err := ctx.Repo.FileExists(".devcontainer/devcontainer.json", ctx.Repo.BranchName)
if err != nil || !fileDevcontainerJsonExists {
return false
}
// 3. TODO: DevContainer 格式正确
return true
}
// isUserDevcontainerAlreadyInRepository 辅助判断当前用户在当前仓库是否已有 Dev Container
func isUserDevcontainerAlreadyInRepository(ctx *gitea_web_context.Context) bool {
opts := &DevcontainersVO.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
devcontainerDetails, _ := DevcontainersService.GetRepoDevcontainerDetails(ctx, opts)
return devcontainerDetails.DevContainerId > 0
}

repo.diff.view_file

@@ -1,27 +0,0 @@
package devcontainer
import (
"code.gitea.io/gitea/modules/log"
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"
)
// DeleteRepoDevContainerForCurrentActor 删除仓库 当前用户 Dev Container
func DeleteRepoDevContainerForCurrentActor(ctx *gitea_web_context.Context) {
if isUserDevcontainerAlreadyInRepository(ctx) {
opts := &DevcontainersVO.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
err := DevcontainersService.DeleteRepoDevcontainer(ctx, opts)
if err != nil {
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))
}
}
ctx.JSONRedirect(ctx.Repo.RepoLink + "/dev-container")
}

repo.diff.view_file

@@ -1,44 +0,0 @@
package devcontainer
import (
"code.gitea.io/gitea/modules/base"
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"
"net/http"
)
const (
tplGetRepoDevcontainerDetail base.TplName = "repo/devcontainer/details"
)
// GetRepoDevContainerDetails 获取仓库 Dev Container 详细信息
func GetRepoDevContainerDetails(ctx *gitea_web_context.Context) {
// 1. 查询当前 Repo 已有的 Dev Container 信息
opts := &DevcontainersVO.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
//ctx.Repo.RepoLink == ctx.Repo.Repository.Link()
devContainerMetadata, err := DevcontainersService.GetRepoDevcontainerDetails(ctx, opts)
hasDevContainer := err == nil && devContainerMetadata.DevContainerId > 0
ctx.Data["HasDevContainer"] = hasDevContainer
if hasDevContainer {
ctx.Data["DevContainer"] = devContainerMetadata
}
// 2. 检查当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json
isValidRepoDevcontainerJson := isValidRepoDevcontainerJsonFile(ctx)
if !hasDevContainer && !isValidRepoDevcontainerJson {
ctx.Flash.Error(ctx.Tr("repo.dev_container_invalid_config_prompt"), true)
}
// 3. 携带数据渲染页面,返回
ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container")
ctx.Data["PageIsRepoDevcontainerDetails"] = true
ctx.Data["HasValidDevContainerJSON"] = isValidRepoDevcontainerJson
ctx.Data["Repository"] = ctx.Repo.Repository
ctx.Data["ContextUser"] = ctx.Doer
ctx.HTML(http.StatusOK, tplGetRepoDevcontainerDetail)
}

repo.diff.view_file

@@ -0,0 +1,207 @@
package devcontainer
import (
"code.gitea.io/gitea/modules/log"
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
context "code.gitea.io/gitea/services/context"
DevcontainersService "code.gitea.io/gitea/services/devcontainer"
"fmt"
"net/http"
"strings"
"code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"encoding/json"
"io"
"code.gitea.io/gitea/services/devcontainer/api_services"
"code.gitea.io/gitea/services/devcontainer/options"
)
const (
tplGetRepoDevcontainerDetail base.TplName = "repo/devcontainer/details"
)
// 获取仓库 Dev Container 详细信息
func GetRepoDevContainerDetails(ctx *context.Context) {
// 1. 查询当前 Repo 已有的 Dev Container 信息
opts := &DevcontainersVO.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
//ctx.Repo.RepoLink == ctx.Repo.Repository.Link()
devContainerMetadata, err := DevcontainersService.GetRepoDevcontainerDetails(ctx, opts)
hasDevContainer := err == nil && devContainerMetadata.DevContainerId > 0
ctx.Data["HasDevContainer"] = hasDevContainer
if hasDevContainer {
ctx.Data["DevContainer"] = devContainerMetadata
}
// 2. 检查当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json
isValidRepoDevcontainerJson := isValidRepoDevcontainerJsonFile(ctx)
if !hasDevContainer && !isValidRepoDevcontainerJson {
ctx.Flash.Error(ctx.Tr("repo.dev_container_invalid_config_prompt"), true)
}
// 从devcontainer.json文件提取image字段解析成仓库地址、命名空间、镜像名
imageName := DevcontainersService.GetDefaultDevcontainerImageFromRepoDevcontainerJSON(ctx, ctx.Repo.Repository)
registry, namespace, repo, tag := ParseImageName(imageName)
// 获取SSH端口没有该服务就不显示
ctx.Data["HasWebSSH"] = false
var port uint16
switch setting.Devcontainer.Agent {
case setting.KUBERNETES:
//k8s处理
case setting.DOCKER:
apiRequestContext := ctx.Req.Context()
port = docker.GetSSHPort(&apiRequestContext, devContainerMetadata.DevContainerName)
if port != 0 {
ctx.Data["HasWebSSH"] = true
ctx.Data["WebSSHUrl"] = "http://" + setting.Devcontainer.Host + ":" + fmt.Sprintf("%d", port) + "/"
}
default:
//默认处理
}
// 3. 携带数据渲染页面,返回
ctx.Data["RepositoryAddress"] = registry
ctx.Data["RepositoryUsername"] = namespace
ctx.Data["ImageName"] = ctx.Repo.Repository.Name + "-" + repo + ":" + tag
ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container")
ctx.Data["PageIsRepoDevcontainerDetails"] = true
ctx.Data["HasValidDevContainerJSON"] = isValidRepoDevcontainerJson
ctx.Data["Repository"] = ctx.Repo.Repository
ctx.Data["ContextUser"] = ctx.Doer
ctx.HTML(http.StatusOK, tplGetRepoDevcontainerDetail)
}
func ParseImageName(imageName string) (registry, namespace, repo, tag string) {
// 分离仓库地址和命名空间
parts := strings.Split(imageName, "/")
if len(parts) == 3 {
registry = parts[0]
namespace = parts[1]
repo = parts[2]
} else if len(parts) == 2 {
registry = parts[0]
repo = parts[1]
} else {
repo = imageName
}
// 分离标签
parts = strings.Split(repo, ":")
if len(parts) > 1 {
tag = parts[1]
repo = parts[0]
} else {
tag = "latest"
}
if registry == "" {
registry = "docker.io"
}
return registry, namespace, repo, tag
}
// 创建仓库 Dev Container
func CreateRepoDevContainer(ctx *context.Context) {
if !isUserDevcontainerAlreadyInRepository(ctx) {
opts := &DevcontainersVO.CreateRepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
err := DevcontainersService.CreateRepoDevcontainer(ctx, opts)
if err != nil {
log.Error("failed to create devContainer with option{%v}: %v", opts, err.Error())
ctx.Flash.Error(ctx.Tr("repo.dev_container_control.creation_failed_for_user", ctx.Doer.Name))
} else {
ctx.Flash.Success(ctx.Tr("repo.dev_container_control.creation_success_for_user", ctx.Doer.Name))
}
}
ctx.Redirect(ctx.Repo.RepoLink + "/dev-container")
}
// 辅助判断当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json
func isValidRepoDevcontainerJsonFile(ctx *context.Context) bool {
// 1. 仓库非空,且非 Archived 状态
if ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsArchived {
return false
}
// 2. 当前分支的目录 .devcontainer 下存在 devcontainer.json 文件
fileDevcontainerJsonExists, err := ctx.Repo.FileExists(".devcontainer/devcontainer.json", ctx.Repo.BranchName)
if err != nil || !fileDevcontainerJsonExists {
return false
}
// 3. TODO: DevContainer 格式正确
return true
}
// 辅助判断当前用户在当前仓库是否已有 Dev Container
func isUserDevcontainerAlreadyInRepository(ctx *context.Context) bool {
opts := &DevcontainersVO.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
devcontainerDetails, _ := DevcontainersService.GetRepoDevcontainerDetails(ctx, opts)
return devcontainerDetails.DevContainerId > 0
}
func UpdateRepoDevContainerForCurrentActor(ctx *context.Context) {
opts1 := &DevcontainersVO.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
devContainerMetadata, _ := DevcontainersService.GetRepoDevcontainerDetails(ctx, opts1)
// 取得参数
body, _ := io.ReadAll(ctx.Req.Body)
log.Info(string(body))
var updateInfo DevcontainersVO.UpdateInfo
err := json.Unmarshal(body, &updateInfo)
if err != nil {
log.Info("UpdateRepoDevcontainer 反序列化失败:", err)
ctx.JSON(400, map[string]string{
"message": "error input"})
return
}
opts := &options.UpdateDevcontainerOptions{
ImageName: updateInfo.ImageName,
PassWord: updateInfo.PassWord,
RepositoryAddress: updateInfo.RepositoryAddress,
RepositoryUsername: updateInfo.RepositoryUsername,
DevContainerName: devContainerMetadata.DevContainerName,
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
api_services.UpdateDevcontainerAPIService(ctx, opts)
}
// 删除仓库 当前用户 Dev Container
func DeleteRepoDevContainerForCurrentActor(ctx *context.Context) {
if isUserDevcontainerAlreadyInRepository(ctx) {
opts := &DevcontainersVO.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
err := DevcontainersService.DeleteRepoDevcontainer(ctx, opts)
if err != nil {
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))
}
}
ctx.JSONRedirect(ctx.Repo.RepoLink + "/dev-container")
}

repo.diff.view_file

@@ -1,104 +0,0 @@
package devcontainer
import (
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/webhook"
gitea_web_context "code.gitea.io/gitea/services/context"
"github.com/emirpasic/gods/utils"
"github.com/nektos/act/pkg/jobparser"
"gopkg.in/yaml.v3"
)
func AddWorkflowTask(ctx *gitea_web_context.Context, cmd []string, runnerLabel string) {
//构建Workflow对象
steps := make([]Step, len(cmd))
for i, run := range cmd {
//校验合法性、安全性, 暂无
steps[i] = Step{
Name: "Step" + utils.ToString(i+1),
Run: run,
}
}
wf := Workflow{
Name: "DevStar Workflow",
On: On{
Push: struct {
Branches []string `yaml:"branches"`
}{
Branches: []string{"push"},
},
},
Jobs: Jobs{
CMD: struct {
RunsOn string `yaml:"runs-on"`
Steps []Step `yaml:"steps"`
}{
RunsOn: runnerLabel,
Steps: steps,
},
},
}
// 将Workflow对象编码为YAML格式的字节流
yamlBytes, err := yaml.Marshal(&wf)
if err != nil {
log.Error("Error marshaling workflow to YAML: %v", err)
}
branch := ctx.Repo.BranchName
if branch == "" {
branch = ctx.Repo.Repository.DefaultBranch
}
commit, _ := ctx.Repo.GitRepo.GetBranchCommit(branch)
run := &actions_model.ActionRun{
Title: ctx.Repo.Repository.Name, //strings.SplitN(commit.CommitMessage, "\n", 2)[0],
RepoID: ctx.Repo.Repository.ID, //input.Repo.ID
OwnerID: ctx.ContextUser.ID, //input.Repo.OwnerID
WorkflowID: "DevStarInternal.yaml", //dwf.EntryName
TriggerUserID: ctx.Doer.ID, //input.Doer.ID,
Ref: ctx.Repo.RefName, // ref
CommitSHA: commit.ID.String(), //commit.ID.String()
IsForkPullRequest: false, //isForkPullRequest
Event: webhook.HookEventPush, //input.Event
EventPayload: string(""), //json.Marshal(input.Payload)
TriggerEvent: "push", //dwf.TriggerEvent.Name,
Status: actions_model.StatusWaiting,
}
vars := map[string]string{}
jobs, err := jobparser.Parse(yamlBytes, jobparser.WithVars(vars))
if err != nil {
log.Error("jobparser.Parse: %v", err)
}
if err := actions_model.InsertRun(ctx, run, jobs); err != nil {
log.Error("InsertRun: %v", err)
}
}
// Workflow 定义GitHub Actions的工作流结构
type Workflow struct {
Name string `yaml:"name"`
On On `yaml:"on"`
Jobs Jobs `yaml:"jobs"`
}
// On 定义触发条件
type On struct {
Push struct {
Branches []string `yaml:"branches"`
} `yaml:"push"`
}
// Jobs 定义工作的集合
type Jobs struct {
CMD struct {
RunsOn string `yaml:"runs-on"`
Steps []Step `yaml:"steps"`
} `yaml:"CMD"`
}
// Step 定义单个工作步骤
type Step struct {
Name string `yaml:"name"`
Run string `yaml:"run"`
}

repo.diff.view_file

@@ -1,13 +1,14 @@
package setting
import (
"net/http"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
gitea_context "code.gitea.io/gitea/services/context"
devstar_devcontainer_service "code.gitea.io/gitea/services/devstar_devcontainer"
"net/http"
devstar_devcontainer_service "code.gitea.io/gitea/services/devcontainer"
)
const (

repo.diff.view_file

@@ -1344,6 +1344,7 @@ func registerRoutes(m *web.Router) {
m.Get("", devcontainer_web.GetRepoDevContainerDetails)
m.Get("/create", devcontainer_web.CreateRepoDevContainer, context.RepoMustNotBeArchived()) // 仓库状态非 Archived 才可以创建 DevContainer
m.Post("/delete", devcontainer_web.DeleteRepoDevContainerForCurrentActor)
m.Post("/update", devcontainer_web.UpdateRepoDevContainerForCurrentActor)
},
// 进入 Dev Container 编辑页面需要符合条件:
// 1. 已登录

repo.diff.view_file

@@ -1,14 +1,14 @@
package devstar_devcontainer
package devcontainer
import (
"code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
gitea_web_context "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/devstar_devcontainer/docker_agent"
devcontainer_service_errors "code.gitea.io/gitea/services/devstar_devcontainer/errors"
k8s_agent_services "code.gitea.io/gitea/services/devstar_devcontainer/k8s_agent"
devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
devcontainer_service_vo "code.gitea.io/gitea/services/devstar_devcontainer/vo"
devcontainer_service_errors "code.gitea.io/gitea/services/devcontainer/errors"
k8s_agent_services "code.gitea.io/gitea/services/devcontainer/k8s_agent"
devcontainer_service_options "code.gitea.io/gitea/services/devcontainer/options"
devcontainer_service_vo "code.gitea.io/gitea/services/devcontainer/vo"
)
// OpenDevcontainerService 获取 DevContainer 连接信息,抽象方法,适配多种 DevContainer Agent
@@ -22,7 +22,7 @@ func OpenDevcontainerService(ctx *gitea_web_context.Context, opts *devcontainer_
}
// 1. 检查 DevContainer 功能是否开启
if setting.Devstar.Devcontainer.Enabled == false {
if setting.Devcontainer.Enabled == false {
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
Action: "check availability of DevStar DevContainer",
Message: "DevContainer is turned off globally",
@@ -32,8 +32,8 @@ func OpenDevcontainerService(ctx *gitea_web_context.Context, opts *devcontainer_
// 2. 根据 DevContainer Agent 类型分发任务
apiRequestContext := ctx.Req.Context()
openDevcontainerAbstractAgentVO := &devcontainer_service_vo.OpenDevcontainerAbstractAgentVO{}
switch setting.Devstar.Devcontainer.Agent {
case setting.DEVCONTAINER_AGENT_NAME_K8S:
switch setting.Devcontainer.Agent {
case setting.KUBERNETES:
devcontainerApp, err := k8s_agent_services.AssignDevcontainerGetting2K8sOperator(&apiRequestContext, opts)
if err != nil {
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
@@ -42,8 +42,8 @@ func OpenDevcontainerService(ctx *gitea_web_context.Context, opts *devcontainer_
}
}
openDevcontainerAbstractAgentVO.NodePortAssigned = devcontainerApp.Status.NodePortAssigned
case setting.DEVCONTAINER_AGENT_NAME_DOCKER:
port, err := docker_agent.AssignDevcontainerGettingDockerOperator(&apiRequestContext, opts)
case setting.DOCKER:
port, err := docker.AssignDevcontainerGettingDockerOperator(&apiRequestContext, opts)
log.Info("port %d", port)
if err != nil {
return nil, devcontainer_service_errors.ErrOperateDevcontainer{

repo.diff.view_file

@@ -1,24 +1,29 @@
package devstar_devcontainer
package devcontainer
import (
"code.gitea.io/gitea/services/devstar_devcontainer/docker_agent"
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/models/db"
devstar_devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer"
devstar_devcontainer_models_errors "code.gitea.io/gitea/models/devstar_devcontainer/errors"
devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer"
devcontainer_models_errors "code.gitea.io/gitea/models/devstar_devcontainer/errors"
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"
devcontainer_service_dto "code.gitea.io/gitea/services/devcontainer/dto"
devcontainer_service_errors "code.gitea.io/gitea/services/devcontainer/errors"
devcontainer_k8s_agent_service "code.gitea.io/gitea/services/devcontainer/k8s_agent"
"code.gitea.io/gitea/services/devstar_ssh_key_pair/api_service"
"github.com/google/uuid"
"xorm.io/builder"
)
@@ -31,7 +36,7 @@ func GetRepoDevcontainerDetails(ctx context.Context, opts *DevcontainersVO.RepoD
// 1. 检查参数是否有效
if opts == nil || opts.Actor == nil || opts.Repository == nil {
return resultRepoDevcontainerDetail, devstar_devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
return resultRepoDevcontainerDetail, devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
Action: "construct query condition for devContainer user list",
Message: "invalid search condition",
}
@@ -78,7 +83,7 @@ func GetRepoDevcontainerDetails(ctx context.Context, opts *DevcontainersVO.RepoD
// 3. 返回
if err != nil {
return resultRepoDevcontainerDetail, devstar_devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
return resultRepoDevcontainerDetail, devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
Action: fmt.Sprintf("query devcontainer with repo '%v' and username '%v'", opts.Repository.Name, opts.Actor.Name),
Message: err.Error(),
}
@@ -101,9 +106,9 @@ func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.CreateRep
unixTimestamp := time.Now().Unix()
newDevcontainer := &devcontainer_service_dto.CreateDevcontainerDTO{
DevstarDevcontainer: devstar_devcontainer_models.DevstarDevcontainer{
DevstarDevcontainer: devcontainer_models.DevstarDevcontainer{
Name: getSanitizedDevcontainerName(username, repoName),
DevcontainerHost: setting.Devstar.Devcontainer.Host,
DevcontainerHost: setting.Devcontainer.Host,
DevcontainerUsername: "root",
DevcontainerWorkDir: "/data/workspace",
RepoId: opts.Repository.ID,
@@ -136,18 +141,26 @@ func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.CreateRep
}
}
newDevcontainer.SSHPublicKeyList = append(userSSHPublicKeyList, opts.SSHPublicKeyList...)
if len(userSSHPublicKeyList) <= 0 {
// API没提供临时SSH公钥用户后台也没有永久SSH公钥直接结束并回滚事务
devstarPublicKey := getDevStarPublicKey()
if devstarPublicKey == "" {
return devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Check SSH Public Key List",
Message: "禁止创建无法连通的DevContainer用户未提供 SSH 公钥请先使用API临时创建SSH密钥对、或在Web端手动添加SSH公钥",
Action: fmt.Sprintf("devstar SSH Public Key Error "),
Message: err.Error(),
}
}
newDevcontainer.SSHPublicKeyList = append(newDevcontainer.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. 调用 k8s Operator Agent创建 DevContainer 资源同时更新k8s调度器分配的 NodePort
err = claimDevcontainerResource(&ctx, newDevcontainer)
if err != nil {
return devstar_devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
return devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
Action: fmt.Sprintf("claim resource for Dev Container %v", newDevcontainer),
Message: err.Error(),
}
@@ -157,12 +170,12 @@ func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.CreateRep
Table("devstar_devcontainer").
Insert(newDevcontainer.DevstarDevcontainer)
if err != nil {
return devstar_devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
return devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
Message: err.Error(),
}
} else if rowsAffect == 0 {
return devstar_devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
return devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
Message: "expected 1 row to be inserted, but got 0",
}
@@ -172,11 +185,60 @@ func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.CreateRep
return dbTransactionErr
}
func getDevStarPublicKey() string {
// 获取当前用户的主目录
homeDir, err := os.UserHomeDir()
if err != nil {
log.Info("Failed to get home directory: %s", err)
}
// 构建公钥文件的路径
publicKeyPath := filepath.Join(homeDir, ".ssh", "id_rsa_devstar.pub")
privateKeyPath := filepath.Join(homeDir, ".ssh", "id_rsa_devstar")
if !fileExists(publicKeyPath) || !fileExists(privateKeyPath) {
err, key := api_service.GenerateNewRSASSHSessionKeyPair()
if err != nil {
log.Info("无法创建密钥:", err)
return ""
}
// 确保~/.ssh目录存在
sshDir := filepath.Join(homeDir, ".ssh")
if err := os.MkdirAll(sshDir, 0700); err != nil {
log.Info("无法创建~/.ssh目录:", err)
return ""
}
// 创建密钥文件
if err := ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKeyPEM), 0600); err != nil {
log.Info("无法写入私钥文件:", err)
return ""
}
if err := ioutil.WriteFile(publicKeyPath, []byte(key.PublicKeySsh), 0600); err != nil {
log.Info("无法写入公钥文件:", err)
return ""
}
}
// 读取公钥文件内容
publicKeyBytes, err := ioutil.ReadFile(publicKeyPath)
if err != nil {
log.Info("Failed to read public key file: %s", err)
return ""
}
// 将文件内容转换为字符串
return string(publicKeyBytes)
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
// DeleteRepoDevcontainer 按照 仓库 和/或 用户信息删除 DevContainer(s)
func DeleteRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevcontainerOptions) error {
if ctx == nil || opts == nil || (opts.Actor == nil && opts.Repository == nil) {
return devstar_devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
return devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
Action: "construct query parameters",
Message: "Invalid parameters",
}
@@ -190,7 +252,7 @@ func DeleteRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevco
if opts.Repository != nil {
sqlDevcontainerCondition = sqlDevcontainerCondition.And(builder.Eq{"repo_id": opts.Repository.ID})
}
var devcontainersList []devstar_devcontainer_models.DevstarDevcontainer
var devcontainersList []devcontainer_models.DevstarDevcontainer
// 2. 开启事务:先获取 devcontainer列表后删除
dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error {
@@ -201,7 +263,7 @@ func DeleteRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevco
Where(sqlDevcontainerCondition).
Find(&devcontainersList)
if err != nil {
return devstar_devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
return devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
Action: fmt.Sprintf("find devcontainer(s) with condition '%v'", sqlDevcontainerCondition),
Message: err.Error(),
}
@@ -209,7 +271,7 @@ func DeleteRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevco
// 2.2 空列表,直接结束事务(由于前一个操作只是查询,所以回滚事务不会导致数据不一致问题)
if len(devcontainersList) == 0 {
return devstar_devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
return devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
Action: fmt.Sprintf("find devcontainer(s) with condition '%v'", sqlDevcontainerCondition),
Message: "No DevContainer found",
}
@@ -221,7 +283,7 @@ func DeleteRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevco
Where(sqlDevcontainerCondition).
Delete()
if err != nil {
return devstar_devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
return devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
Action: fmt.Sprintf("MARK devcontainer(s) as DELETED with condition '%v'", sqlDevcontainerCondition),
Message: err.Error(),
}
@@ -266,24 +328,24 @@ func getSanitizedDevcontainerName(username, repoName string) string {
}
// purgeDevcontainersResource 辅助函数用于goroutine后台执行回收DevContainer资源
func purgeDevcontainersResource(ctx *context.Context, devcontainersList *[]devstar_devcontainer_models.DevstarDevcontainer) error {
func purgeDevcontainersResource(ctx *context.Context, devcontainersList *[]devcontainer_models.DevstarDevcontainer) error {
// 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束,不会真正执行删除操作
if !setting.Devstar.Devcontainer.Enabled {
if !setting.Devcontainer.Enabled {
// 如果用户设置禁用 DevContainer无法删除资源会直接忽略而数据库相关记录会继续清空、不会发生回滚
log.Warn("Orphan DevContainers in namespace `%s` left undeleted: %v", setting.Devstar.Devcontainer.Namespace, devcontainersList)
log.Warn("Orphan DevContainers in namespace `%s` left undeleted: %v", setting.Devcontainer.Namespace, devcontainersList)
return nil
}
// 2. 根据配置文件中指定的 DevContainer Agent 派遣创建任务
switch setting.Devstar.Devcontainer.Agent {
case setting.DEVCONTAINER_AGENT_NAME_K8S:
switch setting.Devcontainer.Agent {
case setting.KUBERNETES:
return devcontainer_k8s_agent_service.AssignDevcontainerDeletion2K8sOperator(ctx, devcontainersList)
case setting.DEVCONTAINER_AGENT_NAME_DOCKER:
return docker_agent.AssignDevcontainerDeletionDockerOperator(ctx, devcontainersList)
case setting.DOCKER:
return docker.AssignDevcontainerDeletionDockerOperator(ctx, devcontainersList)
default:
// 未知 Agent直接报错
return devstar_devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
return devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
Action: "dispatch DevContainer deletion",
Message: fmt.Sprintf("unknown DevContainer agent `%s`, please check your config files", setting.Devstar.Devcontainer.Agent),
Message: fmt.Sprintf("unknown DevContainer agent `%s`, please check your config files", setting.Devcontainer.Agent),
}
}
}
@@ -291,25 +353,25 @@ 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 {
return devstar_devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
if !setting.Devcontainer.Enabled {
return devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
Action: "Check for DevContainer functionality switch",
Message: "DevContainer is disabled globally, please check your configuration files",
}
}
// 2. 根据配置文件中指定的 DevContainer Agent 派遣创建任务
switch setting.Devstar.Devcontainer.Agent {
case setting.DEVCONTAINER_AGENT_NAME_K8S:
switch setting.Devcontainer.Agent {
case setting.KUBERNETES, "k8s":
// k8s Operator
return devcontainer_k8s_agent_service.AssignDevcontainerCreation2K8sOperator(ctx, newDevContainer)
case setting.DEVCONTAINER_AGENT_NAME_DOCKER:
return docker_agent.AssignDevcontainerCreationDockerOperator(ctx, newDevContainer)
case setting.DOCKER:
return docker.AssignDevcontainerCreationDockerOperator(ctx, newDevContainer)
default:
// 未知 Agent直接报错
return devstar_devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
return devcontainer_models_errors.ErrFailedToOperateDevstarDevcontainerDB{
Action: "dispatch DevContainer creation",
Message: fmt.Sprintf("unknown DevContainer agent `%s`, please check your config files", setting.Devstar.Devcontainer.Agent),
Message: fmt.Sprintf("unknown DevContainer agent `%s`, please check your config files", setting.Devcontainer.Agent),
}
}
}
@@ -320,14 +382,14 @@ func GetDefaultDevcontainerImageFromRepoDevcontainerJSON(ctx context.Context, re
// 1. 获取默认分支名
branchName := repo.DefaultBranch
if len(branchName) == 0 {
branchName = setting.Devstar.Devcontainer.DefaultGitBranchName
branchName = setting.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
return setting.Devcontainer.DefaultDevcontainerImageName
}
defer func(gitRepoEntity *git_module.Repository) {
_ = gitRepoEntity.Close()
@@ -336,7 +398,7 @@ func GetDefaultDevcontainerImageFromRepoDevcontainerJSON(ctx context.Context, re
// 3. 获取分支名称
commit, err := gitRepoEntity.GetBranchCommit(branchName)
if err != nil {
return setting.Devstar.Devcontainer.DefaultDevcontainerImageName
return setting.Devcontainer.DefaultDevcontainerImageName
}
// 4. 读取 .devcontainer/devcontainer.json 文件
@@ -344,19 +406,19 @@ func GetDefaultDevcontainerImageFromRepoDevcontainerJSON(ctx context.Context, re
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
return setting.Devcontainer.DefaultDevcontainerImageName
}
// 5. 解析 JSON
devContainerJSON, err := devstar_devcontainer_models.Unmarshal(devcontainerJSONContent)
devContainerJSON, err := devcontainer_models.Unmarshal(devcontainerJSONContent)
if err != nil {
log.Error("Failed to unmarshal .devcontainer/devcontainer.json: %v", err)
return setting.Devstar.Devcontainer.DefaultDevcontainerImageName
return setting.Devcontainer.DefaultDevcontainerImageName
}
// 6. 解析并返回
if len(devContainerJSON.Image) == 0 {
return setting.Devstar.Devcontainer.DefaultDevcontainerImageName
return setting.Devcontainer.DefaultDevcontainerImageName
}
return devContainerJSON.Image
}

repo.diff.view_file

@@ -1,12 +1,13 @@
package devstar_devcontainer
package devcontainer
import (
"code.gitea.io/gitea/models/db"
devstar_devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer/errors"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/api/devcontainer/vo"
"context"
"fmt"
"code.gitea.io/gitea/models/db"
devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer/errors"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/api/devcontainer/vo"
"xorm.io/builder"
)
@@ -24,7 +25,7 @@ func GetUserDevcontainersList(ctx context.Context, opts *vo.SearchUserDevcontain
// 1. 查询参数预处理
if opts == nil || opts.Actor == nil {
return resultDevContainerListVO, devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
return resultDevContainerListVO, devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
Action: "construct query condition for devContainer user list",
Message: "invalid search condition",
}
@@ -63,7 +64,7 @@ func GetUserDevcontainersList(ctx context.Context, opts *vo.SearchUserDevcontain
Where(sqlCondition).
Count()
if err != nil {
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
return devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
Action: "count devContainer item numbers",
Message: err.Error(),
}
@@ -121,7 +122,7 @@ func GetUserDevcontainersList(ctx context.Context, opts *vo.SearchUserDevcontain
Find(&resultDevContainerListVO.DevContainers)
if err != nil {
// 查询出错,返回错误信息,结束事务
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
return devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
Action: fmt.Sprintf("list devContainer for user '%v' at page %v", opts.Actor.Name, opts.PaginationOptions.Page),
Message: err.Error(),
}
@@ -133,7 +134,7 @@ func GetUserDevcontainersList(ctx context.Context, opts *vo.SearchUserDevcontain
if errDbTransaction != nil {
return resultDevContainerListVO,
devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
Action: "query user DevContainer List",
Message: errDbTransaction.Error(),
}

repo.diff.view_file

@@ -1,15 +1,16 @@
package api_services
import (
"context"
"regexp"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/repo"
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_errors "code.gitea.io/gitea/services/devstar_devcontainer/errors"
devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
"context"
"regexp"
DevcontainersService "code.gitea.io/gitea/services/devcontainer"
devcontainer_service_errors "code.gitea.io/gitea/services/devcontainer/errors"
devcontainer_service_options "code.gitea.io/gitea/services/devcontainer/options"
)
// CreateDevcontainerAPIService API专用创建 DevContainer Service

repo.diff.view_file

@@ -1,14 +1,15 @@
package api_services
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/repo"
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_errors "code.gitea.io/gitea/services/devstar_devcontainer/errors"
devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
"context"
DevcontainersService "code.gitea.io/gitea/services/devcontainer"
devcontainer_service_errors "code.gitea.io/gitea/services/devcontainer/errors"
devcontainer_service_options "code.gitea.io/gitea/services/devcontainer/options"
)
// DeleteDevcontainerAPIService API 专用删除 DevContainer Service

repo.diff.view_file

@@ -7,9 +7,9 @@ import (
"code.gitea.io/gitea/models/repo"
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_errors "code.gitea.io/gitea/services/devstar_devcontainer/errors"
devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
DevcontainersService "code.gitea.io/gitea/services/devcontainer"
devcontainer_service_errors "code.gitea.io/gitea/services/devcontainer/errors"
devcontainer_service_options "code.gitea.io/gitea/services/devcontainer/options"
)
// OpenDevcontainerAPIService API 专用获取 DevContainer Service
@@ -55,9 +55,10 @@ func OpenDevcontainerAPIService(ctx *gitea_web_context.Context, opts *devcontain
// 2. 调用抽象层获取 DevContainer 最新状态(需要根据用户传入的 wait 参数决定是否要阻塞等待 DevContainer 就绪)
optsOpenDevcontainer := &devcontainer_service_options.OpenDevcontainerAppDispatcherOptions{
Name: devcontainerDetails.DevContainerName,
Port: devcontainerDetails.DevContainerPort,
Wait: opts.Wait,
Name: devcontainerDetails.DevContainerName,
Port: devcontainerDetails.DevContainerPort,
Wait: opts.Wait,
UserPublicKey: opts.UserPublicKey,
}
openDevcontainerAbstractAgentVO, err := DevcontainersService.OpenDevcontainerService(ctx, optsOpenDevcontainer)

repo.diff.view_file

@@ -0,0 +1,19 @@
package api_services
import (
"code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/setting"
gitea_web_context "code.gitea.io/gitea/services/context"
devcontainer_service_options "code.gitea.io/gitea/services/devcontainer/options"
)
func UpdateDevcontainerAPIService(ctx *gitea_web_context.Context, opts *devcontainer_service_options.UpdateDevcontainerOptions) {
switch setting.Devcontainer.Agent {
case setting.KUBERNETES:
//k8s处理
case setting.DOCKER:
docker.AssignDevcontainerUpdateDockerOperator(ctx, opts)
default:
//默认处理
}
}

repo.diff.view_file

@@ -5,7 +5,7 @@ import (
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"
devcontainer_service_dto "code.gitea.io/gitea/services/devcontainer/dto"
apimachinery_meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
devcontainer_k8s_agent_module "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent"
@@ -31,7 +31,7 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine
opts := &devcontainer_dto.CreateDevcontainerOptions{
CreateOptions: apimachinery_meta_v1.CreateOptions{},
Name: newDevContainer.Name,
Namespace: setting.Devstar.Devcontainer.Namespace,
Namespace: setting.Devcontainer.Namespace,
Image: newDevContainer.Image,
/**
* 配置 Kubernetes 主容器启动命令注意事项

repo.diff.view_file

@@ -1,6 +1,9 @@
package k8s_agent
import (
"context"
"fmt"
devstar_devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer"
devcontainer_k8s_agent_module "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent"
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
@@ -8,8 +11,6 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/devstar_cloud_provider"
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -29,7 +30,7 @@ func AssignDevcontainerDeletion2K8sOperator(ctx *context.Context, devcontainersL
// 2. 调用 modules 层 k8s Agent执行删除资源
opts := &devcontainer_dto.DeleteDevcontainerOptions{
DeleteOptions: metav1.DeleteOptions{},
Namespace: setting.Devstar.Devcontainer.Namespace,
Namespace: setting.Devcontainer.Namespace,
}
if devcontainersList == nil || len(*devcontainersList) == 0 {
return devcontainer_errors.ErrOperateDevcontainer{

repo.diff.view_file

@@ -1,15 +1,16 @@
package k8s_agent
import (
"context"
"fmt"
devcontainer_k8s_agent_module "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent"
devcontainer_api_v1 "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/api/v1"
devcontainer_dto "code.gitea.io/gitea/modules/devstar_devcontainer/k8s_agent/dto"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/devstar_devcontainer/errors"
devcontainer_k8s_agent_errors "code.gitea.io/gitea/services/devstar_devcontainer/k8s_agent/errors"
devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
"context"
"fmt"
"code.gitea.io/gitea/services/devcontainer/errors"
devcontainer_k8s_agent_errors "code.gitea.io/gitea/services/devcontainer/k8s_agent/errors"
devcontainer_service_options "code.gitea.io/gitea/services/devcontainer/options"
apimachinery_api_metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -37,7 +38,7 @@ func AssignDevcontainerGetting2K8sOperator(ctx *context.Context, opts *devcontai
optsGetDevcontainer := &devcontainer_dto.GetDevcontainerOptions{
GetOptions: apimachinery_api_metav1.GetOptions{},
Name: opts.Name,
Namespace: setting.Devstar.Devcontainer.Namespace,
Namespace: setting.Devcontainer.Namespace,
Wait: opts.Wait,
}
devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(ctx, client, optsGetDevcontainer)

repo.diff.view_file

@@ -0,0 +1,11 @@
package options
import user_model "code.gitea.io/gitea/models/user"
// AbstractOpenDevcontainerOptions 封装 API 获取 DevContainer 数据,即 router 层 GET /api/devcontainer 向下传递的数据结构
type AbstractOpenDevcontainerOptions struct {
Wait bool // 标记用户在API调用时候是否希望阻塞等待 DevContainer 就绪
Actor *user_model.User // 当前操作用户实体
RepoId int64 // 仓库ID用于 API Service 层查询数据库
UserPublicKey string
}

repo.diff.view_file

@@ -0,0 +1,8 @@
package options
type OpenDevcontainerAppDispatcherOptions struct {
Name string `json:"name"`
Wait bool `json:"wait"`
Port uint16
UserPublicKey string
}

repo.diff.view_file

@@ -0,0 +1,16 @@
package options
import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
)
type UpdateDevcontainerOptions struct {
ImageName string
PassWord string
RepositoryAddress string
RepositoryUsername string
DevContainerName string
Actor *user_model.User
Repository *repo_model.Repository
}

repo.diff.view_file

@@ -9,12 +9,12 @@ import (
// CreateNATRulePort 抽象接口,创建 NAT 端口映射规则
func CreateNATRulePort(privatePort, publicPort uint64, description string) error {
if setting.Devstar.Cloud.Enabled == false {
if setting.Cloud.Enabled == false {
return devstar_cloud_provider_errors.ErrCloudNATProviderDisabled{}
}
// 根据配置文件指定云服务厂商创建 NAT Rule
switch setting.Devstar.Cloud.Provider {
switch setting.Cloud.Provider {
case setting.CLOUD_PROVIDER_TENCENT:
// 指定腾讯云执行 NAT 端口创建
return devstar_cloud_provider_tencent_nat.AssignDevstarCloudNATPortForwarding2TencentCloud(privatePort, publicPort, description)

repo.diff.view_file

@@ -9,12 +9,12 @@ import (
// DeleteNATRulePort 抽象接口,根据 NAT 端口映射规则列表描述需要删除的信息
func DeleteNATRulePort(description string) error {
if setting.Devstar.Cloud.Enabled == false {
if setting.Cloud.Enabled == false {
return devstar_cloud_provider_errors.ErrCloudNATProviderDisabled{}
}
// 根据配置文件指定云服务厂商创建 NAT Rule
switch setting.Devstar.Cloud.Provider {
switch setting.Cloud.Provider {
case setting.CLOUD_PROVIDER_TENCENT:
// 指定腾讯云执行 NAT 端口删除
return devstar_cloud_provider_tencent_nat.AssignDeletingDevstarCloudNATPortForwarding2TencentCloud(description)

repo.diff.view_file

@@ -1,10 +1,11 @@
package nat_port_mapping
import (
"fmt"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
devstar_cloud_provider_service_errors "code.gitea.io/gitea/services/devstar_cloud_provider/errors"
"fmt"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312"
)
@@ -17,20 +18,20 @@ func AssignDevstarCloudNATPortForwarding2TencentCloud(privatePort, publicPort ui
client, err := GetTencentNATRuleClient()
if err != nil {
return devstar_cloud_provider_service_errors.ErrFailed2GetNATClient{
CloudProviderName: setting.Devstar.Cloud.Provider,
CloudProviderName: setting.Cloud.Provider,
Reason: err.Error(),
}
}
// 2. 实例化 request 请求对象
request := vpc.NewCreateNatGatewayDestinationIpPortTranslationNatRuleRequest()
request.NatGatewayId = common.StringPtr(setting.Devstar.Cloud.Tencent.NatGatewayId)
request.NatGatewayId = common.StringPtr(setting.Cloud.Tencent.NatGatewayId)
request.DestinationIpPortTranslationNatRules = []*vpc.DestinationIpPortTranslationNatRule{
&vpc.DestinationIpPortTranslationNatRule{
IpProtocol: common.StringPtr(setting.Devstar.Cloud.Tencent.IpProtocol),
PublicIpAddress: common.StringPtr(setting.Devstar.Cloud.Tencent.PublicIpAddress),
IpProtocol: common.StringPtr(setting.Cloud.Tencent.IpProtocol),
PublicIpAddress: common.StringPtr(setting.Cloud.Tencent.PublicIpAddress),
PublicPort: common.Uint64Ptr(publicPort),
PrivateIpAddress: common.StringPtr(setting.Devstar.Cloud.Tencent.PrivateIpAddress),
PrivateIpAddress: common.StringPtr(setting.Cloud.Tencent.PrivateIpAddress),
PrivatePort: common.Uint64Ptr(privatePort),
Description: common.StringPtr(portDescription),
},
@@ -64,7 +65,7 @@ func AssignDeletingDevstarCloudNATPortForwarding2TencentCloud(portDescription st
client, err := GetTencentNATRuleClient()
if err != nil {
err = devstar_cloud_provider_service_errors.ErrFailed2GetNATClient{
CloudProviderName: setting.Devstar.Cloud.Provider,
CloudProviderName: setting.Cloud.Provider,
Reason: err.Error(),
}
return devstar_cloud_provider_service_errors.ErrFailed2DeleteNatRules{
@@ -101,7 +102,7 @@ func GetDevstarCloudTencentNATPortForwarding(client *vpc.Client, portDescription
request.Filters = []*vpc.Filter{
&vpc.Filter{
Name: common.StringPtr("nat-gateway-id"),
Values: []*string{common.StringPtr(setting.Devstar.Cloud.Tencent.NatGatewayId)},
Values: []*string{common.StringPtr(setting.Cloud.Tencent.NatGatewayId)},
},
&vpc.Filter{
Name: common.StringPtr("description"),
@@ -126,7 +127,7 @@ func GetDevstarCloudTencentNATPortForwarding(client *vpc.Client, portDescription
// DeleteDevstarCloudTencentNATPortForwardingRuleSet 删除 NAT Result Set 中的规则()
func DeleteDevstarCloudTencentNATPortForwardingRuleSet(client *vpc.Client, natRuleSet []*vpc.NatGatewayDestinationIpPortTranslationNatRule) (response *vpc.DeleteNatGatewayDestinationIpPortTranslationNatRuleResponse, err error) {
request := vpc.NewDeleteNatGatewayDestinationIpPortTranslationNatRuleRequest()
request.NatGatewayId = common.StringPtr(setting.Devstar.Cloud.Tencent.NatGatewayId)
request.NatGatewayId = common.StringPtr(setting.Cloud.Tencent.NatGatewayId)
request.DestinationIpPortTranslationNatRules = []*vpc.DestinationIpPortTranslationNatRule{}
for _, natRule := range natRuleSet {
request.DestinationIpPortTranslationNatRules = append(request.DestinationIpPortTranslationNatRules, &vpc.DestinationIpPortTranslationNatRule{

repo.diff.view_file

@@ -14,13 +14,13 @@ func GetTencentNATRuleClient() (client *vpc.Client, err error) {
// 以下代码示例仅供参考建议采用更安全的方式来使用密钥请参见https://cloud.tencent.com/document/product/1278/85305
// 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取
credential := common.NewCredential(
setting.Devstar.Cloud.Tencent.SecretId,
setting.Devstar.Cloud.Tencent.SecretKey,
setting.Cloud.Tencent.SecretId,
setting.Cloud.Tencent.SecretKey,
)
// 实例化一个client选项可选的没有特殊需求可以跳过
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = setting.Devstar.Cloud.Tencent.Endpoint
cpf.HttpProfile.Endpoint = setting.Cloud.Tencent.Endpoint
// 实例化要请求产品的client对象,clientProfile是可选的
return vpc.NewClient(credential, setting.Devstar.Cloud.Tencent.Region, cpf)
return vpc.NewClient(credential, setting.Cloud.Tencent.Region, cpf)
}

repo.diff.view_file

@@ -1,47 +0,0 @@
package docker_agent
import (
"context"
"strconv"
"code.gitea.io/gitea/modules/log"
devcontainer_service_errors "code.gitea.io/gitea/services/devstar_devcontainer/errors"
devcontainer_service_options "code.gitea.io/gitea/services/devstar_devcontainer/options"
)
func AssignDevcontainerGettingDockerOperator(ctx *context.Context, opts *devcontainer_service_options.OpenDevcontainerAppDispatcherOptions) (uint16, error) {
// 1. 创建docker client
cli, err := CreateDockerClient(ctx)
if err != nil {
return 0, err
}
if cli != nil {
defer cli.Close()
}
// 获取容器详细信息
containerJSON, err := cli.ContainerInspect(context.Background(), opts.Name)
if err != nil {
return 0, err
}
// 获取端口映射信息
portBindings := containerJSON.NetworkSettings.Ports
for containerPort, bindings := range portBindings {
for _, binding := range bindings {
log.Info("Container Port %s is mapped to Host Port %s on IP %s\n", containerPort, binding.HostPort, binding.HostIP)
if containerPort.Port() == "22" {
v, err := strconv.ParseUint(binding.HostPort, 10, 16)
if err != nil {
return 0, err
}
return uint16(v), nil
}
}
}
return 0, devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Open DevContainer in docker",
Message: "cannot find SSH containerPort 22 for DevContainer " + opts.Name,
}
}

repo.diff.view_file

@@ -1,10 +0,0 @@
package options
import user_model "code.gitea.io/gitea/models/user"
// AbstractOpenDevcontainerOptions 封装 API 获取 DevContainer 数据,即 router 层 GET /api/devcontainer 向下传递的数据结构
type AbstractOpenDevcontainerOptions struct {
Wait bool // 标记用户在API调用时候是否希望阻塞等待 DevContainer 就绪
Actor *user_model.User // 当前操作用户实体
RepoId int64 // 仓库ID用于 API Service 层查询数据库
}

repo.diff.view_file

@@ -1,7 +0,0 @@
package options
type OpenDevcontainerAppDispatcherOptions struct {
Name string `json:"name"`
Wait bool `json:"wait"`
Port uint16
}

repo.diff.view_file

@@ -1,22 +1,23 @@
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"
"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"
"golang.org/x/crypto/ssh"
)
// 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)
// 1. 生成 SSH 密钥对 (算法 RSA长度 setting.SSHKeypair.KeySize
privateKey, err := rsa.GenerateKey(rand.Reader, setting.SSHKeypair.KeySize)
if err != nil {
return err, nil
}
@@ -73,6 +74,6 @@ func GenerateNewRSASSHSessionKeyPair() (error, *vo.GenerateNewRSASSHSessionKeyPa
PublicKeyPEM: publicKeyPemStr,
PrivateKeyPEM: privateKeyPemStr,
PublicKeySsh: sshPublicKeyStr,
KeySize: strconv.Itoa(setting.Devstar.SSHKeypair.KeySize),
KeySize: strconv.Itoa(setting.SSHKeypair.KeySize),
}
}

repo.diff.view_file

@@ -4,11 +4,12 @@
package repository
import (
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
DevcontainersService "code.gitea.io/gitea/services/devstar_devcontainer"
"context"
"fmt"
DevcontainersVO "code.gitea.io/gitea/routers/api/devcontainer/vo"
DevcontainersService "code.gitea.io/gitea/services/devcontainer"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"

repo.diff.view_file

@@ -61,6 +61,9 @@
</table>
<p>*See GET /api/devcontainer with param 'repoId' and 'wait'</p>
{{end}}
<!-- {{if .HasWebSSH}}
<iframe src="{{.WebSSHUrl}}" style="width: 100%"></iframe>
{{end}}-->
</div>
<!-- 结束Dev Container 正文内容 - 左侧主展示区 -->
@@ -74,6 +77,10 @@
<div class="item">{{svg "octicon-person" 16 "tw-mr-2"}} <a href="{{.ContextUser.HomeLink}}">{{.ContextUser.Name}}</a></div>
{{if .HasDevContainer}}
<div class="item"><a class="delete-button flex-text-inline" data-modal="#delete-repo-devcontainer-of-user-modal" href="#" data-url="{{.Repository.Link}}/dev-container/delete">{{svg "octicon-trash" 14}}{{ctx.Locale.Tr "repo.dev_container_control.delete"}}</a></div>
<div class="item"><a class="delete-button flex-text-inline" style="color:black;" data-modal-id="updatemodal" href="#">{{svg "octicon-database"}}{{ctx.Locale.Tr "repo.dev_container_control.update"}}</a></div>
<div class="item"><a class="flex-text-inline" style="color:black;" href="{{.WebSSHUrl}}" target="_blank">{{svg "octicon-code" 14}}open with WebTerminal</a></div>
<div class="item"><a class="flex-text-inline" style="color:black;" href="vscode://devstar-open?repo=GIT-URL" target="_blank">{{svg "octicon-code" 14}}open with VSCode</a></div>
{{else if .HasValidDevContainerJSON}}
<div class="item">
<a class="button flex-text-inline" href="{{.Repository.Link}}/dev-container/create">{{svg "octicon-terminal" 14 "tw-mr-2"}} {{ctx.Locale.Tr "repo.dev_container_control.create"}}</a>
@@ -101,5 +108,74 @@
</div>
{{template "base/modal_actions_confirm" .}}
</div>
<!-- 确认 Dev Container 模态对话框 -->
<div class="ui g-modal-confirm delete modal" style="width: 35%" id="updatemodal">
<div class="header">
{{ctx.Locale.Tr "repo.dev_container_control.update"}}
</div>
<script>
function submitForm(event) {
event.preventDefault(); // 阻止默认的表单提交行为
const {csrfToken} = window.config;
const {appSubUrl} = window.config;
const form = document.getElementById('updateForm');
const submitButton = document.getElementById('updateSubmitButton');
const closeButton = document.getElementById('updateCloseButton');
submitButton.disabled = true;
const formData = new FormData(form);
fetch('{{.Repository.Link}}'+'/dev-container/update', {
method: 'POST',
headers: {
'x-csrf-token': csrfToken, // 如果需要认证
'content-type' : 'application/json',
},
body: JSON.stringify({
RepositoryAddress: formData.get('RepositoryAddress'),
RepositoryUsername: formData.get('RepositoryUsername'),
RepositoryPassword: formData.get('RepositoryPassword'),
ImageName: formData.get('ImageName'),
})
})
.then(response => response.json())
.then(data => {
submitButton.disabled = false;
alert(data.message);
if(data.redirect){
closeButton.click()
}
})
.catch((error) => {
alert('提交失败,请重试。');
});
}
</script>
<div class="content">
<form class="ui form tw-max-w-2xl tw-m-auto" id="updateForm" onsubmit="submitForm(event)">
<div class="required field ">
<label for="RepositoryAddress">Registry:</label>
<input style="border: 1px solid black;" type="text" id="RepositoryAddress" name="RepositoryAddress" value="{{.RepositoryAddress}}">
</div>
<div class="required field ">
<label for="RepositoryUsername">Registry Username:</label>
<input style="border: 1px solid black;" type="text" id="RepositoryUsername" name="RepositoryUsername" value="{{.RepositoryUsername}}">
</div>
<div class="required field ">
<label for="RepositoryPassword">Registry Password:</label>
<input style="border: 1px solid black;" type="text" id="RepositoryPassword" name="RepositoryPassword" required>
</div>
<div class="required field ">
<label for="ImageName">Image(name:tag):</label>
<input style="border: 1px solid black;" type="text" id="ImageName" name="ImageName" value="{{.ImageName}}">
</div>
<div class="actions">
<button class="ui primary button" type="submit" id="updateSubmitButton" >Submit</button>
<button class="ui cancel button" id="updateCloseButton">Close</button>
</div>
</form>
</div>
</div>
{{template "base/footer" .}}