!67 增加了重启停止容器、dockerfile方式创建保存容器功能

* change initializeScript path
* Merge branch 'add-dockerfile-method-and-start-stop-container' of https…
* 更新了容器镜像方式的构建、安装和使用方法,但是devcontainer功能还有问题
* fix run postCreateCommand bug
* sh文件方式管理启动脚本
* add restart command and fix bug
* add dockerfile method to create container and save container .restart …
This commit is contained in:
xinitx
2025-05-07 11:10:30 +00:00
repo.diff.committed_by 孟宁
repo.diff.parent fbd405af67
repo.diff.commit 02baa3b7af
repo.diff.stats_desc%!(EXTRA int=16, int=844, int=285)

repo.diff.view_file

@@ -75,6 +75,8 @@ RUN mkdir -p /var/lib/gitea /etc/gitea
RUN chown git:git /var/lib/gitea /etc/gitea RUN chown git:git /var/lib/gitea /etc/gitea
COPY --from=build-env /tmp/local / COPY --from=build-env /tmp/local /
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/devcontainer_init.sh /app/gitea/devcontainer_init.sh
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/devcontainer_restart.sh /app/gitea/devcontainer_restart.sh
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh

repo.diff.view_file

@@ -50,7 +50,7 @@ git checkout -b YOUR_BRANCH
code devstar code devstar
# in VS Code Terminal # in VS Code Terminal
TAGS="bindata timetzdata sqlite sqlite_unlock_notify" make watch # for debuging TAGS="timetzdata sqlite sqlite_unlock_notify" make watch # for debuging
make test # testing make test # testing
TAGS="bindata timetzdata sqlite sqlite_unlock_notify" make build # 生成可执行文件 TAGS="bindata timetzdata sqlite sqlite_unlock_notify" make build # 生成可执行文件
./gitea ./gitea
@@ -60,23 +60,25 @@ git add FILES
git commit -m "commit log" git commit -m "commit log"
git push git push
``` ```
在DevStar Git仓库发起Pull Request合并代码后会自动触发CI流水线完成容器镜像的构建并上传到devstar.cn/devstar/devstar-studio:latest
### Start from Container Image #### Start from Container Image
``` ```
sudo apt update && sudo apt install docker.io make docker
sudo docker pull devstar.cn/devstar/devstar-studio:latest public/assets/install.sh start --image=devstar-studio:latest
# 创建devstar_data目录用于持久化存储DevStar相关的配置和用户数据
mkdir ~/devstar_data
# 启动devstar-studio容器
sudo docker run --restart=always --name devstar-studio -d -p 8080:3000 -v /var/run/docker.sock:/var/run/docker.sock -v ~/devstar_data:/var/lib/gitea -v ~/devstar_data:/etc/gitea devstar.cn/devstar/devstar-studio:latest
# 打开 `http://localhost:8080` 完成安装。
# 查看devstar-studio容器的启动日志 # 查看日志
sudo docker logs devstar-studio public/assets/install.sh logs
# 停止并删除devstar-studio容器 # 停止并删除devstar-studio容器
sudo docker stop devstar-studio && sudo docker rm -f devstar-studio public/assets/install.sh clean
# 删除所有容器
sudo docker stop $(docker ps -aq) && sudo docker rm -f $(docker ps -aq)
```
在DevStar Git仓库发起Pull Request合并代码后会自动触发CI流水线完成容器镜像的构建并上传到devstar.cn/devstar/devstar-studio:latest
```
public/assets/install.sh start
``` ```
## 提示 ## 提示

74
devcontainer_init.sh Normal file
repo.diff.view_file

@@ -0,0 +1,74 @@
#!/bin/bash
# Copyright 2025 Mengning Software All rights reserved.
# Exit immediately if a command exits with a non-zero status
set -e
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Function to display success message
function success {
echo -e "${GREEN}$1${NC}"
}
# Function to display failure message
function failure {
echo -e "${RED}$1${NC}"
}
# Detect the OS type and install dependencies
function install_dependencies {
# Install dependencies based on the OS type
success "dependencies install begin: "
OS_ID=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
case $OS_ID in
ubuntu|debian)
sudo apt-get update -y
sudo apt-get install ssh -y
;;
centos)
# sudo yum update -y
# sudo yum install -y epel-release
# sudo yum groupinstall -y "Development Tools"
# sudo yum install -y yaml-cpp yaml-cpp-devel
;;
fedora)
# sudo dnf update -y
# sudo dnf group install -y "Development Tools"
# sudo dnf install -y yaml-cpp yaml-cpp-devel
;;
*)
failure "Unsupported OS: $OS_ID"
exit 1
;;
esac
}
install_dependencies
echo -e "PubkeyAuthentication yes\nPermitRootLogin yes\n" | tee -a /etc/ssh/sshd_config
rm -f /etc/ssh/ssh_host_*; ssh-keygen -A; service ssh restart
if [ -z "${host_docker_internal+x}" ]; then
echo "$HOST_DOCKER_INTERNAL host.docker.internal" | tee -a /etc/hosts;
fi
if [ ! -d "$WORKDIR" ]; then
git clone $REPO_URL $WORKDIR && echo "Git Repository cloned.";
else
echo "Folder already exists.";
fi
mkdir -p ~/test
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "$AUTHORIZED_KEYS" > ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
git clone "https://devstar.cn/init/ttyd.git" "/usr/bin/ttyd"
apt-get install -y build-essential cmake git libjson-c-dev libwebsockets-dev
/usr/bin/ttyd/ttyd/ttyd -W -w $WORKDIR bash &

5
devcontainer_restart.sh Normal file
repo.diff.view_file

@@ -0,0 +1,5 @@
#!/bin/bash
# Copyright 2025 Mengning Software All rights reserved.
service ssh restart
/usr/bin/ttyd/ttyd/ttyd -W -w $WORKDIR bash &

repo.diff.view_file

@@ -1,6 +1,7 @@
package docker package docker
import ( import (
"archive/tar"
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
@@ -126,16 +127,15 @@ func GetAllMappedPort(cli *client.Client, containerID string) (string, error) {
func PushImage(dockerHost string, username string, password string, registryUrl string, imageRef string) error { func PushImage(dockerHost string, username string, password string, registryUrl string, imageRef string) error {
script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " " script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " "
cmd := exec.Command("sh", "-c", script) cmd := exec.Command("sh", "-c", script)
output, err := cmd.CombinedOutput() _, err := cmd.CombinedOutput()
log.Info(string(output))
if err != nil { if err != nil {
return err return err
} }
// 推送到仓库 // 推送到仓库
script = "docker " + "-H " + dockerHost + " push " + imageRef script = "docker " + "-H " + dockerHost + " push " + imageRef
cmd = exec.Command("sh", "-c", script) cmd = exec.Command("sh", "-c", script)
output, err = cmd.CombinedOutput() _, err = cmd.CombinedOutput()
log.Info(string(output))
if err != nil { if err != nil {
return err return err
} }
@@ -248,7 +248,6 @@ func CreateAndStartContainer(cli *client.Client, opts *CreateDevcontainerOptions
}, },
PortBindings: opts.PortBindings, PortBindings: opts.PortBindings,
} }
log.Info("%v", opts.PortBindings)
if len(opts.Binds) > 0 { if len(opts.Binds) > 0 {
hostConfig.Binds = opts.Binds hostConfig.Binds = opts.Binds
} }
@@ -264,6 +263,22 @@ func CreateAndStartContainer(cli *client.Client, opts *CreateDevcontainerOptions
log.Info("fail to start container %v", err) log.Info("fail to start container %v", err)
return "", err return "", err
} }
// 创建 tar 归档文件
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加文件到 tar 归档
addFileToTar(tw, "devcontainer_init.sh", opts.InitializeCommand, 0777)
addFileToTar(tw, "devcontainer_restart.sh", opts.RestartCommand, 0777)
//ExecCommandInContainer(&ctx, cli, resp.ID, "touch /home/devcontainer_init.sh && chomd +x /home/devcontainer_init.sh")
err = cli.CopyToContainer(ctx, resp.ID, "/home", bytes.NewReader(buf.Bytes()), types.CopyToContainerOptions{})
if err != nil {
log.Info("%v", err)
return "", err
}
// 获取日志流 // 获取日志流
out, _ := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ out, _ := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{
ShowStdout: true, ShowStdout: true,
@@ -276,3 +291,19 @@ func CreateAndStartContainer(cli *client.Client, opts *CreateDevcontainerOptions
_, _ = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, out) _, _ = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, out)
return stdoutBuf.String() + "\n" + stderrBuf.String(), nil return stdoutBuf.String() + "\n" + stderrBuf.String(), nil
} }
// addFileToTar 将文件添加到 tar 归档
func addFileToTar(tw *tar.Writer, filename string, content string, mode int64) error {
hdr := &tar.Header{
Name: filename,
Mode: mode,
Size: int64(len(content)),
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if _, err := tw.Write([]byte(content)); err != nil {
return err
}
return nil
}

repo.diff.view_file

@@ -6,20 +6,22 @@ import (
// CreateDevcontainerOptions 定义创建开发容器选项 // CreateDevcontainerOptions 定义创建开发容器选项
type CreateDevcontainerOptions struct { type CreateDevcontainerOptions struct {
Name string `json:"name"` DockerfileContent string
Namespace string `json:"namespace"` Name string `json:"name"`
Image string `json:"image"` Namespace string `json:"namespace"`
CommandList []string `json:"command"` Image string `json:"image"`
ContainerPort uint16 `json:"containerPort"` CommandList []string `json:"command"`
ServicePort uint16 `json:"servicePort"` ContainerPort uint16 `json:"containerPort"`
SSHPublicKeyList []string `json:"sshPublicKeyList"` ServicePort uint16 `json:"servicePort"`
GitRepositoryURL string `json:"gitRepositoryURL"` SSHPublicKeyList []string `json:"sshPublicKeyList"`
RepoId int64 GitRepositoryURL string `json:"gitRepositoryURL"`
UserId int64 RepoId int64
ForwardPorts nat.PortSet UserId int64
ContainerEnv []string ForwardPorts nat.PortSet
PostCreateCommand []string ContainerEnv []string
Binds []string PostCreateCommand []string
SystemCommandCount int Binds []string
PortBindings nat.PortMap PortBindings nat.PortMap
InitializeCommand string
RestartCommand string
} }

repo.diff.view_file

@@ -8,7 +8,8 @@ IMAGE_NAME=devstar-studio
VERSION=latest # DevStar Studio的默认版本为最新版本 VERSION=latest # DevStar Studio的默认版本为最新版本
PORT=8080 # 设置端口默认值为 8080 PORT=8080 # 设置端口默认值为 8080
SSH_PORT=2222 # 设置ssh默认端口号2222 SSH_PORT=2222 # 设置ssh默认端口号2222
DATA_DIR=~/devstar_data DATA_DIR=${HOME}/devstar_data
APP_INI=${DATA_DIR}/app.ini
# 错误处理函数 # 错误处理函数
error_handler() { error_handler() {
@@ -89,17 +90,35 @@ function install {
# Function to start # Function to start
function start { function start {
install if [[ -z "$IMAGE_STR" ]]; then
install
fi
# 创建devstar_data目录用于持久化存储DevStar相关的配置和用户数据 # 创建devstar_data目录用于持久化存储DevStar相关的配置和用户数据
mkdir -p $DATA_DIR mkdir -p $DATA_DIR
sudo chown 1000:1000 $DATA_DIR sudo chown 1000:1000 $DATA_DIR
sudo chmod 666 /var/run/docker.sock sudo chmod 666 /var/run/docker.sock
if [ ! -f "$APP_INI" ]; then
DOMAIN_NAME=$(hostname -I | awk '{print $1}')
echo "DOMAIN_NAME=$DOMAIN_NAME"
else
# 读取 DOMAIN 值
DOMAIN_NAME=$(grep -E '^\s*DOMAIN\s*=' "$APP_INI" | cut -d'=' -f2 | xargs)
# 检查是否成功读取到值
if [[ -z "$DOMAIN_NAME" ]]; then
DOMAIN_NAME="localhost"
fi
echo "DOMAIN_NAME=$DOMAIN_NAME"
fi
# 启动devstar-studio容器 # 启动devstar-studio容器
stop stop
sudo docker run --restart=always --name $NAME -d -p $PORT:3000 -p $SSH_PORT:$SSH_PORT -v /var/run/docker.sock:/var/run/docker.sock -v ~/devstar_data:/var/lib/gitea -v ~/devstar_data:/etc/gitea $IMAGE_REGISTRY_USER/$IMAGE_NAME:$VERSION if [[ -z "$IMAGE_STR" ]]; then
IMAGE_STR="$IMAGE_REGISTRY_USER/$IMAGE_NAME:$VERSION"
fi
echo "image=$IMAGE_STR"
sudo docker run --restart=always --name $NAME -d -p $PORT:3000 -p $SSH_PORT:$SSH_PORT -v /var/run/docker.sock:/var/run/docker.sock -v ~/devstar_data:/var/lib/gitea -v ~/devstar_data:/etc/gitea $IMAGE_STR
# 打开 `http://localhost:8080` 完成安装。 # 打开 `http://localhost:8080` 完成安装。
success "-------------------------------------------------------" success "-------------------------------------------------------"
success "DevStar started in http://localhost:$PORT successfully!" success "DevStar started in http://$DOMAIN_NAME:$PORT successfully!"
success "-------------------------------------------------------" success "-------------------------------------------------------"
exit 0 exit 0
} }
@@ -109,7 +128,9 @@ function stop {
if [ $(docker ps -a --filter "name=^/${NAME}$" -q | wc -l) -gt 0 ]; then if [ $(docker ps -a --filter "name=^/${NAME}$" -q | wc -l) -gt 0 ]; then
sudo docker stop $NAME && sudo docker rm -f $NAME sudo docker stop $NAME && sudo docker rm -f $NAME
fi fi
if [ $(docker ps -a --filter "name=^/devstar-studio$" -q | wc -l) -gt 0 ]; then
sudo docker stop devstar-studio && sudo docker rm -f devstar-studio
fi
} }
# Function to logs # Function to logs
@@ -133,6 +154,7 @@ function usage {
success " --port=<arg> Specify the port number (default port is 8080)" success " --port=<arg> Specify the port number (default port is 8080)"
success " --ssh-port=<arg> Specify the ssh-port number (default ssh-port is 2222)" success " --ssh-port=<arg> Specify the ssh-port number (default ssh-port is 2222)"
success " --version=<arg> Specify the DevStar Studio Image Version (default verson is latest)" success " --version=<arg> Specify the DevStar Studio Image Version (default verson is latest)"
success " --image=<arg> Specify the DevStar Studio Image example: devstar-studio:latest "
success " stop Stop the running DevStar Studio" success " stop Stop the running DevStar Studio"
success " logs View the logs of the devstar-studio container" success " logs View the logs of the devstar-studio container"
failure " clean Clean up the running DevStar Studio, including deleting user data. Please use with caution." failure " clean Clean up the running DevStar Studio, including deleting user data. Please use with caution."
@@ -146,7 +168,7 @@ case "$1" in
usage usage
;; ;;
start) start)
ARGS=$(getopt --long port::,ssh-port::,version:: -- "$@") ARGS=$(getopt --long port::,ssh-port::,version::,image:: -- "$@")
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
failure "ARGS ERROR!" failure "ARGS ERROR!"
exit 1 exit 1
@@ -166,7 +188,11 @@ case "$1" in
--version) --version)
VERSION="$2" VERSION="$2"
echo "The DevStar Studio Image Version is: $VERSION" echo "The DevStar Studio Image Version is: $VERSION"
shift 2 ;; shift 2 ;;
--image)
IMAGE_STR="$2"
echo "The DevStar Studio Image: $IMAGE_STR"
shift 2 ;;
--) --)
shift shift
break ;; break ;;

repo.diff.view_file

@@ -35,6 +35,7 @@ func GetRepoDevContainerDetails(ctx *context.Context) {
Where("user_id = ? AND repo_id = ?", ctx.Doer.ID, ctx.Repo.Repository.ID). Where("user_id = ? AND repo_id = ?", ctx.Doer.ID, ctx.Repo.Repository.ID).
Find(&devContainerOutput) Find(&devContainerOutput)
ctx.Data["isCreatingDevcontainer"] = false ctx.Data["isCreatingDevcontainer"] = false
ctx.Data["InitializedContainer"] = true
if err == nil && len(devContainerOutput) > 0 { if err == nil && len(devContainerOutput) > 0 {
ctx.Data["isCreatingDevcontainer"] = true ctx.Data["isCreatingDevcontainer"] = true
} }
@@ -43,6 +44,9 @@ func GetRepoDevContainerDetails(ctx *context.Context) {
if item.ListId > 0 && item.Status == "success" { if item.ListId > 0 && item.Status == "success" {
created = true created = true
} }
if item.Status != "success" {
ctx.Data["InitializedContainer"] = false
}
} }
//ctx.Repo.RepoLink == ctx.Repo.Repository.Link() //ctx.Repo.RepoLink == ctx.Repo.Repository.Link()
@@ -65,9 +69,10 @@ func GetRepoDevContainerDetails(ctx *context.Context) {
if err == nil { if err == nil {
imageName := devcontainerJson.Image imageName := devcontainerJson.Image
registry, namespace, repo, tag := ParseImageName(imageName) registry, namespace, repo, tag := ParseImageName(imageName)
log.Info("%v %v", repo, tag)
ctx.Data["RepositoryAddress"] = registry ctx.Data["RepositoryAddress"] = registry
ctx.Data["RepositoryUsername"] = namespace ctx.Data["RepositoryUsername"] = namespace
ctx.Data["ImageName"] = ctx.Repo.Repository.Name + "-" + repo + ":" + tag ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest"
} }
// 获取WebSSH服务端口 // 获取WebSSH服务端口
@@ -96,11 +101,13 @@ func GetRepoDevContainerDetails(ctx *context.Context) {
ctx.Data["HasValidDevContainerJSON"] = isValidRepoDevcontainerJson ctx.Data["HasValidDevContainerJSON"] = isValidRepoDevcontainerJson
ctx.Data["Repository"] = ctx.Repo.Repository ctx.Data["Repository"] = ctx.Repo.Repository
ctx.Data["ContextUser"] = ctx.Doer ctx.Data["ContextUser"] = ctx.Doer
ctx.Data["CreateDevcontainerSettingUrl"] = "/" + ctx.Doer.Name + "/" + ctx.Repo.Repository.Name + "/dev-container/createConfiguration" ctx.Data["CreateDevcontainerSettingUrl"] = "/" + ctx.ContextUser.Name + "/" + ctx.Repo.Repository.Name + "/dev-container/createConfiguration"
ctx.Data["EditDevcontainerConfigurationUrl"] = ctx.Repo.RepoLink + "/_edit/" + ctx.Repo.Repository.DefaultBranch + "/.devcontainer/devcontainer.json" ctx.Data["EditDevcontainerConfigurationUrl"] = ctx.Repo.RepoLink + "/_edit/" + ctx.Repo.Repository.DefaultBranch + "/.devcontainer/devcontainer.json"
ctx.Data["TreeNames"] = []string{".devcontainer", "devcontainer.json"} ctx.Data["TreeNames"] = []string{".devcontainer", "devcontainer.json"}
ctx.Data["TreePaths"] = []string{".devcontainer", ".devcontainer/devcontainer.json"} ctx.Data["TreePaths"] = []string{".devcontainer", ".devcontainer/devcontainer.json"}
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
ctx.Data["SaveMethods"] = []string{"Container", "DockerFile"}
ctx.Data["SaveMethod"] = "Container"
ctx.HTML(http.StatusOK, tplGetRepoDevcontainerDetail) ctx.HTML(http.StatusOK, tplGetRepoDevcontainerDetail)
} }
@@ -205,6 +212,7 @@ func UpdateRepoDevContainerForCurrentActor(ctx *context.Context) {
DevContainerName: devContainerMetadata.DevContainerName, DevContainerName: devContainerMetadata.DevContainerName,
Actor: ctx.Doer, Actor: ctx.Doer,
Repository: ctx.Repo.Repository, Repository: ctx.Repo.Repository,
SaveMethod: updateInfo.SaveMethod,
} }
err = devcontainer_service.UpdateDevcontainerAPIService(ctx, opts) err = devcontainer_service.UpdateDevcontainerAPIService(ctx, opts)
if err != nil { if err != nil {
@@ -290,6 +298,35 @@ func GetContainerOutput(ctx *context.Context) {
} }
ctx.JSON(http.StatusOK, resp) ctx.JSON(http.StatusOK, resp)
return return
} else {
resp := &OutputResponse{}
ctx.JSON(http.StatusOK, resp)
} }
ctx.Done()
}
func RestartContainer(ctx *context.Context) {
opt := &devcontainer_service.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
devContainerMetadata, _ := devcontainer_service.GetRepoDevcontainerDetails(ctx, opt)
err := devcontainer_service.RestartDevcontainer(*ctx, &devContainerMetadata)
if err != nil {
ctx.Flash.Error("fail to restart container")
}
ctx.JSON(http.StatusOK, map[string]string{})
}
func StopContainer(ctx *context.Context) {
opt := &devcontainer_service.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
devContainerMetadata, _ := devcontainer_service.GetRepoDevcontainerDetails(ctx, opt)
err := devcontainer_service.StopDevcontainer(ctx, &devContainerMetadata)
if err != nil {
ctx.Flash.Error("fail to stop container")
}
ctx.JSON(http.StatusOK, map[string]string{})
} }

repo.diff.view_file

@@ -523,6 +523,7 @@ func registerRoutes(m *web.Router) {
// 列举某用户已创建的所有 DevContainer // 列举某用户已创建的所有 DevContainer
m.Get("/user", devcontainer_api.ListUserDevcontainers) m.Get("/user", devcontainer_api.ListUserDevcontainers)
}) })
// ***** END: DevContainer ***** // ***** END: DevContainer *****
@@ -1344,6 +1345,8 @@ func registerRoutes(m *web.Router) {
m.Get("/createConfiguration", devcontainer_web.CreateRepoDevContainerConfiguration) m.Get("/createConfiguration", devcontainer_web.CreateRepoDevContainerConfiguration)
m.Get("/create", devcontainer_web.CreateRepoDevContainer, context.RepoMustNotBeArchived()) // 仓库状态非 Archived 才可以创建 DevContainer m.Get("/create", devcontainer_web.CreateRepoDevContainer, context.RepoMustNotBeArchived()) // 仓库状态非 Archived 才可以创建 DevContainer
m.Get("/output", devcontainer_web.GetContainerOutput) m.Get("/output", devcontainer_web.GetContainerOutput)
m.Get("/restart", devcontainer_web.RestartContainer)
m.Get("/stop", devcontainer_web.StopContainer)
m.Post("/delete", devcontainer_web.DeleteRepoDevContainerForCurrentActor) m.Post("/delete", devcontainer_web.DeleteRepoDevContainerForCurrentActor)
m.Post("/update", devcontainer_web.UpdateRepoDevContainerForCurrentActor) m.Post("/update", devcontainer_web.UpdateRepoDevContainerForCurrentActor)
}, },

repo.diff.view_file

@@ -44,7 +44,7 @@ func GetWechatQRTicket(ctx *context.Context) (wechatQrTicket string, QRImageURL
qrExpireSeconds := setting.Wechat.TempQrExpireSeconds qrExpireSeconds := setting.Wechat.TempQrExpireSeconds
// 构建请求的 URL // 构建请求的 URL
url := fmt.Sprintf("https://%s/api/wechat/official-account/login/qr/generate?qrExpireSeconds=%d&sceneStr=%s", setting.Wechat.DefaultDomainName, qrExpireSeconds, sceneStr) url := fmt.Sprintf("https://%s/api/wechat/login/qr/generate?qrExpireSeconds=%d&sceneStr=%s", setting.Wechat.DefaultDomainName, qrExpireSeconds, sceneStr)
// 发送 GET 请求 // 发送 GET 请求
resp, err := http.Get(url) resp, err := http.Get(url)
@@ -103,7 +103,7 @@ type Response struct {
// 假设这是用于检查二维码状态的函数 // 假设这是用于检查二维码状态的函数
func checkWechatQrTicketStatus(ctx *context.Context, qrTicket string, quit chan bool) { func checkWechatQrTicketStatus(ctx *context.Context, qrTicket string, quit chan bool) {
url := fmt.Sprintf("https://%s/api/wechat/official-account/login/qr/check-status?ticket=%s&_=%d", url := fmt.Sprintf("https://%s/api/wechat/login/qr/check-status?ticket=%s&_=%d",
setting.Wechat.DefaultDomainName, qrTicket, time.Now().UnixMilli()) setting.Wechat.DefaultDomainName, qrTicket, time.Now().UnixMilli())
resp, err := http.Get(url) resp, err := http.Get(url)

repo.diff.view_file

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -168,10 +169,24 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt
Message: err.Error(), Message: err.Error(),
} }
} }
var dockerfileContent string
if devContainerJson.DockerfilePath != "" {
dockerfileContent, err = GetDockerfileContent(ctx, opts.Repository)
if err != nil {
return devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Get DockerFileContent Error",
Message: err.Error(),
}
}
}
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
}
newDevcontainer := &CreateDevcontainerDTO{ newDevcontainer := &CreateDevcontainerDTO{
Devcontainer: devcontainer_model.Devcontainer{ Devcontainer: devcontainer_model.Devcontainer{
Name: getSanitizedDevcontainerName(username, repoName), Name: getSanitizedDevcontainerName(username, repoName),
DevcontainerHost: setting.Devcontainer.Host, DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(),
DevcontainerUsername: "root", DevcontainerUsername: "root",
DevcontainerWorkDir: "/data/workspace", DevcontainerWorkDir: "/data/workspace",
RepoId: opts.Repository.ID, RepoId: opts.Repository.ID,
@@ -179,8 +194,9 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt
CreatedUnix: unixTimestamp, CreatedUnix: unixTimestamp,
UpdatedUnix: unixTimestamp, UpdatedUnix: unixTimestamp,
}, },
Image: devContainerJson.Image, DockerfileContent: dockerfileContent,
GitRepositoryURL: strings.TrimSuffix(setting.AppURL, "/") + opts.Repository.Link(), Image: devContainerJson.Image,
GitRepositoryURL: strings.TrimSuffix(setting.AppURL, "/") + opts.Repository.Link(),
} }
// 在数据库事务中创建 Dev Container 分配资源,出错时自动回滚相对应数据库字段,保证数据一致 // 在数据库事务中创建 Dev Container 分配资源,出错时自动回滚相对应数据库字段,保证数据一致
@@ -315,7 +331,11 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er
if err != nil { if err != nil {
return "", err return "", err
} }
return "http://" + setting.Devcontainer.Host + ":" + port + "/", nil cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
}
return "http://" + cfg.Section("server").Key("DOMAIN").Value() + ":" + port + "/", nil
default: default:
return "", fmt.Errorf("unknown agent") return "", fmt.Errorf("unknown agent")
} }
@@ -323,7 +343,6 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er
func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContainer) (string, error) { func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContainer) (string, error) {
var access_token string var access_token string
defalut_ctx := context.Background() defalut_ctx := context.Background()
log.Info("%v", devcontainer)
// 获取端口号 // 获取端口号
cli, err := docker.CreateDockerClient(&defalut_ctx) cli, err := docker.CreateDockerClient(&defalut_ctx)
if err != nil { if err != nil {
@@ -555,13 +574,81 @@ func claimDevcontainerResource(ctx *context.Context, newDevContainer *CreateDevc
} }
} }
// 解析仓库 URL
parsedURL, err := url.Parse(newDevContainer.GitRepositoryURL)
if err != nil {
log.Info("解析仓库URL失败: %v", err)
return err
}
hostParts := strings.Split(parsedURL.Host, ":")
port := ""
if len(hostParts) > 1 {
port = hostParts[1]
}
newHost := "host.docker.internal"
if port != "" {
newHost += ":" + port
}
parsedURL.Host = newHost
// 生成git仓库的 URL
newURL := parsedURL.String()
// Read the init script from file
var initializeScriptContent, restartScriptContent []byte
_, err = os.Stat("devcontainer_init.sh")
if os.IsNotExist(err) {
_, err = os.Stat("/app/gitea/devcontainer_init.sh")
if os.IsNotExist(err) {
return fmt.Errorf("读取初始化脚本失败: %v", err)
} else {
initializeScriptContent, err = os.ReadFile("/app/gitea/devcontainer_init.sh")
if err != nil {
return fmt.Errorf("读取初始化脚本失败: %v", err)
}
}
} else {
initializeScriptContent, err = os.ReadFile("devcontainer_init.sh")
if err != nil {
return fmt.Errorf("读取初始化脚本失败: %v", err)
}
}
_, err = os.Stat("devcontainer_restart.sh")
if os.IsNotExist(err) {
_, err = os.Stat("/app/gitea/devcontainer_restart.sh")
if os.IsNotExist(err) {
return fmt.Errorf("读取初始化脚本失败: %v", err)
} else {
restartScriptContent, err = os.ReadFile("/app/gitea/devcontainer_restart.sh")
if err != nil {
return fmt.Errorf("读取初始化脚本失败: %v", err)
}
}
} else {
restartScriptContent, err = os.ReadFile("devcontainer_restart.sh")
if err != nil {
return fmt.Errorf("读取初始化脚本失败: %v", err)
}
}
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
return err
}
initializeScript := strings.ReplaceAll(string(initializeScriptContent), "$AUTHORIZED_KEYS", strings.Join(newDevContainer.SSHPublicKeyList, "\n"))
initializeScript = strings.ReplaceAll(initializeScript, "$HOST_DOCKER_INTERNAL", cfg.Section("server").Key("DOMAIN").Value())
initializeScript = strings.ReplaceAll(initializeScript, "$WORKDIR", newDevContainer.DevcontainerWorkDir)
initializeScript = strings.ReplaceAll(initializeScript, "$REPO_URL", newURL)
restartScript := strings.ReplaceAll(string(restartScriptContent), "$WORKDIR", newDevContainer.DevcontainerWorkDir)
// 2. 根据配置文件中指定的 DevContainer Agent 派遣创建任务 // 2. 根据配置文件中指定的 DevContainer Agent 派遣创建任务
switch setting.Devcontainer.Agent { switch setting.Devcontainer.Agent {
case setting.KUBERNETES, "k8s": case setting.KUBERNETES, "k8s":
// k8s Operator // k8s Operator
return AssignDevcontainerCreation2K8sOperator(ctx, newDevContainer) return AssignDevcontainerCreation2K8sOperator(ctx, newDevContainer)
case setting.DOCKER: case setting.DOCKER:
return CreateDevcontainer(ctx, newDevContainer, devContainerJSON) return CreateDevcontainer(ctx, newDevContainer, devContainerJSON, initializeScript, restartScript)
default: default:
// 未知 Agent直接报错 // 未知 Agent直接报错
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
@@ -570,3 +657,31 @@ func claimDevcontainerResource(ctx *context.Context, newDevContainer *CreateDevc
} }
} }
} }
func RestartDevcontainer(gitea_ctx gitea_context.Context, opts *RepoDevContainer) error {
switch setting.Devcontainer.Agent {
case setting.KUBERNETES:
//k8s处理
return fmt.Errorf("暂时不支持的Agent")
case setting.DOCKER:
return DockerRestartContainer(&gitea_ctx, opts)
default:
return fmt.Errorf("不支持的Agent")
//默认处理
}
}
func StopDevcontainer(gitea_ctx context.Context, opts *RepoDevContainer) error {
switch setting.Devcontainer.Agent {
case setting.KUBERNETES:
//k8s处理
return fmt.Errorf("暂时不支持的Agent")
case setting.DOCKER:
return DockerStopContainer(&gitea_ctx, opts)
default:
return fmt.Errorf("不支持的Agent")
//默认处理
}
}

repo.diff.view_file

@@ -27,6 +27,7 @@ type DevStarJSON struct {
ForwardPorts nat.PortSet ForwardPorts nat.PortSet
ContainerEnv []string ContainerEnv []string
Image string Image string
DockerfilePath string
InitializeCommand []string InitializeCommand []string
PostCreateCommand []string PostCreateCommand []string
RunArgs []string RunArgs []string
@@ -54,7 +55,7 @@ func CreateDevcontainerJSON(ctx *gitea_context.Context) {
"8888:8888" "8888:8888"
] ]
}` }`
resp, err := files_service.ChangeRepoFiles(db.DefaultContext, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ _, err := files_service.ChangeRepoFiles(db.DefaultContext, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{ Files: []*files_service.ChangeRepoFile{
{ {
Operation: "create", Operation: "create",
@@ -66,7 +67,6 @@ func CreateDevcontainerJSON(ctx *gitea_context.Context) {
NewBranch: "main", NewBranch: "main",
Message: "add container configuration", Message: "add container configuration",
}) })
log.Info(resp.Commit.URL)
if err != nil { if err != nil {
log.Info("error ChangeRepoFiles:", err) log.Info("error ChangeRepoFiles:", err)
ctx.JSON(500, map[string]string{ ctx.JSON(500, map[string]string{
@@ -114,7 +114,6 @@ func GetDevcontainerJsonModel(ctx context.Context, repo *repo.Repository) (*DevS
log.Error("Failed to unmarshal .devcontainer/devcontainer.json: %v", err) log.Error("Failed to unmarshal .devcontainer/devcontainer.json: %v", err)
return nil, err return nil, err
} }
log.Info("%v", devContainerJson)
devStarJson := &DevStarJSON{ devStarJson := &DevStarJSON{
Image: devContainerJson.Image, Image: devContainerJson.Image,
@@ -134,11 +133,13 @@ func GetDevcontainerJsonModel(ctx context.Context, repo *repo.Repository) (*DevS
devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c4...) devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c4...)
devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c5...) devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c5...)
devStarJson.RunArgs = devContainerJson.RunArgs devStarJson.RunArgs = devContainerJson.RunArgs
log.Info("%v", devStarJson) if devContainerJson.Build != nil {
devStarJson.DockerfilePath = devContainerJson.Build.Dockerfile
}
return devStarJson, nil return devStarJson, nil
} }
func GetFileContentByPath(ctx context.Context, repo *repo.Repository, path string) (string, error) {
func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (string, error) {
// 1. 获取默认分支名 // 1. 获取默认分支名
branchName := repo.DefaultBranch branchName := repo.DefaultBranch
if len(branchName) == 0 { if len(branchName) == 0 {
@@ -161,7 +162,7 @@ func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (stri
return "", err return "", err
} }
entry, err := commit.GetTreeEntryByPath(".devcontainer/devcontainer.json") entry, err := commit.GetTreeEntryByPath(path)
if err != nil { if err != nil {
log.Info("Repo.Commit.GetTreeEntryByPath %v", err.Error()) log.Info("Repo.Commit.GetTreeEntryByPath %v", err.Error())
return "", err return "", err
@@ -169,13 +170,13 @@ func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (stri
// No way to edit a directory online. // No way to edit a directory online.
if entry.IsDir() { if entry.IsDir() {
return "", fmt.Errorf(".devcontainer/devcontainer.json entry.IsDir") return "", fmt.Errorf(path + " entry.IsDir")
} }
blob := entry.Blob() blob := entry.Blob()
if blob.Size() >= setting.UI.MaxDisplayFileSize { if blob.Size() >= setting.UI.MaxDisplayFileSize {
return "", fmt.Errorf(".devcontainer/devcontainer.json blob.Size overflow") return "", fmt.Errorf(path + " blob.Size overflow")
} }
dataRc, err := blob.DataAsync() dataRc, err := blob.DataAsync()
@@ -204,6 +205,35 @@ func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (stri
} else { } else {
return content, nil return content, nil
} }
}
func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (string, error) {
return GetFileContentByPath(ctx, repo, ".devcontainer/devcontainer.json")
}
func GetDockerfileContent(ctx context.Context, repo *repo.Repository) (string, error) {
devcontainerJSONContent, err := GetDevcontainerJsonString(ctx, repo)
var devContainerJson *devcontainer_model.DevContainerJSON
if err != nil {
return "", err
}
// 1. 移除注释
cleanedContent, err := removeComments(devcontainerJSONContent)
if err != nil {
log.Error("Failed to remove comments from .devcontainer/devcontainer.json: %v", err)
return "", err
}
// 2. 解析 JSON
devContainerJson, err = devcontainer_model.Unmarshal(cleanedContent)
if err != nil {
log.Error("Failed to unmarshal .devcontainer/devcontainer.json: %v", err)
return "", err
}
if devContainerJson.Build.Dockerfile == "" {
return "", nil
}
return GetFileContentByPath(ctx, repo, ".devcontainer/"+devContainerJson.Build.Dockerfile)
} }
// 移除 JSON 文件中的注释 // 移除 JSON 文件中的注释

repo.diff.view_file

@@ -62,6 +62,7 @@ type UpdateInfo struct {
PassWord string `json:"RepositoryPassword"` PassWord string `json:"RepositoryPassword"`
RepositoryAddress string `json:"RepositoryAddress"` RepositoryAddress string `json:"RepositoryAddress"`
RepositoryUsername string `json:"RepositoryUsername"` RepositoryUsername string `json:"RepositoryUsername"`
SaveMethod string `json:"SaveMethod"`
} }
// CreateDevcontainerOptions 封装 API 创建 DevContainer 数据,即 router 层 POST /api/devcontainer 向下传递的数据结构 // CreateDevcontainerOptions 封装 API 创建 DevContainer 数据,即 router 层 POST /api/devcontainer 向下传递的数据结构
@@ -88,6 +89,7 @@ type UpdateDevcontainerOptions struct {
DevContainerName string DevContainerName string
Actor *user_model.User Actor *user_model.User
Repository *repo_model.Repository Repository *repo_model.Repository
SaveMethod string
} }
// AbstractOpenDevcontainerOptions 封装 API 获取 DevContainer 数据,即 router 层 GET /api/devcontainer 向下传递的数据结构 // AbstractOpenDevcontainerOptions 封装 API 获取 DevContainer 数据,即 router 层 GET /api/devcontainer 向下传递的数据结构
@@ -105,7 +107,8 @@ type AbstractDeleteDevcontainerOptions struct {
type CreateDevcontainerDTO struct { type CreateDevcontainerDTO struct {
devcontainer_model.Devcontainer devcontainer_model.Devcontainer
SSHPublicKeyList []string SSHPublicKeyList []string
GitRepositoryURL string GitRepositoryURL string
Image string Image string
DockerfileContent string
} }

repo.diff.view_file

@@ -1,10 +1,11 @@
package devcontainer package devcontainer
import ( import (
"archive/tar"
"bufio" "bufio"
"bytes"
"context" "context"
"fmt" "fmt"
"net/url"
"os/exec" "os/exec"
"regexp" "regexp"
"strconv" "strconv"
@@ -15,15 +16,17 @@ import (
devcontainer_models "code.gitea.io/gitea/models/devcontainer" devcontainer_models "code.gitea.io/gitea/models/devcontainer"
docker_module "code.gitea.io/gitea/modules/docker" docker_module "code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
gitea_web_context "code.gitea.io/gitea/services/context" gitea_web_context "code.gitea.io/gitea/services/context"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
) )
func CreateDevcontainer(ctx *context.Context, newDevContainer *CreateDevcontainerDTO, devContainerJSON *DevStarJSON) error { func CreateDevcontainer(ctx *context.Context, newDevContainer *CreateDevcontainerDTO, devContainerJSON *DevStarJSON, initializeScript string, restartScript string) error {
log.Info("开始创建容器.....") log.Info("开始创建容器.....")
// 1. 创建docker client // 1. 创建docker client
cli, err := docker_module.CreateDockerClient(ctx) cli, err := docker_module.CreateDockerClient(ctx)
if err != nil { if err != nil {
@@ -31,40 +34,6 @@ func CreateDevcontainer(ctx *context.Context, newDevContainer *CreateDevcontaine
} }
defer cli.Close() defer cli.Close()
// 将公钥数组转换为字符串
keysString := strings.Join(newDevContainer.SSHPublicKeyList, "\n")
// 解析仓库 URL
parsedURL, err := url.Parse(newDevContainer.GitRepositoryURL)
if err != nil {
log.Info("解析仓库URL失败: %v", err)
return err
}
// 获取ip和端口
hostParts := strings.Split(parsedURL.Host, ":")
port := ""
if len(hostParts) > 1 {
port = hostParts[1]
}
newHost := "host.docker.internal"
if port != "" {
newHost += ":" + port
}
parsedURL.Host = newHost
// 生成git仓库的 URL
newURL := parsedURL.String()
cmd := []string{
"apt-get update -y; ",
"apt-get install ssh -y;",
"echo \"PubkeyAuthentication yes\nPermitRootLogin yes\n\" | tee -a /etc/ssh/sshd_config",
"rm -f /etc/ssh/ssh_host_*; ssh-keygen -A ; service ssh restart",
"if [ -z \"${host_docker_internal+x}\" ];then echo \"" + setting.Devcontainer.Host + " host.docker.internal\" | tee -a /etc/hosts; fi; ",
"if [ ! -d '/data/workspace' ]; then git clone " + newURL + " /data/workspace && echo \"Git Repository cloned.\"; else echo \"Folder already exists.\"; fi; ",
"mkdir -p ~/test; mkdir -p ~/.ssh ; chmod 700 ~/.ssh; echo \"" + keysString + "\" > ~/.ssh/authorized_keys ; chmod 600 ~/.ssh/authorized_keys; ",
"git clone " + "https://devstar.cn/init/ttyd.git" + " /usr/bin/ttyd;",
"apt-get install -y build-essential cmake git libjson-c-dev libwebsockets-dev;",
"/usr/bin/ttyd/ttyd/ttyd -W -w " + newDevContainer.DevcontainerWorkDir + " bash & ",
}
// 加入22 7681集合 // 加入22 7681集合
natPort22 := nat.Port("22/tcp") natPort22 := nat.Port("22/tcp")
natPort7681 := nat.Port("7681/tcp") natPort7681 := nat.Port("7681/tcp")
@@ -72,21 +41,23 @@ func CreateDevcontainer(ctx *context.Context, newDevContainer *CreateDevcontaine
devContainerJSON.ForwardPorts[natPort7681] = struct{}{} devContainerJSON.ForwardPorts[natPort7681] = struct{}{}
// 2. 创建容器 // 2. 创建容器
opts := &docker_module.CreateDevcontainerOptions{ opts := &docker_module.CreateDevcontainerOptions{
Name: newDevContainer.Name, DockerfileContent: newDevContainer.DockerfileContent,
Image: newDevContainer.Image, Name: newDevContainer.Name,
Image: newDevContainer.Image,
CommandList: []string{ CommandList: []string{
"sh", "sh",
"-c", "-c",
strings.Join(devContainerJSON.InitializeCommand, "") + "tail -f /dev/null;", strings.Join(devContainerJSON.InitializeCommand, "") + "tail -f /dev/null;",
}, },
RepoId: newDevContainer.RepoId, RepoId: newDevContainer.RepoId,
UserId: newDevContainer.UserId, UserId: newDevContainer.UserId,
SSHPublicKeyList: newDevContainer.SSHPublicKeyList, SSHPublicKeyList: newDevContainer.SSHPublicKeyList,
GitRepositoryURL: newDevContainer.GitRepositoryURL, GitRepositoryURL: newDevContainer.GitRepositoryURL,
ContainerEnv: devContainerJSON.ContainerEnv, ContainerEnv: devContainerJSON.ContainerEnv,
PostCreateCommand: append(cmd, devContainerJSON.PostCreateCommand...), PostCreateCommand: append([]string{"/home/devcontainer_init.sh"}, devContainerJSON.PostCreateCommand...),
ForwardPorts: devContainerJSON.ForwardPorts, ForwardPorts: devContainerJSON.ForwardPorts,
SystemCommandCount: 8, InitializeCommand: initializeScript,
RestartCommand: restartScript,
} }
var flag string var flag string
for _, content := range devContainerJSON.RunArgs { for _, content := range devContainerJSON.RunArgs {
@@ -132,7 +103,6 @@ func CreateDevcontainer(ctx *context.Context, newDevContainer *CreateDevcontaine
if err != nil { if err != nil {
return fmt.Errorf("创建容器失败:%v", err) return fmt.Errorf("创建容器失败:%v", err)
} }
return nil return nil
} }
@@ -198,21 +168,58 @@ func SaveDevcontainer(ctx *gitea_web_context.Context, opts *UpdateDevcontainerOp
// 创建docker client // 创建docker client
reqctx := ctx.Req.Context() reqctx := ctx.Req.Context()
cli, err := docker_module.CreateDockerClient(&reqctx) cli, err := docker_module.CreateDockerClient(&reqctx)
imageRef := opts.RepositoryAddress + "/" + opts.RepositoryUsername + "/" + opts.ImageName
if err != nil { if err != nil {
return fmt.Errorf("创建docker client失败 %v", err) return fmt.Errorf("创建docker client失败 %v", err)
} }
defer cli.Close() defer cli.Close()
// 获取容器ID if opts.SaveMethod == "Container" {
containerID, err := docker_module.GetContainerID(cli, opts.DevContainerName) // 获取容器ID
if err != nil { containerID, err := docker_module.GetContainerID(cli, opts.DevContainerName)
return fmt.Errorf("获取容器ID失败 %v", err) if err != nil {
} return fmt.Errorf("获取容器ID失败 %v", err)
// 提交容器 }
imageRef := opts.RepositoryAddress + "/" + opts.RepositoryUsername + "/" + opts.ImageName // 提交容器
_, err = cli.ContainerCommit(ctx, containerID, types.ContainerCommitOptions{Reference: imageRef}) _, err = cli.ContainerCommit(ctx, containerID, types.ContainerCommitOptions{Reference: imageRef})
if err != nil { if err != nil {
return fmt.Errorf("提交容器失败 %v", err) return fmt.Errorf("提交容器失败 %v", err)
}
} else if opts.SaveMethod == "DockerFile" {
// 创建构建上下文包含Dockerfile的tar包
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加Dockerfile到tar包
dockerfile := "Dockerfile"
dockerfileContent, err := GetDockerfileContent(ctx, opts.Repository)
content := []byte(dockerfileContent)
header := &tar.Header{
Name: dockerfile,
Size: int64(len(content)),
Mode: 0644,
}
if err := tw.WriteHeader(header); err != nil {
panic(err)
}
if _, err := tw.Write(content); err != nil {
panic(err)
}
buildOptions := types.ImageBuildOptions{
Tags: []string{imageRef}, // 镜像标签
}
_, err = cli.ImageBuild(
context.Background(),
&buf,
buildOptions,
)
if err != nil {
log.Info(err.Error())
return err
}
} }
// 推送到仓库 // 推送到仓库
dockerHost, err := docker_module.GetDockerSocketPath() dockerHost, err := docker_module.GetDockerSocketPath()
if err != nil { if err != nil {
@@ -228,105 +235,144 @@ func SaveDevcontainer(ctx *gitea_web_context.Context, opts *UpdateDevcontainerOp
return UpdateDevcontainerJSON(ctx, newJSONStr) return UpdateDevcontainerJSON(ctx, newJSONStr)
} }
// pullImage 用于拉取指定的 Docker 镜像
func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, dockerHost string, opts *docker_module.CreateDevcontainerOptions) error { func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, dockerHost string, opts *docker_module.CreateDevcontainerOptions) error {
var stdoutScanner, stderrScanner *bufio.Scanner
// 创建扫描器来读取输出
script := "docker " + "-H " + dockerHost + " pull " + opts.Image if opts.DockerfileContent != "" {
cmd := exec.Command("sh", "-c", script)
// 获取标准输出和标准错误输出的管道
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
err = cmd.Start()
if err != nil {
return err
} else {
// 创建扫描器来读取输出 // 创建构建上下文包含Dockerfile的tar包
stdoutScanner := bufio.NewScanner(stdout) var buf bytes.Buffer
stderrScanner := bufio.NewScanner(stderr) tw := tar.NewWriter(&buf)
dbEngine := db.GetEngine(*ctx) defer tw.Close()
var pullImageOutput = devcontainer_models.DevcontainerOutput{ // 添加Dockerfile到tar包
Output: "", dockerfile := "Dockerfile"
ListId: 0, content := []byte(opts.DockerfileContent)
Status: "running", header := &tar.Header{
UserId: opts.UserId, Name: dockerfile,
RepoId: opts.RepoId, Size: int64(len(content)),
Command: "Pull Image", Mode: 0644,
} }
if _, err := dbEngine.Table("devcontainer_output").Insert(&pullImageOutput); err != nil { if err := tw.WriteHeader(header); err != nil {
log.Info("Failed to insert record: %v", err) panic(err)
return err
} }
_, err := dbEngine.Table("devcontainer_output"). if _, err := tw.Write(content); err != nil {
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId). panic(err)
Get(&pullImageOutput) }
// 执行镜像构建
opts.Image = fmt.Sprintf("%d", opts.UserId) + "-" + fmt.Sprintf("%d", opts.RepoId) + "-dockerfileimage"
buildOptions := types.ImageBuildOptions{
Tags: []string{opts.Image}, // 镜像标签
}
buildResponse, err := cli.ImageBuild(
context.Background(),
&buf,
buildOptions,
)
if err != nil { if err != nil {
log.Info("err %v", err) log.Info(err.Error())
return err return err
} }
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{ stdoutScanner = bufio.NewScanner(buildResponse.Body)
} else {
script := "docker " + "-H " + dockerHost + " pull " + opts.Image
cmd := exec.Command("sh", "-c", script)
// 获取标准输出和标准错误输出的管道
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
err = cmd.Start()
if err != nil {
return err
}
stdoutScanner = bufio.NewScanner(stdout)
stderrScanner = bufio.NewScanner(stderr)
}
dbEngine := db.GetEngine(*ctx)
var pullImageOutput = devcontainer_models.DevcontainerOutput{
Output: "",
ListId: 0,
Status: "running",
UserId: opts.UserId,
RepoId: opts.RepoId,
Command: "Pull Image",
}
if _, err := dbEngine.Table("devcontainer_output").Insert(&pullImageOutput); err != nil {
log.Info("Failed to insert record: %v", err)
return err
}
_, err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Get(&pullImageOutput)
if err != nil {
log.Info("err %v", err)
return err
}
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
Status: "running",
UserId: opts.UserId,
RepoId: opts.RepoId,
Command: "Initialize Workspace",
ListId: 1,
}); err != nil {
log.Info("Failed to insert record: %v", err)
return err
}
_, err = dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
Status: "running",
UserId: opts.UserId,
RepoId: opts.RepoId,
Command: "Initialize DevStar",
ListId: 2,
})
if err != nil {
log.Info("Failed to insert record: %v", err)
return err
}
if len(opts.PostCreateCommand) > 1 {
_, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "", Output: "",
Status: "running", Status: "running",
UserId: opts.UserId, UserId: opts.UserId,
RepoId: opts.RepoId, RepoId: opts.RepoId,
Command: "Initialize Workspace", Command: "Run postCreateCommand",
ListId: 1, ListId: 3,
}); err != nil {
log.Info("Failed to insert record: %v", err)
return err
}
_, err = dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
Status: "running",
UserId: opts.UserId,
RepoId: opts.RepoId,
Command: "Initialize DevStar",
ListId: 2,
}) })
if err != nil { if err != nil {
log.Info("Failed to insert record: %v", err) log.Info("Failed to insert record: %v", err)
return err return err
} }
}
if len(opts.PostCreateCommand) >= opts.SystemCommandCount { // 使用 goroutine 来读取标准输出
_, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{ go func() {
Output: "", var output string
Status: "running", var cur int = 0
UserId: opts.UserId, for stdoutScanner.Scan() {
RepoId: opts.RepoId, output += "\n" + stdoutScanner.Text()
Command: "Run postCreateCommand", cur++
ListId: 3, if cur%10 == 0 {
}) _, err = dbEngine.Table("devcontainer_output").
if err != nil { Where("id = ?", pullImageOutput.Id).
log.Info("Failed to insert record: %v", err) Update(&devcontainer_models.DevcontainerOutput{
return err Output: output})
} if err != nil {
} log.Info("err %v", err)
// 使用 goroutine 来读取标准输出
go func() {
var output string
var cur int = 0
for stdoutScanner.Scan() {
output += "\n" + stdoutScanner.Text()
cur++
if cur%10 == 0 {
_, err = dbEngine.Table("devcontainer_output").
Where("id = ?", pullImageOutput.Id).
Update(&devcontainer_models.DevcontainerOutput{
Output: output})
if err != nil {
log.Info("err %v", err)
}
} }
} }
}
if stderrScanner != nil {
for stderrScanner.Scan() { for stderrScanner.Scan() {
output += "\n" + stderrScanner.Text() output += "\n" + stderrScanner.Text()
cur++ cur++
@@ -341,82 +387,48 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d
} }
} }
} }
_, err = dbEngine.Table("devcontainer_output"). }
Where("id = ?", pullImageOutput.Id). _, err = dbEngine.Table("devcontainer_output").
Update(&devcontainer_models.DevcontainerOutput{ Where("id = ?", pullImageOutput.Id).
Output: output}) Update(&devcontainer_models.DevcontainerOutput{
if err != nil { Output: output})
log.Info("err %v", err) if err != nil {
log.Info("err %v", err)
} }
dbEngine.Table("devcontainer_output"). dbEngine.Table("devcontainer_output").
Where("id = ?", pullImageOutput.Id). Where("id = ?", pullImageOutput.Id).
Update(&devcontainer_models.DevcontainerOutput{Status: "success"}) Update(&devcontainer_models.DevcontainerOutput{Status: "success"})
}()
// 等待命令执行完毕 // 创建并启动容器
go func() { output, err := docker_module.CreateAndStartContainer(cli, opts)
err := cmd.Wait() if err != nil {
log.Info("创建或启动容器失败: %v", err)
}
containerID, err := docker_module.GetContainerID(cli, opts.Name)
if err != nil {
log.Info("获取容器ID:%v", err)
}
portInfo, err := docker_module.GetAllMappedPort(cli, containerID)
if err != nil {
log.Info("创建或启动容器失败:%v", err)
}
// 存储到数据库
if _, err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, 1).
Update(&devcontainer_models.DevcontainerOutput{
Output: output + portInfo,
Status: "success",
}); err != nil {
log.Info("Error storing output for command %v: %v\n", opts.CommandList[2], err)
}
// 创建 exec 实例
if err != nil { var buffer string = ""
log.Info("fail to pull image") var state int = 2
} else { for index, cmd := range opts.PostCreateCommand {
// 创建并启动容器
output, err := docker_module.CreateAndStartContainer(cli, opts)
if err != nil {
log.Info("创建或启动容器失败: %v", err)
}
containerID, err := docker_module.GetContainerID(cli, opts.Name)
if err != nil {
log.Info("获取容器ID:%v", err)
}
portInfo, err := docker_module.GetAllMappedPort(cli, containerID)
if err != nil {
log.Info("创建或启动容器失败:%v", err)
}
// 存储到数据库
if _, err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, 1).
Update(&devcontainer_models.DevcontainerOutput{
Output: output + portInfo,
Status: "success",
}); err != nil {
log.Info("Error storing output for command %v: %v\n", opts.CommandList[2], err)
}
// 创建 exec 实例
var buffer string = "" if index == 1 {
var state int = 2
for index, cmd := range opts.PostCreateCommand {
if index == opts.SystemCommandCount {
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Status: "success",
})
if err != nil {
log.Info("Error storing output for command %v: %v\n", cmd, err)
}
buffer = ""
state = 3
}
output, err = docker_module.ExecCommandInContainer(ctx, cli, containerID, cmd)
buffer += output
if err != nil {
log.Info("执行命令失败:%v", err)
}
_, err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Output: buffer,
})
if err != nil {
log.Info("Error storing output for command %v: %v\n", cmd, err)
}
}
_, err = dbEngine.Table("devcontainer_output"). _, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state). Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{ Update(&devcontainer_models.DevcontainerOutput{
@@ -425,10 +437,163 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d
if err != nil { if err != nil {
log.Info("Error storing output for command %v: %v\n", cmd, err) log.Info("Error storing output for command %v: %v\n", cmd, err)
} }
buffer = ""
state = 3
continue
} }
}()
} output, err = docker_module.ExecCommandInContainer(ctx, cli, containerID, cmd)
buffer += output
if err != nil {
log.Info("执行命令失败:%v", err)
}
_, err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Output: buffer,
})
if err != nil {
log.Info("Error storing output for command %v: %v\n", cmd, err)
}
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Status: "success",
})
if err != nil {
log.Info("Error storing output for command pull image: %v\n", err)
}
}()
return nil return nil
} }
func DockerRestartContainer(gitea_ctx *gitea_web_context.Context, opts *RepoDevContainer) error {
// 创建docker client
ctx := context.Background()
cli, err := docker_module.CreateDockerClient(&ctx)
if err != nil {
return fmt.Errorf("创建docker client失败 %v", err)
}
defer cli.Close()
// 获取容器ID
containerID, err := docker_module.GetContainerID(cli, opts.DevContainerName)
if err != nil {
return fmt.Errorf("获取容器ID失败 %v", err)
}
timeout := 10 // 超时时间(秒)
err = cli.ContainerRestart(context.Background(), containerID, container.StopOptions{
Timeout: &timeout,
})
if err != nil {
return fmt.Errorf("重启容器失败: %s\n", err)
} else {
log.Info("容器已重启")
}
devContainerJson, err := GetDevcontainerJsonModel(*gitea_ctx, gitea_ctx.Repo.Repository)
if err != nil {
return err
}
cmd := []string{"/home/devcontainer_restart.sh"}
postCreateCommand := append(cmd, devContainerJson.PostCreateCommand...)
// 创建 exec 实例
dbEngine := db.GetEngine(ctx)
var buffer string = ""
var state int = 2
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Status: "running",
})
if err != nil {
return err
}
if len(devContainerJson.PostCreateCommand) > 1 {
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state+1).
Update(&devcontainer_models.DevcontainerOutput{
Status: "running",
})
if err != nil {
return err
}
}
for index, cmd := range postCreateCommand {
if index == 1 {
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Status: "success",
})
if err != nil {
log.Info("Error storing output for command %v: %v\n", cmd, err)
}
buffer = ""
state = 3
continue
}
output, err := docker_module.ExecCommandInContainer(&ctx, cli, containerID, cmd)
buffer += output
if err != nil {
log.Info("执行命令失败:%v", err)
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Output: buffer,
})
if err != nil {
log.Info("Error storing output for command %v: %v\n", cmd, err)
return err
}
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, 2).
Update(&devcontainer_models.DevcontainerOutput{
Status: "success",
})
if err != nil {
return err
}
if len(devContainerJson.PostCreateCommand) > 1 {
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, 3).
Update(&devcontainer_models.DevcontainerOutput{
Status: "success",
})
if err != nil {
log.Info("Error storing output for command %v: %v\n", cmd, err)
return err
}
}
return nil
}
func DockerStopContainer(ctx *context.Context, opts *RepoDevContainer) error {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return fmt.Errorf("创建docker client失败 %v", err)
}
defer cli.Close()
// 获取容器ID
containerID, err := docker_module.GetContainerID(cli, opts.DevContainerName)
if err != nil {
return fmt.Errorf("获取容器ID失败 %v", err)
}
timeout := 10 // 超时时间(秒)
err = cli.ContainerStop(context.Background(), containerID, container.StopOptions{
Timeout: &timeout,
})
if err != nil {
return fmt.Errorf("停止容器失败: %s\n", err)
} else {
log.Info("容器已停止")
return nil
}
}

repo.diff.view_file

@@ -50,10 +50,16 @@
{{if .HasDevContainer}} {{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" 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="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;" onclick="window.location.href = '{{.VSCodeUrl}}'">{{svg "octicon-code" 14}}open with VSCode</a ></div> {{if .InitializedContainer}}
<div class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.CursorUrl}}'">{{svg "octicon-code" 14}}open with Cursor</a ></div> <div class="item"><button id="restartButton" class="flex-text-inline" style="color:black;" onclick="handleClick(event, '{{.Repository.Link}}/dev-container/restart', document.getElementById('stopButton'))" >{{svg "octicon-terminal" 14 "tw-mr-2"}} Restart Dev Container</button></div>
<div class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.WindsurfUrl}}'">{{svg "octicon-code" 14}}open with Windsurf</a ></div> <div class="item"><button id="stopButton" class="flex-text-inline" style="color:black;" onclick="handleClick(event, '{{.Repository.Link}}/dev-container/stop', document.getElementById('restartButton'))" >{{svg "octicon-terminal" 14 "tw-mr-2"}} Stop Dev Container</button></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;" onclick="window.location.href = '{{.VSCodeUrl}}'">{{svg "octicon-code" 14}}open with VSCode</a ></div>
<div class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.CursorUrl}}'">{{svg "octicon-code" 14}}open with Cursor</a ></div>
<div class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.WindsurfUrl}}'">{{svg "octicon-code" 14}}open with Windsurf</a ></div>
{{end}}
{{else if .HasValidDevContainerJSON}} {{else if .HasValidDevContainerJSON}}
<div class="item"> <div class="item">
{{if not .isCreatingDevcontainer}} {{if not .isCreatingDevcontainer}}
@@ -111,6 +117,7 @@
RepositoryAddress: formData.get('RepositoryAddress'), RepositoryAddress: formData.get('RepositoryAddress'),
RepositoryUsername: formData.get('RepositoryUsername'), RepositoryUsername: formData.get('RepositoryUsername'),
RepositoryPassword: formData.get('RepositoryPassword'), RepositoryPassword: formData.get('RepositoryPassword'),
SaveMethod: formData.get('SaveMethod'),
ImageName: formData.get('ImageName'), ImageName: formData.get('ImageName'),
}) })
@@ -130,6 +137,18 @@
</script> </script>
<div class="content"> <div class="content">
<form class="ui form tw-max-w-2xl tw-m-auto" id="updateForm" onsubmit="submitForm(event)"> <form class="ui form tw-max-w-2xl tw-m-auto" id="updateForm" onsubmit="submitForm(event)">
<div class="inline field">
<label>SaveMethod</label>
<div class="ui selection owner dropdown">
<input type="hidden" id="SaveMethod" name="SaveMethod" value="{{.SaveMethod}}">
<div class="default text">Container</div>
<div class="menu">
{{range .SaveMethods}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
</div>
<div class="required field "> <div class="required field ">
<label for="RepositoryAddress">Registry:</label> <label for="RepositoryAddress">Registry:</label>
<input style="border: 1px solid black;" type="text" id="RepositoryAddress" name="RepositoryAddress" value="{{.RepositoryAddress}}"> <input style="border: 1px solid black;" type="text" id="RepositoryAddress" name="RepositoryAddress" value="{{.RepositoryAddress}}">
@@ -157,5 +176,43 @@
</div> </div>
<script>
function handleClick(event, targetLink, other) {
event.preventDefault();
const link = event.target;
link.disabled = true; // 禁用链接
link.style.cursor = 'auto';
link.style.color = 'gray';
if(other){
other.disabled = true;
other.style.cursor = 'auto';
other.style.color = 'gray';
}
console.log(targetLink);
// 发送网络请求
fetch(targetLink)
.then(response => response.json())
.then(data => {
//console.log('响应数据:', data);
// 处理响应数据
})
.catch(error => {
//console.error('请求错误:', error);
})
.finally(() => {
// 无论请求成功还是失败,都重新启用链接
link.style.color = 'black';
if(other){
other.disabled = false;
other.style.color = 'black';
other.style.cursor = 'pointer';
}
link.disabled = false;
link.style.cursor = 'pointer';
location.reload();
});
}
</script>
{{template "base/footer" .}} {{template "base/footer" .}}

repo.diff.view_file

@@ -89,16 +89,23 @@
if(this.currentDevcontainer.detail == "running" && job.currentDevcontainer.detail == "created"){ if(this.currentDevcontainer.detail == "running" && job.currentDevcontainer.detail == "created"){
location.reload(); location.reload();
} }
if((this.currentDevcontainer.detail == "created" || this.currentDevcontainer.detail == "running") && job.currentDevcontainer.detail == "success"){
location.reload();
}
// save the state to Vue data, then the UI will be updated // save the state to Vue data, then the UI will be updated
this.currentDevcontainer = job.currentDevcontainer; this.currentDevcontainer = job.currentDevcontainer;
// sync the currentJobStepsStates to store the job step states // sync the currentJobStepsStates to store the job step states
for (let i = 0; i < this.currentDevcontainer.steps.length; i++) { if (this.currentDevcontainer.steps != null){
if (!this.currentJobStepsStates[i]) { for (let i = 0; i < this.currentDevcontainer.steps.length; i++) {
// initial states for job steps if (!this.currentJobStepsStates[i]) {
this.currentJobStepsStates[i] = {expanded: false}; // initial states for job steps
this.currentJobStepsStates[i] = {expanded: false};
}
} }
} }
if (this.isDone(this.currentDevcontainer.detail) && this.intervalID) { if (this.isDone(this.currentDevcontainer.detail) && this.intervalID) {
clearInterval(this.intervalID); clearInterval(this.intervalID);
this.intervalID = null; this.intervalID = null;
@@ -149,7 +156,7 @@
</p> </p>
</div> </div>
</div> </div>
<div class="job-step-container" ref="steps" v-if="currentDevcontainer.steps.length"> <div class="job-step-container" ref="steps" v-if="currentDevcontainer.steps && currentDevcontainer.steps.length">
<div class="job-step-section" v-for="(jobStep, i) in currentDevcontainer.steps" :key="i"> <div class="job-step-section" v-for="(jobStep, i) in currentDevcontainer.steps" :key="i">
<div class="job-step-summary" @click.stop="isExpandable(jobStep.status) && toggleStepLogs(i)" :class="[currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']"> <div class="job-step-summary" @click.stop="isExpandable(jobStep.status) && toggleStepLogs(i)" :class="[currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']">
<!-- If the job is done and the job step log is loaded for the first time, show the loading icon <!-- If the job is done and the job step log is loaded for the first time, show the loading icon