mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-07 22:16:16 +08:00
feat: Node.js 运行环境增加多端口 (#2691)
Refs https://github.com/1Panel-dev/1Panel/issues/2566
This commit is contained in:
parent
8857d97dd7
commit
048df6f390
9 changed files with 189 additions and 27 deletions
|
@ -23,9 +23,15 @@ type RuntimeCreate struct {
|
|||
}
|
||||
|
||||
type NodeConfig struct {
|
||||
Install bool `json:"install"`
|
||||
Clean bool `json:"clean"`
|
||||
Port int `json:"port"`
|
||||
Install bool `json:"install"`
|
||||
Clean bool `json:"clean"`
|
||||
Port int `json:"port"`
|
||||
ExposedPorts []ExposedPort `json:"exposedPorts"`
|
||||
}
|
||||
|
||||
type ExposedPort struct {
|
||||
HostPort int `json:"hostPort"`
|
||||
ContainerPort int `json:"containerPort"`
|
||||
}
|
||||
|
||||
type RuntimeDelete struct {
|
||||
|
|
|
@ -1,28 +1,30 @@
|
|||
package response
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RuntimeDTO struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Resource string `json:"resource"`
|
||||
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"`
|
||||
Port int `json:"port"`
|
||||
Path string `json:"path"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Resource string `json:"resource"`
|
||||
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"`
|
||||
Port int `json:"port"`
|
||||
Path string `json:"path"`
|
||||
ExposedPorts []request.ExposedPort `json:"exposedPorts"`
|
||||
}
|
||||
|
||||
type PackageScripts struct {
|
||||
|
|
|
@ -3,6 +3,7 @@ package service
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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/response"
|
||||
|
@ -20,6 +21,7 @@ import (
|
|||
"github.com/subosito/gotenv"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -286,7 +288,26 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
|
|||
}
|
||||
res.Params[k] = port
|
||||
default:
|
||||
res.Params[k] = v
|
||||
if strings.Contains(k, "CONTAINER_PORT") || strings.Contains(k, "HOST_PORT") {
|
||||
if strings.Contains(k, "CONTAINER_PORT") {
|
||||
r := regexp.MustCompile(`_(\d+)$`)
|
||||
matches := r.FindStringSubmatch(k)
|
||||
containerPort, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hostPort, err := strconv.Atoi(envs[fmt.Sprintf("HOST_PORT_%s", matches[1])])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.ExposedPorts = append(res.ExposedPorts, request.ExposedPort{
|
||||
ContainerPort: containerPort,
|
||||
HostPort: hostPort,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
res.Params[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok {
|
||||
|
@ -361,8 +382,9 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
|
|||
CodeDir: req.CodeDir,
|
||||
Version: req.Version,
|
||||
NodeConfig: request.NodeConfig{
|
||||
Port: req.Port,
|
||||
Install: true,
|
||||
Port: req.Port,
|
||||
Install: true,
|
||||
ExposedPorts: req.ExposedPorts,
|
||||
},
|
||||
}
|
||||
composeContent, envContent, _, err := handleParams(create, projectDir)
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/subosito/gotenv"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -321,6 +322,11 @@ func handleParams(create request.RuntimeCreate, projectDir string) (composeConte
|
|||
create.Params["RUN_INSTALL"] = "0"
|
||||
}
|
||||
create.Params["CONTAINER_PACKAGE_URL"] = create.Source
|
||||
|
||||
composeContent, err = handleNodeCompose(env, composeContent, create, projectDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
newMap := make(map[string]string)
|
||||
|
@ -328,6 +334,7 @@ func handleParams(create request.RuntimeCreate, projectDir string) (composeConte
|
|||
for k, v := range newMap {
|
||||
env[k] = v
|
||||
}
|
||||
|
||||
envStr, err := gotenv.Marshal(env)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -339,6 +346,58 @@ func handleParams(create request.RuntimeCreate, projectDir string) (composeConte
|
|||
return
|
||||
}
|
||||
|
||||
func handleNodeCompose(env gotenv.Env, composeContent []byte, create request.RuntimeCreate, projectDir string) (composeByte []byte, err error) {
|
||||
existMap := make(map[string]interface{})
|
||||
composeMap := make(map[string]interface{})
|
||||
if err = yaml.Unmarshal(composeContent, &composeMap); err != nil {
|
||||
return
|
||||
}
|
||||
services, serviceValid := composeMap["services"].(map[string]interface{})
|
||||
if !serviceValid {
|
||||
err = buserr.New(constant.ErrFileParse)
|
||||
return
|
||||
}
|
||||
serviceName := ""
|
||||
serviceValue := make(map[string]interface{})
|
||||
for name, service := range services {
|
||||
serviceName = name
|
||||
serviceValue = service.(map[string]interface{})
|
||||
ports, ok := serviceValue["ports"].([]interface{})
|
||||
if ok {
|
||||
ports = []interface{}{}
|
||||
ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${NODE_APP_PORT}")
|
||||
for i, port := range create.ExposedPorts {
|
||||
containerPortStr := fmt.Sprintf("CONTAINER_PORT_%d", i)
|
||||
hostPortStr := fmt.Sprintf("HOST_PORT_%d", i)
|
||||
existMap[containerPortStr] = struct{}{}
|
||||
existMap[hostPortStr] = struct{}{}
|
||||
ports = append(ports, fmt.Sprintf("${HOST_IP}:${%s}:${%s}", hostPortStr, containerPortStr))
|
||||
create.Params[containerPortStr] = port.ContainerPort
|
||||
create.Params[hostPortStr] = port.HostPort
|
||||
}
|
||||
serviceValue["ports"] = ports
|
||||
}
|
||||
break
|
||||
}
|
||||
for k := range env {
|
||||
if strings.Contains(k, "CONTAINER_PORT_") || strings.Contains(k, "HOST_PORT_") {
|
||||
if _, ok := existMap[k]; !ok {
|
||||
delete(env, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
services[serviceName] = serviceValue
|
||||
composeMap["services"] = services
|
||||
composeByte, err = yaml.Marshal(composeMap)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
_ = fileOp.SaveFile(path.Join(projectDir, "docker-compose.yml"), string(composeByte), 0644)
|
||||
return
|
||||
}
|
||||
|
||||
func checkContainerName(name string) error {
|
||||
dockerCli, err := docker.NewClient()
|
||||
if err != nil {
|
||||
|
|
|
@ -37,6 +37,7 @@ export namespace Runtime {
|
|||
appID: number;
|
||||
source?: string;
|
||||
path?: string;
|
||||
exposedPorts?: ExposedPort[];
|
||||
}
|
||||
|
||||
export interface RuntimeCreate {
|
||||
|
@ -53,6 +54,12 @@ export namespace Runtime {
|
|||
source?: string;
|
||||
codeDir?: string;
|
||||
port?: number;
|
||||
exposedPorts?: ExposedPort[];
|
||||
}
|
||||
|
||||
export interface ExposedPort {
|
||||
hostPort: number;
|
||||
containerPort: number;
|
||||
}
|
||||
|
||||
export interface RuntimeUpdate {
|
||||
|
|
|
@ -1834,6 +1834,7 @@ const message = {
|
|||
'Is {0} {1} module? The operation may cause abnormality in the operating environment, please confirm before proceeding',
|
||||
customScript: 'Custom startup command',
|
||||
customScriptHelper: 'Please fill in the complete startup command, for example: npm run start',
|
||||
portError: 'Cannot fill in the same port',
|
||||
},
|
||||
process: {
|
||||
pid: 'Process ID',
|
||||
|
|
|
@ -1729,6 +1729,7 @@ const message = {
|
|||
nodeOperatorHelper: '是否{0} {1} 模組? 操作可能導致運轉環境異常,請確認後操作',
|
||||
customScript: '自訂啟動指令',
|
||||
customScriptHelper: '請填寫完整的啟動指令,例如:npm run start',
|
||||
portError: '不能填寫相同連接埠',
|
||||
},
|
||||
process: {
|
||||
pid: '進程ID',
|
||||
|
|
|
@ -1729,6 +1729,7 @@ const message = {
|
|||
nodeOperatorHelper: '是否{0} {1} 模块?操作可能导致运行环境异常,请确认后操作',
|
||||
customScript: '自定义启动命令',
|
||||
customScriptHelper: '请填写完整的启动命令,例如:npm run start',
|
||||
portError: '不能填写相同端口',
|
||||
},
|
||||
process: {
|
||||
pid: '进程ID',
|
||||
|
|
|
@ -105,18 +105,25 @@
|
|||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="9">
|
||||
<el-col :span="7">
|
||||
<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="9">
|
||||
<el-col :span="7">
|
||||
<el-form-item :label="$t('runtime.externalPort')" prop="port">
|
||||
<el-input v-model.number="runtime.port" />
|
||||
<span class="input-help">{{ $t('runtime.externalPortHelper') }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item :label="$t('commons.button.add') + $t('commons.table.port')">
|
||||
<el-button @click="addPort">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="$t('app.allowPort')" prop="params.HOST_IP">
|
||||
<el-switch
|
||||
|
@ -127,6 +134,31 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" v-for="(port, index) of runtime.exposedPorts" :key="index">
|
||||
<el-col :span="7">
|
||||
<el-form-item
|
||||
:prop="'exposedPorts.' + index + '.containerPort'"
|
||||
:rules="rules.params.NODE_APP_PORT"
|
||||
>
|
||||
<el-input v-model.number="port.containerPort" :placeholder="$t('runtime.appPort')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="7">
|
||||
<el-form-item
|
||||
:prop="'exposedPorts.' + index + '.hostPort'"
|
||||
:rules="rules.params.NODE_APP_PORT"
|
||||
>
|
||||
<el-input v-model.number="port.hostPort" :placeholder="$t('runtime.externalPort')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="removePort(index)" link>
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</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>
|
||||
|
@ -170,7 +202,7 @@ import { GetApp, GetAppDetail, SearchApp } from '@/api/modules/app';
|
|||
import { CreateRuntime, GetNodeScripts, GetRuntime, UpdateRuntime } from '@/api/modules/runtime';
|
||||
import { Rules, checkNumberRange } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||
|
@ -209,6 +241,7 @@ const initData = (type: string) => ({
|
|||
codeDir: '/',
|
||||
port: 3000,
|
||||
source: 'https://registry.npmjs.org/',
|
||||
exposedPorts: [],
|
||||
});
|
||||
let runtime = reactive<Runtime.RuntimeCreate>(initData('node'));
|
||||
const rules = ref<any>({
|
||||
|
@ -217,7 +250,6 @@ const rules = ref<any>({
|
|||
codeDir: [Rules.requiredInput],
|
||||
port: [Rules.requiredInput, Rules.paramPort, checkNumberRange(1, 65535)],
|
||||
source: [Rules.requiredSelect],
|
||||
|
||||
params: {
|
||||
NODE_APP_PORT: [Rules.requiredInput, Rules.paramPort, checkNumberRange(1, 65535)],
|
||||
PACKAGE_MANAGER: [Rules.requiredSelect],
|
||||
|
@ -283,6 +315,17 @@ const changeScriptType = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const addPort = () => {
|
||||
runtime.exposedPorts.push({
|
||||
hostPort: undefined,
|
||||
containerPort: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const removePort = (index: number) => {
|
||||
runtime.exposedPorts.splice(index, 1);
|
||||
};
|
||||
|
||||
const getScripts = () => {
|
||||
GetNodeScripts({ codeDir: runtime.codeDir }).then((res) => {
|
||||
scripts.value = res.data;
|
||||
|
@ -348,6 +391,25 @@ const submit = async (formEl: FormInstance | undefined) => {
|
|||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
if (runtime.exposedPorts && runtime.exposedPorts.length > 0) {
|
||||
const containerPortMap = new Map();
|
||||
const hostPortMap = new Map();
|
||||
containerPortMap[runtime.params['NODE_APP_PORT']] = true;
|
||||
hostPortMap[runtime.port] = true;
|
||||
for (const port of runtime.exposedPorts) {
|
||||
if (containerPortMap[port.containerPort]) {
|
||||
MsgError(i18n.global.t('runtime.portError'));
|
||||
return;
|
||||
}
|
||||
if (hostPortMap[port.hostPort]) {
|
||||
MsgError(i18n.global.t('runtime.portError'));
|
||||
return;
|
||||
}
|
||||
hostPortMap[port.hostPort] = true;
|
||||
containerPortMap[port.containerPort] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (mode.value == 'create') {
|
||||
loading.value = true;
|
||||
CreateRuntime(runtime)
|
||||
|
@ -391,6 +453,7 @@ const getRuntime = async (id: number) => {
|
|||
codeDir: data.codeDir,
|
||||
port: data.port,
|
||||
});
|
||||
runtime.exposedPorts = data.exposedPorts || [];
|
||||
editParams.value = data.appParams;
|
||||
searchApp(data.appID);
|
||||
if (data.params['CUSTOM_SCRIPT'] == '0') {
|
||||
|
|
Loading…
Add table
Reference in a new issue