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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -1729,6 +1729,7 @@ const message = {
nodeOperatorHelper: '是否{0} {1} 模組 操作可能導致運轉環境異常請確認後操作',
customScript: '自訂啟動指令',
customScriptHelper: '請填寫完整的啟動指令例如npm run start',
portError: '不能填寫相同連接埠',
},
process: {
pid: '進程ID',

View file

@ -1729,6 +1729,7 @@ const message = {
nodeOperatorHelper: '是否{0} {1} 模块操作可能导致运行环境异常请确认后操作',
customScript: '自定义启动命令',
customScriptHelper: '请填写完整的启动命令例如npm run start',
portError: '不能填写相同端口',
},
process: {
pid: '进程ID',

View file

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