mirror of
				https://gitee.com/devstar/devstar.git
				synced 2025-11-03 08:50:36 +00:00 
			
		
		
		
	add output feature
This commit is contained in:
		@@ -89,24 +89,22 @@ func GetContainerStatus(cli *client.Client, containerID string) (string, error)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	state := containerInfo.State
 | 
			
		||||
	return state.Status, nil
 | 
			
		||||
}
 | 
			
		||||
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)
 | 
			
		||||
	_, err := cmd.CombinedOutput()
 | 
			
		||||
	output, err := cmd.CombinedOutput()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
		return fmt.Errorf("%s \n 镜像登录失败: %s", string(output), err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	// 推送到仓库
 | 
			
		||||
	script = "docker " + "-H " + dockerHost + " push " + imageRef
 | 
			
		||||
	cmd = exec.Command("sh", "-c", script)
 | 
			
		||||
	_, err = cmd.CombinedOutput()
 | 
			
		||||
 | 
			
		||||
	output, err = cmd.CombinedOutput()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
		return fmt.Errorf("%s \n 镜像推送失败: %s", string(output), err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -324,9 +324,36 @@ func GetDevContainerOutput(ctx *context.Context) {
 | 
			
		||||
	ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
 | 
			
		||||
	ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
 | 
			
		||||
	ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
 | 
			
		||||
	output, err := devcontainer_service.GetDevContainerOutput(ctx, ctx.Doer, ctx.Repo.Repository)
 | 
			
		||||
	query := ctx.Req.URL.Query()
 | 
			
		||||
	output, err := devcontainer_service.GetDevContainerOutput(ctx, query.Get("user"), ctx.Repo.Repository)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Info(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	ctx.JSON(http.StatusOK, output)
 | 
			
		||||
	ctx.JSON(http.StatusOK, map[string]string{"output": output})
 | 
			
		||||
}
 | 
			
		||||
func SaveDevContainerOutput(ctx *context.Context) {
 | 
			
		||||
	// 设置 CORS 响应头
 | 
			
		||||
	ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
 | 
			
		||||
	ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
 | 
			
		||||
	ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
 | 
			
		||||
	// 处理 OPTIONS 预检请求
 | 
			
		||||
	if ctx.Req.Method == "OPTIONS" {
 | 
			
		||||
		ctx.JSON(http.StatusOK, "")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	query := ctx.Req.URL.Query()
 | 
			
		||||
 | 
			
		||||
	// 从请求体中读取输出内容
 | 
			
		||||
	body, err := io.ReadAll(ctx.Req.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Failed to read request body: %v", err)
 | 
			
		||||
		ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Failed to read request body"})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	err = devcontainer_service.SaveDevContainerOutput(ctx, query.Get("user"), ctx.Repo.Repository, string(body))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Info(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	ctx.JSON(http.StatusOK, "")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1434,13 +1434,14 @@ func registerWebRoutes(m *web.Router) {
 | 
			
		||||
		m.Get("/status", devcontainer_web.GetDevContainerStatus)
 | 
			
		||||
		m.Get("/command", devcontainer_web.GetTerminalCommand)
 | 
			
		||||
		m.Get("/output", devcontainer_web.GetDevContainerOutput)
 | 
			
		||||
		m.Methods("POST, OPTIONS", "/output", devcontainer_web.SaveDevContainerOutput)
 | 
			
		||||
	},
 | 
			
		||||
		//  解析仓库信息
 | 
			
		||||
		//  具有code读取权限
 | 
			
		||||
		context.RepoAssignment, reqUnitCodeReader,
 | 
			
		||||
	)
 | 
			
		||||
	m.Get("/devstar-home", devcontainer_web.VscodeHome) // 旧地址,保留兼容性
 | 
			
		||||
	m.Get("/vscode-home",  devcontainer_web.VscodeHome)
 | 
			
		||||
	m.Get("/vscode-home", devcontainer_web.VscodeHome)
 | 
			
		||||
	m.Group("/api/devcontainer", func() {
 | 
			
		||||
		// 获取 某用户在某仓库中的 DevContainer 细节(包括SSH连接信息),默认不会等待 (wait = false)
 | 
			
		||||
		// 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true   // 无需传入 userId,直接从 token 中提取
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,6 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -456,7 +454,6 @@ func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Reposit
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	otherCtx := context.Background()
 | 
			
		||||
	if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
 | 
			
		||||
		//k8s的逻辑
 | 
			
		||||
@@ -655,67 +652,49 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
 | 
			
		||||
	}
 | 
			
		||||
	return cmd, fmt.Sprintf("%d", realTimeStatus), nil
 | 
			
		||||
}
 | 
			
		||||
func GetDevContainerOutput(ctx context.Context, doer *user.User, repo *repo.Repository) (OutputResponse, error) {
 | 
			
		||||
	var devContainerOutput []devcontainer_models.DevcontainerOutput
 | 
			
		||||
func GetDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository) (string, error) {
 | 
			
		||||
	var devContainerOutput string
 | 
			
		||||
	dbEngine := db.GetEngine(ctx)
 | 
			
		||||
	resp := OutputResponse{}
 | 
			
		||||
	var status string
 | 
			
		||||
	var containerName string
 | 
			
		||||
	_, err := dbEngine.
 | 
			
		||||
		Table("devcontainer").
 | 
			
		||||
		Select("devcontainer_status, name").
 | 
			
		||||
		Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
 | 
			
		||||
		Get(&status, &containerName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return resp, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = dbEngine.Table("devcontainer_output").
 | 
			
		||||
		Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
 | 
			
		||||
		Find(&devContainerOutput)
 | 
			
		||||
	_, err := dbEngine.Table("devcontainer_output").
 | 
			
		||||
		Select("output").
 | 
			
		||||
		Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
 | 
			
		||||
		Get(&devContainerOutput)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return resp, err
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(devContainerOutput) > 0 {
 | 
			
		||||
	return devContainerOutput, nil
 | 
			
		||||
}
 | 
			
		||||
func SaveDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository, newoutput string) error {
 | 
			
		||||
	var devContainerOutput string
 | 
			
		||||
	var finalOutput string
 | 
			
		||||
	dbEngine := db.GetEngine(ctx)
 | 
			
		||||
 | 
			
		||||
		resp.CurrentJob.Title = repo.Name + " Devcontainer Info"
 | 
			
		||||
		resp.CurrentJob.Detail = status
 | 
			
		||||
		if status == "4" {
 | 
			
		||||
			// 获取WebSSH服务端口
 | 
			
		||||
			webTerminalURL, err := GetWebTerminalURL(ctx, doer.ID, repo.ID)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				return resp, err
 | 
			
		||||
			}
 | 
			
		||||
			// 解析URL
 | 
			
		||||
			u, err := url.Parse(webTerminalURL)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return resp, err
 | 
			
		||||
			}
 | 
			
		||||
			// 分离主机和端口
 | 
			
		||||
			terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
 | 
			
		||||
			resp.CurrentJob.IP = terminalHost
 | 
			
		||||
			resp.CurrentJob.Port = terminalPort
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return resp, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		for _, item := range devContainerOutput {
 | 
			
		||||
			logLines := []ViewStepLogLine{}
 | 
			
		||||
			logLines = append(logLines, ViewStepLogLine{
 | 
			
		||||
				Index:   1,
 | 
			
		||||
				Message: item.Output,
 | 
			
		||||
			})
 | 
			
		||||
			resp.CurrentJob.Steps = append(resp.CurrentJob.Steps, &ViewJobStep{
 | 
			
		||||
				Summary: item.Command,
 | 
			
		||||
				Status:  item.Status,
 | 
			
		||||
				Logs:    logLines,
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	// 从数据库中获取现有的输出内容
 | 
			
		||||
	_, err := dbEngine.Table("devcontainer_output").
 | 
			
		||||
		Select("output").
 | 
			
		||||
		Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
 | 
			
		||||
		Get(&devContainerOutput)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return resp, nil
 | 
			
		||||
	devContainerOutput = strings.TrimSuffix(devContainerOutput, "\r\n")
 | 
			
		||||
	if newoutput == "\b \b" {
 | 
			
		||||
		finalOutput = devContainerOutput[:len(devContainerOutput)-1]
 | 
			
		||||
	} else {
 | 
			
		||||
		finalOutput = devContainerOutput + newoutput
 | 
			
		||||
	}
 | 
			
		||||
	_, err = dbEngine.Table("devcontainer_output").
 | 
			
		||||
		Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
 | 
			
		||||
		Update(map[string]interface{}{
 | 
			
		||||
			"output": finalOutput + "\r\n",
 | 
			
		||||
		})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) {
 | 
			
		||||
	cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
 | 
			
		||||
 
 | 
			
		||||
@@ -84,6 +84,16 @@
 | 
			
		||||
		<!-- 结束Dev Container 正文内容 -->
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
<!-- 自定义警告框 -->
 | 
			
		||||
    <div id="customAlert" class="custom-alert">
 | 
			
		||||
        <div class="alert-content">
 | 
			
		||||
            <div class="alert-header">
 | 
			
		||||
                <strong>提示信息</strong>
 | 
			
		||||
                <button class="alert-close" onclick="closeCustomAlert()">×</button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div id="alertText" class="alert-body"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
<!-- 确认删除 Dev Container 模态对话框 -->
 | 
			
		||||
<div class="ui g-modal-confirm delete modal" id="delete-repo-devcontainer-of-user-modal">
 | 
			
		||||
@@ -406,11 +416,31 @@ function togglePasswordVisibility(passwordFieldId, button) {
 | 
			
		||||
        button.style.color = '#666'; // 恢复默认颜色
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
function showCustomAlert(message, title = "提示信息") {
 | 
			
		||||
	const alertBox = document.getElementById('customAlert');
 | 
			
		||||
	const alertText = document.getElementById('alertText');
 | 
			
		||||
	const alertHeader = alertBox.querySelector('.alert-header strong');
 | 
			
		||||
	
 | 
			
		||||
	alertHeader.textContent = title;
 | 
			
		||||
	alertText.textContent = message;
 | 
			
		||||
	alertBox.style.display = 'block';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function closeCustomAlert() {
 | 
			
		||||
	document.getElementById('customAlert').style.display = 'none';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 点击背景关闭
 | 
			
		||||
document.getElementById('customAlert').addEventListener('click', function(e) {
 | 
			
		||||
	if (e.target === this) {
 | 
			
		||||
		closeCustomAlert();
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
      
 | 
			
		||||
function submitForm(event) {
 | 
			
		||||
	event.preventDefault(); // 阻止默认的表单提交行为
 | 
			
		||||
	const {csrfToken} = window.config;
 | 
			
		||||
	const {appSubUrl} = window.config;
 | 
			
		||||
	console.log("????")
 | 
			
		||||
	const formModal = document.getElementById('updatemodal');
 | 
			
		||||
	const form = document.getElementById('updateForm');
 | 
			
		||||
	const submitButton = document.getElementById('updateSubmitButton');
 | 
			
		||||
@@ -436,9 +466,9 @@ function submitForm(event) {
 | 
			
		||||
		.then(data => {
 | 
			
		||||
			submitButton.disabled = false;
 | 
			
		||||
			formModal.classList.remove('is-loading')
 | 
			
		||||
			alert(data.message);
 | 
			
		||||
			showCustomAlert(data.message);
 | 
			
		||||
			if(data.redirect){
 | 
			
		||||
				closeButton.click()
 | 
			
		||||
				closeCustomAlert()
 | 
			
		||||
			}
 | 
			
		||||
			intervalID = setInterval(getStatus, 3000);
 | 
			
		||||
		})
 | 
			
		||||
@@ -468,6 +498,69 @@ function submitForm(event) {
 | 
			
		||||
  0%{-webkit-transform:rotate(0deg)}
 | 
			
		||||
  100%{-webkit-transform:rotate(360deg)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.custom-alert {
 | 
			
		||||
	display: none;
 | 
			
		||||
	position: fixed;
 | 
			
		||||
	top: 0;
 | 
			
		||||
	left: 0;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	height: 100%;
 | 
			
		||||
	background: rgba(0,0,0,0.5);
 | 
			
		||||
	z-index: 10000;
 | 
			
		||||
}
 | 
			
		||||
.alert-content {
 | 
			
		||||
	color: black;
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	top: 50%;
 | 
			
		||||
	left: 50%;
 | 
			
		||||
	transform: translate(-50%, -50%);
 | 
			
		||||
	background: white;
 | 
			
		||||
	padding: 0; /* 移除内边距,在内部元素中设置 */
 | 
			
		||||
	border-radius: 8px;
 | 
			
		||||
	width: 80%;
 | 
			
		||||
	max-width: 600px;
 | 
			
		||||
	max-height: 80%;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
	box-shadow: 0 4px 6px rgba(0,0,0,0.1);
 | 
			
		||||
}
 | 
			
		||||
.alert-header {
 | 
			
		||||
	padding: 15px 20px;
 | 
			
		||||
	border-bottom: 1px solid #eee;
 | 
			
		||||
	background: #f8f9fa;
 | 
			
		||||
	border-radius: 8px 8px 0 0;
 | 
			
		||||
	position: sticky;
 | 
			
		||||
	top: 0;
 | 
			
		||||
	z-index: 10;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	justify-content: space-between;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
}
 | 
			
		||||
.alert-close {
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
	font-size: 24px;
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
	color: #666;
 | 
			
		||||
	background: none;
 | 
			
		||||
	border: none;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	width: 30px;
 | 
			
		||||
	height: 30px;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	justify-content: center;
 | 
			
		||||
	border-radius: 50%;
 | 
			
		||||
}
 | 
			
		||||
.alert-close:hover {
 | 
			
		||||
	background: #e9ecef;
 | 
			
		||||
	color: #000;
 | 
			
		||||
}
 | 
			
		||||
.alert-body {
 | 
			
		||||
	padding: 20px;
 | 
			
		||||
	overflow-y: auto;
 | 
			
		||||
	max-height: calc(80vh - 100px); /* 减去头部高度 */
 | 
			
		||||
	white-space: pre-wrap;
 | 
			
		||||
	word-wrap: break-word;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
{{template "base/footer" .}}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user