From 02baa3b7af15a4910f303f128e1a1c31d003372a Mon Sep 17 00:00:00 2001 From: xinitx Date: Wed, 7 May 2025 11:10:30 +0000 Subject: [PATCH] =?UTF-8?q?!67=20=E5=A2=9E=E5=8A=A0=E4=BA=86=E9=87=8D?= =?UTF-8?q?=E5=90=AF=E5=81=9C=E6=AD=A2=E5=AE=B9=E5=99=A8=E3=80=81dockerfil?= =?UTF-8?q?e=E6=96=B9=E5=BC=8F=E5=88=9B=E5=BB=BA=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E5=8A=9F=E8=83=BD=20*=20change=20initializeS?= =?UTF-8?q?cript=20path=20*=20Merge=20branch=20'add-dockerfile-method-and-?= =?UTF-8?q?start-stop-container'=20of=20https=E2=80=A6=20*=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E4=BA=86=E5=AE=B9=E5=99=A8=E9=95=9C=E5=83=8F=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E7=9A=84=E6=9E=84=E5=BB=BA=E3=80=81=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=92=8C=E4=BD=BF=E7=94=A8=E6=96=B9=E6=B3=95=EF=BC=8C=E4=BD=86?= =?UTF-8?q?=E6=98=AFdevcontainer=E5=8A=9F=E8=83=BD=E8=BF=98=E6=9C=89?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20*=20fix=20run=20postCreateCommand=20bug=20?= =?UTF-8?q?*=20sh=E6=96=87=E4=BB=B6=E6=96=B9=E5=BC=8F=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E8=84=9A=E6=9C=AC=20*=20add=20restart=20comm?= =?UTF-8?q?and=20and=20fix=20bug=20*=20add=20dockerfile=20method=20to=20cr?= =?UTF-8?q?eate=20container=20and=20save=20container=20.restart=20?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile.rootless | 2 + README.md | 28 +- devcontainer_init.sh | 74 +++ devcontainer_restart.sh | 5 + modules/docker/docker_api.go | 41 +- modules/docker/docker_types.go | 34 +- public/assets/install.sh | 40 +- routers/web/devcontainer/devcontainer.go | 43 +- routers/web/web.go | 3 + services/auth/wechat_qr.go | 4 +- services/devcontainer/devcontainer.go | 127 +++- services/devcontainer/devcontainer_json.go | 48 +- services/devcontainer/devcontainer_type.go | 9 +- services/devcontainer/docker_agent.go | 587 +++++++++++------- templates/repo/devcontainer/details.tmpl | 67 +- .../js/components/RepoDevcontainerView.vue | 17 +- 16 files changed, 844 insertions(+), 285 deletions(-) create mode 100644 devcontainer_init.sh create mode 100644 devcontainer_restart.sh diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 74d7ce30f6..3714a59518 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -75,6 +75,8 @@ RUN mkdir -p /var/lib/gitea /etc/gitea RUN chown git:git /var/lib/gitea /etc/gitea 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/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 diff --git a/README.md b/README.md index 4df2b70c90..7ac05d436d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ git checkout -b YOUR_BRANCH code devstar # 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 TAGS="bindata timetzdata sqlite sqlite_unlock_notify" make build # 生成可执行文件 ./gitea @@ -60,23 +60,25 @@ git add FILES git commit -m "commit log" 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 -sudo docker pull devstar.cn/devstar/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` 完成安装。 +make docker +public/assets/install.sh start --image=devstar-studio:latest -# 查看devstar-studio容器的启动日志 -sudo docker logs devstar-studio +# 查看日志 +public/assets/install.sh logs # 停止并删除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 ``` ## 提示 diff --git a/devcontainer_init.sh b/devcontainer_init.sh new file mode 100644 index 0000000000..78a1da760d --- /dev/null +++ b/devcontainer_init.sh @@ -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 & \ No newline at end of file diff --git a/devcontainer_restart.sh b/devcontainer_restart.sh new file mode 100644 index 0000000000..3974f7b2aa --- /dev/null +++ b/devcontainer_restart.sh @@ -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 & \ No newline at end of file diff --git a/modules/docker/docker_api.go b/modules/docker/docker_api.go index 83006e5239..2535b4dea1 100644 --- a/modules/docker/docker_api.go +++ b/modules/docker/docker_api.go @@ -1,6 +1,7 @@ package docker import ( + "archive/tar" "bytes" "context" "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 { script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " " cmd := exec.Command("sh", "-c", script) - output, err := cmd.CombinedOutput() - log.Info(string(output)) + _, err := cmd.CombinedOutput() if err != nil { return err } // 推送到仓库 script = "docker " + "-H " + dockerHost + " push " + imageRef cmd = exec.Command("sh", "-c", script) - output, err = cmd.CombinedOutput() - log.Info(string(output)) + _, err = cmd.CombinedOutput() + if err != nil { return err } @@ -248,7 +248,6 @@ func CreateAndStartContainer(cli *client.Client, opts *CreateDevcontainerOptions }, PortBindings: opts.PortBindings, } - log.Info("%v", opts.PortBindings) if len(opts.Binds) > 0 { hostConfig.Binds = opts.Binds } @@ -264,6 +263,22 @@ func CreateAndStartContainer(cli *client.Client, opts *CreateDevcontainerOptions log.Info("fail to start container %v", 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{ ShowStdout: true, @@ -276,3 +291,19 @@ func CreateAndStartContainer(cli *client.Client, opts *CreateDevcontainerOptions _, _ = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, out) 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 +} diff --git a/modules/docker/docker_types.go b/modules/docker/docker_types.go index ed1498a1c9..39c547ad5e 100644 --- a/modules/docker/docker_types.go +++ b/modules/docker/docker_types.go @@ -6,20 +6,22 @@ import ( // CreateDevcontainerOptions 定义创建开发容器选项 type CreateDevcontainerOptions struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Image string `json:"image"` - CommandList []string `json:"command"` - ContainerPort uint16 `json:"containerPort"` - ServicePort uint16 `json:"servicePort"` - SSHPublicKeyList []string `json:"sshPublicKeyList"` - GitRepositoryURL string `json:"gitRepositoryURL"` - RepoId int64 - UserId int64 - ForwardPorts nat.PortSet - ContainerEnv []string - PostCreateCommand []string - Binds []string - SystemCommandCount int - PortBindings nat.PortMap + DockerfileContent string + Name string `json:"name"` + Namespace string `json:"namespace"` + Image string `json:"image"` + CommandList []string `json:"command"` + ContainerPort uint16 `json:"containerPort"` + ServicePort uint16 `json:"servicePort"` + SSHPublicKeyList []string `json:"sshPublicKeyList"` + GitRepositoryURL string `json:"gitRepositoryURL"` + RepoId int64 + UserId int64 + ForwardPorts nat.PortSet + ContainerEnv []string + PostCreateCommand []string + Binds []string + PortBindings nat.PortMap + InitializeCommand string + RestartCommand string } diff --git a/public/assets/install.sh b/public/assets/install.sh index 05e7148c8c..c5e91ea80b 100755 --- a/public/assets/install.sh +++ b/public/assets/install.sh @@ -8,7 +8,8 @@ IMAGE_NAME=devstar-studio VERSION=latest # DevStar Studio的默认版本为最新版本 PORT=8080 # 设置端口默认值为 8080 SSH_PORT=2222 # 设置ssh默认端口号2222 -DATA_DIR=~/devstar_data +DATA_DIR=${HOME}/devstar_data +APP_INI=${DATA_DIR}/app.ini # 错误处理函数 error_handler() { @@ -89,17 +90,35 @@ function install { # Function to start function start { - install + if [[ -z "$IMAGE_STR" ]]; then + install + fi # 创建devstar_data目录用于持久化存储DevStar相关的配置和用户数据 mkdir -p $DATA_DIR sudo chown 1000:1000 $DATA_DIR 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容器 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` 完成安装。 success "-------------------------------------------------------" - success "DevStar started in http://localhost:$PORT successfully!" + success "DevStar started in http://$DOMAIN_NAME:$PORT successfully!" success "-------------------------------------------------------" exit 0 } @@ -109,7 +128,9 @@ function stop { if [ $(docker ps -a --filter "name=^/${NAME}$" -q | wc -l) -gt 0 ]; then sudo docker stop $NAME && sudo docker rm -f $NAME 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 @@ -133,6 +154,7 @@ function usage { success " --port= Specify the port number (default port is 8080)" success " --ssh-port= Specify the ssh-port number (default ssh-port is 2222)" success " --version= Specify the DevStar Studio Image Version (default verson is latest)" + success " --image= Specify the DevStar Studio Image example: devstar-studio:latest " success " stop Stop the running DevStar Studio" 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." @@ -146,7 +168,7 @@ case "$1" in usage ;; start) - ARGS=$(getopt --long port::,ssh-port::,version:: -- "$@") + ARGS=$(getopt --long port::,ssh-port::,version::,image:: -- "$@") if [ $? -ne 0 ]; then failure "ARGS ERROR!" exit 1 @@ -166,7 +188,11 @@ case "$1" in --version) VERSION="$2" 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 break ;; diff --git a/routers/web/devcontainer/devcontainer.go b/routers/web/devcontainer/devcontainer.go index 34a1f900f3..596142a62e 100644 --- a/routers/web/devcontainer/devcontainer.go +++ b/routers/web/devcontainer/devcontainer.go @@ -35,6 +35,7 @@ func GetRepoDevContainerDetails(ctx *context.Context) { Where("user_id = ? AND repo_id = ?", ctx.Doer.ID, ctx.Repo.Repository.ID). Find(&devContainerOutput) ctx.Data["isCreatingDevcontainer"] = false + ctx.Data["InitializedContainer"] = true if err == nil && len(devContainerOutput) > 0 { ctx.Data["isCreatingDevcontainer"] = true } @@ -43,6 +44,9 @@ func GetRepoDevContainerDetails(ctx *context.Context) { if item.ListId > 0 && item.Status == "success" { created = true } + if item.Status != "success" { + ctx.Data["InitializedContainer"] = false + } } //ctx.Repo.RepoLink == ctx.Repo.Repository.Link() @@ -65,9 +69,10 @@ func GetRepoDevContainerDetails(ctx *context.Context) { if err == nil { imageName := devcontainerJson.Image registry, namespace, repo, tag := ParseImageName(imageName) + log.Info("%v %v", repo, tag) ctx.Data["RepositoryAddress"] = registry ctx.Data["RepositoryUsername"] = namespace - ctx.Data["ImageName"] = ctx.Repo.Repository.Name + "-" + repo + ":" + tag + ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest" } // 获取WebSSH服务端口 @@ -96,11 +101,13 @@ func GetRepoDevContainerDetails(ctx *context.Context) { ctx.Data["HasValidDevContainerJSON"] = isValidRepoDevcontainerJson ctx.Data["Repository"] = ctx.Repo.Repository 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["TreeNames"] = []string{".devcontainer", "devcontainer.json"} ctx.Data["TreePaths"] = []string{".devcontainer", ".devcontainer/devcontainer.json"} 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) } @@ -205,6 +212,7 @@ func UpdateRepoDevContainerForCurrentActor(ctx *context.Context) { DevContainerName: devContainerMetadata.DevContainerName, Actor: ctx.Doer, Repository: ctx.Repo.Repository, + SaveMethod: updateInfo.SaveMethod, } err = devcontainer_service.UpdateDevcontainerAPIService(ctx, opts) if err != nil { @@ -290,6 +298,35 @@ func GetContainerOutput(ctx *context.Context) { } ctx.JSON(http.StatusOK, resp) 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{}) } diff --git a/routers/web/web.go b/routers/web/web.go index 99565ddddd..50db645b80 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -523,6 +523,7 @@ func registerRoutes(m *web.Router) { // 列举某用户已创建的所有 DevContainer m.Get("/user", devcontainer_api.ListUserDevcontainers) + }) // ***** END: DevContainer ***** @@ -1344,6 +1345,8 @@ func registerRoutes(m *web.Router) { m.Get("/createConfiguration", devcontainer_web.CreateRepoDevContainerConfiguration) m.Get("/create", devcontainer_web.CreateRepoDevContainer, context.RepoMustNotBeArchived()) // 仓库状态非 Archived 才可以创建 DevContainer 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("/update", devcontainer_web.UpdateRepoDevContainerForCurrentActor) }, diff --git a/services/auth/wechat_qr.go b/services/auth/wechat_qr.go index 38603e98db..dd902b0a42 100644 --- a/services/auth/wechat_qr.go +++ b/services/auth/wechat_qr.go @@ -44,7 +44,7 @@ func GetWechatQRTicket(ctx *context.Context) (wechatQrTicket string, QRImageURL qrExpireSeconds := setting.Wechat.TempQrExpireSeconds // 构建请求的 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 请求 resp, err := http.Get(url) @@ -103,7 +103,7 @@ type Response struct { // 假设这是用于检查二维码状态的函数 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()) resp, err := http.Get(url) diff --git a/services/devcontainer/devcontainer.go b/services/devcontainer/devcontainer.go index 831971f473..fb7e49a49e 100644 --- a/services/devcontainer/devcontainer.go +++ b/services/devcontainer/devcontainer.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io/ioutil" + "net/url" "os" "path/filepath" "regexp" @@ -168,10 +169,24 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt 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{ Devcontainer: devcontainer_model.Devcontainer{ Name: getSanitizedDevcontainerName(username, repoName), - DevcontainerHost: setting.Devcontainer.Host, + DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(), DevcontainerUsername: "root", DevcontainerWorkDir: "/data/workspace", RepoId: opts.Repository.ID, @@ -179,8 +194,9 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt CreatedUnix: unixTimestamp, UpdatedUnix: unixTimestamp, }, - Image: devContainerJson.Image, - GitRepositoryURL: strings.TrimSuffix(setting.AppURL, "/") + opts.Repository.Link(), + DockerfileContent: dockerfileContent, + Image: devContainerJson.Image, + GitRepositoryURL: strings.TrimSuffix(setting.AppURL, "/") + opts.Repository.Link(), } // 在数据库事务中创建 Dev Container 分配资源,出错时自动回滚相对应数据库字段,保证数据一致 @@ -315,7 +331,11 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er if err != nil { 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: 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) { var access_token string defalut_ctx := context.Background() - log.Info("%v", devcontainer) // 获取端口号 cli, err := docker.CreateDockerClient(&defalut_ctx) 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 派遣创建任务 switch setting.Devcontainer.Agent { case setting.KUBERNETES, "k8s": // k8s Operator return AssignDevcontainerCreation2K8sOperator(ctx, newDevContainer) case setting.DOCKER: - return CreateDevcontainer(ctx, newDevContainer, devContainerJSON) + return CreateDevcontainer(ctx, newDevContainer, devContainerJSON, initializeScript, restartScript) default: // 未知 Agent,直接报错 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") + //默认处理 + } + +} diff --git a/services/devcontainer/devcontainer_json.go b/services/devcontainer/devcontainer_json.go index 27d75ff8a9..a4754b42f2 100644 --- a/services/devcontainer/devcontainer_json.go +++ b/services/devcontainer/devcontainer_json.go @@ -27,6 +27,7 @@ type DevStarJSON struct { ForwardPorts nat.PortSet ContainerEnv []string Image string + DockerfilePath string InitializeCommand []string PostCreateCommand []string RunArgs []string @@ -54,7 +55,7 @@ func CreateDevcontainerJSON(ctx *gitea_context.Context) { "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{ { Operation: "create", @@ -66,7 +67,6 @@ func CreateDevcontainerJSON(ctx *gitea_context.Context) { NewBranch: "main", Message: "add container configuration", }) - log.Info(resp.Commit.URL) if err != nil { log.Info("error ChangeRepoFiles:", err) 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) return nil, err } - log.Info("%v", devContainerJson) devStarJson := &DevStarJSON{ 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, c5...) devStarJson.RunArgs = devContainerJson.RunArgs - log.Info("%v", devStarJson) + if devContainerJson.Build != nil { + devStarJson.DockerfilePath = devContainerJson.Build.Dockerfile + } + return devStarJson, nil } - -func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (string, error) { +func GetFileContentByPath(ctx context.Context, repo *repo.Repository, path string) (string, error) { // 1. 获取默认分支名 branchName := repo.DefaultBranch if len(branchName) == 0 { @@ -161,7 +162,7 @@ func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (stri return "", err } - entry, err := commit.GetTreeEntryByPath(".devcontainer/devcontainer.json") + entry, err := commit.GetTreeEntryByPath(path) if err != nil { log.Info("Repo.Commit.GetTreeEntryByPath %v", err.Error()) return "", err @@ -169,13 +170,13 @@ func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (stri // No way to edit a directory online. if entry.IsDir() { - return "", fmt.Errorf(".devcontainer/devcontainer.json entry.IsDir") + return "", fmt.Errorf(path + " entry.IsDir") } blob := entry.Blob() 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() @@ -204,6 +205,35 @@ func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (stri } else { 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 文件中的注释 diff --git a/services/devcontainer/devcontainer_type.go b/services/devcontainer/devcontainer_type.go index f872fb2091..ace4709a22 100644 --- a/services/devcontainer/devcontainer_type.go +++ b/services/devcontainer/devcontainer_type.go @@ -62,6 +62,7 @@ type UpdateInfo struct { PassWord string `json:"RepositoryPassword"` RepositoryAddress string `json:"RepositoryAddress"` RepositoryUsername string `json:"RepositoryUsername"` + SaveMethod string `json:"SaveMethod"` } // CreateDevcontainerOptions 封装 API 创建 DevContainer 数据,即 router 层 POST /api/devcontainer 向下传递的数据结构 @@ -88,6 +89,7 @@ type UpdateDevcontainerOptions struct { DevContainerName string Actor *user_model.User Repository *repo_model.Repository + SaveMethod string } // AbstractOpenDevcontainerOptions 封装 API 获取 DevContainer 数据,即 router 层 GET /api/devcontainer 向下传递的数据结构 @@ -105,7 +107,8 @@ type AbstractDeleteDevcontainerOptions struct { type CreateDevcontainerDTO struct { devcontainer_model.Devcontainer - SSHPublicKeyList []string - GitRepositoryURL string - Image string + SSHPublicKeyList []string + GitRepositoryURL string + Image string + DockerfileContent string } diff --git a/services/devcontainer/docker_agent.go b/services/devcontainer/docker_agent.go index 0d4d9767c4..f7009e3bfa 100644 --- a/services/devcontainer/docker_agent.go +++ b/services/devcontainer/docker_agent.go @@ -1,10 +1,11 @@ package devcontainer import ( + "archive/tar" "bufio" + "bytes" "context" "fmt" - "net/url" "os/exec" "regexp" "strconv" @@ -15,15 +16,17 @@ import ( devcontainer_models "code.gitea.io/gitea/models/devcontainer" docker_module "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" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "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("开始创建容器.....") + // 1. 创建docker client cli, err := docker_module.CreateDockerClient(ctx) if err != nil { @@ -31,40 +34,6 @@ func CreateDevcontainer(ctx *context.Context, newDevContainer *CreateDevcontaine } 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集合 natPort22 := nat.Port("22/tcp") natPort7681 := nat.Port("7681/tcp") @@ -72,21 +41,23 @@ func CreateDevcontainer(ctx *context.Context, newDevContainer *CreateDevcontaine devContainerJSON.ForwardPorts[natPort7681] = struct{}{} // 2. 创建容器 opts := &docker_module.CreateDevcontainerOptions{ - Name: newDevContainer.Name, - Image: newDevContainer.Image, + DockerfileContent: newDevContainer.DockerfileContent, + Name: newDevContainer.Name, + Image: newDevContainer.Image, CommandList: []string{ "sh", "-c", strings.Join(devContainerJSON.InitializeCommand, "") + "tail -f /dev/null;", }, - RepoId: newDevContainer.RepoId, - UserId: newDevContainer.UserId, - SSHPublicKeyList: newDevContainer.SSHPublicKeyList, - GitRepositoryURL: newDevContainer.GitRepositoryURL, - ContainerEnv: devContainerJSON.ContainerEnv, - PostCreateCommand: append(cmd, devContainerJSON.PostCreateCommand...), - ForwardPorts: devContainerJSON.ForwardPorts, - SystemCommandCount: 8, + RepoId: newDevContainer.RepoId, + UserId: newDevContainer.UserId, + SSHPublicKeyList: newDevContainer.SSHPublicKeyList, + GitRepositoryURL: newDevContainer.GitRepositoryURL, + ContainerEnv: devContainerJSON.ContainerEnv, + PostCreateCommand: append([]string{"/home/devcontainer_init.sh"}, devContainerJSON.PostCreateCommand...), + ForwardPorts: devContainerJSON.ForwardPorts, + InitializeCommand: initializeScript, + RestartCommand: restartScript, } var flag string for _, content := range devContainerJSON.RunArgs { @@ -132,7 +103,6 @@ func CreateDevcontainer(ctx *context.Context, newDevContainer *CreateDevcontaine if err != nil { return fmt.Errorf("创建容器失败:%v", err) } - return nil } @@ -198,21 +168,58 @@ func SaveDevcontainer(ctx *gitea_web_context.Context, opts *UpdateDevcontainerOp // 创建docker client reqctx := ctx.Req.Context() cli, err := docker_module.CreateDockerClient(&reqctx) + imageRef := opts.RepositoryAddress + "/" + opts.RepositoryUsername + "/" + opts.ImageName 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) - } - // 提交容器 - imageRef := opts.RepositoryAddress + "/" + opts.RepositoryUsername + "/" + opts.ImageName - _, err = cli.ContainerCommit(ctx, containerID, types.ContainerCommitOptions{Reference: imageRef}) - if err != nil { - return fmt.Errorf("提交容器失败 %v", err) + if opts.SaveMethod == "Container" { + // 获取容器ID + containerID, err := docker_module.GetContainerID(cli, opts.DevContainerName) + if err != nil { + return fmt.Errorf("获取容器ID失败 %v", err) + } + // 提交容器 + _, err = cli.ContainerCommit(ctx, containerID, types.ContainerCommitOptions{Reference: imageRef}) + if err != nil { + 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() if err != nil { @@ -228,105 +235,144 @@ func SaveDevcontainer(ctx *gitea_web_context.Context, opts *UpdateDevcontainerOp return UpdateDevcontainerJSON(ctx, newJSONStr) } -// pullImage 用于拉取指定的 Docker 镜像 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 - 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 { + if opts.DockerfileContent != "" { - // 创建扫描器来读取输出 - 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", + // 创建构建上下文(包含Dockerfile的tar包) + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + defer tw.Close() + // 添加Dockerfile到tar包 + dockerfile := "Dockerfile" + content := []byte(opts.DockerfileContent) + header := &tar.Header{ + Name: dockerfile, + Size: int64(len(content)), + Mode: 0644, } - if _, err := dbEngine.Table("devcontainer_output").Insert(&pullImageOutput); err != nil { - log.Info("Failed to insert record: %v", err) - return err + if err := tw.WriteHeader(header); err != nil { + panic(err) } - _, err := dbEngine.Table("devcontainer_output"). - Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId). - Get(&pullImageOutput) + if _, err := tw.Write(content); err != nil { + panic(err) + } + // 执行镜像构建 + 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 { - log.Info("err %v", err) + log.Info(err.Error()) 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: "", 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, + Command: "Run postCreateCommand", + ListId: 3, }) if err != nil { log.Info("Failed to insert record: %v", err) return err } + } - if len(opts.PostCreateCommand) >= opts.SystemCommandCount { - _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{ - Output: "", - Status: "running", - UserId: opts.UserId, - RepoId: opts.RepoId, - Command: "Run postCreateCommand", - ListId: 3, - }) - if err != nil { - log.Info("Failed to insert record: %v", err) - return 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) - // 使用 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() { output += "\n" + stderrScanner.Text() cur++ @@ -341,82 +387,48 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d } } } - _, err = dbEngine.Table("devcontainer_output"). - Where("id = ?", pullImageOutput.Id). - Update(&devcontainer_models.DevcontainerOutput{ - Output: output}) - if err != nil { - log.Info("err %v", err) + } + _, err = dbEngine.Table("devcontainer_output"). + Where("id = ?", pullImageOutput.Id). + Update(&devcontainer_models.DevcontainerOutput{ + Output: output}) + if err != nil { + log.Info("err %v", err) - } - dbEngine.Table("devcontainer_output"). - Where("id = ?", pullImageOutput.Id). - Update(&devcontainer_models.DevcontainerOutput{Status: "success"}) - }() + } + dbEngine.Table("devcontainer_output"). + Where("id = ?", pullImageOutput.Id). + Update(&devcontainer_models.DevcontainerOutput{Status: "success"}) - // 等待命令执行完毕 - go func() { - err := cmd.Wait() + // 创建并启动容器 + 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 实例 - if err != nil { - log.Info("fail to pull image") - } else { - // 创建并启动容器 - 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 = "" + var state int = 2 + for index, cmd := range opts.PostCreateCommand { - var buffer string = "" - 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) - } - } + 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{ @@ -425,10 +437,163 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d 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) + } + } + _, 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 } +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 + } +} diff --git a/templates/repo/devcontainer/details.tmpl b/templates/repo/devcontainer/details.tmpl index 9674c2b579..88d9f94eba 100644 --- a/templates/repo/devcontainer/details.tmpl +++ b/templates/repo/devcontainer/details.tmpl @@ -50,10 +50,16 @@ {{if .HasDevContainer}} - - - - + + {{if .InitializedContainer}} +
+
+ + + + + + {{end}} {{else if .HasValidDevContainerJSON}}
{{if not .isCreatingDevcontainer}} @@ -111,6 +117,7 @@ RepositoryAddress: formData.get('RepositoryAddress'), RepositoryUsername: formData.get('RepositoryUsername'), RepositoryPassword: formData.get('RepositoryPassword'), + SaveMethod: formData.get('SaveMethod'), ImageName: formData.get('ImageName'), }) @@ -130,6 +137,18 @@
+
+ + +
@@ -157,5 +176,43 @@
- + {{template "base/footer" .}} diff --git a/web_src/js/components/RepoDevcontainerView.vue b/web_src/js/components/RepoDevcontainerView.vue index cb411a1edb..a6350143fe 100644 --- a/web_src/js/components/RepoDevcontainerView.vue +++ b/web_src/js/components/RepoDevcontainerView.vue @@ -89,16 +89,23 @@ if(this.currentDevcontainer.detail == "running" && job.currentDevcontainer.detail == "created"){ 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 this.currentDevcontainer = job.currentDevcontainer; // sync the currentJobStepsStates to store the job step states - for (let i = 0; i < this.currentDevcontainer.steps.length; i++) { - if (!this.currentJobStepsStates[i]) { - // initial states for job steps - this.currentJobStepsStates[i] = {expanded: false}; + if (this.currentDevcontainer.steps != null){ + for (let i = 0; i < this.currentDevcontainer.steps.length; i++) { + if (!this.currentJobStepsStates[i]) { + // initial states for job steps + this.currentJobStepsStates[i] = {expanded: false}; + } } } + if (this.isDone(this.currentDevcontainer.detail) && this.intervalID) { clearInterval(this.intervalID); this.intervalID = null; @@ -149,7 +156,7 @@

-
+