mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-08 22:46:51 +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
|
@ -26,6 +26,12 @@ type NodeConfig struct {
|
||||||
Install bool `json:"install"`
|
Install bool `json:"install"`
|
||||||
Clean bool `json:"clean"`
|
Clean bool `json:"clean"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
|
ExposedPorts []ExposedPort `json:"exposedPorts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExposedPort struct {
|
||||||
|
HostPort int `json:"hostPort"`
|
||||||
|
ContainerPort int `json:"containerPort"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeDelete struct {
|
type RuntimeDelete struct {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package response
|
package response
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
|
||||||
"github.com/1Panel-dev/1Panel/backend/app/model"
|
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -23,6 +24,7 @@ type RuntimeDTO struct {
|
||||||
AppParams []AppParam `json:"appParams"`
|
AppParams []AppParam `json:"appParams"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
ExposedPorts []request.ExposedPort `json:"exposedPorts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PackageScripts struct {
|
type PackageScripts struct {
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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"
|
||||||
|
@ -20,6 +21,7 @@ import (
|
||||||
"github.com/subosito/gotenv"
|
"github.com/subosito/gotenv"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -286,9 +288,28 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
|
||||||
}
|
}
|
||||||
res.Params[k] = port
|
res.Params[k] = port
|
||||||
default:
|
default:
|
||||||
|
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
|
res.Params[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok {
|
if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok {
|
||||||
res.Source = v
|
res.Source = v
|
||||||
}
|
}
|
||||||
|
@ -363,6 +384,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
|
||||||
NodeConfig: request.NodeConfig{
|
NodeConfig: request.NodeConfig{
|
||||||
Port: req.Port,
|
Port: req.Port,
|
||||||
Install: true,
|
Install: true,
|
||||||
|
ExposedPorts: req.ExposedPorts,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
composeContent, envContent, _, err := handleParams(create, projectDir)
|
composeContent, envContent, _, err := handleParams(create, projectDir)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"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"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -321,6 +322,11 @@ func handleParams(create request.RuntimeCreate, projectDir string) (composeConte
|
||||||
create.Params["RUN_INSTALL"] = "0"
|
create.Params["RUN_INSTALL"] = "0"
|
||||||
}
|
}
|
||||||
create.Params["CONTAINER_PACKAGE_URL"] = create.Source
|
create.Params["CONTAINER_PACKAGE_URL"] = create.Source
|
||||||
|
|
||||||
|
composeContent, err = handleNodeCompose(env, composeContent, create, projectDir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newMap := make(map[string]string)
|
newMap := make(map[string]string)
|
||||||
|
@ -328,6 +334,7 @@ func handleParams(create request.RuntimeCreate, projectDir string) (composeConte
|
||||||
for k, v := range newMap {
|
for k, v := range newMap {
|
||||||
env[k] = v
|
env[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
envStr, err := gotenv.Marshal(env)
|
envStr, err := gotenv.Marshal(env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -339,6 +346,58 @@ func handleParams(create request.RuntimeCreate, projectDir string) (composeConte
|
||||||
return
|
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 {
|
func checkContainerName(name string) error {
|
||||||
dockerCli, err := docker.NewClient()
|
dockerCli, err := docker.NewClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -37,6 +37,7 @@ export namespace Runtime {
|
||||||
appID: number;
|
appID: number;
|
||||||
source?: string;
|
source?: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
|
exposedPorts?: ExposedPort[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeCreate {
|
export interface RuntimeCreate {
|
||||||
|
@ -53,6 +54,12 @@ export namespace Runtime {
|
||||||
source?: string;
|
source?: string;
|
||||||
codeDir?: string;
|
codeDir?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
exposedPorts?: ExposedPort[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExposedPort {
|
||||||
|
hostPort: number;
|
||||||
|
containerPort: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeUpdate {
|
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',
|
'Is {0} {1} module? The operation may cause abnormality in the operating environment, please confirm before proceeding',
|
||||||
customScript: 'Custom startup command',
|
customScript: 'Custom startup command',
|
||||||
customScriptHelper: 'Please fill in the complete startup command, for example: npm run start',
|
customScriptHelper: 'Please fill in the complete startup command, for example: npm run start',
|
||||||
|
portError: 'Cannot fill in the same port',
|
||||||
},
|
},
|
||||||
process: {
|
process: {
|
||||||
pid: 'Process ID',
|
pid: 'Process ID',
|
||||||
|
|
|
@ -1729,6 +1729,7 @@ const message = {
|
||||||
nodeOperatorHelper: '是否{0} {1} 模組? 操作可能導致運轉環境異常,請確認後操作',
|
nodeOperatorHelper: '是否{0} {1} 模組? 操作可能導致運轉環境異常,請確認後操作',
|
||||||
customScript: '自訂啟動指令',
|
customScript: '自訂啟動指令',
|
||||||
customScriptHelper: '請填寫完整的啟動指令,例如:npm run start',
|
customScriptHelper: '請填寫完整的啟動指令,例如:npm run start',
|
||||||
|
portError: '不能填寫相同連接埠',
|
||||||
},
|
},
|
||||||
process: {
|
process: {
|
||||||
pid: '進程ID',
|
pid: '進程ID',
|
||||||
|
|
|
@ -1729,6 +1729,7 @@ const message = {
|
||||||
nodeOperatorHelper: '是否{0} {1} 模块?操作可能导致运行环境异常,请确认后操作',
|
nodeOperatorHelper: '是否{0} {1} 模块?操作可能导致运行环境异常,请确认后操作',
|
||||||
customScript: '自定义启动命令',
|
customScript: '自定义启动命令',
|
||||||
customScriptHelper: '请填写完整的启动命令,例如:npm run start',
|
customScriptHelper: '请填写完整的启动命令,例如:npm run start',
|
||||||
|
portError: '不能填写相同端口',
|
||||||
},
|
},
|
||||||
process: {
|
process: {
|
||||||
pid: '进程ID',
|
pid: '进程ID',
|
||||||
|
|
|
@ -105,18 +105,25 @@
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="20">
|
<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-form-item :label="$t('runtime.appPort')" prop="params.NODE_APP_PORT">
|
||||||
<el-input v-model.number="runtime.params['NODE_APP_PORT']" />
|
<el-input v-model.number="runtime.params['NODE_APP_PORT']" />
|
||||||
<span class="input-help">{{ $t('runtime.appPortHelper') }}</span>
|
<span class="input-help">{{ $t('runtime.appPortHelper') }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="9">
|
<el-col :span="7">
|
||||||
<el-form-item :label="$t('runtime.externalPort')" prop="port">
|
<el-form-item :label="$t('runtime.externalPort')" prop="port">
|
||||||
<el-input v-model.number="runtime.port" />
|
<el-input v-model.number="runtime.port" />
|
||||||
<span class="input-help">{{ $t('runtime.externalPortHelper') }}</span>
|
<span class="input-help">{{ $t('runtime.externalPortHelper') }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</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-col :span="6">
|
||||||
<el-form-item :label="$t('app.allowPort')" prop="params.HOST_IP">
|
<el-form-item :label="$t('app.allowPort')" prop="params.HOST_IP">
|
||||||
<el-switch
|
<el-switch
|
||||||
|
@ -127,6 +134,31 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</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-form-item :label="$t('runtime.packageManager')" prop="params.PACKAGE_MANAGER">
|
||||||
<el-select v-model="runtime.params['PACKAGE_MANAGER']">
|
<el-select v-model="runtime.params['PACKAGE_MANAGER']">
|
||||||
<el-option label="npm" value="npm"></el-option>
|
<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 { CreateRuntime, GetNodeScripts, GetRuntime, UpdateRuntime } from '@/api/modules/runtime';
|
||||||
import { Rules, checkNumberRange } from '@/global/form-rules';
|
import { Rules, checkNumberRange } from '@/global/form-rules';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { MsgSuccess } from '@/utils/message';
|
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||||
import { FormInstance } from 'element-plus';
|
import { FormInstance } from 'element-plus';
|
||||||
import { reactive, ref, watch } from 'vue';
|
import { reactive, ref, watch } from 'vue';
|
||||||
import DrawerHeader from '@/components/drawer-header/index.vue';
|
import DrawerHeader from '@/components/drawer-header/index.vue';
|
||||||
|
@ -209,6 +241,7 @@ const initData = (type: string) => ({
|
||||||
codeDir: '/',
|
codeDir: '/',
|
||||||
port: 3000,
|
port: 3000,
|
||||||
source: 'https://registry.npmjs.org/',
|
source: 'https://registry.npmjs.org/',
|
||||||
|
exposedPorts: [],
|
||||||
});
|
});
|
||||||
let runtime = reactive<Runtime.RuntimeCreate>(initData('node'));
|
let runtime = reactive<Runtime.RuntimeCreate>(initData('node'));
|
||||||
const rules = ref<any>({
|
const rules = ref<any>({
|
||||||
|
@ -217,7 +250,6 @@ const rules = ref<any>({
|
||||||
codeDir: [Rules.requiredInput],
|
codeDir: [Rules.requiredInput],
|
||||||
port: [Rules.requiredInput, Rules.paramPort, checkNumberRange(1, 65535)],
|
port: [Rules.requiredInput, Rules.paramPort, checkNumberRange(1, 65535)],
|
||||||
source: [Rules.requiredSelect],
|
source: [Rules.requiredSelect],
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
NODE_APP_PORT: [Rules.requiredInput, Rules.paramPort, checkNumberRange(1, 65535)],
|
NODE_APP_PORT: [Rules.requiredInput, Rules.paramPort, checkNumberRange(1, 65535)],
|
||||||
PACKAGE_MANAGER: [Rules.requiredSelect],
|
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 = () => {
|
const getScripts = () => {
|
||||||
GetNodeScripts({ codeDir: runtime.codeDir }).then((res) => {
|
GetNodeScripts({ codeDir: runtime.codeDir }).then((res) => {
|
||||||
scripts.value = res.data;
|
scripts.value = res.data;
|
||||||
|
@ -348,6 +391,25 @@ const submit = async (formEl: FormInstance | undefined) => {
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return;
|
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') {
|
if (mode.value == 'create') {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
CreateRuntime(runtime)
|
CreateRuntime(runtime)
|
||||||
|
@ -391,6 +453,7 @@ const getRuntime = async (id: number) => {
|
||||||
codeDir: data.codeDir,
|
codeDir: data.codeDir,
|
||||||
port: data.port,
|
port: data.port,
|
||||||
});
|
});
|
||||||
|
runtime.exposedPorts = data.exposedPorts || [];
|
||||||
editParams.value = data.appParams;
|
editParams.value = data.appParams;
|
||||||
searchApp(data.appID);
|
searchApp(data.appID);
|
||||||
if (data.params['CUSTOM_SCRIPT'] == '0') {
|
if (data.params['CUSTOM_SCRIPT'] == '0') {
|
||||||
|
|
Loading…
Add table
Reference in a new issue