mirror of
https://gitee.com/devstar/devstar.git
synced 2025-11-04 09:00: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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
state := containerInfo.State
|
state := containerInfo.State
|
||||||
return state.Status, nil
|
return state.Status, nil
|
||||||
}
|
}
|
||||||
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)
|
||||||
_, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("%s \n 镜像登录失败: %s", string(output), err.Error())
|
||||||
}
|
}
|
||||||
// 推送到仓库
|
// 推送到仓库
|
||||||
script = "docker " + "-H " + dockerHost + " push " + imageRef
|
script = "docker " + "-H " + dockerHost + " push " + imageRef
|
||||||
cmd = exec.Command("sh", "-c", script)
|
cmd = exec.Command("sh", "-c", script)
|
||||||
_, err = cmd.CombinedOutput()
|
output, err = cmd.CombinedOutput()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("%s \n 镜像推送失败: %s", string(output), err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
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-Origin", "*")
|
||||||
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
|
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
|
||||||
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
|
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 {
|
if err != nil {
|
||||||
log.Info(err.Error())
|
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("/status", devcontainer_web.GetDevContainerStatus)
|
||||||
m.Get("/command", devcontainer_web.GetTerminalCommand)
|
m.Get("/command", devcontainer_web.GetTerminalCommand)
|
||||||
m.Get("/output", devcontainer_web.GetDevContainerOutput)
|
m.Get("/output", devcontainer_web.GetDevContainerOutput)
|
||||||
|
m.Methods("POST, OPTIONS", "/output", devcontainer_web.SaveDevContainerOutput)
|
||||||
},
|
},
|
||||||
// 解析仓库信息
|
// 解析仓库信息
|
||||||
// 具有code读取权限
|
// 具有code读取权限
|
||||||
context.RepoAssignment, reqUnitCodeReader,
|
context.RepoAssignment, reqUnitCodeReader,
|
||||||
)
|
)
|
||||||
m.Get("/devstar-home", devcontainer_web.VscodeHome) // 旧地址,保留兼容性
|
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() {
|
m.Group("/api/devcontainer", func() {
|
||||||
// 获取 某用户在某仓库中的 DevContainer 细节(包括SSH连接信息),默认不会等待 (wait = false)
|
// 获取 某用户在某仓库中的 DevContainer 细节(包括SSH连接信息),默认不会等待 (wait = false)
|
||||||
// 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true // 无需传入 userId,直接从 token 中提取
|
// 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true // 无需传入 userId,直接从 token 中提取
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -456,7 +454,6 @@ func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Reposit
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
otherCtx := context.Background()
|
otherCtx := context.Background()
|
||||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||||
//k8s的逻辑
|
//k8s的逻辑
|
||||||
@@ -655,67 +652,49 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
|
|||||||
}
|
}
|
||||||
return cmd, fmt.Sprintf("%d", realTimeStatus), nil
|
return cmd, fmt.Sprintf("%d", realTimeStatus), nil
|
||||||
}
|
}
|
||||||
func GetDevContainerOutput(ctx context.Context, doer *user.User, repo *repo.Repository) (OutputResponse, error) {
|
func GetDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository) (string, error) {
|
||||||
var devContainerOutput []devcontainer_models.DevcontainerOutput
|
var devContainerOutput string
|
||||||
dbEngine := db.GetEngine(ctx)
|
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").
|
_, err := dbEngine.Table("devcontainer_output").
|
||||||
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
|
Select("output").
|
||||||
Find(&devContainerOutput)
|
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
||||||
|
Get(&devContainerOutput)
|
||||||
|
|
||||||
if err != nil {
|
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
|
_, err := dbEngine.Table("devcontainer_output").
|
||||||
if status == "4" {
|
Select("output").
|
||||||
// 获取WebSSH服务端口
|
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
||||||
webTerminalURL, err := GetWebTerminalURL(ctx, doer.ID, repo.ID)
|
Get(&devContainerOutput)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
return resp, err
|
return 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,
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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) {
|
func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) {
|
||||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||||
|
|||||||
@@ -84,6 +84,16 @@
|
|||||||
<!-- 结束Dev Container 正文内容 -->
|
<!-- 结束Dev Container 正文内容 -->
|
||||||
</div>
|
</div>
|
||||||
</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 模态对话框 -->
|
<!-- 确认删除 Dev Container 模态对话框 -->
|
||||||
<div class="ui g-modal-confirm delete modal" id="delete-repo-devcontainer-of-user-modal">
|
<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'; // 恢复默认颜色
|
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) {
|
function submitForm(event) {
|
||||||
event.preventDefault(); // 阻止默认的表单提交行为
|
event.preventDefault(); // 阻止默认的表单提交行为
|
||||||
const {csrfToken} = window.config;
|
const {csrfToken} = window.config;
|
||||||
const {appSubUrl} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
console.log("????")
|
|
||||||
const formModal = document.getElementById('updatemodal');
|
const formModal = document.getElementById('updatemodal');
|
||||||
const form = document.getElementById('updateForm');
|
const form = document.getElementById('updateForm');
|
||||||
const submitButton = document.getElementById('updateSubmitButton');
|
const submitButton = document.getElementById('updateSubmitButton');
|
||||||
@@ -436,9 +466,9 @@ function submitForm(event) {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
formModal.classList.remove('is-loading')
|
formModal.classList.remove('is-loading')
|
||||||
alert(data.message);
|
showCustomAlert(data.message);
|
||||||
if(data.redirect){
|
if(data.redirect){
|
||||||
closeButton.click()
|
closeCustomAlert()
|
||||||
}
|
}
|
||||||
intervalID = setInterval(getStatus, 3000);
|
intervalID = setInterval(getStatus, 3000);
|
||||||
})
|
})
|
||||||
@@ -468,6 +498,69 @@ function submitForm(event) {
|
|||||||
0%{-webkit-transform:rotate(0deg)}
|
0%{-webkit-transform:rotate(0deg)}
|
||||||
100%{-webkit-transform:rotate(360deg)}
|
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>
|
</style>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
|||||||
Reference in New Issue
Block a user