diff --git a/apps/docker-compose.common.yml b/apps/docker-compose.common.yml new file mode 100644 index 000000000..59a600b49 --- /dev/null +++ b/apps/docker-compose.common.yml @@ -0,0 +1,4 @@ +version: '3' +networks: + 1panel: + driver: bridge \ No newline at end of file diff --git a/apps/list.json b/apps/list.json index 4f71a2c82..7cf657b91 100644 --- a/apps/list.json +++ b/apps/list.json @@ -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/" } ] } \ No newline at end of file diff --git a/apps/mysql/5.7.39/docker-compose.yml b/apps/mysql/5.7.39/docker-compose.yml index 9f9be00a0..0cdcf2df9 100644 --- a/apps/mysql/5.7.39/docker-compose.yml +++ b/apps/mysql/5.7.39/docker-compose.yml @@ -11,6 +11,8 @@ services: MYSQL_USER: ${USER} MYSQL_PASSWORD: ${PASSWORD} MYSQL_ROOT_PASSWORD: ${ROOT_PASSWORD} + networks: + - 1panel ports: - ${PORT}:3306 volumes: diff --git a/apps/mysql/8.0.30/conf/my.cnf b/apps/mysql/8.0.30/conf/my.cnf index 7be15c415..f8582b4b1 100644 --- a/apps/mysql/8.0.30/conf/my.cnf +++ b/apps/mysql/8.0.30/conf/my.cnf @@ -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/ \ No newline at end of file diff --git a/apps/mysql/8.0.30/docker-compose.yml b/apps/mysql/8.0.30/docker-compose.yml index 4d1780665..20349a7b1 100644 --- a/apps/mysql/8.0.30/docker-compose.yml +++ b/apps/mysql/8.0.30/docker-compose.yml @@ -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 \ No newline at end of file + +networks: + 1panel: + external: true \ No newline at end of file diff --git a/apps/wordpress/6.0.1/docker-compose.yml b/apps/wordpress/6.0.1/docker-compose.yml index 6d4ff9ba1..a2e2d6e9d 100644 --- a/apps/wordpress/6.0.1/docker-compose.yml +++ b/apps/wordpress/6.0.1/docker-compose.yml @@ -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: diff --git a/apps/wordpress/6.0.1/params.json b/apps/wordpress/6.0.1/params.json new file mode 100644 index 000000000..378eff384 --- /dev/null +++ b/apps/wordpress/6.0.1/params.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/backend/app/api/v1/app.go b/backend/app/api/v1/app.go index ad30faab9..39f4ef4dc 100644 --- a/backend/app/api/v1/app.go +++ b/backend/app/api/v1/app.go @@ -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) +} diff --git a/backend/app/dto/app.go b/backend/app/dto/app.go index a58e80e50..8e0118159 100644 --- a/backend/app/dto/app.go +++ b/backend/app/dto/app.go @@ -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"` +} diff --git a/backend/app/model/app.go b/backend/app/model/app.go index fe906b0c3..421a01022 100644 --- a/backend/app/model/app.go +++ b/backend/app/model/app.go @@ -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:"-" ` diff --git a/backend/app/model/app_container.go b/backend/app/model/app_container.go index 83131effb..42c17ec91 100644 --- a/backend/app/model/app_container.go +++ b/backend/app/model/app_container.go @@ -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"` } diff --git a/backend/app/model/app_install.go b/backend/app/model/app_install.go index 6339676cc..ecf606510 100644 --- a/backend/app/model/app_install.go +++ b/backend/app/model/app_install.go @@ -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 +} diff --git a/backend/app/repo/app.go b/backend/app/repo/app.go index a43a158f1..71e43b225 100644 --- a/backend/app/repo/app.go +++ b/backend/app/repo/app.go @@ -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{}) diff --git a/backend/app/repo/app_container.go b/backend/app/repo/app_container.go index 5fa529514..775fe6ae6 100644 --- a/backend/app/repo/app_container.go +++ b/backend/app/repo/app_container.go @@ -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 diff --git a/backend/app/repo/app_install.go b/backend/app/repo/app_install.go index 0a72c23be..dde550864 100644 --- a/backend/app/repo/app_install.go +++ b/backend/app/repo/app_install.go @@ -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{}) diff --git a/backend/app/service/app.go b/backend/app/service/app.go index c15e9fa0c..6c550a329 100644 --- a/backend/app/service/app.go +++ b/backend/app/service/app.go @@ -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 { diff --git a/backend/router/ro_app.go b/backend/router/ro_app.go index a5b6566a7..85405bc90 100644 --- a/backend/router/ro_app.go +++ b/backend/router/ro_app.go @@ -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) } } diff --git a/frontend/src/api/interface/app.ts b/frontend/src/api/interface/app.ts index 1af5fc8ef..14f1c77a7 100644 --- a/frontend/src/api/interface/app.ts +++ b/frontend/src/api/interface/app.ts @@ -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; + } } diff --git a/frontend/src/api/modules/app.ts b/frontend/src/api/modules/app.ts index b7ee43670..a64534aa9 100644 --- a/frontend/src/api/modules/app.ts +++ b/frontend/src/api/modules/app.ts @@ -15,7 +15,7 @@ export const GetApp = (id: number) => { }; export const GetAppDetail = (id: number, version: string) => { - return http.get('apps/detail/' + id + '/' + version); + return http.get(`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('apps/installed/sync', {}); }; + +export const GetAppService = (key: string | undefined) => { + return http.get(`apps/services/${key}`); +}; diff --git a/frontend/src/views/app-store/detail/install.vue b/frontend/src/views/app-store/detail/install.vue index 2b46919cd..6af7546a8 100644 --- a/frontend/src/views/app-store/detail/install.vue +++ b/frontend/src/views/app-store/detail/install.vue @@ -1,5 +1,5 @@