feat: Node.js 运行环境增加多端口 (#2691)

Refs https://github.com/1Panel-dev/1Panel/issues/2566
This commit is contained in:
zhengkunwang 2023-10-27 10:12:15 +08:00 committed by GitHub
parent 8857d97dd7
commit 048df6f390
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 189 additions and 27 deletions

View file

@ -23,9 +23,15 @@ type RuntimeCreate struct {
} }
type NodeConfig struct { 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 {

View file

@ -1,28 +1,30 @@
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"
) )
type RuntimeDTO struct { type RuntimeDTO struct {
ID uint `json:"id"` ID uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Resource string `json:"resource"` Resource string `json:"resource"`
AppDetailID uint `json:"appDetailID"` AppDetailID uint `json:"appDetailID"`
AppID uint `json:"appID"` AppID uint `json:"appID"`
Source string `json:"source"` Source string `json:"source"`
Status string `json:"status"` Status string `json:"status"`
Type string `json:"type"` Type string `json:"type"`
Image string `json:"image"` Image string `json:"image"`
Params map[string]interface{} `json:"params"` Params map[string]interface{} `json:"params"`
Message string `json:"message"` Message string `json:"message"`
Version string `json:"version"` Version string `json:"version"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
CodeDir string `json:"codeDir"` CodeDir string `json:"codeDir"`
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 {

View file

@ -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,7 +288,26 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
} }
res.Params[k] = port res.Params[k] = port
default: 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 { if v, ok := envs["CONTAINER_PACKAGE_URL"]; ok {
@ -361,8 +382,9 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
CodeDir: req.CodeDir, CodeDir: req.CodeDir,
Version: req.Version, Version: req.Version,
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)

View file

@ -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 {

View file

@ -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 {

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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') {