mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-31 14:08:06 +08:00
feat: Node.js 环境增加启动、停止、重启操作 (#2394)
This commit is contained in:
parent
1130a70052
commit
4ca25d51d7
@ -93,14 +93,14 @@ func (b *BaseApi) GetApp(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /apps/detail/:appId/:version/:type [get]
|
||||
func (b *BaseApi) GetAppDetail(c *gin.Context) {
|
||||
appId, err := helper.GetIntParamByKey(c, "appId")
|
||||
appID, err := helper.GetIntParamByKey(c, "appId")
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil)
|
||||
return
|
||||
}
|
||||
version := c.Param("version")
|
||||
appType := c.Param("type")
|
||||
appDetailDTO, err := appService.GetAppDetail(appId, version, appType)
|
||||
appDetailDTO, err := appService.GetAppDetail(appID, version, appType)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
|
@ -143,3 +143,26 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
|
||||
}
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags Runtime
|
||||
// @Summary Operate runtime
|
||||
// @Description 操作运行环境
|
||||
// @Accept json
|
||||
// @Param request body request.RuntimeOperate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /runtimes/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
|
||||
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
||||
var req request.RuntimeOperate
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||
return
|
||||
}
|
||||
err := runtimeService.OperateRuntime(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ type AppSearch struct {
|
||||
Tags []string `json:"tags"`
|
||||
Type string `json:"type"`
|
||||
Recommend bool `json:"recommend"`
|
||||
Resource string `json:"resource"`
|
||||
}
|
||||
|
||||
type AppInstallCreate struct {
|
||||
|
@ -47,3 +47,8 @@ type RuntimeUpdate struct {
|
||||
type NodePackageReq struct {
|
||||
CodeDir string `json:"codeDir"`
|
||||
}
|
||||
|
||||
type RuntimeOperate struct {
|
||||
Operate string `json:"operate"`
|
||||
ID uint `json:"ID"`
|
||||
}
|
||||
|
@ -61,6 +61,9 @@ func (a AppService) PageApp(req request.AppSearch) (interface{}, error) {
|
||||
if req.Recommend {
|
||||
opts = append(opts, appRepo.GetRecommend())
|
||||
}
|
||||
if req.Resource != "" {
|
||||
opts = append(opts, appRepo.WithResource(req.Resource))
|
||||
}
|
||||
if len(req.Tags) != 0 {
|
||||
tags, err := tagRepo.GetByKeys(req.Tags)
|
||||
if err != nil {
|
||||
@ -151,12 +154,12 @@ func (a AppService) GetApp(key string) (*response.AppDTO, error) {
|
||||
return &appDTO, nil
|
||||
}
|
||||
|
||||
func (a AppService) GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error) {
|
||||
func (a AppService) GetAppDetail(appID uint, version, appType string) (response.AppDetailDTO, error) {
|
||||
var (
|
||||
appDetailDTO response.AppDetailDTO
|
||||
opts []repo.DBOption
|
||||
)
|
||||
opts = append(opts, appDetailRepo.WithAppId(appId), appDetailRepo.WithVersion(version))
|
||||
opts = append(opts, appDetailRepo.WithAppId(appID), appDetailRepo.WithVersion(version))
|
||||
detail, err := appDetailRepo.GetFirst(opts...)
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
@ -165,7 +168,7 @@ func (a AppService) GetAppDetail(appId uint, version, appType string) (response.
|
||||
appDetailDTO.Enable = true
|
||||
|
||||
if appType == "runtime" {
|
||||
app, err := appRepo.GetFirst(commonRepo.WithByID(appId))
|
||||
app, err := appRepo.GetFirst(commonRepo.WithByID(appID))
|
||||
if err != nil {
|
||||
return appDetailDTO, err
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ type IRuntimeService interface {
|
||||
Update(req request.RuntimeUpdate) error
|
||||
Get(id uint) (res *response.RuntimeDTO, err error)
|
||||
GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error)
|
||||
OperateRuntime(req request.RuntimeOperate) error
|
||||
}
|
||||
|
||||
func NewRuntimeService() IRuntimeService {
|
||||
@ -357,3 +358,42 @@ func (r *RuntimeService) GetNodePackageRunScript(req request.NodePackageReq) ([]
|
||||
}
|
||||
return packageScripts, nil
|
||||
}
|
||||
|
||||
func (r *RuntimeService) OperateRuntime(req request.RuntimeOperate) error {
|
||||
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
runtime.Status = constant.RuntimeError
|
||||
runtime.Message = err.Error()
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
}
|
||||
}()
|
||||
switch req.Operate {
|
||||
case constant.RuntimeUp:
|
||||
if err = runComposeCmdWithLog(req.Operate, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = SyncRuntimeContainerStatus(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.RuntimeDown:
|
||||
if err = runComposeCmdWithLog(req.Operate, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Status = constant.RuntimeStopped
|
||||
case constant.RuntimeRestart:
|
||||
if err = runComposeCmdWithLog(constant.RuntimeDown, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = runComposeCmdWithLog(constant.RuntimeUp, runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = SyncRuntimeContainerStatus(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return runtimeRepo.Save(runtime)
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ func handleNode(create request.RuntimeCreate, runtime *model.Runtime, fileOp fil
|
||||
}
|
||||
runtime.DockerCompose = string(composeContent)
|
||||
runtime.Env = string(envContent)
|
||||
runtime.Status = constant.RuntimeStarting
|
||||
runtime.Status = constant.RuntimeCreating
|
||||
runtime.CodeDir = create.CodeDir
|
||||
|
||||
go startRuntime(runtime)
|
||||
@ -88,29 +88,39 @@ func handlePHP(create request.RuntimeCreate, runtime *model.Runtime, fileOp file
|
||||
}
|
||||
|
||||
func startRuntime(runtime *model.Runtime) {
|
||||
cmd := exec.Command("docker-compose", "-f", runtime.GetComposePath(), "up", "-d")
|
||||
logPath := runtime.GetLogPath()
|
||||
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Failed to open log file: %v", err)
|
||||
if err := runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
|
||||
runtime.Status = constant.RuntimeError
|
||||
runtime.Message = err.Error()
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
return
|
||||
}
|
||||
multiWriterStdout := io.MultiWriter(os.Stdout, logFile)
|
||||
cmd.Stdout = multiWriterStdout
|
||||
var stderrBuf bytes.Buffer
|
||||
multiWriterStderr := io.MultiWriter(&stderrBuf, logFile, os.Stderr)
|
||||
cmd.Stderr = multiWriterStderr
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
if err := SyncRuntimeContainerStatus(runtime); err != nil {
|
||||
runtime.Status = constant.RuntimeError
|
||||
runtime.Message = buserr.New(constant.ErrRuntimeStart).Error() + ":" + stderrBuf.String()
|
||||
} else {
|
||||
runtime.Status = constant.RuntimeRunning
|
||||
runtime.Message = ""
|
||||
runtime.Message = err.Error()
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
func reCreateRuntime(runtime *model.Runtime) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
runtime.Status = constant.RuntimeError
|
||||
runtime.Message = err.Error()
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
}
|
||||
}()
|
||||
if err = runComposeCmdWithLog("down", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
|
||||
return
|
||||
}
|
||||
if err = runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
|
||||
return
|
||||
}
|
||||
if err := SyncRuntimeContainerStatus(runtime); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func runComposeCmdWithLog(operate string, composePath string, logPath string) error {
|
||||
@ -136,23 +146,57 @@ func runComposeCmdWithLog(operate string, composePath string, logPath string) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func reCreateRuntime(runtime *model.Runtime) {
|
||||
var err error
|
||||
defer func() {
|
||||
func SyncRuntimeContainerStatus(runtime *model.Runtime) error {
|
||||
env, err := gotenv.Unmarshal(runtime.Env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var containerNames []string
|
||||
if containerName, ok := env["CONTAINER_NAME"]; !ok {
|
||||
return buserr.New("ErrContainerNameNotFound")
|
||||
} else {
|
||||
containerNames = append(containerNames, containerName)
|
||||
}
|
||||
cli, err := docker.NewClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containers, err := cli.ListContainersByName(containerNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
return buserr.WithNameAndErr("ErrContainerNotFound", containerNames[0], nil)
|
||||
}
|
||||
container := containers[0]
|
||||
|
||||
interval := 10 * time.Second
|
||||
retries := 60
|
||||
for i := 0; i < retries; i++ {
|
||||
resp, err := cli.InspectContainer(container.ID)
|
||||
if err != nil {
|
||||
runtime.Status = constant.RuntimeError
|
||||
runtime.Message = err.Error()
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
time.Sleep(interval)
|
||||
continue
|
||||
}
|
||||
}()
|
||||
if err = runComposeCmdWithLog("down", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
|
||||
return
|
||||
if resp.State.Health != nil {
|
||||
status := strings.ToLower(resp.State.Health.Status)
|
||||
switch status {
|
||||
case "starting":
|
||||
runtime.Status = constant.RuntimeStarting
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
case "healthy":
|
||||
runtime.Status = constant.RuntimeRunning
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
return nil
|
||||
case "unhealthy":
|
||||
runtime.Status = constant.RuntimeUnhealthy
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
time.Sleep(interval)
|
||||
}
|
||||
if err = runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil {
|
||||
return
|
||||
}
|
||||
runtime.Status = constant.RuntimeRunning
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) {
|
||||
|
@ -10,10 +10,17 @@ const (
|
||||
RuntimeStarting = "starting"
|
||||
RuntimeRunning = "running"
|
||||
RuntimeReCreating = "recreating"
|
||||
RuntimeStopped = "stopped"
|
||||
RuntimeUnhealthy = "unhealthy"
|
||||
RuntimeCreating = "creating"
|
||||
|
||||
RuntimePHP = "php"
|
||||
RuntimeNode = "node"
|
||||
|
||||
RuntimeProxyUnix = "unix"
|
||||
RuntimeProxyTcp = "tcp"
|
||||
|
||||
RuntimeUp = "up"
|
||||
RuntimeDown = "down"
|
||||
RuntimeRestart = "restart"
|
||||
)
|
||||
|
@ -102,6 +102,7 @@ ErrDelWithWebsite: "The operating environment has been associated with a website
|
||||
ErrRuntimeStart: "Failed to start"
|
||||
ErrPackageJsonNotFound: "package.json file does not exist"
|
||||
ErrScriptsNotFound: "No scripts configuration item was found in package.json"
|
||||
ErrContainerNameNotFound: "Unable to get container name, please check .env file"
|
||||
|
||||
#setting
|
||||
ErrBackupInUsed: "The backup account is already being used in a cronjob and cannot be deleted."
|
||||
|
@ -102,6 +102,7 @@ ErrDelWithWebsite: "運行環境已經關聯網站,無法刪除"
|
||||
ErrRuntimeStart: "啟動失敗"
|
||||
ErrPackageJsonNotFound: "package.json 文件不存在"
|
||||
ErrScriptsNotFound: "沒有在 package.json 中找到 scripts 配置項"
|
||||
ErrContainerNameNotFound: "無法取得容器名稱,請檢查 .env 文件"
|
||||
|
||||
#setting
|
||||
ErrBackupInUsed: "該備份帳號已在計劃任務中使用,無法刪除"
|
||||
|
@ -102,6 +102,7 @@ ErrDelWithWebsite: "运行环境已经关联网站,无法删除"
|
||||
ErrRuntimeStart: "启动失败"
|
||||
ErrPackageJsonNotFound: "package.json 文件不存在"
|
||||
ErrScriptsNotFound: "没有在 package.json 中找到 scripts 配置项"
|
||||
ErrContainerNameNotFound: "无法获取容器名称,请检查 .env 文件"
|
||||
|
||||
#setting
|
||||
ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除"
|
||||
|
@ -21,5 +21,7 @@ func (r *RuntimeRouter) InitRuntimeRouter(Router *gin.RouterGroup) {
|
||||
groupRouter.POST("/update", baseApi.UpdateRuntime)
|
||||
groupRouter.GET("/:id", baseApi.GetRuntime)
|
||||
groupRouter.POST("/node/package", baseApi.GetNodePackageRunScript)
|
||||
groupRouter.POST("/operate", baseApi.OperateRuntime)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -80,6 +80,10 @@ func (c Client) DeleteImage(imageID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) InspectContainer(containerID string) (types.ContainerJSON, error) {
|
||||
return c.cli.ContainerInspect(context.Background(), containerID)
|
||||
}
|
||||
|
||||
func (c Client) PullImage(imageName string, force bool) error {
|
||||
if !force {
|
||||
exist, err := c.CheckImageExist(imageName)
|
||||
|
@ -7863,6 +7863,48 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/runtimes/operate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "操作运行环境",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Runtime"
|
||||
],
|
||||
"summary": "Operate runtime",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/request.RuntimeOperate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"x-panel-log": {
|
||||
"BeforeFuntions": [],
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"formatEN": "Operate runtime [name]",
|
||||
"formatZH": "操作运行环境 [name]",
|
||||
"paramKeys": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"/runtimes/search": {
|
||||
"post": {
|
||||
"security": [
|
||||
@ -15732,6 +15774,9 @@ const docTemplate = `{
|
||||
"recommend": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"resource": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -16491,6 +16536,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"request.RuntimeOperate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ID": {
|
||||
"type": "integer"
|
||||
},
|
||||
"operate": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"request.RuntimeSearch": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -7856,6 +7856,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/runtimes/operate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "操作运行环境",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Runtime"
|
||||
],
|
||||
"summary": "Operate runtime",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/request.RuntimeOperate"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
},
|
||||
"x-panel-log": {
|
||||
"BeforeFuntions": [],
|
||||
"bodyKeys": [
|
||||
"id"
|
||||
],
|
||||
"formatEN": "Operate runtime [name]",
|
||||
"formatZH": "操作运行环境 [name]",
|
||||
"paramKeys": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"/runtimes/search": {
|
||||
"post": {
|
||||
"security": [
|
||||
@ -15725,6 +15767,9 @@
|
||||
"recommend": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"resource": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -16484,6 +16529,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"request.RuntimeOperate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ID": {
|
||||
"type": "integer"
|
||||
},
|
||||
"operate": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"request.RuntimeSearch": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -2639,6 +2639,8 @@ definitions:
|
||||
type: integer
|
||||
recommend:
|
||||
type: boolean
|
||||
resource:
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
@ -3151,6 +3153,13 @@ definitions:
|
||||
id:
|
||||
type: integer
|
||||
type: object
|
||||
request.RuntimeOperate:
|
||||
properties:
|
||||
ID:
|
||||
type: integer
|
||||
operate:
|
||||
type: string
|
||||
type: object
|
||||
request.RuntimeSearch:
|
||||
properties:
|
||||
name:
|
||||
@ -9086,6 +9095,33 @@ paths:
|
||||
summary: Get Node package scripts
|
||||
tags:
|
||||
- Runtime
|
||||
/runtimes/operate:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 操作运行环境
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.RuntimeOperate'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Operate runtime
|
||||
tags:
|
||||
- Runtime
|
||||
x-panel-log:
|
||||
BeforeFuntions: []
|
||||
bodyKeys:
|
||||
- id
|
||||
formatEN: Operate runtime [name]
|
||||
formatZH: 操作运行环境 [name]
|
||||
paramKeys: []
|
||||
/runtimes/search:
|
||||
post:
|
||||
consumes:
|
||||
|
@ -68,4 +68,9 @@ export namespace Runtime {
|
||||
id: number;
|
||||
forceDelete: boolean;
|
||||
}
|
||||
|
||||
export interface RuntimeOperate {
|
||||
ID: number;
|
||||
operate: string;
|
||||
}
|
||||
}
|
||||
|
@ -25,3 +25,7 @@ export const UpdateRuntime = (req: Runtime.RuntimeUpdate) => {
|
||||
export const GetNodeScripts = (req: Runtime.NodeReq) => {
|
||||
return http.post<Runtime.NodeScripts[]>(`/runtimes/node/package`, req);
|
||||
};
|
||||
|
||||
export const OperateRuntime = (req: Runtime.RuntimeOperate) => {
|
||||
return http.post<any>(`/runtimes/operate`, req);
|
||||
};
|
||||
|
@ -34,7 +34,16 @@ const getType = (status: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadingStatus = ['installing', 'building', 'restarting', 'upgrading', 'rebuilding', 'recreating', 'creating'];
|
||||
const loadingStatus = [
|
||||
'installing',
|
||||
'building',
|
||||
'restarting',
|
||||
'upgrading',
|
||||
'rebuilding',
|
||||
'recreating',
|
||||
'creating',
|
||||
'starting',
|
||||
];
|
||||
|
||||
const loadingIcon = (status: string): boolean => {
|
||||
return loadingStatus.indexOf(status) > -1;
|
||||
|
@ -230,6 +230,7 @@ const message = {
|
||||
unUsed: 'Unused',
|
||||
starting: 'Starting',
|
||||
recreating: 'Recreating',
|
||||
creating: 'Creating',
|
||||
},
|
||||
units: {
|
||||
second: 'Second',
|
||||
@ -242,6 +243,11 @@ const message = {
|
||||
time: 'Time',
|
||||
core: 'Core',
|
||||
},
|
||||
operate: {
|
||||
down: 'Stop',
|
||||
up: 'Start',
|
||||
restart: 'Restart',
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
home: 'Overview',
|
||||
@ -1703,6 +1709,10 @@ const message = {
|
||||
runScriptHelper: 'The startup command list is parsed from the package.json file in the source directory',
|
||||
open: 'Open',
|
||||
close: 'Close',
|
||||
operatorHelper:
|
||||
'The {0} operation will be performed on the selected operating environment. Do you want to continue? ',
|
||||
statusHelper:
|
||||
'Status description: Starting - the container has been started, but the application is starting; abnormal - the container has been started, but the application status is abnormal',
|
||||
},
|
||||
process: {
|
||||
pid: 'Process ID',
|
||||
|
@ -228,6 +228,7 @@ const message = {
|
||||
unUsed: '未使用',
|
||||
starting: '啟動中',
|
||||
recreating: '重建中',
|
||||
creating: '創建中',
|
||||
},
|
||||
units: {
|
||||
second: '秒',
|
||||
@ -240,6 +241,11 @@ const message = {
|
||||
time: '次',
|
||||
core: '核',
|
||||
},
|
||||
operate: {
|
||||
down: '停止',
|
||||
up: '启动',
|
||||
restart: '重启',
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
home: '概覽',
|
||||
@ -1612,6 +1618,8 @@ const message = {
|
||||
runScriptHelper: '啟動命令是指容器啟動後運行的命令',
|
||||
open: '開啟',
|
||||
close: '關閉',
|
||||
operatorHelper: '將對選取的執行環境進行 {0} 操作,是否繼續? ',
|
||||
statusHelper: '狀態說明:啟動中-容器已啟動,但應用正在啟動;異常-容器已啟動,但應用狀態異常',
|
||||
},
|
||||
process: {
|
||||
pid: '進程ID',
|
||||
|
@ -228,6 +228,7 @@ const message = {
|
||||
unUsed: '未使用',
|
||||
starting: '启动中',
|
||||
recreating: '重建中',
|
||||
creating: '创建中',
|
||||
},
|
||||
units: {
|
||||
second: '秒',
|
||||
@ -240,6 +241,11 @@ const message = {
|
||||
time: '次',
|
||||
core: '核',
|
||||
},
|
||||
operate: {
|
||||
down: '停止',
|
||||
up: '启动',
|
||||
restart: '重启',
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
home: '概览',
|
||||
@ -1612,6 +1618,8 @@ const message = {
|
||||
runScriptHelper: '启动命令列表是从源码目录下的 package.json 文件中解析而来',
|
||||
open: '放开',
|
||||
close: '关闭',
|
||||
operatorHelper: '将对选中的运行环境进行 {0} 操作,是否继续?',
|
||||
statusHelper: '状态说明:启动中-容器已启动,但应用正在启动;异常-容器已启动,但应用状态异常',
|
||||
},
|
||||
process: {
|
||||
pid: '进程ID',
|
||||
|
@ -130,7 +130,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
<Detail :id="appId" ref="detailRef"></Detail>
|
||||
<Detail ref="detailRef"></Detail>
|
||||
<Install ref="installRef" />
|
||||
</template>
|
||||
|
||||
@ -174,7 +174,6 @@ const colorArr = ['#005eeb', '#008B45', '#BEBEBE', '#FFF68F', '#FFFF00', '#8B000
|
||||
const loading = ref(false);
|
||||
const activeTag = ref('all');
|
||||
const showDetail = ref(false);
|
||||
const appId = ref(0);
|
||||
const canUpdate = ref(false);
|
||||
const syncing = ref(false);
|
||||
const detailRef = ref();
|
||||
|
@ -131,14 +131,19 @@ const toLink = (link: string) => {
|
||||
};
|
||||
|
||||
const openInstall = () => {
|
||||
if (app.value.type === 'php') {
|
||||
router.push({ path: '/websites/runtime/php' });
|
||||
} else {
|
||||
const params = {
|
||||
app: app.value,
|
||||
};
|
||||
installRef.value.acceptParams(params);
|
||||
open.value = false;
|
||||
switch (app.value.type) {
|
||||
case 'php':
|
||||
router.push({ path: '/websites/runtimes/php' });
|
||||
break;
|
||||
case 'node':
|
||||
router.push({ path: '/websites/runtimes/node' });
|
||||
break;
|
||||
default:
|
||||
const params = {
|
||||
app: app.value,
|
||||
};
|
||||
installRef.value.acceptParams(params);
|
||||
open.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,337 +0,0 @@
|
||||
<template>
|
||||
<el-drawer :close-on-click-modal="false" v-model="open" size="50%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('runtime.' + mode)" :resource="runtime.name" :back="handleClose" />
|
||||
</template>
|
||||
<el-row v-loading="loading">
|
||||
<el-col :span="22" :offset="1">
|
||||
<el-form
|
||||
ref="runtimeForm"
|
||||
label-position="top"
|
||||
:model="runtime"
|
||||
label-width="125px"
|
||||
:rules="rules"
|
||||
:validate-on-rule-change="false"
|
||||
>
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
<el-input :disabled="mode === 'edit'" v-model="runtime.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('runtime.app')" prop="appId">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-select
|
||||
v-model="runtime.appId"
|
||||
:disabled="mode === 'edit'"
|
||||
@change="changeApp(runtime.appId)"
|
||||
>
|
||||
<el-option
|
||||
v-for="(app, index) in apps"
|
||||
:key="index"
|
||||
:label="app.name"
|
||||
:value="app.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-select
|
||||
v-model="runtime.version"
|
||||
:disabled="mode === 'edit'"
|
||||
@change="changeVersion()"
|
||||
>
|
||||
<el-option
|
||||
v-for="(version, index) in appVersions"
|
||||
:key="index"
|
||||
:label="version"
|
||||
:value="version"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('runtime.codeDir')" prop="codeDir">
|
||||
<el-input v-model.trim="runtime.codeDir">
|
||||
<template #prepend>
|
||||
<FileList :path="runtime.codeDir" @choose="getPath" :dir="true"></FileList>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('runtime.runScript')" prop="params.EXEC_SCRIPT">
|
||||
<el-select v-model="runtime.params['EXEC_SCRIPT']">
|
||||
<el-option
|
||||
v-for="(script, index) in scripts"
|
||||
:key="index"
|
||||
:label="script.name + ' 【 ' + script.script + ' 】'"
|
||||
:value="script.name"
|
||||
>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="4">{{ script.name }}</el-col>
|
||||
<el-col :span="10">{{ ' 【 ' + script.script + ' 】' }}</el-col>
|
||||
</el-row>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="10">
|
||||
<el-form-item :label="$t('runtime.appPort')" prop="params.NODE_APP_PORT">
|
||||
<el-input v-model.number="runtime.params['NODE_APP_PORT']" />
|
||||
<span class="input-help">{{ $t('runtime.appPortHelper') }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-form-item :label="$t('runtime.externalPort')" prop="params.PANEL_APP_PORT_HTTP">
|
||||
<el-input v-model.number="runtime.params['PANEL_APP_PORT_HTTP']" />
|
||||
<span class="input-help">{{ $t('runtime.externalPortHelper') }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="4">
|
||||
<el-form-item :label="$t('app.allowPort')" prop="params.HOST_IP">
|
||||
<el-select v-model="runtime.params['HOST_IP']">
|
||||
<el-option label="放开" value="0.0.0.0"></el-option>
|
||||
<el-option label="不放开" value="127.0.0.1"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('runtime.packageManager')" prop="params.PACKAGE_MANAGER">
|
||||
<el-select v-model="runtime.params['PACKAGE_MANAGER']">
|
||||
<el-option label="npm" value="npm"></el-option>
|
||||
<el-option label="yarn" value="yarn"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('app.containerName')" prop="params.CONTAINER_NAME">
|
||||
<el-input v-model.trim="runtime.params['CONTAINER_NAME']"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<template #footer>
|
||||
<span>
|
||||
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submit(runtimeForm)" :disabled="loading">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { App } from '@/api/interface/app';
|
||||
import { Runtime } from '@/api/interface/runtime';
|
||||
import { GetApp, GetAppDetail, SearchApp } from '@/api/modules/app';
|
||||
import { CreateRuntime, GetNodeScripts, GetRuntime, UpdateRuntime } from '@/api/modules/runtime';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
|
||||
interface OperateRrops {
|
||||
id?: number;
|
||||
mode: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const open = ref(false);
|
||||
const apps = ref<App.App[]>([]);
|
||||
const runtimeForm = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
const mode = ref('create');
|
||||
const editParams = ref<App.InstallParams[]>();
|
||||
const appVersions = ref<string[]>([]);
|
||||
const appReq = reactive({
|
||||
type: 'node',
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
const initData = (type: string) => ({
|
||||
name: '',
|
||||
appDetailId: undefined,
|
||||
image: '',
|
||||
params: {
|
||||
PACKAGE_MANAGER: 'npm',
|
||||
HOST_IP: '0.0.0.0',
|
||||
},
|
||||
type: type,
|
||||
resource: 'appstore',
|
||||
rebuild: false,
|
||||
codeDir: '/',
|
||||
});
|
||||
let runtime = reactive<Runtime.RuntimeCreate>(initData('node'));
|
||||
const rules = ref<any>({
|
||||
name: [Rules.appName],
|
||||
appId: [Rules.requiredSelect],
|
||||
codeDir: [Rules.requiredInput],
|
||||
params: {
|
||||
NODE_APP_PORT: [Rules.requiredInput, Rules.port],
|
||||
PANEL_APP_PORT_HTTP: [Rules.requiredInput, Rules.port],
|
||||
PACKAGE_MANAGER: [Rules.requiredSelect],
|
||||
HOST_IP: [Rules.requiredSelect],
|
||||
EXEC_SCRIPT: [Rules.requiredSelect],
|
||||
CONTAINER_NAME: [Rules.requiredInput],
|
||||
},
|
||||
});
|
||||
const scripts = ref<Runtime.NodeScripts[]>([]);
|
||||
const em = defineEmits(['close']);
|
||||
|
||||
watch(
|
||||
() => runtime.params['NODE_APP_PORT'],
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
runtime.params['PANEL_APP_PORT_HTTP'] = newVal;
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => runtime.name,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
runtime.params['CONTAINER_NAME'] = newVal;
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
em('close', false);
|
||||
};
|
||||
|
||||
const getPath = (codeDir: string) => {
|
||||
runtime.codeDir = codeDir;
|
||||
getScripts();
|
||||
};
|
||||
|
||||
const getScripts = () => {
|
||||
GetNodeScripts({ codeDir: runtime.codeDir }).then((res) => {
|
||||
scripts.value = res.data;
|
||||
if (scripts.value.length > 0) {
|
||||
runtime.params['EXEC_SCRIPT'] = scripts.value[0].script;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const searchApp = (appId: number) => {
|
||||
SearchApp(appReq).then((res) => {
|
||||
apps.value = res.data.items || [];
|
||||
if (res.data && res.data.items && res.data.items.length > 0) {
|
||||
if (appId == null) {
|
||||
runtime.appId = res.data.items[0].id;
|
||||
getApp(res.data.items[0].key, mode.value);
|
||||
} else {
|
||||
res.data.items.forEach((item) => {
|
||||
if (item.id === appId) {
|
||||
getApp(item.key, mode.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const changeApp = (appId: number) => {
|
||||
for (const app of apps.value) {
|
||||
if (app.id === appId) {
|
||||
getApp(app.key, mode.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const changeVersion = () => {
|
||||
loading.value = true;
|
||||
GetAppDetail(runtime.appId, runtime.version, 'runtime')
|
||||
.then((res) => {
|
||||
runtime.appDetailId = res.data.id;
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const getApp = (appkey: string, mode: string) => {
|
||||
GetApp(appkey).then((res) => {
|
||||
appVersions.value = res.data.versions || [];
|
||||
if (res.data.versions.length > 0) {
|
||||
runtime.version = res.data.versions[0];
|
||||
if (mode === 'create') {
|
||||
changeVersion();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
await formEl.validate((valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
if (mode.value == 'create') {
|
||||
loading.value = true;
|
||||
CreateRuntime(runtime)
|
||||
.then(() => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
|
||||
handleClose();
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
} else {
|
||||
loading.value = true;
|
||||
UpdateRuntime(runtime)
|
||||
.then(() => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
|
||||
handleClose();
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getRuntime = async (id: number) => {
|
||||
try {
|
||||
const res = await GetRuntime(id);
|
||||
const data = res.data;
|
||||
Object.assign(runtime, {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
appDetailId: data.appDetailId,
|
||||
image: data.image,
|
||||
params: {},
|
||||
type: data.type,
|
||||
resource: data.resource,
|
||||
appId: data.appId,
|
||||
version: data.version,
|
||||
rebuild: true,
|
||||
source: data.source,
|
||||
});
|
||||
editParams.value = data.appParams;
|
||||
if (mode.value == 'create') {
|
||||
searchApp(data.appId);
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const acceptParams = async (props: OperateRrops) => {
|
||||
mode.value = props.mode;
|
||||
if (props.mode === 'create') {
|
||||
Object.assign(runtime, initData(props.type));
|
||||
searchApp(null);
|
||||
} else {
|
||||
searchApp(null);
|
||||
getRuntime(props.id);
|
||||
}
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
@ -2,6 +2,13 @@
|
||||
<div>
|
||||
<RouterMenu />
|
||||
<LayoutContent :title="'Node.js'" v-loading="loading">
|
||||
<template #prompt>
|
||||
<el-alert type="info" :closable="false">
|
||||
<template #default>
|
||||
<span><span v-html="$t('runtime.statusHelper')"></span></span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</template>
|
||||
<template #toolbar>
|
||||
<el-button type="primary" @click="openCreate">
|
||||
{{ $t('runtime.create') }}
|
||||
@ -56,7 +63,7 @@
|
||||
/>
|
||||
<fu-table-operations
|
||||
:ellipsis="10"
|
||||
width="120px"
|
||||
width="250px"
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
@ -73,7 +80,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { Runtime } from '@/api/interface/runtime';
|
||||
import { SearchRuntimes } from '@/api/modules/runtime';
|
||||
import { OperateRuntime, SearchRuntimes } from '@/api/modules/runtime';
|
||||
import { dateFormat } from '@/utils/util';
|
||||
import OperateNode from '@/views/website/runtime/node/operate/index.vue';
|
||||
import Status from '@/components/status/index.vue';
|
||||
@ -97,13 +104,40 @@ const req = reactive<Runtime.RuntimeReq>({
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('container.stop'),
|
||||
click: function (row: Runtime.Runtime) {
|
||||
operateRuntime('down', row.id);
|
||||
},
|
||||
disabled: function (row: Runtime.Runtime) {
|
||||
return disabledRuntime(row) || row.status === 'stopped';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('container.start'),
|
||||
click: function (row: Runtime.Runtime) {
|
||||
operateRuntime('up', row.id);
|
||||
},
|
||||
disabled: function (row: Runtime.Runtime) {
|
||||
return disabledRuntime(row) || row.status === 'running';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('container.restart'),
|
||||
click: function (row: Runtime.Runtime) {
|
||||
operateRuntime('restart', row.id);
|
||||
},
|
||||
disabled: function (row: Runtime.Runtime) {
|
||||
return disabledRuntime(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
click: function (row: Runtime.Runtime) {
|
||||
openDetail(row);
|
||||
},
|
||||
disabled: function (row: Runtime.Runtime) {
|
||||
return row.status === 'starting' || row.status === 'recreating';
|
||||
return disabledRuntime(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -118,6 +152,10 @@ const items = ref<Runtime.RuntimeDTO[]>([]);
|
||||
const operateRef = ref();
|
||||
const deleteRef = ref();
|
||||
|
||||
const disabledRuntime = (row: Runtime.Runtime) => {
|
||||
return row.status === 'starting' || row.status === 'recreating';
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
req.page = paginationConfig.currentPage;
|
||||
req.pageSize = paginationConfig.pageSize;
|
||||
@ -144,6 +182,28 @@ const openDelete = async (row: Runtime.Runtime) => {
|
||||
deleteRef.value.acceptParams(row.id, row.name);
|
||||
};
|
||||
|
||||
const operateRuntime = async (operate: string, ID: number) => {
|
||||
try {
|
||||
const action = await ElMessageBox.confirm(
|
||||
i18n.global.t('runtime.operatorHelper', [i18n.global.t('commons.operate.' + operate)]),
|
||||
i18n.global.t('commons.operate.' + operate),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
},
|
||||
);
|
||||
if (action === 'confirm') {
|
||||
loading.value = true;
|
||||
await OperateRuntime({ operate: operate, ID: ID });
|
||||
search();
|
||||
}
|
||||
} catch (error) {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toFolder = (folder: string) => {
|
||||
router.push({ path: '/hosts/files', query: { path: folder } });
|
||||
};
|
||||
|
@ -156,6 +156,7 @@ const appReq = reactive({
|
||||
type: 'node',
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
resource: 'remote',
|
||||
});
|
||||
const initData = (type: string) => ({
|
||||
name: '',
|
||||
|
Loading…
x
Reference in New Issue
Block a user