From 80e22ffc8247c82155747f27fc3ce94f71f79a3d Mon Sep 17 00:00:00 2001 From: zhengkunwang223 <31820853+zhengkunwang223@users.noreply.github.com> Date: Wed, 17 May 2023 13:46:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=AB=98=E7=BA=A7=E8=AE=BE=E7=BD=AE=20(#1060?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/dto/request/app.go | 14 +- backend/app/service/app.go | 47 +++++++ backend/app/service/app_utils.go | 3 + backend/app/service/website.go | 2 +- backend/constant/errs.go | 2 +- backend/utils/ssl/acme_test.go | 128 ++++++++++++++++++ frontend/src/global/form-rules.ts | 21 ++- frontend/src/lang/modules/zh.ts | 7 + frontend/src/views/app-store/detail/index.vue | 2 +- .../views/app-store/detail/install/index.vue | 77 +++++++++-- 10 files changed, 280 insertions(+), 23 deletions(-) diff --git a/backend/app/dto/request/app.go b/backend/app/dto/request/app.go index af0c4eb66..ab6bd8803 100644 --- a/backend/app/dto/request/app.go +++ b/backend/app/dto/request/app.go @@ -14,10 +14,16 @@ type AppSearch struct { } type AppInstallCreate struct { - AppDetailId uint `json:"appDetailId" validate:"required"` - Params map[string]interface{} `json:"params"` - Name string `json:"name" validate:"required"` - Services map[string]string `json:"services"` + AppDetailId uint `json:"appDetailId" validate:"required"` + Params map[string]interface{} `json:"params"` + Name string `json:"name" validate:"required"` + Services map[string]string `json:"services"` + Advanced bool `json:"advanced"` + CpuQuota float64 `json:"cpuQuota"` + MemoryLimit float64 `json:"memoryLimit"` + MemoryUnit string `json:"memoryUnit"` + ContainerName string `json:"containerName"` + AllowPort bool `json:"allowPort"` } type AppInstalledSearch struct { diff --git a/backend/app/service/app.go b/backend/app/service/app.go index d41abd6ad..9b9467f2d 100644 --- a/backend/app/service/app.go +++ b/backend/app/service/app.go @@ -12,6 +12,7 @@ import ( "os" "path" "strconv" + "strings" "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto/request" @@ -285,6 +286,9 @@ func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) ( serviceName := k + "-" + common.RandStr(4) changeKeys[k] = serviceName containerName := constant.ContainerPrefix + k + "-" + common.RandStr(4) + if req.Advanced && req.ContainerName != "" { + containerName = req.ContainerName + } if index > 0 { continue } @@ -297,6 +301,49 @@ func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) ( servicesMap[v] = servicesMap[k] delete(servicesMap, k) } + serviceValue := servicesMap[appInstall.ServiceName].(map[string]interface{}) + if req.Advanced && (req.CpuQuota > 0 || req.MemoryLimit > 0) { + deploy := map[string]interface{}{ + "resources": map[string]interface{}{ + "limits": map[string]interface{}{ + "cpus": "${CPUS}", + "memory": "${MEMORY_LIMIT}", + }, + }, + } + req.Params["CPUS"] = "0" + if req.CpuQuota > 0 { + req.Params["CPUS"] = req.CpuQuota + } + req.Params["MEMORY_LIMIT"] = "0" + if req.MemoryLimit > 0 { + req.Params["MEMORY_LIMIT"] = strconv.FormatFloat(req.MemoryLimit, 'f', -1, 32) + req.MemoryUnit + } + serviceValue["deploy"] = deploy + } + + ports, ok := serviceValue["ports"].([]interface{}) + if ok { + allowHost := "127.0.0.1" + if req.AllowPort { + allowHost = "0.0.0.0" + } + req.Params["HOST_IP"] = allowHost + for i, port := range ports { + portStr, portOK := port.(string) + if !portOK { + continue + } + portArray := strings.Split(portStr, ":") + if len(portArray) == 2 { + portArray = append([]string{"${HOST_IP}"}, portArray...) + } + ports[i] = strings.Join(portArray, ":") + } + serviceValue["ports"] = ports + } + + servicesMap[appInstall.ServiceName] = serviceValue var ( composeByte []byte diff --git a/backend/app/service/app_utils.go b/backend/app/service/app_utils.go index cb0b32247..e9d424b0a 100644 --- a/backend/app/service/app_utils.go +++ b/backend/app/service/app_utils.go @@ -393,6 +393,9 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App if err = env.Write(envParams, envPath); err != nil { return } + if err := fileOp.WriteFile(appInstall.GetComposePath(), strings.NewReader(appInstall.DockerCompose), 0755); err != nil { + return err + } return } diff --git a/backend/app/service/website.go b/backend/app/service/website.go index 6dec4c46a..0ba7f1aac 100644 --- a/backend/app/service/website.go +++ b/backend/app/service/website.go @@ -859,7 +859,7 @@ func (w WebsiteService) OpWebsiteLog(req request.WebsiteLogReq) (*response.Websi if err != nil { return nil, err } - if fileInfo.Size() > 10<<20 { + if fileInfo.Size() > 20<<20 { return nil, buserr.New(constant.ErrFileToLarge) } fileInfo.Size() diff --git a/backend/constant/errs.go b/backend/constant/errs.go index f9f5fe23a..fb3ce4723 100644 --- a/backend/constant/errs.go +++ b/backend/constant/errs.go @@ -46,7 +46,7 @@ var ( var ( ErrPortInUsed = "ErrPortInUsed" ErrAppLimit = "ErrAppLimit" - ErrAppRequired = "ErrAppRequired" + ErrFileToLarge = "ErrFileToLarge" ErrFileCanNotRead = "ErrFileCanNotRead" ErrNotInstall = "ErrNotInstall" ErrPortInOtherApp = "ErrPortInOtherApp" diff --git a/backend/utils/ssl/acme_test.go b/backend/utils/ssl/acme_test.go index 681f6eb8c..286c45ad9 100644 --- a/backend/utils/ssl/acme_test.go +++ b/backend/utils/ssl/acme_test.go @@ -1,12 +1,17 @@ package ssl import ( + "bytes" "crypto/rand" "crypto/rsa" "crypto/x509" + "encoding/json" "encoding/pem" "fmt" + "github.com/1Panel-dev/1Panel/backend/utils/files" + "gopkg.in/yaml.v3" "os" + "path" "testing" "time" @@ -19,6 +24,129 @@ import ( "github.com/go-acme/lego/v4/registration" ) +type AppList struct { + Version string `json:"version"` + Tags []Tag `json:"tags"` + Items []AppDefine `json:"items"` +} + +type NewAppDefine struct { + Name string `yaml:"name"` + Tags []string `yaml:"tags"` + Title string `yaml:"title"` + Type string `yaml:"type"` + Description string `yaml:"description"` + AdditionalProperties AppDefine `yaml:"additionalProperties"` +} + +type NewAppConfig struct { + AdditionalProperties map[string]interface{} `yaml:"additionalProperties"` +} + +type AppDefine struct { + Key string `json:"key" yaml:"key"` + Name string `json:"name" yaml:"name"` + Tags []string `json:"tags" yaml:"tags"` + Versions []string `json:"versions" yaml:"-"` + ShortDescZh string `json:"shortDescZh" yaml:"shortDescZh"` + ShortDescEn string `json:"shortDescEn" yaml:"shortDescEn"` + Type string `json:"type" yaml:"type"` + CrossVersionUpdate bool `json:"crossVersionUpdate" yaml:"crossVersionUpdate"` + Limit int `json:"limit" yaml:"limit"` + Recommend int `json:"recommend" yaml:"recommend"` + Website string `json:"website" yaml:"website"` + Github string `json:"github" yaml:"github"` + Document string `json:"document" yaml:"document"` +} + +type Tag struct { + Key string `json:"key" yaml:"key"` + Name string `json:"name" yaml:"name"` +} + +func getTagName(key string, tags []Tag) string { + result := "应用" + for _, tag := range tags { + if tag.Key == key { + return tag.Name + } + } + return result +} + +func TestAppToV2(t *testing.T) { + oldDir := "/Users/wangzhengkun/projects/github.com/1Panel-dev/appstore/apps" + newDir := "/Users/wangzhengkun/projects/github.com/1Panel-dev/appstore/apps_new" + listJsonDir := path.Join(oldDir, "list.json") + fileOp := files.NewFileOp() + content, err := fileOp.GetContent(listJsonDir) + if err != nil { + panic(err) + } + appList := &AppList{} + if err = json.Unmarshal(content, appList); err != nil { + panic(err) + } + + for _, appDefine := range appList.Items { + newAppDefine := &NewAppDefine{ + Name: appDefine.Name, + Tags: []string{getTagName(appDefine.Tags[0], appList.Tags)}, + Type: getTagName(appDefine.Tags[0], appList.Tags), + Title: appDefine.ShortDescZh, + Description: appDefine.ShortDescZh, + AdditionalProperties: appDefine, + } + + yamlContent, err := yaml.Marshal(newAppDefine) + if err != nil { + panic(err) + } + oldAppDir := oldDir + "/" + appDefine.Key + newAppDir := newDir + "/" + appDefine.Key + if !fileOp.Stat(newAppDir) { + fileOp.CreateDir(newAppDir, 0755) + } + // logo + oldLogoPath := oldAppDir + "/metadata/logo.png" + if err := fileOp.CopyFile(oldLogoPath, newAppDir); err != nil { + panic(err) + } + for _, version := range appDefine.Versions { + oldVersionDir := oldAppDir + "/versions/" + version + if err := fileOp.CopyDir(oldVersionDir, newAppDir); err != nil { + panic(err) + } + oldConfigPath := oldVersionDir + "/config.json" + configContent, err := fileOp.GetContent(oldConfigPath) + if err != nil { + panic(err) + } + var result map[string]interface{} + if err := json.Unmarshal(configContent, &result); err != nil { + panic(err) + } + newConfigD := &NewAppConfig{} + newConfigD.AdditionalProperties = result + configYamlByte, err := yaml.Marshal(newConfigD) + if err != nil { + panic(err) + } + newVersionDir := newAppDir + "/" + version + if err := fileOp.WriteFile(newVersionDir+"/data.yml", bytes.NewReader(configYamlByte), 0755); err != nil { + panic(err) + } + if err := fileOp.WriteFile(newAppDir+"/data.yml", bytes.NewReader(yamlContent), 0755); err != nil { + panic(err) + } + _ = fileOp.DeleteFile(newVersionDir + "/config.json") + oldReadMefile := newVersionDir + "/README.md" + _ = fileOp.Cut([]string{oldReadMefile}, newAppDir) + _ = fileOp.DeleteFile(oldReadMefile) + } + } +} + func TestCreatePrivate(t *testing.T) { priKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { diff --git a/frontend/src/global/form-rules.ts b/frontend/src/global/form-rules.ts index 7ed511dc2..198dbb11b 100644 --- a/frontend/src/global/form-rules.ts +++ b/frontend/src/global/form-rules.ts @@ -255,7 +255,7 @@ const checkDoc = (rule: any, value: any, callback: any) => { export function checkNumberRange(min: number, max: number): FormItemRule { return { - required: true, + required: false, trigger: 'blur', min: min, max: max, @@ -264,6 +264,19 @@ export function checkNumberRange(min: number, max: number): FormItemRule { }; } +const checkConatinerName = (rule: any, value: any, callback: any) => { + if (value === '' || typeof value === 'undefined' || value == null) { + callback(); + } else { + const reg = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,127}$/; + if (!reg.test(value) && value !== '') { + callback(new Error(i18n.global.t('commons.rule.conatinerName'))); + } else { + callback(); + } + } +}; + interface CommonRule { requiredInput: FormItemRule; requiredSelect: FormItemRule; @@ -286,6 +299,7 @@ interface CommonRule { databaseName: FormItemRule; nginxDoc: FormItemRule; appName: FormItemRule; + containerName: FormItemRule; paramCommon: FormItemRule; paramComplexity: FormItemRule; @@ -427,4 +441,9 @@ export const Rules: CommonRule = { trigger: 'blur', validator: checkAppName, }, + containerName: { + required: false, + trigger: 'blur', + validator: checkConatinerName, + }, }; diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index aaa9acaef..8523718e6 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -158,6 +158,7 @@ const message = { paramUrlAndPort: '格式为 http(s)://(域名/ip):(端口)', nginxDoc: '仅支持英文大小写,数字,和.', appName: '支持英文、数字、-和_,长度2-30,并且不能以-_开头和结尾', + conatinerName: '支持字母、数字、下划线、连字符和点,不能以连字符-或点.结尾', }, res: { paramError: '请求失败,请稍后重试!', @@ -1052,6 +1053,12 @@ const message = { updateWarn: '更新参数需要重建应用,是否继续?', busPort: '服务端口', syncStart: '开始同步!请稍后刷新应用商店', + advanced: '高级设置', + cpuCore: '核心数', + containerName: '容器名称', + conatinerNameHelper: '可以为空,为空自动生成', + allowPort: '端口外部访问', + allowPortHelper: '允许外部端口访问会放开防火墙端口,php运行环境请勿放开', }, website: { website: '网站', diff --git a/frontend/src/views/app-store/detail/index.vue b/frontend/src/views/app-store/detail/index.vue index 4d0743778..e05fd221c 100644 --- a/frontend/src/views/app-store/detail/index.vue +++ b/frontend/src/views/app-store/detail/index.vue @@ -83,7 +83,7 @@