feat: 创建应用增加高级设置 (#1060)

This commit is contained in:
zhengkunwang223 2023-05-17 13:46:28 +08:00 committed by GitHub
parent 872581fa4b
commit 80e22ffc82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 280 additions and 23 deletions

View File

@ -14,10 +14,16 @@ type AppSearch struct {
} }
type AppInstallCreate struct { type AppInstallCreate struct {
AppDetailId uint `json:"appDetailId" validate:"required"` AppDetailId uint `json:"appDetailId" validate:"required"`
Params map[string]interface{} `json:"params"` Params map[string]interface{} `json:"params"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Services map[string]string `json:"services"` 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 { type AppInstalledSearch struct {

View File

@ -12,6 +12,7 @@ import (
"os" "os"
"path" "path"
"strconv" "strconv"
"strings"
"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"
@ -285,6 +286,9 @@ func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) (
serviceName := k + "-" + common.RandStr(4) serviceName := k + "-" + common.RandStr(4)
changeKeys[k] = serviceName changeKeys[k] = serviceName
containerName := constant.ContainerPrefix + k + "-" + common.RandStr(4) containerName := constant.ContainerPrefix + k + "-" + common.RandStr(4)
if req.Advanced && req.ContainerName != "" {
containerName = req.ContainerName
}
if index > 0 { if index > 0 {
continue continue
} }
@ -297,6 +301,49 @@ func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) (
servicesMap[v] = servicesMap[k] servicesMap[v] = servicesMap[k]
delete(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 ( var (
composeByte []byte composeByte []byte

View File

@ -393,6 +393,9 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
if err = env.Write(envParams, envPath); err != nil { if err = env.Write(envParams, envPath); err != nil {
return return
} }
if err := fileOp.WriteFile(appInstall.GetComposePath(), strings.NewReader(appInstall.DockerCompose), 0755); err != nil {
return err
}
return return
} }

View File

@ -859,7 +859,7 @@ func (w WebsiteService) OpWebsiteLog(req request.WebsiteLogReq) (*response.Websi
if err != nil { if err != nil {
return nil, err return nil, err
} }
if fileInfo.Size() > 10<<20 { if fileInfo.Size() > 20<<20 {
return nil, buserr.New(constant.ErrFileToLarge) return nil, buserr.New(constant.ErrFileToLarge)
} }
fileInfo.Size() fileInfo.Size()

View File

@ -46,7 +46,7 @@ var (
var ( var (
ErrPortInUsed = "ErrPortInUsed" ErrPortInUsed = "ErrPortInUsed"
ErrAppLimit = "ErrAppLimit" ErrAppLimit = "ErrAppLimit"
ErrAppRequired = "ErrAppRequired" ErrFileToLarge = "ErrFileToLarge"
ErrFileCanNotRead = "ErrFileCanNotRead" ErrFileCanNotRead = "ErrFileCanNotRead"
ErrNotInstall = "ErrNotInstall" ErrNotInstall = "ErrNotInstall"
ErrPortInOtherApp = "ErrPortInOtherApp" ErrPortInOtherApp = "ErrPortInOtherApp"

View File

@ -1,12 +1,17 @@
package ssl package ssl
import ( import (
"bytes"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/json"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"gopkg.in/yaml.v3"
"os" "os"
"path"
"testing" "testing"
"time" "time"
@ -19,6 +24,129 @@ import (
"github.com/go-acme/lego/v4/registration" "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) { func TestCreatePrivate(t *testing.T) {
priKey, err := rsa.GenerateKey(rand.Reader, 2048) priKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil { if err != nil {

View File

@ -255,7 +255,7 @@ const checkDoc = (rule: any, value: any, callback: any) => {
export function checkNumberRange(min: number, max: number): FormItemRule { export function checkNumberRange(min: number, max: number): FormItemRule {
return { return {
required: true, required: false,
trigger: 'blur', trigger: 'blur',
min: min, min: min,
max: max, 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 { interface CommonRule {
requiredInput: FormItemRule; requiredInput: FormItemRule;
requiredSelect: FormItemRule; requiredSelect: FormItemRule;
@ -286,6 +299,7 @@ interface CommonRule {
databaseName: FormItemRule; databaseName: FormItemRule;
nginxDoc: FormItemRule; nginxDoc: FormItemRule;
appName: FormItemRule; appName: FormItemRule;
containerName: FormItemRule;
paramCommon: FormItemRule; paramCommon: FormItemRule;
paramComplexity: FormItemRule; paramComplexity: FormItemRule;
@ -427,4 +441,9 @@ export const Rules: CommonRule = {
trigger: 'blur', trigger: 'blur',
validator: checkAppName, validator: checkAppName,
}, },
containerName: {
required: false,
trigger: 'blur',
validator: checkConatinerName,
},
}; };

View File

@ -158,6 +158,7 @@ const message = {
paramUrlAndPort: '格式为 http(s)://(域名/ip):(端口)', paramUrlAndPort: '格式为 http(s)://(域名/ip):(端口)',
nginxDoc: '仅支持英文大小写数字.', nginxDoc: '仅支持英文大小写数字.',
appName: '支持英文数字-和_,长度2-30,并且不能以-_开头和结尾', appName: '支持英文数字-和_,长度2-30,并且不能以-_开头和结尾',
conatinerName: '支持字母数字下划线连字符和点,不能以连字符-或点.结尾',
}, },
res: { res: {
paramError: '请求失败,请稍后重试!', paramError: '请求失败,请稍后重试!',
@ -1052,6 +1053,12 @@ const message = {
updateWarn: '更新参数需要重建应用是否继续', updateWarn: '更新参数需要重建应用是否继续',
busPort: '服务端口', busPort: '服务端口',
syncStart: '开始同步请稍后刷新应用商店', syncStart: '开始同步请稍后刷新应用商店',
advanced: '高级设置',
cpuCore: '核心数',
containerName: '容器名称',
conatinerNameHelper: '可以为空为空自动生成',
allowPort: '端口外部访问',
allowPortHelper: '允许外部端口访问会放开防火墙端口php运行环境请勿放开',
}, },
website: { website: {
website: '网站', website: '网站',

View File

@ -83,7 +83,7 @@
</div> </div>
</div> </div>
<div style="margin-left: 10px"> <div style="margin-left: 10px">
<MdEditor v-model="app.readMe" previewOnly :theme="globalStore.$state.themeConfig.theme || 'light'" /> <MdEditor v-model="app.readMe" previewOnly :themes="globalStore.$state.themeConfig.theme || 'light'" />
</div> </div>
</template> </template>
</LayoutContent> </LayoutContent>

View File

@ -16,20 +16,54 @@
@submit.prevent @submit.prevent
ref="paramForm" ref="paramForm"
label-position="top" label-position="top"
:model="form" :model="req"
label-width="150px" label-width="150px"
:rules="rules" :rules="rules"
:validate-on-rule-change="false" :validate-on-rule-change="false"
> >
<el-form-item :label="$t('app.name')" prop="NAME"> <el-form-item :label="$t('app.name')" prop="name">
<el-input v-model.trim="form['NAME']"></el-input> <el-input v-model.trim="req.name"></el-input>
</el-form-item> </el-form-item>
<Params <Params
v-if="open" v-if="open"
v-model:form="form" v-model:form="req.params"
v-model:params="installData.params" v-model:params="installData.params"
v-model:rules="rules" v-model:rules="rules.params"
:propStart="'params.'"
></Params> ></Params>
<el-form-item prop="advanced">
<el-checkbox v-model="req.advanced" :label="$t('app.advanced')" size="large" />
</el-form-item>
<div v-if="req.advanced">
<el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input
v-model.trim="req.containerName"
:placeholder="$t('app.conatinerNameHelper')"
></el-input>
</el-form-item>
<el-form-item :label="$t('container.cpuQuota')" prop="cpuQuota">
<el-input type="number" style="width: 40%" v-model.number="req.cpuQuota" maxlength="5">
<template #append>{{ $t('app.cpuCore') }}</template>
</el-input>
<span class="input-help">{{ $t('container.limitHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('container.memoryLimit')" prop="memoryLimit">
<el-input style="width: 40%" v-model.number="req.memoryLimit" maxlength="10">
<template #append>
<el-select v-model="req.memoryUnit" placeholder="Select" style="width: 85px">
<el-option label="KB" value="K" />
<el-option label="MB" value="M" />
<el-option label="GB" value="G" />
</el-select>
</template>
</el-input>
<span class="input-help">{{ $t('container.limitHelper') }}</span>
</el-form-item>
<el-form-item prop="allowPort">
<el-checkbox v-model="req.allowPort" :label="$t('app.allowPort')" size="large" />
<span class="input-help">{{ $t('app.allowPortHelper') }}</span>
</el-form-item>
</div>
</el-form> </el-form>
</el-col> </el-col>
</el-row> </el-row>
@ -48,7 +82,7 @@
<script lang="ts" setup name="appInstall"> <script lang="ts" setup name="appInstall">
import { App } from '@/api/interface/app'; import { App } from '@/api/interface/app';
import { InstallApp } from '@/api/modules/app'; import { InstallApp } from '@/api/modules/app';
import { Rules } from '@/global/form-rules'; import { Rules, checkNumberRange } from '@/global/form-rules';
import { FormInstance, FormRules } from 'element-plus'; import { FormInstance, FormRules } from 'element-plus';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -65,19 +99,33 @@ interface InstallRrops {
const installData = ref<InstallRrops>({ const installData = ref<InstallRrops>({
appDetailId: 0, appDetailId: 0,
}); });
let open = ref(false); const open = ref(false);
let form = ref<{ [key: string]: any }>({}); const rules = ref<FormRules>({
let rules = ref<FormRules>({ name: [Rules.appName],
NAME: [Rules.appName], params: [],
containerName: [Rules.containerName],
cpuQuota: [checkNumberRange(0, 99999)],
memoryLimit: [checkNumberRange(0, 9999999999)],
}); });
let loading = ref(false); const loading = ref(false);
const paramForm = ref<FormInstance>(); const paramForm = ref<FormInstance>();
const req = reactive({
const form = ref<{ [key: string]: any }>({});
const initData = () => ({
appDetailId: 0, appDetailId: 0,
params: {}, params: form.value,
name: '', name: '',
advanced: false,
cpuQuota: 0,
memoryLimit: 0,
memoryUnit: 'MB',
containerName: '',
allowPort: false,
}); });
const req = reactive(initData());
const handleClose = () => { const handleClose = () => {
open.value = false; open.value = false;
resetForm(); resetForm();
@ -88,6 +136,7 @@ const resetForm = () => {
paramForm.value.clearValidate(); paramForm.value.clearValidate();
paramForm.value.resetFields(); paramForm.value.resetFields();
} }
Object.assign(req, initData());
}; };
const acceptParams = (props: InstallRrops): void => { const acceptParams = (props: InstallRrops): void => {
@ -103,8 +152,6 @@ const submit = async (formEl: FormInstance | undefined) => {
return; return;
} }
req.appDetailId = installData.value.appDetailId; req.appDetailId = installData.value.appDetailId;
req.params = form.value;
req.name = form.value['NAME'];
loading.value = true; loading.value = true;
InstallApp(req) InstallApp(req)
.then(() => { .then(() => {