diff --git a/backend/app/dto/request/runtime.go b/backend/app/dto/request/runtime.go
index 32469d990..d9c218500 100644
--- a/backend/app/dto/request/runtime.go
+++ b/backend/app/dto/request/runtime.go
@@ -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 {
diff --git a/backend/app/dto/response/runtime.go b/backend/app/dto/response/runtime.go
index 258168613..d743a24c9 100644
--- a/backend/app/dto/response/runtime.go
+++ b/backend/app/dto/response/runtime.go
@@ -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 {
diff --git a/backend/app/service/runtime.go b/backend/app/service/runtime.go
index 649fbd403..61c36b7a8 100644
--- a/backend/app/service/runtime.go
+++ b/backend/app/service/runtime.go
@@ -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)
diff --git a/backend/app/service/runtime_utils.go b/backend/app/service/runtime_utils.go
index 6cc9a2619..bf40cf385 100644
--- a/backend/app/service/runtime_utils.go
+++ b/backend/app/service/runtime_utils.go
@@ -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 {
diff --git a/frontend/src/api/interface/runtime.ts b/frontend/src/api/interface/runtime.ts
index 9c2e1ac48..b327aea22 100644
--- a/frontend/src/api/interface/runtime.ts
+++ b/frontend/src/api/interface/runtime.ts
@@ -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 {
diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts
index a5f8e3a62..736834fac 100644
--- a/frontend/src/lang/modules/en.ts
+++ b/frontend/src/lang/modules/en.ts
@@ -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',
diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts
index cd9a58755..cfec83793 100644
--- a/frontend/src/lang/modules/tw.ts
+++ b/frontend/src/lang/modules/tw.ts
@@ -1729,6 +1729,7 @@ const message = {
nodeOperatorHelper: '是否{0} {1} 模組? 操作可能導致運轉環境異常,請確認後操作',
customScript: '自訂啟動指令',
customScriptHelper: '請填寫完整的啟動指令,例如:npm run start',
+ portError: '不能填寫相同連接埠',
},
process: {
pid: '進程ID',
diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts
index 9b7a9d86b..247472e91 100644
--- a/frontend/src/lang/modules/zh.ts
+++ b/frontend/src/lang/modules/zh.ts
@@ -1729,6 +1729,7 @@ const message = {
nodeOperatorHelper: '是否{0} {1} 模块?操作可能导致运行环境异常,请确认后操作',
customScript: '自定义启动命令',
customScriptHelper: '请填写完整的启动命令,例如:npm run start',
+ portError: '不能填写相同端口',
},
process: {
pid: '进程ID',
diff --git a/frontend/src/views/website/runtime/node/operate/index.vue b/frontend/src/views/website/runtime/node/operate/index.vue
index b5279fad9..96cba9b9c 100644
--- a/frontend/src/views/website/runtime/node/operate/index.vue
+++ b/frontend/src/views/website/runtime/node/operate/index.vue
@@ -105,18 +105,25 @@
-
+
{{ $t('runtime.appPortHelper') }}
-
+
{{ $t('runtime.externalPortHelper') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('commons.button.delete') }}
+
+
+
+
@@ -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(initData('node'));
const rules = ref({
@@ -217,7 +250,6 @@ const rules = ref({
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') {