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