1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 08:19:15 +08:00

feat: node.js 运行环境增加模块管理 (#2488)

This commit is contained in:
zhengkunwang 2023-10-10 04:34:28 -05:00 committed by GitHub
parent 42e0bff8f7
commit cbd4dd9e9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 338 additions and 18 deletions

View File

@ -5,6 +5,7 @@ import (
"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/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -188,3 +189,29 @@ func (b *BaseApi) GetNodeModules(c *gin.Context) {
} }
helper.SuccessWithData(c, res) helper.SuccessWithData(c, res)
} }
// @Tags Runtime
// @Summary Operate Node modules
// @Description 操作 Node 项目 modules
// @Accept json
// @Param request body request.NodeModuleReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /runtimes/node/modules/operate [post]
func (b *BaseApi) OperateNodeModules(c *gin.Context) {
var req request.NodeModuleReq
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
err := runtimeService.OperateNodeModules(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}

View File

@ -55,7 +55,8 @@ type RuntimeOperate struct {
} }
type NodeModuleReq struct { type NodeModuleReq struct {
Operate string `json:"operate"` Operate string `json:"operate" validate:"oneof=install uninstall update"`
ID uint `json:"ID"` ID uint `json:"ID" validate:"required"`
Module string `json:"module"` Module string `json:"module"`
PkgManager string `json:"pkgManager" validate:"oneof=npm yarn"`
} }

View File

@ -11,8 +11,10 @@ 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"
cmd2 "github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/compose" "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/env"
"github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/subosito/gotenv" "github.com/subosito/gotenv"
@ -20,6 +22,7 @@ import (
"path" "path"
"strconv" "strconv"
"strings" "strings"
"time"
) )
type RuntimeService struct { type RuntimeService struct {
@ -34,6 +37,7 @@ type IRuntimeService interface {
GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error) GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error)
OperateRuntime(req request.RuntimeOperate) error OperateRuntime(req request.RuntimeOperate) error
GetNodeModules(req request.NodeModuleReq) ([]response.NodeModule, error) GetNodeModules(req request.NodeModuleReq) ([]response.NodeModule, error)
OperateNodeModules(req request.NodeModuleReq) error
} }
func NewRuntimeService() IRuntimeService { func NewRuntimeService() IRuntimeService {
@ -477,3 +481,33 @@ func (r *RuntimeService) GetNodeModules(req request.NodeModuleReq) ([]response.N
} }
return res, nil return res, nil
} }
func (r *RuntimeService) OperateNodeModules(req request.NodeModuleReq) error {
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
containerName, err := env.GetEnvValueByKey(runtime.GetEnvPath(), "CONTAINER_NAME")
if err != nil {
return err
}
cmd := req.PkgManager
switch req.Operate {
case constant.RuntimeInstall:
if req.PkgManager == constant.RuntimeNpm {
cmd += " install"
} else {
cmd += " add"
}
case constant.RuntimeUninstall:
if req.PkgManager == constant.RuntimeNpm {
cmd += " uninstall"
} else {
cmd += " remove"
}
case constant.RuntimeUpdate:
cmd += " update"
}
cmd += " " + req.Module
return cmd2.ExecContainerScript(containerName, cmd, 5*time.Minute)
}

View File

@ -23,4 +23,11 @@ const (
RuntimeUp = "up" RuntimeUp = "up"
RuntimeDown = "down" RuntimeDown = "down"
RuntimeRestart = "restart" RuntimeRestart = "restart"
RuntimeInstall = "install"
RuntimeUninstall = "uninstall"
RuntimeUpdate = "update"
RuntimeNpm = "npm"
RuntimeYarn = "yarn"
) )

View File

@ -23,6 +23,7 @@ func (r *RuntimeRouter) InitRuntimeRouter(Router *gin.RouterGroup) {
groupRouter.POST("/node/package", baseApi.GetNodePackageRunScript) groupRouter.POST("/node/package", baseApi.GetNodePackageRunScript)
groupRouter.POST("/operate", baseApi.OperateRuntime) groupRouter.POST("/operate", baseApi.OperateRuntime)
groupRouter.POST("/node/modules", baseApi.GetNodeModules) groupRouter.POST("/node/modules", baseApi.GetNodeModules)
groupRouter.POST("/node/modules/operate", baseApi.OperateNodeModules)
} }
} }

View File

@ -68,6 +68,18 @@ func ExecWithTimeOut(cmdStr string, timeout time.Duration) (string, error) {
return stdout.String(), nil return stdout.String(), nil
} }
func ExecContainerScript(containerName, cmdStr string, timeout time.Duration) error {
cmdStr = fmt.Sprintf("docker exec -i %s bash -c '%s'", containerName, cmdStr)
out, err := ExecWithTimeOut(cmdStr, timeout)
if err != nil {
if out != "" {
return fmt.Errorf("%s; err: %v", out, err)
}
return err
}
return nil
}
func ExecCronjobWithTimeOut(cmdStr string, workdir string, timeout time.Duration) (string, error) { func ExecCronjobWithTimeOut(cmdStr string, workdir string, timeout time.Duration) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()

View File

@ -2,6 +2,7 @@ package env
import ( import (
"fmt" "fmt"
"github.com/joho/godotenv"
"os" "os"
"sort" "sort"
"strconv" "strconv"
@ -37,3 +38,15 @@ func Marshal(envMap map[string]string) (string, error) {
sort.Strings(lines) sort.Strings(lines)
return strings.Join(lines, "\n"), nil return strings.Join(lines, "\n"), nil
} }
func GetEnvValueByKey(envPath, key string) (string, error) {
envMap, err := godotenv.Read(envPath)
if err != nil {
return "", err
}
value, ok := envMap[key]
if !ok {
return "", fmt.Errorf("key %s not found in %s", key, envPath)
}
return value, nil
}

View File

@ -7943,6 +7943,39 @@ const docTemplate = `{
} }
} }
}, },
"/runtimes/node/modules/operate": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "操作 Node 项目的 modules",
"consumes": [
"application/json"
],
"tags": [
"Runtime"
],
"summary": "Operate Node modules",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.NodeModuleReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/runtimes/node/package": { "/runtimes/node/package": {
"post": { "post": {
"security": [ "security": [
@ -16691,6 +16724,9 @@ const docTemplate = `{
}, },
"request.NodeModuleReq": { "request.NodeModuleReq": {
"type": "object", "type": "object",
"required": [
"ID"
],
"properties": { "properties": {
"ID": { "ID": {
"type": "integer" "type": "integer"
@ -16699,7 +16735,19 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"operate": { "operate": {
"type": "string" "type": "string",
"enum": [
"install",
"uninstall",
"update"
]
},
"pkgManager": {
"type": "string",
"enum": [
"npm",
"yarn"
]
} }
} }
}, },

View File

@ -7936,6 +7936,39 @@
} }
} }
}, },
"/runtimes/node/modules/operate": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "操作 Node 项目的 modules",
"consumes": [
"application/json"
],
"tags": [
"Runtime"
],
"summary": "Operate Node modules",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.NodeModuleReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/runtimes/node/package": { "/runtimes/node/package": {
"post": { "post": {
"security": [ "security": [
@ -16684,6 +16717,9 @@
}, },
"request.NodeModuleReq": { "request.NodeModuleReq": {
"type": "object", "type": "object",
"required": [
"ID"
],
"properties": { "properties": {
"ID": { "ID": {
"type": "integer" "type": "integer"
@ -16692,7 +16728,19 @@
"type": "string" "type": "string"
}, },
"operate": { "operate": {
"type": "string" "type": "string",
"enum": [
"install",
"uninstall",
"update"
]
},
"pkgManager": {
"type": "string",
"enum": [
"npm",
"yarn"
]
} }
} }
}, },

View File

@ -3138,7 +3138,18 @@ definitions:
module: module:
type: string type: string
operate: operate:
enum:
- install
- uninstall
- update
type: string type: string
pkgManager:
enum:
- npm
- yarn
type: string
required:
- ID
type: object type: object
request.NodePackageReq: request.NodePackageReq:
properties: properties:
@ -9192,6 +9203,26 @@ paths:
summary: Get Node modules summary: Get Node modules
tags: tags:
- Runtime - Runtime
/runtimes/node/modules/operate:
post:
consumes:
- application/json
description: 操作 Node 项目的 modules
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.NodeModuleReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Operate Node modules
tags:
- Runtime
/runtimes/node/package: /runtimes/node/package:
post: post:
consumes: consumes:

View File

@ -44,7 +44,7 @@ export namespace Runtime {
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;
@ -85,5 +85,8 @@ export namespace Runtime {
export interface NodeModuleReq { export interface NodeModuleReq {
ID: number; ID: number;
Operate?: string;
Module?: string;
PkgManager?: string;
} }
} }

View File

@ -1,6 +1,7 @@
import http from '@/api'; import http from '@/api';
import { ResPage } from '../interface'; import { ResPage } from '../interface';
import { Runtime } from '../interface/runtime'; import { Runtime } from '../interface/runtime';
import { TimeoutEnum } from '@/enums/http-enum';
export const SearchRuntimes = (req: Runtime.RuntimeReq) => { export const SearchRuntimes = (req: Runtime.RuntimeReq) => {
return http.post<ResPage<Runtime.RuntimeDTO>>(`/runtimes/search`, req); return http.post<ResPage<Runtime.RuntimeDTO>>(`/runtimes/search`, req);
@ -33,3 +34,7 @@ export const OperateRuntime = (req: Runtime.RuntimeOperate) => {
export const GetNodeModules = (req: Runtime.NodeModuleReq) => { export const GetNodeModules = (req: Runtime.NodeModuleReq) => {
return http.post<Runtime.NodeModule[]>(`/runtimes/node/modules`, req); return http.post<Runtime.NodeModule[]>(`/runtimes/node/modules`, req);
}; };
export const OperateNodeModule = (req: Runtime.NodeModuleReq) => {
return http.post<any>(`/runtimes/node/modules/operate`, req, TimeoutEnum.T_10M);
};

View File

@ -82,7 +82,7 @@ const logSearch = reactive({
isWatch: true, isWatch: true,
compose: '', compose: '',
mode: 'all', mode: 'all',
tail: 1000, tail: 500,
}); });
const handleClose = () => { const handleClose = () => {
@ -167,7 +167,7 @@ interface DialogProps {
const acceptParams = (props: DialogProps): void => { const acceptParams = (props: DialogProps): void => {
logSearch.compose = props.compose; logSearch.compose = props.compose;
logSearch.tail = 1000; logSearch.tail = 200;
logSearch.mode = timeOptions.value[3].value; logSearch.mode = timeOptions.value[3].value;
logSearch.isWatch = true; logSearch.isWatch = true;
resource.value = props.resource; resource.value = props.resource;

View File

@ -246,6 +246,9 @@ const message = {
down: 'Stop', down: 'Stop',
up: 'Start', up: 'Start',
restart: 'Restart', restart: 'Restart',
install: 'Install',
uninstall: 'Uninstall',
update: 'Update',
}, },
}, },
menu: { menu: {
@ -1783,6 +1786,8 @@ const message = {
imageSource: 'Image source', imageSource: 'Image source',
moduleManager: 'Module Management', moduleManager: 'Module Management',
module: 'Module', module: 'Module',
nodeOperatorHelper:
'Is {0} {1} module? The operation may cause abnormality in the operating environment, please confirm before proceeding',
}, },
process: { process: {
pid: 'Process ID', pid: 'Process ID',

View File

@ -242,8 +242,11 @@ const message = {
}, },
operate: { operate: {
down: '停止', down: '停止',
up: '启动', up: '啟動',
restart: '重启', restart: '重啟',
install: '安裝',
uninstall: '卸載',
update: '更新',
}, },
}, },
menu: { menu: {
@ -1685,6 +1688,7 @@ const message = {
imageSource: '鏡像源', imageSource: '鏡像源',
moduleManager: '模塊管理', moduleManager: '模塊管理',
module: '模塊', module: '模塊',
nodeOperatorHelper: '是否{0} {1} 模組 操作可能導致運轉環境異常請確認後操作',
}, },
process: { process: {
pid: '進程ID', pid: '進程ID',

View File

@ -244,6 +244,9 @@ const message = {
down: '停止', down: '停止',
up: '启动', up: '启动',
restart: '重启', restart: '重启',
install: '安装',
uninstall: '卸载',
update: '更新',
}, },
}, },
menu: { menu: {
@ -1684,6 +1687,7 @@ const message = {
imageSource: '镜像源', imageSource: '镜像源',
moduleManager: '模块管理', moduleManager: '模块管理',
module: '模块', module: '模块',
nodeOperatorHelper: '是否{0} {1} 模块操作可能导致运行环境异常请确认后操作',
}, },
process: { process: {
pid: '进程ID', pid: '进程ID',

View File

@ -193,7 +193,7 @@ const search = async () => {
}; };
const openModules = (row: Runtime.Runtime) => { const openModules = (row: Runtime.Runtime) => {
moduleRef.value.acceptParams({ id: row.id }); moduleRef.value.acceptParams({ id: row.id, packageManager: row.params['PACKAGE_MANAGER'] });
}; };
const openCreate = () => { const openCreate = () => {

View File

@ -3,9 +3,19 @@
<template #header> <template #header>
<DrawerHeader :header="$t('runtime.moduleManager')" :back="handleClose" /> <DrawerHeader :header="$t('runtime.moduleManager')" :back="handleClose" />
</template> </template>
<el-row> <el-row :gutter="20" v-loading="loading">
<el-col :span="24"> <el-col :span="10">
<ComplexTable :data="data" @search="search()" :height="650"> <el-input v-model="module">
<template #prepend>{{ packageManager }}</template>
</el-input>
</el-col>
<el-col :span="14">
<el-button @click="operateModule('install', module)" type="primary" :disabled="module === ''">
{{ $t('commons.operate.install') }}
</el-button>
</el-col>
<el-col>
<ComplexTable :data="data" @search="search()" class="mt-5" :height="800">
<el-table-column :label="$t('commons.table.name')" prop="name" min-width="100px"></el-table-column> <el-table-column :label="$t('commons.table.name')" prop="name" min-width="100px"></el-table-column>
<el-table-column :label="$t('container.version')" prop="version" width="80px"></el-table-column> <el-table-column :label="$t('container.version')" prop="version" width="80px"></el-table-column>
<el-table-column <el-table-column
@ -19,6 +29,14 @@
min-width="120px" min-width="120px"
prop="description" prop="description"
></el-table-column> ></el-table-column>
<fu-table-operations
:ellipsis="10"
width="150px"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
fix
/>
</ComplexTable> </ComplexTable>
</el-col> </el-col>
</el-row> </el-row>
@ -26,7 +44,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { GetNodeModules } from '@/api/modules/runtime'; import { GetNodeModules, OperateNodeModule } from '@/api/modules/runtime';
import { MsgError, MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
import { Runtime } from '@/api/interface/runtime';
interface NoodeRrops { interface NoodeRrops {
packageManager: string; packageManager: string;
@ -36,22 +57,78 @@ interface NoodeRrops {
const open = ref(false); const open = ref(false);
const id = ref(0); const id = ref(0);
const data = ref([]); const data = ref([]);
const module = ref('');
const packageManager = ref('');
const loading = ref(false);
const buttons = [
{
label: i18n.global.t('commons.operate.update'),
click: function (row: Runtime.Runtime) {
operateModule('update', row.name);
},
},
{
label: i18n.global.t('commons.operate.uninstall'),
click: function (row: Runtime.Runtime) {
operateModule('uninstall', row.name);
},
},
];
const acceptParams = async (props: NoodeRrops) => { const acceptParams = async (props: NoodeRrops) => {
id.value = props.id; id.value = props.id;
packageManager.value = props.packageManager;
module.value = '';
data.value = []; data.value = [];
search();
open.value = true; open.value = true;
loading.value = true;
await search();
loading.value = false;
}; };
const search = async () => { const search = async () => {
try { try {
const res = await GetNodeModules({ ID: id.value }); const res = await GetNodeModules({ ID: id.value });
data.value = res.data; data.value = res.data;
console.log(res);
} catch (error) {} } catch (error) {}
}; };
const operateModule = (operate: string, moduleName: string) => {
ElMessageBox.confirm(
i18n.global.t('runtime.nodeOperatorHelper', [i18n.global.t('commons.operate.' + operate), moduleName]),
i18n.global.t('commons.operate.' + operate),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(async () => {
loading.value = true;
try {
await OperateNodeModule({
ID: id.value,
Operate: operate,
Module: moduleName,
PkgManager: packageManager.value,
});
loading.value = false;
MsgSuccess(i18n.global.t('commons.operate.' + operate) + i18n.global.t('commons.status.success'));
await search();
module.value = '';
} catch (error) {
MsgError(
i18n.global.t('commons.operate.' + operate) +
i18n.global.t('commons.status.failed') +
' ' +
error.message,
);
} finally {
loading.value = false;
}
});
};
const handleClose = () => { const handleClose = () => {
open.value = false; open.value = false;
}; };

View File

@ -301,8 +301,8 @@ const getApp = (appkey: string, mode: string) => {
GetApp(appkey).then((res) => { GetApp(appkey).then((res) => {
appVersions.value = res.data.versions || []; appVersions.value = res.data.versions || [];
if (res.data.versions.length > 0) { if (res.data.versions.length > 0) {
runtime.version = res.data.versions[0];
if (mode === 'create') { if (mode === 'create') {
runtime.version = res.data.versions[0];
changeVersion(); changeVersion();
} }
} }