1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-03-13 17:24:44 +08:00

feat: 增加wordpress应用

This commit is contained in:
zhengkunwang223 2022-10-07 15:49:39 +08:00 committed by zhengkunwang223
parent 362d2d2616
commit dc70d7cfc1
20 changed files with 272 additions and 115 deletions

View File

@ -0,0 +1,4 @@
version: '3'
networks:
1panel:
driver: bridge

View File

@ -26,6 +26,7 @@
"type": "internal",
"required": [""],
"crossVersionUpdate": false,
"limit": 0,
"source": "https://www.mysql.com"
},
{
@ -38,8 +39,23 @@
"author": "Nginx",
"type": "internal",
"required": [""],
"limit": 0,
"crossVersionUpdate": true,
"source": "http://nginx.org/"
},
{
"key": "wordpress",
"name": "Wordpress",
"tags": ["WebSite"],
"versions": ["6.0.1"],
"short_desc": "老牌博客网站模版",
"icon": "wordpress.png",
"author": "Wordpress",
"type": "internal",
"required": ["mysql"],
"limit": 0,
"crossVersionUpdate": true,
"source": "http://wordpress.org/"
}
]
}

View File

@ -11,6 +11,8 @@ services:
MYSQL_USER: ${USER}
MYSQL_PASSWORD: ${PASSWORD}
MYSQL_ROOT_PASSWORD: ${ROOT_PASSWORD}
networks:
- 1panel
ports:
- ${PORT}:3306
volumes:

View File

@ -1,83 +1,13 @@
[client]
port = 3306
socket = /var/run/mysqld/mysqld.sock
[mysqld_safe]
socket = /var/run/mysqld/mysqld.sock
nice = 0
[mysqld]
user = mysql
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
port = 3306
basedir = /usr
datadir = /var/lib/mysql
tmpdir = /tmp
lc-messages-dir = /usr/share/mysql
skip-external-locking
skip-character-set-client-handshake
default-storage-engine = InnoDB
character-set-server = utf8
transaction-isolation = READ-COMMITTED
skip-host-cache
skip-name-resolve
datadir=/var/lib/mysql
socket=/var/run/mysqld/mysqld.sock
secure-file-priv=/var/lib/mysql-files
user=mysql
pid-file=/var/run/mysqld/mysqld.pid
[client]
socket=/var/run/mysqld/mysqld.sock
bind-address = 127.0.0.1
key_buffer = 16M
max_allowed_packet = 16M
thread_stack = 192K
thread_cache_size = 16
myisam-recover = BACKUP
max_connections = 300
table_open_cache = 64
thread_concurrency = 10
table_open_cache = 32
thread_concurrency = 4
query_cache_type = 1
query_cache_limit = 1M
query_cache_size = 8M
general_log_file = /var/log/mysql/mysql.log
#general_log = 1
log_error = /var/log/mysql/error.log
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1
#log-queries-not-using-indexes
#server-id = 1
#log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 14
max_binlog_size = 1G
#binlog_do_db = include_database_name
#binlog_ignore_db = include_database_name
# ssl-ca=/etc/mysql/cacert.pem
# ssl-cert=/etc/mysql/server-cert.pem
# ssl-key=/etc/mysql/server-key.pem
innodb_data_file_path = ibdata1:128M:autoextend
innodb_file_per_table = 1
skip-innodb_doublewrite
innodb_additional_mem_pool_size = 12M
innodb_buffer_pool_size = 256M
innodb_log_buffer_size = 8M
innodb_log_file_size = 8M
innodb_flush_log_at_trx_commit = 0
innodb_flush_method = O_DIRECT
innodb_support_xa = OFF
[mysqldump]
quick
quote-names
max_allowed_packet = 16M
[mysql]
#no-auto-rehash # faster start of mysql but no tab completition
[isamchk]
key_buffer = 16M
!includedir /etc/mysql/conf.d/

View File

@ -11,14 +11,15 @@ services:
MYSQL_USER: ${USER}
MYSQL_PASSWORD: ${PASSWORD}
MYSQL_ROOT_PASSWORD: ${ROOT_PASSWORD}
networks:
- 1panel
ports:
- ${PORT}:3306
volumes:
- ./data/:/var/lib/mysql
- ./conf/my.cnf:/etc/mysql/my.cnf
- ./conf/my.cnf:/etc/my.cnf
- ./log:/var/log/mysql
command:
--character-set-server=utf8mb4
--collation-server=utf8mb4_general_ci
--explicit_defaults_for_timestamp=true
--lower_case_table_names=1
networks:
1panel:
external: true

View File

@ -4,17 +4,17 @@ services:
image: wordpress:6.0.1
container_name: 1panel_wordpress
ports:
- "8080:80"
- ${PORT}:80
restart: always
networks:
- 1panel
volumes:
- ./data:/var/www/html
environment:
WORDPRESS_DB_HOST: 1panel_mysql
WORDPRESS_DB_NAME: wpdb
WORDPRESS_DB_USER: root
WORDPRESS_DB_PASSWORD: Password@123
WORDPRESS_DB_HOST: ${WORDPRESS_DB_HOST}
WORDPRESS_DB_NAME: ${WORDPRESS_DB_NAME}
WORDPRESS_DB_USER: ${WORDPRESS_DB_USER}
WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
WORDPRESS_DEBUG: 1
networks:

View File

@ -0,0 +1,45 @@
{
"formFields": [
{
"type": "service",
"key": "mysql",
"labelZh": "数据库服务",
"labelEn": "Database Service",
"required": true,
"default": "1Panel-mysql",
"envKey": "WORDPRESS_DB_HOST"
},
{
"type": "text",
"labelZh": "数据库名",
"labelEn": "Database",
"required": true,
"default": "db",
"envKey": "WORDPRESS_DB_NAME"
},
{
"type": "text",
"labelZh": "数据库用户",
"labelEn": "User",
"required": true,
"default": "wordpress_user",
"envKey": "WORDPRESS_DB_USER"
},
{
"type": "text",
"labelZh": "数据库用户密码",
"labelEn": "Password",
"required": true,
"default": "1qaz@WSX",
"envKey": "WORDPRESS_DB_PASSWORD"
},
{
"type": "number",
"labelZh": "端口",
"labelEn": "Port",
"required": true,
"default": 8080,
"envKey": "PORT"
}
]
}

View File

@ -114,3 +114,15 @@ func (b *BaseApi) InstalledSync(c *gin.Context) {
}
helper.SuccessWithData(c, "")
}
func (b *BaseApi) GetServices(c *gin.Context) {
key := c.Param("key")
services, err := appService.GetServices(key)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, services)
}

View File

@ -77,6 +77,7 @@ type AppInstallRequest 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"`
}
type AppInstalled struct {
@ -106,10 +107,7 @@ type AppInstallOperate struct {
Operate AppOperate `json:"operate" validate:"required"`
}
//type AppContainer struct {
// Names []string `json:"names"`
// Image string `json:"image"`
// Ports string `json:"ports"`
// Status string `json:"status"`
// State string `json:"state"`
//}
type AppService struct {
Label string `json:"label"`
Value string `json:"value"`
}

View File

@ -12,6 +12,7 @@ type App struct {
Status string `json:"status" gorm:"type:varchar(64);not null"`
Required string `json:"required" gorm:"type:varchar(64);not null"`
CrossVersionUpdate bool `json:"crossVersionUpdate"`
Limit int `json:"limit" gorm:"type:Integer;not null"`
Details []AppDetail `json:"-"`
TagsKey []string `json:"-" gorm:"-"`
AppTags []AppTag `json:"-" `

View File

@ -2,7 +2,8 @@ package model
type AppContainer struct {
BaseModel
ServiceName string `json:"serviceName"`
ContainerName string `json:"containerName"`
AppInstallId uint `json:"appInstallId"`
ServiceName string `json:"serviceName" gorm:"type:varchar(64);not null"`
ContainerName string `json:"containerName" gorm:"type:varchar(64);not null"`
AppInstallId uint `json:"appInstallId" gorm:"type:integer;not null"`
Port int `json:"port" gorm:"type:integer;not null"`
}

View File

@ -2,6 +2,7 @@ package model
import (
"github.com/1Panel-dev/1Panel/global"
"gorm.io/gorm"
"path"
)
@ -20,10 +21,19 @@ type AppInstall struct {
Containers []AppContainer `json:"containers"`
}
func (i AppInstall) GetPath() string {
func (i *AppInstall) GetPath() string {
return path.Join(global.CONF.System.AppDir, i.App.Key, i.Name)
}
func (i AppInstall) GetComposePath() string {
func (i *AppInstall) GetComposePath() string {
return path.Join(global.CONF.System.AppDir, i.App.Key, i.Name, "docker-compose.yml")
}
func (i *AppInstall) BeforeDelete(tx *gorm.DB) (err error) {
if err = tx.Where("app_install_id = ?", i.ID).Delete(&AppContainer{}).Error; err != nil {
return err
}
return
}

View File

@ -11,6 +11,12 @@ import (
type AppRepo struct {
}
func (a AppRepo) WithKey(key string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("key = ?", key)
}
}
func (a AppRepo) Page(page, size int, opts ...DBOption) (int64, []model.App, error) {
var apps []model.App
db := global.DB.Model(&model.App{})

View File

@ -10,6 +10,22 @@ import (
type AppContainerRepo struct {
}
func (a AppContainerRepo) WithAppId(appId uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("app_id = ?", appId)
}
}
func (a AppContainerRepo) GetBy(opts ...DBOption) ([]model.AppContainer, error) {
db := global.DB.Model(&model.AppContainer{})
var appContainers []model.AppContainer
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&appContainers).Error
return appContainers, err
}
func (a AppContainerRepo) Create(container *model.AppContainer) error {
db := global.DB.Model(&model.AppContainer{})
return db.Create(&container).Error

View File

@ -13,6 +13,16 @@ func (a AppInstallRepo) WithDetailIdsIn(detailIds []uint) DBOption {
return g.Where("app_detail_id in (?)", detailIds)
}
}
func (a AppInstallRepo) WithAppId(appId uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("app_id = ?", appId)
}
}
func (a AppInstallRepo) WithStatus(status string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("status = ?", status)
}
}
func (a AppInstallRepo) GetBy(opts ...DBOption) ([]model.AppInstall, error) {
db := global.DB.Model(&model.AppInstall{})

View File

@ -4,6 +4,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/app/repo"
@ -217,7 +218,7 @@ func (a AppService) Install(name string, appDetailId uint, params map[string]int
if ok {
portStr := strconv.FormatFloat(port.(float64), 'f', -1, 32)
if common.ScanPort(portStr) {
return errors.New("port is in used")
return errors.New("port is in used")
}
}
@ -226,6 +227,38 @@ func (a AppService) Install(name string, appDetailId uint, params map[string]int
return err
}
app, err := appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId))
if err != nil {
return err
}
if app.Required != "" {
var requiredArray []string
if err := json.Unmarshal([]byte(app.Required), &requiredArray); err != nil {
return err
}
for _, key := range requiredArray {
if key == "" {
continue
}
requireApp, err := appRepo.GetFirst(appRepo.WithKey(key))
if err != nil {
return err
}
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(requireApp.ID))
if err != nil {
return err
}
var detailIds []uint
for _, d := range details {
detailIds = append(detailIds, d.ID)
}
_, err = appInstallRepo.GetFirst(appInstallRepo.WithDetailIdsIn(detailIds))
if err != nil {
return errors.New(fmt.Sprintf("%s is required", requireApp.Key))
}
}
}
paramByte, err := json.Marshal(params)
if err != nil {
return err
@ -268,12 +301,8 @@ func (a AppService) Install(name string, appDetailId uint, params map[string]int
return err
}
fileContent, err := os.ReadFile(composeFilePath)
if err != nil {
return err
}
composeMap := make(map[string]interface{})
if err := yaml.Unmarshal(fileContent, &composeMap); err != nil {
if err := yaml.Unmarshal([]byte(appDetail.DockerCompose), &composeMap); err != nil {
return err
}
servicesMap := composeMap["services"].(map[string]interface{})
@ -285,20 +314,38 @@ func (a AppService) Install(name string, appDetailId uint, params map[string]int
value := v.(map[string]interface{})
containerName := constant.ContainerPrefix + k + "-" + common.RandStr(4)
value["container_name"] = containerName
servicePort := 0
if portArray, ok := value["ports"].([]interface{}); ok {
for _, p := range portArray {
if pStr, ok := p.(string); ok {
start := strings.Index(pStr, "{")
end := strings.Index(pStr, "}")
if start > -1 && end > -1 {
portS := pStr[start+1 : end]
if v, ok := envParams[portS]; ok {
portN, _ := strconv.Atoi(v)
servicePort = portN
}
}
}
}
}
appContainers = append(appContainers, &model.AppContainer{
ServiceName: serviceName,
ContainerName: containerName,
Port: servicePort,
})
}
for k, v := range changeKeys {
servicesMap[v] = servicesMap[k]
delete(servicesMap, k)
}
serviceByte, err := yaml.Marshal(servicesMap)
composeByte, err := yaml.Marshal(composeMap)
if err != nil {
return err
}
if err := fileOp.WriteFile(composeFilePath, strings.NewReader(string(serviceByte)), 0775); err != nil {
if err := fileOp.WriteFile(composeFilePath, strings.NewReader(string(composeByte)), 0775); err != nil {
return err
}
@ -346,6 +393,31 @@ func (a AppService) SyncAllInstalled() error {
return nil
}
func (a AppService) GetServices(key string) ([]dto.AppService, error) {
app, err := appRepo.GetFirst(appRepo.WithKey(key))
if err != nil {
return nil, err
}
installs, err := appInstallRepo.GetBy(appInstallRepo.WithAppId(app.ID), appInstallRepo.WithStatus(constant.Running))
if err != nil {
return nil, err
}
var res []dto.AppService
for _, install := range installs {
for _, container := range install.Containers {
value := container.ServiceName
if container.Port > 0 {
value = value + ":" + string(rune(container.Port))
}
res = append(res, dto.AppService{
Label: install.Name,
Value: value,
})
}
}
return res, nil
}
func (a AppService) SyncInstalled(installId uint) error {
appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(installId))
if err != nil {

View File

@ -23,5 +23,6 @@ func (a *AppRouter) InitAppRouter(Router *gin.RouterGroup) {
appRouter.POST("/installed", baseApi.PageInstalled)
appRouter.POST("/installed/op", baseApi.InstallOperate)
appRouter.POST("/installed/sync", baseApi.InstalledSync)
appRouter.GET("/services/:key", baseApi.GetServices)
}
}

View File

@ -54,6 +54,7 @@ export namespace App {
required: boolean;
default: any;
envKey: string;
key?: string;
}
export interface AppInstall {
@ -79,4 +80,9 @@ export namespace App {
installId: number;
operate: string;
}
export interface AppService {
label: string;
value: string;
}
}

View File

@ -15,7 +15,7 @@ export const GetApp = (id: number) => {
};
export const GetAppDetail = (id: number, version: string) => {
return http.get<App.AppDetail>('apps/detail/' + id + '/' + version);
return http.get<App.AppDetail>(`apps/detail/${id}/${version}`);
};
export const InstallApp = (install: App.AppInstall) => {
@ -33,3 +33,7 @@ export const InstalledOp = (op: App.AppInstalledOp) => {
export const SyncInstalledApp = () => {
return http.post<any>('apps/installed/sync', {});
};
export const GetAppService = (key: string | undefined) => {
return http.get<any>(`apps/services/${key}`);
};

View File

@ -1,5 +1,5 @@
<template>
<el-dialog v-model="open" :title="$t('app.install')" width="30%">
<el-dialog v-model="open" :title="$t('app.install')" width="40%">
<el-form ref="paramForm" label-position="left" :model="form" label-width="150px" :rules="rules">
<el-form-item :label="$t('app.name')" prop="NAME">
<el-input v-model="form['NAME']"></el-input>
@ -14,6 +14,14 @@
:type="f.type"
show-password
></el-input>
<el-select v-model="form[f.envKey]" v-if="f.type == 'service'">
<el-option
v-for="service in services"
:key="service.label"
:value="service.value"
:label="service.label"
></el-option>
</el-select>
</el-form-item>
</div>
</el-form>
@ -30,7 +38,7 @@
<script lang="ts" setup name="appInstall">
import { App } from '@/api/interface/app';
import { InstallApp } from '@/api/modules/app';
import { InstallApp, GetAppService } from '@/api/modules/app';
import { Rules } from '@/global/form-rules';
import { FormInstance, FormRules } from 'element-plus';
import { reactive, ref } from 'vue';
@ -47,7 +55,6 @@ const installData = ref<InstallRrops>({
});
let open = ref(false);
let form = reactive<{ [key: string]: any }>({});
let rules = reactive<FormRules>({
NAME: [Rules.requiredInput],
});
@ -58,6 +65,8 @@ const req = reactive({
params: {},
name: '',
});
let services = ref();
const handleClose = () => {
if (paramForm.value) {
paramForm.value.resetFields();
@ -74,11 +83,24 @@ const acceptParams = (props: InstallRrops): void => {
if (p.required) {
rules[p.envKey] = [Rules.requiredInput];
}
if (p.key) {
form[p.envKey] = '';
getServices(form[p.envKey], p.key);
}
}
}
open.value = true;
};
const getServices = (value: any, key: string | undefined) => {
GetAppService(key).then((res) => {
services.value = res.data;
if (services.value != null) {
value = services.value[0].value;
}
});
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {