2025-09-20 01:56:37 +00:00
package devcontainer
import (
"archive/tar"
"bytes"
"context"
"fmt"
"io"
"os/exec"
"regexp"
"strings"
"time"
"code.gitea.io/gitea/models/db"
devcontainer_models "code.gitea.io/gitea/models/devcontainer"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/user"
docker_module "code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/docker/go-connections/nat"
)
func GetDevContainerStatusFromDocker ( ctx context . Context , containerName string ) ( string , error ) {
// 创建docker client
cli , err := docker_module . CreateDockerClient ( ctx )
if err != nil {
return "" , err
}
defer cli . Close ( )
containerID , err := docker_module . GetContainerID ( cli , containerName )
if err != nil {
return "" , err
}
containerStatus , err := docker_module . GetContainerStatus ( cli , containerID )
if err != nil {
return "" , err
}
return containerStatus , nil
}
func CreateDevContainerByDockerAPI ( ctx context . Context , newDevcontainer * devcontainer_models . Devcontainer , imageName string , repo * repo . Repository , publicKeyList [ ] string ) error {
dbEngine := db . GetEngine ( ctx )
configurationString , err := GetDevcontainerConfigurationString ( ctx , repo )
if err != nil {
return err
}
configurationModel , err := UnmarshalDevcontainerConfigContent ( configurationString )
if err != nil {
return err
}
dockerSocket , err := docker_module . GetDockerSocketPath ( )
if err != nil {
return err
}
_ , err = dbEngine . Table ( "devcontainer" ) .
Where ( "user_id = ? AND repo_id = ? " , newDevcontainer . UserId , newDevcontainer . RepoId ) .
Update ( & devcontainer_models . Devcontainer { DevcontainerStatus : 1 } )
if err != nil {
log . Info ( "err %v" , err )
}
cli , err := docker_module . CreateDockerClient ( ctx )
if err != nil {
return err
}
if configurationModel . Build == nil || configurationModel . Build . Dockerfile == "" {
script := "docker " + "-H " + dockerSocket + " pull " + configurationModel . Image
cmd := exec . Command ( "sh" , "-c" , script )
err = cmd . Start ( )
if err != nil {
return err
}
}
_ , err = dbEngine . Table ( "devcontainer" ) .
Where ( "user_id = ? AND repo_id = ? " , newDevcontainer . UserId , newDevcontainer . RepoId ) .
Update ( & devcontainer_models . Devcontainer { DevcontainerStatus : 2 } )
if err != nil {
return err
}
docker_module . CreateAndStartContainer ( ctx , cli , imageName ,
[ ] string {
"sh" ,
"-c" ,
"tail -f /dev/null;" ,
} ,
nil ,
nil ,
nat . PortSet {
nat . Port ( "22/tcp" ) : { } ,
} ,
newDevcontainer . Name )
_ , err = dbEngine . Table ( "devcontainer" ) .
Where ( "user_id = ? AND repo_id = ? " , newDevcontainer . UserId , newDevcontainer . RepoId ) .
Update ( & devcontainer_models . Devcontainer { DevcontainerStatus : 3 } )
if err != nil {
return err
}
2025-10-18 08:53:50 +00:00
2025-09-20 01:56:37 +00:00
output , err := docker_module . ExecCommandInContainer ( ctx , cli , newDevcontainer . Name ,
` echo " ` + newDevcontainer . DevcontainerHost + ` host.docker.internal" | tee -a /etc/hosts;apt update;apt install -y git ;git clone ` + strings . TrimSuffix ( setting . AppURL , "/" ) + repo . Link ( ) + " " + newDevcontainer . DevcontainerWorkDir + "/" + repo . Name + ` ; apt install -y ssh;echo "PubkeyAuthentication yes ` + "\n" + ` PermitRootLogin yes ` + "\n" + ` " | tee -a /etc/ssh/sshd_config;rm -f /etc/ssh/ssh_host_*; ssh-keygen -A; service ssh restart;mkdir -p ~/.ssh;chmod 700 ~/.ssh;echo " ` + strings . Join ( publicKeyList , "\n" ) + ` " > ~/.ssh/authorized_keys;chmod 600 ~/.ssh/authorized_keys; ` ,
)
if err != nil {
return err
}
log . Info ( output )
_ , err = dbEngine . Table ( "devcontainer" ) .
Where ( "user_id = ? AND repo_id = ? " , newDevcontainer . UserId , newDevcontainer . RepoId ) .
Update ( & devcontainer_models . Devcontainer { DevcontainerStatus : 4 } )
if err != nil {
return err
}
return nil
}
func CreateDevContainerByDockerCommand ( ctx context . Context , newDevcontainer * devcontainer_models . Devcontainer , repo * repo . Repository , publicKeyList [ ] string ) ( string , error ) {
dbEngine := db . GetEngine ( ctx )
configurationString , err := GetDevcontainerConfigurationString ( ctx , repo )
if err != nil {
return "" , err
}
configurationModel , err := UnmarshalDevcontainerConfigContent ( configurationString )
if err != nil {
return "" , err
}
var imageName = configurationModel . Image
dockerSocket , err := docker_module . GetDockerSocketPath ( )
if err != nil {
return "" , err
}
if configurationModel . Build != nil && configurationModel . Build . Dockerfile != "" {
dockerfileContent , err := GetFileContentByPath ( ctx , repo , ".devcontainer/" + configurationModel . Build . Dockerfile )
if err != nil {
return "" , err
}
// 创建构建上下文( 包含Dockerfile的tar包)
var buf bytes . Buffer
tw := tar . NewWriter ( & buf )
defer tw . Close ( )
// 添加Dockerfile到tar包
dockerfile := "Dockerfile"
content := [ ] byte ( dockerfileContent )
header := & tar . Header {
Name : dockerfile ,
Size : int64 ( len ( content ) ) ,
Mode : 0644 ,
}
if err := tw . WriteHeader ( header ) ; err != nil {
return "" , err
}
if _ , err := tw . Write ( content ) ; err != nil {
return "" , err
}
// 执行镜像构建
imageName = fmt . Sprintf ( "%d" , newDevcontainer . UserId ) + "-" + fmt . Sprintf ( "%d" , newDevcontainer . RepoId ) + "-dockerfile"
buildOptions := types . ImageBuildOptions {
Tags : [ ] string { imageName } , // 镜像标签
}
cli , err := docker_module . CreateDockerClient ( ctx )
if err != nil {
return "" , err
}
buildResponse , err := cli . ImageBuild (
context . Background ( ) ,
& buf ,
buildOptions ,
)
if err != nil {
return "" , err
}
output , err := io . ReadAll ( buildResponse . Body )
if err != nil {
return "" , err
}
log . Info ( string ( output ) )
}
// 拉取镜像的命令
if _ , err := dbEngine . Table ( "devcontainer_output" ) . Insert ( & devcontainer_models . DevcontainerOutput {
Output : "" ,
ListId : 1 ,
Status : "waitting" ,
UserId : newDevcontainer . UserId ,
RepoId : newDevcontainer . RepoId ,
Command : "docker " + "-H " + dockerSocket + " pull " + imageName + "\n" ,
DevcontainerId : newDevcontainer . Id ,
} ) ; err != nil {
log . Info ( "Failed to insert record: %v" , err )
return imageName , err
}
cfg , err := setting . NewConfigProviderFromFile ( setting . CustomConf )
if err != nil {
return imageName , err
}
2025-10-18 08:53:50 +00:00
var startCommand string = ` docker -H ` + dockerSocket + ` create --restart=always --name ` + newDevcontainer . Name
2025-09-20 01:56:37 +00:00
// 将每个端口转换为 "-p <port>" 格式
var portFlags string = " -p 22 "
exposedPorts := configurationModel . ExtractContainerPorts ( )
for _ , port := range exposedPorts {
portFlags = portFlags + fmt . Sprintf ( " -p %d " , port )
}
startCommand += portFlags
2025-10-18 08:53:50 +00:00
2025-09-20 01:56:37 +00:00
var envFlags string = ` -e RepoLink=" ` + strings . TrimSuffix ( cfg . Section ( "server" ) . Key ( "ROOT_URL" ) . Value ( ) , ` / ` ) + repo . Link ( ) + ` " ` +
2025-10-18 08:53:50 +00:00
` -e DevstarHost=" ` + newDevcontainer . DevcontainerHost + ` " ` +
` -e WorkSpace=" ` + newDevcontainer . DevcontainerWorkDir + ` / ` + repo . Name + ` " ` +
` -e DEVCONTAINER_STATUS="start" `
2025-09-20 01:56:37 +00:00
// 遍历 ContainerEnv 映射中的每个环境变量
for name , value := range configurationModel . ContainerEnv {
// 将每个环境变量转换为 "-e name=value" 格式
envFlags = envFlags + fmt . Sprintf ( " -e %s=\"%s\" " , name , value )
}
startCommand += envFlags
if configurationModel . Init {
startCommand += " --init "
}
if configurationModel . Privileged {
startCommand += " --privileged "
}
var capAddFlags string
// 遍历 CapAdd 列表中的每个能力
for _ , capability := range configurationModel . CapAdd {
// 将每个能力转换为 --cap-add=capability 格式
capAddFlags = capAddFlags + fmt . Sprintf ( " --cap-add %s " , capability )
}
2025-10-18 08:53:50 +00:00
startCommand += capAddFlags
2025-09-20 01:56:37 +00:00
var securityOptFlags string
// 遍历 SecurityOpt 列表中的每个安全选项
for _ , option := range configurationModel . SecurityOpt {
// 将每个选项转换为 --security-opt=option 格式
securityOptFlags = securityOptFlags + fmt . Sprintf ( " --security-opt %s " , option )
}
startCommand += securityOptFlags
startCommand += " " + strings . Join ( configurationModel . ExtractMountFlags ( ) , " " ) + " "
if configurationModel . WorkspaceFolder != "" {
startCommand += fmt . Sprintf ( " -w %s " , configurationModel . WorkspaceFolder )
}
startCommand += " " + strings . Join ( configurationModel . RunArgs , " " ) + " "
2025-10-18 08:53:50 +00:00
overrideCommand := ""
if ! configurationModel . OverrideCommand {
overrideCommand = ` sh -c "/home/webTerminal.sh" `
startCommand += ` --entrypoint="" `
}
2025-09-20 01:56:37 +00:00
//创建并运行容器的命令
if _ , err := dbEngine . Table ( "devcontainer_output" ) . Insert ( & devcontainer_models . DevcontainerOutput {
Output : "" ,
Status : "waitting" ,
UserId : newDevcontainer . UserId ,
RepoId : newDevcontainer . RepoId ,
2025-10-18 08:53:50 +00:00
Command : startCommand + imageName + overrideCommand + "\n" ,
2025-09-20 01:56:37 +00:00
ListId : 2 ,
DevcontainerId : newDevcontainer . Id ,
} ) ; err != nil {
log . Info ( "Failed to insert record: %v" , err )
return imageName , err
}
2025-10-18 08:53:50 +00:00
2025-09-20 01:56:37 +00:00
if _ , err := dbEngine . Table ( "devcontainer_output" ) . Insert ( & devcontainer_models . DevcontainerOutput {
2025-10-18 08:53:50 +00:00
Output : "" ,
Status : "waitting" ,
UserId : newDevcontainer . UserId ,
RepoId : newDevcontainer . RepoId ,
Command : ` docker -H ` + dockerSocket + ` start -a ` + newDevcontainer . Name + "\n" ,
2025-09-20 01:56:37 +00:00
ListId : 3 ,
DevcontainerId : newDevcontainer . Id ,
} ) ; err != nil {
log . Info ( "Failed to insert record: %v" , err )
return imageName , err
}
//连接容器的命令
if _ , err := dbEngine . Table ( "devcontainer_output" ) . Insert ( & devcontainer_models . DevcontainerOutput {
Output : "" ,
Status : "waitting" ,
UserId : newDevcontainer . UserId ,
RepoId : newDevcontainer . RepoId ,
Command : ` docker -H ` + dockerSocket + ` exec -it --workdir ` + newDevcontainer . DevcontainerWorkDir + "/" + repo . Name + ` ` + newDevcontainer . Name + ` sh -c "echo 'Successfully connected to the container';bash" ` + "\n" ,
ListId : 4 ,
DevcontainerId : newDevcontainer . Id ,
} ) ; err != nil {
log . Info ( "Failed to insert record: %v" , err )
return imageName , err
}
return imageName , nil
}
func IsContainerNotFound ( ctx context . Context , containerName string ) ( bool , error ) {
cli , err := docker_module . CreateDockerClient ( ctx )
if err != nil {
return false , err
}
defer cli . Close ( )
containerID , err := docker_module . GetContainerID ( cli , containerName )
if err != nil {
// 检查是否为 "未找到" 错误
if errdefs . IsNotFound ( err ) {
return true , nil
}
return false , err
}
_ , err = cli . ContainerInspect ( ctx , containerID )
if err != nil {
// 检查是否为 "未找到" 错误
if docker_module . IsContainerNotFound ( err ) {
return true , nil
}
// 其他类型的错误
return false , err
}
// 无错误表示容器存在
return false , nil
}
func DeleteDevContainerByDocker ( ctx context . Context , devContainerName string ) error {
// 创建docker client
cli , err := docker_module . CreateDockerClient ( ctx )
if err != nil {
return err
}
defer cli . Close ( )
// 获取容器 ID
containerID , err := docker_module . GetContainerID ( cli , devContainerName )
if err != nil {
if errdefs . IsNotFound ( err ) {
return nil
}
return err
}
// 删除容器
if err := docker_module . DeleteContainer ( ctx , cli , containerID ) ; err != nil {
return err
}
return nil
}
func RestartDevContainerByDocker ( ctx context . Context , devContainerName string ) error {
// 创建docker client
cli , err := docker_module . CreateDockerClient ( ctx )
if err != nil {
return err
}
defer cli . Close ( )
// 获取容器 ID
containerID , err := docker_module . GetContainerID ( cli , devContainerName )
if err != nil {
return err
}
// restart容器
timeout := 10 // 超时时间(秒)
err = cli . ContainerRestart ( context . Background ( ) , containerID , container . StopOptions {
Timeout : & timeout ,
} )
if err != nil {
return err
} else {
log . Info ( "容器已重启" )
}
return nil
}
2025-10-18 08:53:50 +00:00
func StopDevContainerByDocker ( ctx context . Context , devContainerName string ) error {
2025-09-20 01:56:37 +00:00
// 创建docker client
cli , err := docker_module . CreateDockerClient ( ctx )
if err != nil {
return err
}
defer cli . Close ( )
2025-10-18 08:53:50 +00:00
// 获取容器 ID
containerID , err := docker_module . GetContainerID ( cli , devContainerName )
2025-09-20 01:56:37 +00:00
if err != nil {
return err
}
// stop容器
timeout := 10 // 超时时间(秒)
err = cli . ContainerStop ( context . Background ( ) , containerID , container . StopOptions {
Timeout : & timeout ,
} )
if err != nil {
return err
} else {
log . Info ( "容器已停止" )
}
return nil
}
func UpdateDevContainerByDocker ( ctx context . Context , devContainerInfo * devcontainer_models . Devcontainer , updateInfo * UpdateInfo , repo * repo . Repository , doer * user . User ) error {
// 创建docker client
cli , err := docker_module . CreateDockerClient ( ctx )
if err != nil {
return err
}
defer cli . Close ( )
// update容器
imageRef := updateInfo . RepositoryAddress + "/" + updateInfo . RepositoryUsername + "/" + updateInfo . ImageName
configurationString , err := GetDevcontainerConfigurationString ( ctx , repo )
if err != nil {
return err
}
configurationModel , err := UnmarshalDevcontainerConfigContent ( configurationString )
if err != nil {
return err
}
if updateInfo . SaveMethod == "on" {
// 创建构建上下文( 包含Dockerfile的tar包)
var buf bytes . Buffer
tw := tar . NewWriter ( & buf )
defer tw . Close ( )
// 添加Dockerfile到tar包
dockerfile := "Dockerfile"
dockerfileContent , err := GetFileContentByPath ( ctx , repo , ".devcontainer/" + configurationModel . Build . Dockerfile )
if err != nil {
return err
}
content := [ ] byte ( dockerfileContent )
header := & tar . Header {
Name : dockerfile ,
Size : int64 ( len ( content ) ) ,
Mode : 0644 ,
}
if err := tw . WriteHeader ( header ) ; err != nil {
return err
}
if _ , err := tw . Write ( content ) ; err != nil {
return err
}
buildOptions := types . ImageBuildOptions {
Tags : [ ] string { imageRef } , // 镜像标签
}
_ , err = cli . ImageBuild (
context . Background ( ) ,
& buf ,
buildOptions ,
)
if err != nil {
log . Info ( err . Error ( ) )
return err
}
} else {
// 获取容器 ID
containerID , err := docker_module . GetContainerID ( cli , devContainerInfo . Name )
if err != nil {
return err
}
// 提交容器
_ , err = cli . ContainerCommit ( ctx , containerID , types . ContainerCommitOptions { Reference : imageRef } )
if err != nil {
return err
}
}
// 推送到仓库
dockerHost , err := docker_module . GetDockerSocketPath ( )
if err != nil {
return err
}
err = docker_module . PushImage ( dockerHost , updateInfo . RepositoryUsername , updateInfo . PassWord , updateInfo . RepositoryAddress , imageRef )
if err != nil {
return err
}
// 定义正则表达式来匹配 image 字段
re := regexp . MustCompile ( ` "image"\s*:\s*"([^"]+)" ` )
// 使用正则表达式查找并替换 image 字段的值
newConfiguration := re . ReplaceAllString ( configurationString , ` "image": " ` + imageRef + ` " ` )
err = UpdateDevcontainerConfiguration ( newConfiguration , repo , doer )
if err != nil {
return err
}
return nil
}
// ImageExists 检查指定镜像是否存在
// 返回值:
// - bool: 镜像是否存在( true=存在, false=不存在)
// - error: 非空表示检查过程中发生错误
func ImageExists ( ctx context . Context , imageName string ) ( bool , error ) {
// 创建 Docker 客户端
cli , err := docker_module . CreateDockerClient ( ctx )
if err != nil {
return false , err // 其他错误
}
// 获取镜像信息
_ , _ , err = cli . ImageInspectWithRaw ( ctx , imageName )
if err != nil {
if client . IsErrNotFound ( err ) {
return false , nil // 镜像不存在,但不是错误
}
return false , err // 其他错误
}
return true , nil // 镜像存在
}
2025-10-18 08:53:50 +00:00
2025-09-20 01:56:37 +00:00
func CheckDirExistsFromDocker ( ctx context . Context , containerName , dirPath string ) ( bool , error ) {
// 上下文
// 创建 Docker 客户端
cli , err := docker_module . CreateDockerClient ( ctx )
if err != nil {
return false , err
}
// 获取容器 ID
containerID , err := docker_module . GetContainerID ( cli , containerName )
if err != nil {
return false , err
}
// 创建 exec 配置
execConfig := types . ExecConfig {
Cmd : [ ] string { "test" , "-d" , dirPath } , // 检查目录是否存在
AttachStdout : true ,
AttachStderr : true ,
}
// 创建 exec 实例
execResp , err := cli . ContainerExecCreate ( context . Background ( ) , containerID , execConfig )
if err != nil {
return false , err
}
// 执行命令
var exitCode int
err = cli . ContainerExecStart ( context . Background ( ) , execResp . ID , types . ExecStartCheck { } )
if err != nil {
return false , err
}
// 获取命令执行结果
resp , err := cli . ContainerExecInspect ( context . Background ( ) , execResp . ID )
if err != nil {
return false , err
}
exitCode = resp . ExitCode
return exitCode == 0 , nil // 退出码为 0 表示目录存在
}
2025-10-18 08:53:50 +00:00
func CheckFileExistsFromDocker ( ctx context . Context , containerName , filePath string ) ( bool , error ) {
// 上下文
// 创建 Docker 客户端
cli , err := docker_module . CreateDockerClient ( ctx )
if err != nil {
return false , err
}
// 获取容器 ID
containerID , err := docker_module . GetContainerID ( cli , containerName )
if err != nil {
return false , err
}
// 创建 exec 配置
execConfig := types . ExecConfig {
Cmd : [ ] string { "test" , "-e" , filePath } , // 检查文件是否存在
AttachStdout : true ,
AttachStderr : true ,
}
// 创建 exec 实例
execResp , err := cli . ContainerExecCreate ( context . Background ( ) , containerID , execConfig )
if err != nil {
return false , err
}
// 执行命令
var exitCode int
err = cli . ContainerExecStart ( context . Background ( ) , execResp . ID , types . ExecStartCheck { } )
if err != nil {
return false , err
}
// 获取命令执行结果
resp , err := cli . ContainerExecInspect ( context . Background ( ) , execResp . ID )
if err != nil {
return false , err
}
exitCode = resp . ExitCode
return exitCode == 0 , nil // 退出码为 0 表示目录存在
}
2025-09-20 01:56:37 +00:00
func RegistWebTerminal ( ctx context . Context ) error {
log . Info ( "开始构建WebTerminal..." )
cli , err := docker_module . CreateDockerClient ( ctx )
if err != nil {
return err
}
defer cli . Close ( )
//拉取web_terminal镜像
dockerHost , err := docker_module . GetDockerSocketPath ( )
if err != nil {
return fmt . Errorf ( "获取docker socket路径失败:%v" , err )
}
// 拉取镜像
err = docker_module . PullImage ( ctx , cli , dockerHost , setting . DevContainerConfig . Web_Terminal_Image )
if err != nil {
return fmt . Errorf ( "拉取web_terminal镜像失败:%v" , err )
}
timestamp := time . Now ( ) . Format ( "20060102150405" )
binds := [ ] string {
"/var/run/docker.sock:/var/run/docker.sock" ,
}
containerName := "webterminal-" + timestamp
//创建并启动WebTerminal容器
err = docker_module . CreateAndStartContainer ( ctx , cli , setting . DevContainerConfig . Web_Terminal_Image ,
nil ,
nil , binds ,
nat . PortSet {
"7681/tcp" : struct { } { } ,
} ,
containerName )
if err != nil {
return fmt . Errorf ( "创建并注册WebTerminal失败:%v" , err )
}
// Save settings.
cfg , err := setting . NewConfigProviderFromFile ( setting . CustomConf )
if err != nil {
return err
}
_ , err = docker_module . GetMappedPort ( ctx , containerName , "7681" )
if err != nil {
return err
}
cfg . Section ( "devcontainer" ) . Key ( "WEB_TERMINAL_CONTAINER" ) . SetValue ( containerName )
if err = cfg . SaveTo ( setting . CustomConf ) ; err != nil {
return err
}
return nil
}