mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-11-24 02:59:16 +08:00
feat: 创建应用增加高级设置 (#1060)
This commit is contained in:
parent
872581fa4b
commit
80e22ffc82
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -46,7 +46,7 @@ var (
|
||||
var (
|
||||
ErrPortInUsed = "ErrPortInUsed"
|
||||
ErrAppLimit = "ErrAppLimit"
|
||||
ErrAppRequired = "ErrAppRequired"
|
||||
ErrFileToLarge = "ErrFileToLarge"
|
||||
ErrFileCanNotRead = "ErrFileCanNotRead"
|
||||
ErrNotInstall = "ErrNotInstall"
|
||||
ErrPortInOtherApp = "ErrPortInOtherApp"
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -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: '网站',
|
||||
|
@ -83,7 +83,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
|
@ -16,20 +16,54 @@
|
||||
@submit.prevent
|
||||
ref="paramForm"
|
||||
label-position="top"
|
||||
:model="form"
|
||||
:model="req"
|
||||
label-width="150px"
|
||||
:rules="rules"
|
||||
:validate-on-rule-change="false"
|
||||
>
|
||||
<el-form-item :label="$t('app.name')" prop="NAME">
|
||||
<el-input v-model.trim="form['NAME']"></el-input>
|
||||
<el-form-item :label="$t('app.name')" prop="name">
|
||||
<el-input v-model.trim="req.name"></el-input>
|
||||
</el-form-item>
|
||||
<Params
|
||||
v-if="open"
|
||||
v-model:form="form"
|
||||
v-model:form="req.params"
|
||||
v-model:params="installData.params"
|
||||
v-model:rules="rules"
|
||||
v-model:rules="rules.params"
|
||||
:propStart="'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-col>
|
||||
</el-row>
|
||||
@ -48,7 +82,7 @@
|
||||
<script lang="ts" setup name="appInstall">
|
||||
import { App } from '@/api/interface/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 { reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
@ -65,19 +99,33 @@ interface InstallRrops {
|
||||
const installData = ref<InstallRrops>({
|
||||
appDetailId: 0,
|
||||
});
|
||||
let open = ref(false);
|
||||
let form = ref<{ [key: string]: any }>({});
|
||||
let rules = ref<FormRules>({
|
||||
NAME: [Rules.appName],
|
||||
const open = ref(false);
|
||||
const rules = ref<FormRules>({
|
||||
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 req = reactive({
|
||||
|
||||
const form = ref<{ [key: string]: any }>({});
|
||||
|
||||
const initData = () => ({
|
||||
appDetailId: 0,
|
||||
params: {},
|
||||
params: form.value,
|
||||
name: '',
|
||||
advanced: false,
|
||||
cpuQuota: 0,
|
||||
memoryLimit: 0,
|
||||
memoryUnit: 'MB',
|
||||
containerName: '',
|
||||
allowPort: false,
|
||||
});
|
||||
|
||||
const req = reactive(initData());
|
||||
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
resetForm();
|
||||
@ -88,6 +136,7 @@ const resetForm = () => {
|
||||
paramForm.value.clearValidate();
|
||||
paramForm.value.resetFields();
|
||||
}
|
||||
Object.assign(req, initData());
|
||||
};
|
||||
|
||||
const acceptParams = (props: InstallRrops): void => {
|
||||
@ -103,8 +152,6 @@ const submit = async (formEl: FormInstance | undefined) => {
|
||||
return;
|
||||
}
|
||||
req.appDetailId = installData.value.appDetailId;
|
||||
req.params = form.value;
|
||||
req.name = form.value['NAME'];
|
||||
loading.value = true;
|
||||
InstallApp(req)
|
||||
.then(() => {
|
||||
|
Loading…
Reference in New Issue
Block a user