1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-03-14 01:34:47 +08:00

feat: 运行环境增加 Node.js 管理 (#2390)

Refs https://github.com/1Panel-dev/1Panel/issues/397
This commit is contained in:
zhengkunwang 2023-09-25 17:50:14 +08:00 committed by GitHub
parent 38dadf6056
commit 1130a70052
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1974 additions and 396 deletions

View File

@ -70,7 +70,7 @@ func (b *BaseApi) DeleteRuntime(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return return
} }
err := runtimeService.Delete(req.ID) err := runtimeService.Delete(req)
if err != nil { if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
@ -121,3 +121,25 @@ func (b *BaseApi) GetRuntime(c *gin.Context) {
} }
helper.SuccessWithData(c, res) helper.SuccessWithData(c, res)
} }
// @Tags Runtime
// @Summary Get Node package scripts
// @Description 获取 Node 项目的 scripts
// @Accept json
// @Param request body request.NodePackageReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /runtimes/node/package [post]
func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
var req request.NodePackageReq
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
res, err := runtimeService.GetNodePackageRunScript(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}

View File

@ -18,10 +18,18 @@ type RuntimeCreate struct {
Type string `json:"type"` Type string `json:"type"`
Version string `json:"version"` Version string `json:"version"`
Source string `json:"source"` Source string `json:"source"`
CodeDir string `json:"codeDir"`
NodeConfig
}
type NodeConfig struct {
Install bool `json:"install"`
Clean bool `json:"clean"`
} }
type RuntimeDelete struct { type RuntimeDelete struct {
ID uint `json:"id"` ID uint `json:"id"`
ForceDelete bool `json:"forceDelete"`
} }
type RuntimeUpdate struct { type RuntimeUpdate struct {
@ -32,4 +40,10 @@ type RuntimeUpdate struct {
Version string `json:"version"` Version string `json:"version"`
Rebuild bool `json:"rebuild"` Rebuild bool `json:"rebuild"`
Source string `json:"source"` Source string `json:"source"`
CodeDir string `json:"codeDir"`
NodeConfig
}
type NodePackageReq struct {
CodeDir string `json:"codeDir"`
} }

View File

@ -1,10 +1,45 @@
package response package response
import "github.com/1Panel-dev/1Panel/backend/app/model" import (
"github.com/1Panel-dev/1Panel/backend/app/model"
"time"
)
type RuntimeRes struct { type RuntimeDTO struct {
model.Runtime ID uint `json:"id"`
AppParams []AppParam `json:"appParams"` Name string `json:"name"`
AppID uint `json:"appId"` Resource string `json:"resource"`
Source string `json:"source"` AppDetailID uint `json:"appDetailID"`
AppID uint `json:"appID"`
Source string `json:"source"`
Status string `json:"status"`
Type string `json:"type"`
Image string `json:"image"`
Params map[string]interface{} `json:"params"`
Message string `json:"message"`
Version string `json:"version"`
CreatedAt time.Time `json:"createdAt"`
CodeDir string `json:"codeDir"`
AppParams []AppParam `json:"appParams"`
}
type PackageScripts struct {
Name string `json:"name"`
Script string `json:"script"`
}
func NewRuntimeDTO(runtime model.Runtime) RuntimeDTO {
return RuntimeDTO{
ID: runtime.ID,
Name: runtime.Name,
Resource: runtime.Resource,
AppDetailID: runtime.AppDetailID,
Status: runtime.Status,
Type: runtime.Type,
Image: runtime.Image,
Message: runtime.Message,
CreatedAt: runtime.CreatedAt,
CodeDir: runtime.CodeDir,
Version: runtime.Version,
}
} }

View File

@ -1,5 +1,10 @@
package model package model
import (
"github.com/1Panel-dev/1Panel/backend/constant"
"path"
)
type Runtime struct { type Runtime struct {
BaseModel BaseModel
Name string `gorm:"type:varchar;not null" json:"name"` Name string `gorm:"type:varchar;not null" json:"name"`
@ -14,4 +19,21 @@ type Runtime struct {
Status string `gorm:"type:varchar;not null" json:"status"` Status string `gorm:"type:varchar;not null" json:"status"`
Resource string `gorm:"type:varchar;not null" json:"resource"` Resource string `gorm:"type:varchar;not null" json:"resource"`
Message string `gorm:"type:longtext;" json:"message"` Message string `gorm:"type:longtext;" json:"message"`
CodeDir string `gorm:"type:varchar;" json:"codeDir"`
}
func (r *Runtime) GetComposePath() string {
return path.Join(r.GetPath(), "docker-compose.yml")
}
func (r *Runtime) GetEnvPath() string {
return path.Join(r.GetPath(), ".env")
}
func (r *Runtime) GetPath() string {
return path.Join(constant.RuntimeDir, r.Type, r.Name)
}
func (r *Runtime) GetLogPath() string {
return path.Join(r.GetPath(), "build.log")
} }

View File

@ -176,36 +176,39 @@ func (a AppService) GetAppDetail(appId uint, version, appType string) (response.
return appDetailDTO, err return appDetailDTO, err
} }
} }
buildPath := path.Join(versionPath, "build") switch app.Type {
paramsPath := path.Join(buildPath, "config.json") case constant.RuntimePHP:
if !fileOp.Stat(paramsPath) { buildPath := path.Join(versionPath, "build")
return appDetailDTO, buserr.New(constant.ErrFileNotExist) paramsPath := path.Join(buildPath, "config.json")
} if !fileOp.Stat(paramsPath) {
param, err := fileOp.GetContent(paramsPath) return appDetailDTO, buserr.New(constant.ErrFileNotExist)
if err != nil { }
return appDetailDTO, err param, err := fileOp.GetContent(paramsPath)
} if err != nil {
paramMap := make(map[string]interface{}) return appDetailDTO, err
if err := json.Unmarshal(param, &paramMap); err != nil { }
return appDetailDTO, err paramMap := make(map[string]interface{})
} if err := json.Unmarshal(param, &paramMap); err != nil {
appDetailDTO.Params = paramMap return appDetailDTO, err
composePath := path.Join(buildPath, "docker-compose.yml") }
if !fileOp.Stat(composePath) { appDetailDTO.Params = paramMap
return appDetailDTO, buserr.New(constant.ErrFileNotExist) composePath := path.Join(buildPath, "docker-compose.yml")
} if !fileOp.Stat(composePath) {
compose, err := fileOp.GetContent(composePath) return appDetailDTO, buserr.New(constant.ErrFileNotExist)
if err != nil { }
return appDetailDTO, err compose, err := fileOp.GetContent(composePath)
} if err != nil {
composeMap := make(map[string]interface{}) return appDetailDTO, err
if err := yaml.Unmarshal(compose, &composeMap); err != nil { }
return appDetailDTO, err composeMap := make(map[string]interface{})
} if err := yaml.Unmarshal(compose, &composeMap); err != nil {
if service, ok := composeMap["services"]; ok { return appDetailDTO, err
servicesMap := service.(map[string]interface{}) }
for k := range servicesMap { if service, ok := composeMap["services"]; ok {
appDetailDTO.Image = k servicesMap := service.(map[string]interface{})
for k := range servicesMap {
appDetailDTO.Image = k
}
} }
} }
} else { } else {

View File

@ -3,7 +3,6 @@ package service
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/dto/request" "github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/dto/response" "github.com/1Panel-dev/1Panel/backend/app/dto/response"
@ -12,24 +11,26 @@ import (
"github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/compose"
"github.com/1Panel-dev/1Panel/backend/utils/docker" "github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/pkg/errors"
"github.com/subosito/gotenv" "github.com/subosito/gotenv"
"path" "path"
"path/filepath" "strconv"
"strings" "strings"
"time"
) )
type RuntimeService struct { type RuntimeService struct {
} }
type IRuntimeService interface { type IRuntimeService interface {
Page(req request.RuntimeSearch) (int64, []response.RuntimeRes, error) Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error)
Create(create request.RuntimeCreate) error Create(create request.RuntimeCreate) error
Delete(id uint) error Delete(delete request.RuntimeDelete) error
Update(req request.RuntimeUpdate) error Update(req request.RuntimeUpdate) error
Get(id uint) (res *response.RuntimeRes, err error) Get(id uint) (res *response.RuntimeDTO, err error)
GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error)
} }
func NewRuntimeService() IRuntimeService { func NewRuntimeService() IRuntimeService {
@ -37,24 +38,35 @@ func NewRuntimeService() IRuntimeService {
} }
func (r *RuntimeService) Create(create request.RuntimeCreate) (err error) { func (r *RuntimeService) Create(create request.RuntimeCreate) (err error) {
exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithName(create.Name)) exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithName(create.Name), commonRepo.WithByType(create.Type))
if exist != nil { if exist != nil {
return buserr.New(constant.ErrNameIsExist) return buserr.New(constant.ErrNameIsExist)
} }
if create.Resource == constant.ResourceLocal { fileOp := files.NewFileOp()
runtime := &model.Runtime{
Name: create.Name, switch create.Type {
Resource: create.Resource, case constant.RuntimePHP:
Type: create.Type, if create.Resource == constant.ResourceLocal {
Version: create.Version, runtime := &model.Runtime{
Status: constant.RuntimeNormal, Name: create.Name,
Resource: create.Resource,
Type: create.Type,
Version: create.Version,
Status: constant.RuntimeNormal,
}
return runtimeRepo.Create(context.Background(), runtime)
} }
return runtimeRepo.Create(context.Background(), runtime) exist, _ = runtimeRepo.GetFirst(runtimeRepo.WithImage(create.Image))
} if exist != nil {
exist, _ = runtimeRepo.GetFirst(runtimeRepo.WithImage(create.Image)) return buserr.New(constant.ErrImageExist)
if exist != nil { }
return buserr.New(constant.ErrImageExist) case constant.RuntimeNode:
if !fileOp.Stat(create.CodeDir) {
return buserr.New(constant.ErrPathNotFound)
}
create.Install = true
} }
appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(create.AppDetailID)) appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(create.AppDetailID))
if err != nil { if err != nil {
return err return err
@ -63,64 +75,39 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (err error) {
if err != nil { if err != nil {
return err return err
} }
fileOp := files.NewFileOp()
appVersionDir := path.Join(constant.AppResourceDir, app.Resource, app.Key, appDetail.Version) appVersionDir := path.Join(constant.AppResourceDir, app.Resource, app.Key, appDetail.Version)
if !fileOp.Stat(appVersionDir) || appDetail.Update { if !fileOp.Stat(appVersionDir) || appDetail.Update {
if err := downloadApp(app, appDetail, nil); err != nil { if err := downloadApp(app, appDetail, nil); err != nil {
return err return err
} }
} }
buildDir := path.Join(appVersionDir, "build")
if !fileOp.Stat(buildDir) {
return buserr.New(constant.ErrDirNotFound)
}
runtimeDir := path.Join(constant.RuntimeDir, create.Type)
tempDir := filepath.Join(runtimeDir, fmt.Sprintf("%d", time.Now().UnixNano()))
if err = fileOp.CopyDir(buildDir, tempDir); err != nil {
return
}
oldDir := path.Join(tempDir, "build")
newNameDir := path.Join(runtimeDir, create.Name)
defer func() {
if err != nil {
_ = fileOp.DeleteDir(newNameDir)
}
}()
if oldDir != newNameDir {
if err = fileOp.Rename(oldDir, newNameDir); err != nil {
return
}
if err = fileOp.DeleteDir(tempDir); err != nil {
return
}
}
composeContent, envContent, forms, err := handleParams(create.Image, create.Type, newNameDir, create.Source, create.Params)
if err != nil {
return
}
runtime := &model.Runtime{ runtime := &model.Runtime{
Name: create.Name, Name: create.Name,
DockerCompose: string(composeContent), AppDetailID: create.AppDetailID,
Env: string(envContent), Type: create.Type,
AppDetailID: create.AppDetailID, Image: create.Image,
Type: create.Type, Resource: create.Resource,
Image: create.Image, Version: create.Version,
Resource: create.Resource,
Status: constant.RuntimeBuildIng,
Version: create.Version,
Params: string(forms),
} }
if err = runtimeRepo.Create(context.Background(), runtime); err != nil {
return switch create.Type {
case constant.RuntimePHP:
if err = handlePHP(create, runtime, fileOp, appVersionDir); err != nil {
return
}
case constant.RuntimeNode:
if err = handleNode(create, runtime, fileOp, appVersionDir); err != nil {
return
}
} }
go buildRuntime(runtime, "", false) return runtimeRepo.Create(context.Background(), runtime)
return
} }
func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeRes, error) { func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeDTO, error) {
var ( var (
opts []repo.DBOption opts []repo.DBOption
res []response.RuntimeRes res []response.RuntimeDTO
) )
if req.Name != "" { if req.Name != "" {
opts = append(opts, commonRepo.WithLikeName(req.Name)) opts = append(opts, commonRepo.WithLikeName(req.Name))
@ -128,116 +115,158 @@ func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.Runt
if req.Status != "" { if req.Status != "" {
opts = append(opts, runtimeRepo.WithStatus(req.Status)) opts = append(opts, runtimeRepo.WithStatus(req.Status))
} }
if req.Type != "" {
opts = append(opts, commonRepo.WithByType(req.Type))
}
total, runtimes, err := runtimeRepo.Page(req.Page, req.PageSize, opts...) total, runtimes, err := runtimeRepo.Page(req.Page, req.PageSize, opts...)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
for _, runtime := range runtimes { for _, runtime := range runtimes {
res = append(res, response.RuntimeRes{ runtimeDTO := response.NewRuntimeDTO(runtime)
Runtime: runtime, runtimeDTO.Params = make(map[string]interface{})
}) envs, err := gotenv.Unmarshal(runtime.Env)
if err != nil {
return 0, nil, err
}
for k, v := range envs {
runtimeDTO.Params[k] = v
}
res = append(res, runtimeDTO)
} }
return total, res, nil return total, res, nil
} }
func (r *RuntimeService) Delete(id uint) error { func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error {
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(id)) runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(runtimeDelete.ID))
if err != nil { if err != nil {
return err return err
} }
website, _ := websiteRepo.GetFirst(websiteRepo.WithRuntimeID(id)) website, _ := websiteRepo.GetFirst(websiteRepo.WithRuntimeID(runtimeDelete.ID))
if website.ID > 0 { if website.ID > 0 {
return buserr.New(constant.ErrDelWithWebsite) return buserr.New(constant.ErrDelWithWebsite)
} }
if runtime.Resource == constant.ResourceAppstore { if runtime.Resource == constant.ResourceAppstore {
client, err := docker.NewClient() projectDir := runtime.GetPath()
if err != nil { switch runtime.Type {
return err case constant.RuntimePHP:
} client, err := docker.NewClient()
imageID, err := client.GetImageIDByName(runtime.Image) if err != nil {
if err != nil { return err
return err }
} imageID, err := client.GetImageIDByName(runtime.Image)
if imageID != "" { if err != nil {
if err := client.DeleteImage(imageID); err != nil { return err
global.LOG.Errorf("delete image id [%s] error %v", imageID, err) }
if imageID != "" {
if err := client.DeleteImage(imageID); err != nil {
global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
}
}
case constant.RuntimeNode:
if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete {
if out != "" {
return errors.New(out)
}
return err
} }
} }
runtimeDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name) if err := files.NewFileOp().DeleteDir(projectDir); err != nil && !runtimeDelete.ForceDelete {
if err := files.NewFileOp().DeleteDir(runtimeDir); err != nil {
return err return err
} }
} }
return runtimeRepo.DeleteBy(commonRepo.WithByID(id)) return runtimeRepo.DeleteBy(commonRepo.WithByID(runtimeDelete.ID))
} }
func (r *RuntimeService) Get(id uint) (*response.RuntimeRes, error) { func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(id)) runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(id))
if err != nil { if err != nil {
return nil, err return nil, err
} }
res := &response.RuntimeRes{}
res.Runtime = *runtime res := response.NewRuntimeDTO(*runtime)
if runtime.Resource == constant.ResourceLocal { if runtime.Resource == constant.ResourceLocal {
return res, nil return &res, nil
} }
appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID)) appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
res.AppID = appDetail.AppId res.AppID = appDetail.AppId
var ( switch runtime.Type {
appForm dto.AppForm case constant.RuntimePHP:
appParams []response.AppParam var (
) appForm dto.AppForm
if err := json.Unmarshal([]byte(runtime.Params), &appForm); err != nil { appParams []response.AppParam
return nil, err )
} if err := json.Unmarshal([]byte(runtime.Params), &appForm); err != nil {
envs, err := gotenv.Unmarshal(runtime.Env) return nil, err
if err != nil { }
return nil, err envs, err := gotenv.Unmarshal(runtime.Env)
} if err != nil {
if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok { return nil, err
res.Source = v }
} if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok {
for _, form := range appForm.FormFields { res.Source = v
if v, ok := envs[form.EnvKey]; ok { }
appParam := response.AppParam{ for _, form := range appForm.FormFields {
Edit: false, if v, ok := envs[form.EnvKey]; ok {
Key: form.EnvKey, appParam := response.AppParam{
Rule: form.Rule, Edit: false,
Type: form.Type, Key: form.EnvKey,
Required: form.Required, Rule: form.Rule,
} Type: form.Type,
if form.Edit { Required: form.Required,
appParam.Edit = true }
} if form.Edit {
appParam.LabelZh = form.LabelZh appParam.Edit = true
appParam.LabelEn = form.LabelEn }
appParam.Multiple = form.Multiple appParam.LabelZh = form.LabelZh
appParam.Value = v appParam.LabelEn = form.LabelEn
if form.Type == "select" { appParam.Multiple = form.Multiple
if form.Multiple { appParam.Value = v
if v == "" { if form.Type == "select" {
appParam.Value = []string{} if form.Multiple {
if v == "" {
appParam.Value = []string{}
} else {
appParam.Value = strings.Split(v, ",")
}
} else { } else {
appParam.Value = strings.Split(v, ",") for _, fv := range form.Values {
} if fv.Value == v {
} else { appParam.ShowValue = fv.Label
for _, fv := range form.Values { break
if fv.Value == v { }
appParam.ShowValue = fv.Label
break
} }
} }
appParam.Values = form.Values
} }
appParam.Values = form.Values appParams = append(appParams, appParam)
}
}
res.AppParams = appParams
case constant.RuntimeNode:
res.Params = make(map[string]interface{})
envs, err := gotenv.Unmarshal(runtime.Env)
if err != nil {
return nil, err
}
for k, v := range envs {
switch k {
case "NODE_APP_PORT", "PANEL_APP_PORT_HTTP":
port, err := strconv.Atoi(v)
if err != nil {
return nil, err
}
res.Params[k] = port
default:
res.Params[k] = v
} }
appParams = append(appParams, appParam)
} }
} }
res.AppParams = appParams
return res, nil return &res, nil
} }
func (r *RuntimeService) Update(req request.RuntimeUpdate) error { func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
@ -245,36 +274,86 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
if err != nil { if err != nil {
return err return err
} }
oldImage := runtime.Image
if runtime.Resource == constant.ResourceLocal { if runtime.Resource == constant.ResourceLocal {
runtime.Version = req.Version runtime.Version = req.Version
return runtimeRepo.Save(runtime) return runtimeRepo.Save(runtime)
} }
exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithImage(req.Name), runtimeRepo.WithNotId(req.ID)) oldImage := runtime.Image
if exist != nil { switch runtime.Type {
return buserr.New(constant.ErrImageExist) case constant.RuntimePHP:
exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithImage(req.Name), runtimeRepo.WithNotId(req.ID))
if exist != nil {
return buserr.New(constant.ErrImageExist)
}
} }
runtimeDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name)
composeContent, envContent, _, err := handleParams(req.Image, runtime.Type, runtimeDir, req.Source, req.Params) projectDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name)
create := request.RuntimeCreate{
Image: req.Image,
Type: runtime.Type,
Source: req.Source,
Params: req.Params,
CodeDir: req.CodeDir,
Version: req.Version,
}
composeContent, envContent, _, err := handleParams(create, projectDir)
if err != nil { if err != nil {
return err return err
} }
if err != nil {
return err
}
runtime.Image = req.Image
runtime.Env = string(envContent) runtime.Env = string(envContent)
runtime.DockerCompose = string(composeContent) runtime.DockerCompose = string(composeContent)
runtime.Status = constant.RuntimeBuildIng
_ = runtimeRepo.Save(runtime) switch runtime.Type {
client, err := docker.NewClient() case constant.RuntimePHP:
if err != nil { runtime.Image = req.Image
return err runtime.Status = constant.RuntimeBuildIng
_ = runtimeRepo.Save(runtime)
client, err := docker.NewClient()
if err != nil {
return err
}
imageID, err := client.GetImageIDByName(oldImage)
if err != nil {
return err
}
go buildRuntime(runtime, imageID, req.Rebuild)
case constant.RuntimeNode:
runtime.Version = req.Version
runtime.CodeDir = req.CodeDir
runtime.Status = constant.RuntimeReCreating
_ = runtimeRepo.Save(runtime)
go reCreateRuntime(runtime)
} }
imageID, err := client.GetImageIDByName(oldImage)
if err != nil {
return err
}
go buildRuntime(runtime, imageID, req.Rebuild)
return nil return nil
} }
func (r *RuntimeService) GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error) {
fileOp := files.NewFileOp()
if !fileOp.Stat(req.CodeDir) {
return nil, buserr.New(constant.ErrPathNotFound)
}
if !fileOp.Stat(path.Join(req.CodeDir, "package.json")) {
return nil, buserr.New(constant.ErrPackageJsonNotFound)
}
content, err := fileOp.GetContent(path.Join(req.CodeDir, "package.json"))
if err != nil {
return nil, err
}
var packageMap map[string]interface{}
err = json.Unmarshal(content, &packageMap)
if err != nil {
return nil, err
}
scripts, ok := packageMap["scripts"]
if !ok {
return nil, buserr.New(constant.ErrScriptsNotFound)
}
var packageScripts []response.PackageScripts
for k, v := range scripts.(map[string]interface{}) {
packageScripts = append(packageScripts, response.PackageScripts{
Name: k,
Script: v.(string),
})
}
return packageScripts, nil
}

View File

@ -10,22 +10,159 @@ import (
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/docker" "github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/pkg/errors"
"github.com/subosito/gotenv" "github.com/subosito/gotenv"
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path" "path"
"path/filepath"
"strings" "strings"
"time"
) )
func handleNode(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) {
runtimeDir := path.Join(constant.RuntimeDir, create.Type)
if err = fileOp.CopyDir(appVersionDir, runtimeDir); err != nil {
return
}
versionDir := path.Join(runtimeDir, filepath.Base(appVersionDir))
projectDir := path.Join(runtimeDir, create.Name)
defer func() {
if err != nil {
_ = fileOp.DeleteDir(projectDir)
}
}()
if err = fileOp.Rename(versionDir, projectDir); err != nil {
return
}
composeContent, envContent, _, err := handleParams(create, projectDir)
if err != nil {
return
}
runtime.DockerCompose = string(composeContent)
runtime.Env = string(envContent)
runtime.Status = constant.RuntimeStarting
runtime.CodeDir = create.CodeDir
go startRuntime(runtime)
return
}
func handlePHP(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) {
buildDir := path.Join(appVersionDir, "build")
if !fileOp.Stat(buildDir) {
return buserr.New(constant.ErrDirNotFound)
}
runtimeDir := path.Join(constant.RuntimeDir, create.Type)
tempDir := filepath.Join(runtimeDir, fmt.Sprintf("%d", time.Now().UnixNano()))
if err = fileOp.CopyDir(buildDir, tempDir); err != nil {
return
}
oldDir := path.Join(tempDir, "build")
projectDir := path.Join(runtimeDir, create.Name)
defer func() {
if err != nil {
_ = fileOp.DeleteDir(projectDir)
}
}()
if oldDir != projectDir {
if err = fileOp.Rename(oldDir, projectDir); err != nil {
return
}
if err = fileOp.DeleteDir(tempDir); err != nil {
return
}
}
composeContent, envContent, forms, err := handleParams(create, projectDir)
if err != nil {
return
}
runtime.DockerCompose = string(composeContent)
runtime.Env = string(envContent)
runtime.Params = string(forms)
runtime.Status = constant.RuntimeBuildIng
go buildRuntime(runtime, "", false)
return
}
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)
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 {
runtime.Status = constant.RuntimeError
runtime.Message = buserr.New(constant.ErrRuntimeStart).Error() + ":" + stderrBuf.String()
} else {
runtime.Status = constant.RuntimeRunning
runtime.Message = ""
}
_ = runtimeRepo.Save(runtime)
}
func runComposeCmdWithLog(operate string, composePath string, logPath string) error {
cmd := exec.Command("docker-compose", "-f", composePath, operate)
if operate == "up" {
cmd = exec.Command("docker-compose", "-f", composePath, operate, "-d")
}
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)
return err
}
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 {
return errors.New(buserr.New(constant.ErrRuntimeStart).Error() + ":" + stderrBuf.String())
}
return nil
}
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
}
runtime.Status = constant.RuntimeRunning
_ = runtimeRepo.Save(runtime)
}
func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) { func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) {
runtimePath := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name) runtimePath := runtime.GetPath()
composePath := path.Join(runtimePath, "docker-compose.yml") composePath := runtime.GetComposePath()
logPath := path.Join(runtimePath, "build.log") logPath := path.Join(runtimePath, "build.log")
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil { if err != nil {
fmt.Println("Failed to open log file:", err) global.LOG.Errorf("failed to open log file: %v", err)
return return
} }
defer func() { defer func() {
@ -89,35 +226,45 @@ func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) {
_ = runtimeRepo.Save(runtime) _ = runtimeRepo.Save(runtime)
} }
func handleParams(image, runtimeType, runtimeDir, source string, params map[string]interface{}) (composeContent []byte, envContent []byte, forms []byte, err error) { func handleParams(create request.RuntimeCreate, projectDir string) (composeContent []byte, envContent []byte, forms []byte, err error) {
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
composeContent, err = fileOp.GetContent(path.Join(runtimeDir, "docker-compose.yml")) composeContent, err = fileOp.GetContent(path.Join(projectDir, "docker-compose.yml"))
if err != nil { if err != nil {
return return
} }
env, err := gotenv.Read(path.Join(runtimeDir, ".env")) env, err := gotenv.Read(path.Join(projectDir, ".env"))
if err != nil { if err != nil {
return return
} }
forms, err = fileOp.GetContent(path.Join(runtimeDir, "config.json")) switch create.Type {
if err != nil { case constant.RuntimePHP:
return create.Params["IMAGE_NAME"] = create.Image
} forms, err = fileOp.GetContent(path.Join(projectDir, "config.json"))
params["IMAGE_NAME"] = image if err != nil {
if runtimeType == constant.RuntimePHP { return
if extends, ok := params["PHP_EXTENSIONS"]; ok { }
if extends, ok := create.Params["PHP_EXTENSIONS"]; ok {
if extendsArray, ok := extends.([]interface{}); ok { if extendsArray, ok := extends.([]interface{}); ok {
strArray := make([]string, len(extendsArray)) strArray := make([]string, len(extendsArray))
for i, v := range extendsArray { for i, v := range extendsArray {
strArray[i] = strings.ToLower(fmt.Sprintf("%v", v)) strArray[i] = strings.ToLower(fmt.Sprintf("%v", v))
} }
params["PHP_EXTENSIONS"] = strings.Join(strArray, ",") create.Params["PHP_EXTENSIONS"] = strings.Join(strArray, ",")
} }
} }
params["CONTAINER_PACKAGE_URL"] = source create.Params["CONTAINER_PACKAGE_URL"] = create.Source
case constant.RuntimeNode:
create.Params["CODE_DIR"] = create.CodeDir
create.Params["NODE_VERSION"] = create.Version
if create.NodeConfig.Install {
create.Params["RUN_INSTALL"] = "1"
} else {
create.Params["RUN_INSTALL"] = "0"
}
} }
newMap := make(map[string]string) newMap := make(map[string]string)
handleMap(params, newMap) handleMap(create.Params, newMap)
for k, v := range newMap { for k, v := range newMap {
env[k] = v env[k] = v
} }
@ -125,7 +272,7 @@ func handleParams(image, runtimeType, runtimeDir, source string, params map[stri
if err != nil { if err != nil {
return return
} }
if err = gotenv.Write(env, path.Join(runtimeDir, ".env")); err != nil { if err = gotenv.Write(env, path.Join(projectDir, ".env")); err != nil {
return return
} }
envContent = []byte(envStr) envContent = []byte(envStr)

View File

@ -114,11 +114,14 @@ var (
// runtime // runtime
var ( var (
ErrDirNotFound = "ErrDirNotFound" ErrDirNotFound = "ErrDirNotFound"
ErrFileNotExist = "ErrFileNotExist" ErrFileNotExist = "ErrFileNotExist"
ErrImageBuildErr = "ErrImageBuildErr" ErrImageBuildErr = "ErrImageBuildErr"
ErrImageExist = "ErrImageExist" ErrImageExist = "ErrImageExist"
ErrDelWithWebsite = "ErrDelWithWebsite" ErrDelWithWebsite = "ErrDelWithWebsite"
ErrRuntimeStart = "ErrRuntimeStart"
ErrPackageJsonNotFound = "ErrPackageJsonNotFound"
ErrScriptsNotFound = "ErrScriptsNotFound"
) )
var ( var (

View File

@ -4,11 +4,15 @@ const (
ResourceLocal = "local" ResourceLocal = "local"
ResourceAppstore = "appstore" ResourceAppstore = "appstore"
RuntimeNormal = "normal" RuntimeNormal = "normal"
RuntimeError = "error" RuntimeError = "error"
RuntimeBuildIng = "building" RuntimeBuildIng = "building"
RuntimeStarting = "starting"
RuntimeRunning = "running"
RuntimeReCreating = "recreating"
RuntimePHP = "php" RuntimePHP = "php"
RuntimeNode = "node"
RuntimeProxyUnix = "unix" RuntimeProxyUnix = "unix"
RuntimeProxyTcp = "tcp" RuntimeProxyTcp = "tcp"

View File

@ -99,6 +99,9 @@ ErrFileNotExist: "{{ .detail }} file does not exist! Please check source file in
ErrImageBuildErr: "Image build failed" ErrImageBuildErr: "Image build failed"
ErrImageExist: "Image is already exist" ErrImageExist: "Image is already exist"
ErrDelWithWebsite: "The operating environment has been associated with a website and cannot be deleted" ErrDelWithWebsite: "The operating environment has been associated with a website and cannot be deleted"
ErrRuntimeStart: "Failed to start"
ErrPackageJsonNotFound: "package.json file does not exist"
ErrScriptsNotFound: "No scripts configuration item was found in package.json"
#setting #setting
ErrBackupInUsed: "The backup account is already being used in a cronjob and cannot be deleted." ErrBackupInUsed: "The backup account is already being used in a cronjob and cannot be deleted."

View File

@ -99,6 +99,9 @@ ErrFileNotExist: "{{ .detail }} 文件不存在!請檢查源文件完整性!
ErrImageBuildErr: "鏡像 build 失敗" ErrImageBuildErr: "鏡像 build 失敗"
ErrImageExist: "鏡像已存在!" ErrImageExist: "鏡像已存在!"
ErrDelWithWebsite: "運行環境已經關聯網站,無法刪除" ErrDelWithWebsite: "運行環境已經關聯網站,無法刪除"
ErrRuntimeStart: "啟動失敗"
ErrPackageJsonNotFound: "package.json 文件不存在"
ErrScriptsNotFound: "沒有在 package.json 中找到 scripts 配置項"
#setting #setting
ErrBackupInUsed: "該備份帳號已在計劃任務中使用,無法刪除" ErrBackupInUsed: "該備份帳號已在計劃任務中使用,無法刪除"

View File

@ -99,6 +99,9 @@ ErrFileNotExist: "{{ .detail }} 文件不存在!请检查源文件完整性!
ErrImageBuildErr: "镜像 build 失败" ErrImageBuildErr: "镜像 build 失败"
ErrImageExist: "镜像已存在!" ErrImageExist: "镜像已存在!"
ErrDelWithWebsite: "运行环境已经关联网站,无法删除" ErrDelWithWebsite: "运行环境已经关联网站,无法删除"
ErrRuntimeStart: "启动失败"
ErrPackageJsonNotFound: "package.json 文件不存在"
ErrScriptsNotFound: "没有在 package.json 中找到 scripts 配置项"
#setting #setting
ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除" ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除"

View File

@ -45,6 +45,7 @@ func Init() {
migrations.DropDatabaseLocal, migrations.DropDatabaseLocal,
migrations.AddDefaultNetwork, migrations.AddDefaultNetwork,
migrations.UpdateRuntime,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)

View File

@ -15,3 +15,13 @@ var AddDefaultNetwork = &gormigrate.Migration{
return nil return nil
}, },
} }
var UpdateRuntime = &gormigrate.Migration{
ID: "20230920-update-runtime",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Runtime{}); err != nil {
return err
}
return nil
},
}

View File

@ -20,5 +20,6 @@ func (r *RuntimeRouter) InitRuntimeRouter(Router *gin.RouterGroup) {
groupRouter.POST("/del", baseApi.DeleteRuntime) groupRouter.POST("/del", baseApi.DeleteRuntime)
groupRouter.POST("/update", baseApi.UpdateRuntime) groupRouter.POST("/update", baseApi.UpdateRuntime)
groupRouter.GET("/:id", baseApi.GetRuntime) groupRouter.GET("/:id", baseApi.GetRuntime)
groupRouter.POST("/node/package", baseApi.GetNodePackageRunScript)
} }
} }

View File

@ -1,5 +1,5 @@
// Code generated by swaggo/swag. DO NOT EDIT. // Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
package docs package docs
import "github.com/swaggo/swag" import "github.com/swaggo/swag"
@ -7830,6 +7830,39 @@ const docTemplate = `{
} }
} }
}, },
"/runtimes/node/package": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Node 项目的 scripts",
"consumes": [
"application/json"
],
"tags": [
"Runtime"
],
"summary": "Get Node package scripts",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.NodePackageReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/runtimes/search": { "/runtimes/search": {
"post": { "post": {
"security": [ "security": [
@ -12262,8 +12295,26 @@ const docTemplate = `{
"cpuPercent": { "cpuPercent": {
"type": "number" "type": "number"
}, },
"cpuTotalUsage": {
"type": "integer"
},
"memoryCache": {
"type": "integer"
},
"memoryLimit": {
"type": "integer"
},
"memoryPercent": { "memoryPercent": {
"type": "number" "type": "number"
},
"memoryUsage": {
"type": "integer"
},
"percpuUsage": {
"type": "integer"
},
"systemUsage": {
"type": "integer"
} }
} }
}, },
@ -16357,6 +16408,14 @@ const docTemplate = `{
} }
} }
}, },
"request.NodePackageReq": {
"type": "object",
"properties": {
"codeDir": {
"type": "string"
}
}
},
"request.PortUpdate": { "request.PortUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -16388,9 +16447,18 @@ const docTemplate = `{
"appDetailId": { "appDetailId": {
"type": "integer" "type": "integer"
}, },
"clean": {
"type": "boolean"
},
"codeDir": {
"type": "string"
},
"image": { "image": {
"type": "string" "type": "string"
}, },
"install": {
"type": "boolean"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -16415,6 +16483,9 @@ const docTemplate = `{
"request.RuntimeDelete": { "request.RuntimeDelete": {
"type": "object", "type": "object",
"properties": { "properties": {
"forceDelete": {
"type": "boolean"
},
"id": { "id": {
"type": "integer" "type": "integer"
} }
@ -16447,12 +16518,21 @@ const docTemplate = `{
"request.RuntimeUpdate": { "request.RuntimeUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {
"clean": {
"type": "boolean"
},
"codeDir": {
"type": "string"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
"image": { "image": {
"type": "string" "type": "string"
}, },
"install": {
"type": "boolean"
},
"name": { "name": {
"type": "string" "type": "string"
}, },

View File

@ -7823,6 +7823,39 @@
} }
} }
}, },
"/runtimes/node/package": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 Node 项目的 scripts",
"consumes": [
"application/json"
],
"tags": [
"Runtime"
],
"summary": "Get Node package scripts",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.NodePackageReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/runtimes/search": { "/runtimes/search": {
"post": { "post": {
"security": [ "security": [
@ -12255,8 +12288,26 @@
"cpuPercent": { "cpuPercent": {
"type": "number" "type": "number"
}, },
"cpuTotalUsage": {
"type": "integer"
},
"memoryCache": {
"type": "integer"
},
"memoryLimit": {
"type": "integer"
},
"memoryPercent": { "memoryPercent": {
"type": "number" "type": "number"
},
"memoryUsage": {
"type": "integer"
},
"percpuUsage": {
"type": "integer"
},
"systemUsage": {
"type": "integer"
} }
} }
}, },
@ -16350,6 +16401,14 @@
} }
} }
}, },
"request.NodePackageReq": {
"type": "object",
"properties": {
"codeDir": {
"type": "string"
}
}
},
"request.PortUpdate": { "request.PortUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -16381,9 +16440,18 @@
"appDetailId": { "appDetailId": {
"type": "integer" "type": "integer"
}, },
"clean": {
"type": "boolean"
},
"codeDir": {
"type": "string"
},
"image": { "image": {
"type": "string" "type": "string"
}, },
"install": {
"type": "boolean"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -16408,6 +16476,9 @@
"request.RuntimeDelete": { "request.RuntimeDelete": {
"type": "object", "type": "object",
"properties": { "properties": {
"forceDelete": {
"type": "boolean"
},
"id": { "id": {
"type": "integer" "type": "integer"
} }
@ -16440,12 +16511,21 @@
"request.RuntimeUpdate": { "request.RuntimeUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {
"clean": {
"type": "boolean"
},
"codeDir": {
"type": "string"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
"image": { "image": {
"type": "string" "type": "string"
}, },
"install": {
"type": "boolean"
},
"name": { "name": {
"type": "string" "type": "string"
}, },

View File

@ -339,8 +339,20 @@ definitions:
type: string type: string
cpuPercent: cpuPercent:
type: number type: number
cpuTotalUsage:
type: integer
memoryCache:
type: integer
memoryLimit:
type: integer
memoryPercent: memoryPercent:
type: number type: number
memoryUsage:
type: integer
percpuUsage:
type: integer
systemUsage:
type: integer
type: object type: object
dto.ContainerOperate: dto.ContainerOperate:
properties: properties:
@ -3085,6 +3097,11 @@ definitions:
required: required:
- scope - scope
type: object type: object
request.NodePackageReq:
properties:
codeDir:
type: string
type: object
request.PortUpdate: request.PortUpdate:
properties: properties:
key: key:
@ -3105,8 +3122,14 @@ definitions:
properties: properties:
appDetailId: appDetailId:
type: integer type: integer
clean:
type: boolean
codeDir:
type: string
image: image:
type: string type: string
install:
type: boolean
name: name:
type: string type: string
params: params:
@ -3123,6 +3146,8 @@ definitions:
type: object type: object
request.RuntimeDelete: request.RuntimeDelete:
properties: properties:
forceDelete:
type: boolean
id: id:
type: integer type: integer
type: object type: object
@ -3144,10 +3169,16 @@ definitions:
type: object type: object
request.RuntimeUpdate: request.RuntimeUpdate:
properties: properties:
clean:
type: boolean
codeDir:
type: string
id: id:
type: integer type: integer
image: image:
type: string type: string
install:
type: boolean
name: name:
type: string type: string
params: params:
@ -9035,6 +9066,26 @@ paths:
formatEN: Delete website [name] formatEN: Delete website [name]
formatZH: 删除网站 [name] formatZH: 删除网站 [name]
paramKeys: [] paramKeys: []
/runtimes/node/package:
post:
consumes:
- application/json
description: 获取 Node 项目的 scripts
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.NodePackageReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Get Node package scripts
tags:
- Runtime
/runtimes/search: /runtimes/search:
post: post:
consumes: consumes:

View File

@ -3,7 +3,7 @@ import { App } from './app';
export namespace Runtime { export namespace Runtime {
export interface Runtime extends CommonModel { export interface Runtime extends CommonModel {
name: string; name: string;
appDetailId: number; appDetailID: number;
image: string; image: string;
workDir: string; workDir: string;
dockerCompose: string; dockerCompose: string;
@ -13,46 +13,59 @@ export namespace Runtime {
resource: string; resource: string;
version: string; version: string;
status: string; status: string;
codeDir: string;
} }
export interface RuntimeReq extends ReqPage { export interface RuntimeReq extends ReqPage {
name?: string; name?: string;
status?: string; status?: string;
type?: string;
}
export interface NodeReq {
codeDir: string;
}
export interface NodeScripts {
name: string;
script: string;
} }
export interface RuntimeDTO extends Runtime { export interface RuntimeDTO extends Runtime {
appParams: App.InstallParams[]; appParams: App.InstallParams[];
appId: number; appID: number;
source?: string; source?: string;
} }
export interface RuntimeCreate { export interface RuntimeCreate {
id?: number; id?: number;
name: string; name: string;
appDetailId: number; appDetailID: number;
image: string; image: string;
params: object; params: object;
type: string; type: string;
resource: string; resource: string;
appId?: number; appID?: number;
version?: string; version?: string;
rebuild?: boolean; rebuild?: boolean;
source?: string; source?: string;
codeDir?: string;
} }
export interface RuntimeUpdate { export interface RuntimeUpdate {
name: string; name: string;
appDetailId: number; appDetailID: number;
image: string; image: string;
params: object; params: object;
type: string; type: string;
resource: string; resource: string;
appId?: number; appID?: number;
version?: string; version?: string;
rebuild?: boolean; rebuild?: boolean;
} }
export interface RuntimeDelete { export interface RuntimeDelete {
id: number; id: number;
forceDelete: boolean;
} }
} }

View File

@ -21,3 +21,7 @@ export const GetRuntime = (id: number) => {
export const UpdateRuntime = (req: Runtime.RuntimeUpdate) => { export const UpdateRuntime = (req: Runtime.RuntimeUpdate) => {
return http.post<any>(`/runtimes/update`, req); return http.post<any>(`/runtimes/update`, req);
}; };
export const GetNodeScripts = (req: Runtime.NodeReq) => {
return http.post<Runtime.NodeScripts[]>(`/runtimes/node/package`, req);
};

View File

@ -8,7 +8,7 @@
popper-class="file-list" popper-class="file-list"
> >
<template #reference> <template #reference>
<el-button :icon="Folder" @click="popoverVisible = true"></el-button> <el-button :icon="Folder" :disabled="disabled" @click="popoverVisible = true"></el-button>
</template> </template>
<div> <div>
<el-button class="close" link @click="closePage"> <el-button class="close" link @click="closePage">
@ -116,6 +116,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
disabled: {
type: Boolean,
default: false,
},
}); });
const em = defineEmits(['choose']); const em = defineEmits(['choose']);

View File

@ -34,7 +34,7 @@ const getType = (status: string) => {
} }
}; };
const loadingStatus = ['installing', 'building', 'restarting', 'upgrading', 'rebuilding']; const loadingStatus = ['installing', 'building', 'restarting', 'upgrading', 'rebuilding', 'recreating', 'creating'];
const loadingIcon = (status: string): boolean => { const loadingIcon = (status: string): boolean => {
return loadingStatus.indexOf(status) > -1; return loadingStatus.indexOf(status) > -1;

View File

@ -228,6 +228,8 @@ const message = {
accept: 'Accepted', accept: 'Accepted',
used: 'Used', used: 'Used',
unUsed: 'Unused', unUsed: 'Unused',
starting: 'Starting',
recreating: 'Recreating',
}, },
units: { units: {
second: 'Second', second: 'Second',
@ -1691,6 +1693,16 @@ const message = {
xtomhk: 'XTOM Mirror Station (Hong Kong)', xtomhk: 'XTOM Mirror Station (Hong Kong)',
xtom: 'XTOM Mirror Station (Global)', xtom: 'XTOM Mirror Station (Global)',
phpsourceHelper: 'Choose the appropriate source according to your network environment', phpsourceHelper: 'Choose the appropriate source according to your network environment',
appPort: 'App Port',
externalPort: 'External Port',
packageManager: 'Package Manager',
codeDir: 'Code Directory',
appPortHelper: 'The port used by the application',
externalPortHelper: 'The port exposed to the outside world',
runScript: 'Run Script',
runScriptHelper: 'The startup command list is parsed from the package.json file in the source directory',
open: 'Open',
close: 'Close',
}, },
process: { process: {
pid: 'Process ID', pid: 'Process ID',

View File

@ -226,6 +226,8 @@ const message = {
accept: '已放行', accept: '已放行',
used: '已使用', used: '已使用',
unUsed: '未使用', unUsed: '未使用',
starting: '啟動中',
recreating: '重建中',
}, },
units: { units: {
second: '秒', second: '秒',
@ -1600,6 +1602,16 @@ const message = {
xtomhk: 'XTOM 鏡像站香港', xtomhk: 'XTOM 鏡像站香港',
xtom: 'XTOM 鏡像站全球', xtom: 'XTOM 鏡像站全球',
phpsourceHelper: '根據你的網絡環境選擇合適的源', phpsourceHelper: '根據你的網絡環境選擇合適的源',
appPort: '應用端口',
externalPort: '外部映射端口',
packageManager: '包管理器',
codeDir: '源碼目錄',
appPortHelper: '應用端口是指容器內部運行的端口',
externalPortHelper: '外部映射端口是指將容器內部端口映射到外部的端口',
runScript: '啟動命令',
runScriptHelper: '啟動命令是指容器啟動後運行的命令',
open: '開啟',
close: '關閉',
}, },
process: { process: {
pid: '進程ID', pid: '進程ID',

View File

@ -226,6 +226,8 @@ const message = {
accept: '已放行', accept: '已放行',
used: '已使用', used: '已使用',
unUsed: '未使用', unUsed: '未使用',
starting: '启动中',
recreating: '重建中',
}, },
units: { units: {
second: '秒', second: '秒',
@ -1600,6 +1602,16 @@ const message = {
xtomhk: 'XTOM 镜像站香港', xtomhk: 'XTOM 镜像站香港',
xtom: 'XTOM 镜像站全球', xtom: 'XTOM 镜像站全球',
phpsourceHelper: '根据你的网络环境选择合适的源', phpsourceHelper: '根据你的网络环境选择合适的源',
appPort: '应用端口',
externalPort: '外部映射端口',
packageManager: '包管理器',
codeDir: '源码目录',
appPortHelper: '应用端口是指容器内部的端口',
externalPortHelper: '外部映射端口是指容器对外暴露的端口',
runScript: '启动命令',
runScriptHelper: '启动命令列表是从源码目录下的 package.json 文件中解析而来',
open: '放开',
close: '关闭',
}, },
process: { process: {
pid: '进程ID', pid: '进程ID',

View File

@ -40,14 +40,24 @@ const webSiteRouter = {
}, },
}, },
{ {
path: '/websites/runtime/php', path: '/websites/runtimes/php',
name: 'Runtime', name: 'PHP',
component: () => import('@/views/website/runtime/index.vue'), component: () => import('@/views/website/runtime/php/index.vue'),
meta: { meta: {
title: 'menu.runtime', title: 'menu.runtime',
requiresAuth: false, requiresAuth: false,
}, },
}, },
{
path: '/websites/runtimes/node',
name: 'Node',
hidden: true,
component: () => import('@/views/website/runtime/node/index.vue'),
meta: {
activeMenu: '/websites/runtimes/php',
requiresAuth: false,
},
},
], ],
}; };

View File

@ -203,13 +203,18 @@ const search = async (req: App.AppReq) => {
}; };
const openInstall = (app: App.App) => { const openInstall = (app: App.App) => {
if (app.type === 'php') { switch (app.type) {
router.push({ path: '/websites/runtime/php' }); case 'php':
} else { router.push({ path: '/websites/runtimes/php' });
const params = { break;
app: app, case 'node':
}; router.push({ path: '/websites/runtimes/node' });
installRef.value.acceptParams(params); break;
default:
const params = {
app: app,
};
installRef.value.acceptParams(params);
} }
}; };

View File

@ -0,0 +1,77 @@
<template>
<el-dialog
v-model="open"
:close-on-click-modal="false"
:title="$t('commons.button.delete') + ' - ' + resourceName"
width="30%"
:before-close="handleClose"
>
<div :key="key" :loading="loading">
<el-form ref="deleteForm" label-position="left">
<el-form-item>
<el-checkbox v-model="deleteReq.forceDelete" :label="$t('website.forceDelete')" />
<span class="input-help">
{{ $t('website.forceDeleteHelper') }}
</span>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit()" :loading="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { DeleteRuntime } from '@/api/modules/runtime';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
const key = 1;
const open = ref(false);
const loading = ref(false);
const deleteReq = ref({
id: 0,
forceDelete: false,
});
const em = defineEmits(['close']);
const deleteForm = ref<FormInstance>();
const resourceName = ref('');
const handleClose = () => {
open.value = false;
em('close', false);
};
const acceptParams = async (id: number, name: string) => {
deleteReq.value = {
id: id,
forceDelete: false,
};
resourceName.value = name;
open.value = true;
};
const submit = () => {
loading.value = true;
DeleteRuntime(deleteReq.value)
.then(() => {
handleClose();
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
})
.finally(() => {
loading.value = false;
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -1,161 +1,21 @@
<template> <template>
<div> <div>
<RouterButton <RouterButton :buttons="buttons" />
:buttons="[ <LayoutContent>
{ <router-view></router-view>
label: 'PHP',
path: '/runtimes/php',
},
]"
/>
<LayoutContent :title="$t('runtime.runtime')" v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }}
</el-button>
</template>
<template #main>
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
<el-table-column :label="$t('commons.table.name')" fix prop="name" min-width="120px">
<template #default="{ row }">
<Tooltip :text="row.name" @click="openDetail(row)" />
</template>
</el-table-column>
<el-table-column :label="$t('runtime.resource')" prop="resource">
<template #default="{ row }">
<span>{{ $t('runtime.' + toLowerCase(row.resource)) }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('runtime.version')" prop="version"></el-table-column>
<el-table-column :label="$t('runtime.image')" prop="image" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status">
<template #default="{ row }">
<el-popover
v-if="row.status === 'error'"
placement="bottom"
:width="400"
trigger="hover"
:content="row.message"
>
<template #reference>
<Status :key="row.status" :status="row.status"></Status>
</template>
</el-popover>
<div v-else>
<Status :key="row.status" :status="row.status"></Status>
</div>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
min-width="120"
fix
/>
<fu-table-operations
:ellipsis="10"
width="120px"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
</template>
</LayoutContent> </LayoutContent>
<CreateRuntime ref="createRef" @close="search" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { Runtime } from '@/api/interface/runtime';
import { DeleteRuntime, SearchRuntimes } from '@/api/modules/runtime';
import { dateFormat, toLowerCase } from '@/utils/util';
import CreateRuntime from '@/views/website/runtime/create/index.vue';
import Status from '@/components/status/index.vue';
import i18n from '@/lang';
import { useDeleteData } from '@/hooks/use-delete-data';
const paginationConfig = reactive({
cacheSizeKey: 'runtime-page-size',
currentPage: 1,
pageSize: 10,
total: 0,
});
let req = reactive<Runtime.RuntimeReq>({
name: '',
page: 1,
pageSize: 40,
});
let timer: NodeJS.Timer | null = null;
const buttons = [ const buttons = [
{ {
label: i18n.global.t('commons.button.edit'), label: 'PHP',
click: function (row: Runtime.Runtime) { path: '/websites/runtimes/php',
openDetail(row);
},
disabled: function (row: Runtime.Runtime) {
return row.status === 'building';
},
}, },
{ {
label: i18n.global.t('commons.button.delete'), label: 'Node.js',
click: function (row: Runtime.Runtime) { path: '/websites/runtimes/node',
openDelete(row);
},
}, },
]; ];
const loading = ref(false);
const items = ref<Runtime.RuntimeDTO[]>([]);
const createRef = ref();
const search = async () => {
req.page = paginationConfig.currentPage;
req.pageSize = paginationConfig.pageSize;
loading.value = true;
try {
const res = await SearchRuntimes(req);
items.value = res.data.items;
paginationConfig.total = res.data.total;
} catch (error) {
} finally {
loading.value = false;
}
};
const openCreate = () => {
createRef.value.acceptParams({ type: 'php', mode: 'create' });
};
const openDetail = (row: Runtime.Runtime) => {
createRef.value.acceptParams({ type: row.type, mode: 'edit', id: row.id });
};
const openDelete = async (row: Runtime.Runtime) => {
await useDeleteData(DeleteRuntime, { id: row.id }, 'commons.msg.delete');
search();
};
onMounted(() => {
search();
timer = setInterval(() => {
search();
}, 10000 * 3);
});
onUnmounted(() => {
clearInterval(Number(timer));
timer = null;
});
</script> </script>
<style lang="scss" scoped>
.open-warn {
color: $primary-color;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,337 @@
<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>

View File

@ -0,0 +1,164 @@
<template>
<div>
<RouterMenu />
<LayoutContent :title="'Node.js'" v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }}
</el-button>
</template>
<template #main>
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
<el-table-column :label="$t('commons.table.name')" fix prop="name" min-width="120px">
<template #default="{ row }">
<Tooltip :text="row.name" @click="openDetail(row)" />
</template>
</el-table-column>
<el-table-column :label="$t('runtime.codeDir')" prop="codeDir">
<template #default="{ row }">
<el-button type="primary" link @click="toFolder(row.codeDir)">
<el-icon>
<FolderOpened />
</el-icon>
</el-button>
</template>
</el-table-column>
<el-table-column :label="$t('runtime.version')" prop="version"></el-table-column>
<el-table-column
:label="$t('runtime.externalPort')"
prop="params.PANEL_APP_PORT_HTTP"
></el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status">
<template #default="{ row }">
<el-popover
v-if="row.status === 'error'"
placement="bottom"
:width="400"
trigger="hover"
:content="row.message"
>
<template #reference>
<Status :key="row.status" :status="row.status"></Status>
</template>
</el-popover>
<div v-else>
<Status :key="row.status" :status="row.status"></Status>
</div>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
min-width="120"
fix
/>
<fu-table-operations
:ellipsis="10"
width="120px"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<OperateNode ref="operateRef" @close="search" />
<Delete ref="deleteRef" @close="search()" />
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { Runtime } from '@/api/interface/runtime';
import { 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';
import Delete from '@/views/website/runtime/delete/index.vue';
import i18n from '@/lang';
import RouterMenu from '../index.vue';
import router from '@/routers/router';
const paginationConfig = reactive({
cacheSizeKey: 'runtime-page-size',
currentPage: 1,
pageSize: 10,
total: 0,
});
const req = reactive<Runtime.RuntimeReq>({
name: '',
page: 1,
pageSize: 40,
type: 'node',
});
let timer: NodeJS.Timer | null = null;
const buttons = [
{
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';
},
},
{
label: i18n.global.t('commons.button.delete'),
click: function (row: Runtime.Runtime) {
openDelete(row);
},
},
];
const loading = ref(false);
const items = ref<Runtime.RuntimeDTO[]>([]);
const operateRef = ref();
const deleteRef = ref();
const search = async () => {
req.page = paginationConfig.currentPage;
req.pageSize = paginationConfig.pageSize;
loading.value = true;
try {
const res = await SearchRuntimes(req);
items.value = res.data.items;
paginationConfig.total = res.data.total;
} catch (error) {
} finally {
loading.value = false;
}
};
const openCreate = () => {
operateRef.value.acceptParams({ type: 'node', mode: 'create' });
};
const openDetail = (row: Runtime.Runtime) => {
operateRef.value.acceptParams({ type: row.type, mode: 'edit', id: row.id });
};
const openDelete = async (row: Runtime.Runtime) => {
deleteRef.value.acceptParams(row.id, row.name);
};
const toFolder = (folder: string) => {
router.push({ path: '/hosts/files', query: { path: folder } });
};
onMounted(() => {
search();
timer = setInterval(() => {
search();
}, 10000 * 3);
});
onUnmounted(() => {
clearInterval(Number(timer));
timer = null;
});
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,351 @@
<template>
<el-drawer :close-on-click-modal="false" v-model="open" size="50%">
<template #header>
<DrawerHeader
:header="$t('runtime.' + mode)"
:hideResource="mode == 'create'"
: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" :disabled="mode === 'edit'">
<template #prepend>
<FileList
:disabled="mode === 'edit'"
: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>
<span class="input-help">{{ $t('runtime.runScriptHelper') }}</span>
</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="$t('runtime.open')" value="0.0.0.0"></el-option>
<el-option :label="$t('runtime.close')" 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 && mode.value == 'create') {
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);
runtimeForm.value?.resetFields();
};
const getPath = (codeDir: string) => {
runtime.codeDir = codeDir;
getScripts();
};
const getScripts = () => {
GetNodeScripts({ codeDir: runtime.codeDir }).then((res) => {
scripts.value = res.data;
if (mode.value == 'create' && scripts.value.length > 0) {
runtime.params['EXEC_SCRIPT'] = scripts.value[0].name;
}
});
};
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,
type: data.type,
resource: data.resource,
appID: data.appID,
version: data.version,
rebuild: true,
source: data.source,
params: data.params,
codeDir: data.codeDir,
});
editParams.value = data.appParams;
if (mode.value == 'edit') {
searchApp(data.appID);
}
getScripts();
} catch (error) {}
};
const acceptParams = async (props: OperateRrops) => {
mode.value = props.mode;
scripts.value = [];
if (props.mode === 'create') {
Object.assign(runtime, initData(props.type));
searchApp(null);
} else {
getRuntime(props.id);
}
open.value = true;
};
defineExpose({
acceptParams,
});
</script>

View File

@ -35,9 +35,9 @@
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-select <el-select
v-model="runtime.appId" v-model="runtime.appID"
:disabled="mode === 'edit'" :disabled="mode === 'edit'"
@change="changeApp(runtime.appId)" @change="changeApp(runtime.appID)"
> >
<el-option <el-option
v-for="(app, index) in apps" v-for="(app, index) in apps"
@ -178,7 +178,7 @@ const appReq = reactive({
}); });
const initData = (type: string) => ({ const initData = (type: string) => ({
name: '', name: '',
appDetailId: undefined, appDetailID: undefined,
image: '', image: '',
params: {}, params: {},
type: type, type: type,
@ -192,7 +192,7 @@ let runtime = reactive<Runtime.RuntimeCreate>(initData('php'));
const rules = ref<any>({ const rules = ref<any>({
name: [Rules.appName], name: [Rules.appName],
resource: [Rules.requiredInput], resource: [Rules.requiredInput],
appId: [Rules.requiredSelect], appID: [Rules.requiredSelect],
version: [Rules.requiredInput, Rules.paramCommon], version: [Rules.requiredInput, Rules.paramCommon],
image: [Rules.requiredInput, Rules.imageName], image: [Rules.requiredInput, Rules.imageName],
source: [Rules.requiredSelect], source: [Rules.requiredSelect],
@ -238,7 +238,7 @@ const handleClose = () => {
const changeResource = (resource: string) => { const changeResource = (resource: string) => {
if (resource === 'local') { if (resource === 'local') {
runtime.appDetailId = undefined; runtime.appDetailID = undefined;
runtime.version = ''; runtime.version = '';
runtime.params = {}; runtime.params = {};
runtime.image = ''; runtime.image = '';
@ -253,7 +253,7 @@ const searchApp = (appId: number) => {
apps.value = res.data.items || []; apps.value = res.data.items || [];
if (res.data && res.data.items && res.data.items.length > 0) { if (res.data && res.data.items && res.data.items.length > 0) {
if (appId == null) { if (appId == null) {
runtime.appId = res.data.items[0].id; runtime.appID = res.data.items[0].id;
getApp(res.data.items[0].key, mode.value); getApp(res.data.items[0].key, mode.value);
} else { } else {
res.data.items.forEach((item) => { res.data.items.forEach((item) => {
@ -279,9 +279,9 @@ const changeApp = (appId: number) => {
const changeVersion = () => { const changeVersion = () => {
loading.value = true; loading.value = true;
initParam.value = false; initParam.value = false;
GetAppDetail(runtime.appId, runtime.version, 'runtime') GetAppDetail(runtime.appID, runtime.version, 'runtime')
.then((res) => { .then((res) => {
runtime.appDetailId = res.data.id; runtime.appDetailID = res.data.id;
runtime.image = res.data.image + ':' + runtime.version; runtime.image = res.data.image + ':' + runtime.version;
appParams.value = res.data.params; appParams.value = res.data.params;
initParam.value = true; initParam.value = true;
@ -342,19 +342,19 @@ const getRuntime = async (id: number) => {
Object.assign(runtime, { Object.assign(runtime, {
id: data.id, id: data.id,
name: data.name, name: data.name,
appDetailId: data.appDetailId, appDetailID: data.appDetailID,
image: data.image, image: data.image,
params: {}, params: {},
type: data.type, type: data.type,
resource: data.resource, resource: data.resource,
appId: data.appId, appID: data.appID,
version: data.version, version: data.version,
rebuild: true, rebuild: true,
source: data.source, source: data.source,
}); });
editParams.value = data.appParams; editParams.value = data.appParams;
if (mode.value == 'create') { if (mode.value == 'create') {
searchApp(data.appId); searchApp(data.appID);
} else { } else {
initParam.value = true; initParam.value = true;
} }

View File

@ -0,0 +1,156 @@
<template>
<div>
<RouterMenu />
<LayoutContent :title="'PHP'" v-loading="loading">
<template #toolbar>
<el-button type="primary" @click="openCreate">
{{ $t('runtime.create') }}
</el-button>
</template>
<template #main>
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
<el-table-column :label="$t('commons.table.name')" fix prop="name" min-width="120px">
<template #default="{ row }">
<Tooltip :text="row.name" @click="openDetail(row)" />
</template>
</el-table-column>
<el-table-column :label="$t('runtime.resource')" prop="resource">
<template #default="{ row }">
<span>{{ $t('runtime.' + toLowerCase(row.resource)) }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('runtime.version')" prop="version"></el-table-column>
<el-table-column :label="$t('runtime.image')" prop="image" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="status">
<template #default="{ row }">
<el-popover
v-if="row.status === 'error'"
placement="bottom"
:width="400"
trigger="hover"
:content="row.message"
>
<template #reference>
<Status :key="row.status" :status="row.status"></Status>
</template>
</el-popover>
<div v-else>
<Status :key="row.status" :status="row.status"></Status>
</div>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFormat"
show-overflow-tooltip
min-width="120"
fix
/>
<fu-table-operations
:ellipsis="10"
width="120px"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<CreateRuntime ref="createRef" @close="search" />
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { Runtime } from '@/api/interface/runtime';
import { DeleteRuntime, SearchRuntimes } from '@/api/modules/runtime';
import { dateFormat, toLowerCase } from '@/utils/util';
import CreateRuntime from '@/views/website/runtime/php/create/index.vue';
import Status from '@/components/status/index.vue';
import i18n from '@/lang';
import { useDeleteData } from '@/hooks/use-delete-data';
import RouterMenu from '../index.vue';
const paginationConfig = reactive({
cacheSizeKey: 'runtime-page-size',
currentPage: 1,
pageSize: 10,
total: 0,
});
let req = reactive<Runtime.RuntimeReq>({
name: '',
page: 1,
pageSize: 40,
type: 'php',
});
let timer: NodeJS.Timer | null = null;
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
click: function (row: Runtime.Runtime) {
openDetail(row);
},
disabled: function (row: Runtime.Runtime) {
return row.status === 'building';
},
},
{
label: i18n.global.t('commons.button.delete'),
click: function (row: Runtime.Runtime) {
openDelete(row);
},
},
];
const loading = ref(false);
const items = ref<Runtime.RuntimeDTO[]>([]);
const createRef = ref();
const search = async () => {
req.page = paginationConfig.currentPage;
req.pageSize = paginationConfig.pageSize;
loading.value = true;
try {
const res = await SearchRuntimes(req);
items.value = res.data.items;
paginationConfig.total = res.data.total;
} catch (error) {
} finally {
loading.value = false;
}
};
const openCreate = () => {
createRef.value.acceptParams({ type: 'php', mode: 'create' });
};
const openDetail = (row: Runtime.Runtime) => {
createRef.value.acceptParams({ type: row.type, mode: 'edit', id: row.id });
};
const openDelete = async (row: Runtime.Runtime) => {
await useDeleteData(DeleteRuntime, { id: row.id }, 'commons.msg.delete');
search();
};
onMounted(() => {
search();
timer = setInterval(() => {
search();
}, 10000 * 3);
});
onUnmounted(() => {
clearInterval(Number(timer));
timer = null;
});
</script>
<style lang="scss" scoped>
.open-warn {
color: $primary-color;
cursor: pointer;
}
</style>