diff --git a/backend/app/api/v1/app.go b/backend/app/api/v1/app.go index 882ee59ba..90476030d 100644 --- a/backend/app/api/v1/app.go +++ b/backend/app/api/v1/app.go @@ -96,6 +96,28 @@ func (b *BaseApi) GetAppDetail(c *gin.Context) { helper.SuccessWithData(c, appDetailDTO) } +// @Tags App +// @Summary Search app detail by id +// @Description 通过 id 获取应用详情 +// @Accept json +// @Param appId path integer true "id" +// @Success 200 {object} response.AppDetailDTO +// @Security ApiKeyAuth +// @Router /apps/detail/:id[get] +func (b *BaseApi) GetAppDetailByID(c *gin.Context) { + appDetailID, err := helper.GetIntParamByKey(c, "id") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + appDetailDTO, err := appService.GetAppDetailByID(appDetailID) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, appDetailDTO) +} + // @Tags App // @Summary Install app // @Description 安装应用 diff --git a/backend/app/dto/request/website.go b/backend/app/dto/request/website.go index 22c67a6a3..f95170550 100644 --- a/backend/app/dto/request/website.go +++ b/backend/app/dto/request/website.go @@ -23,6 +23,8 @@ type WebsiteCreate struct { AppInstall NewAppInstall `json:"appInstall"` AppID uint `json:"appID"` AppInstallID uint `json:"appInstallID"` + + RuntimeID uint `json:"runtimeID"` } type NewAppInstall struct { diff --git a/backend/app/dto/response/runtime.go b/backend/app/dto/response/runtime.go index df85cc3c8..b6674d284 100644 --- a/backend/app/dto/response/runtime.go +++ b/backend/app/dto/response/runtime.go @@ -6,5 +6,4 @@ type RuntimeRes struct { model.Runtime AppParams []AppParam `json:"appParams"` AppID uint `json:"appId"` - Version string `json:"version"` } diff --git a/backend/app/model/website.go b/backend/app/model/website.go index 7f3a7a099..4b9866d87 100644 --- a/backend/app/model/website.go +++ b/backend/app/model/website.go @@ -19,6 +19,7 @@ type Website struct { ErrorLog bool `json:"errorLog"` AccessLog bool `json:"accessLog"` DefaultServer bool `json:"defaultServer"` + RuntimeID uint `gorm:"type:integer" json:"runtimeID"` Domains []WebsiteDomain `json:"domains" gorm:"-:migration"` WebsiteSSL WebsiteSSL `json:"webSiteSSL" gorm:"-:migration"` } diff --git a/backend/app/repo/runtime.go b/backend/app/repo/runtime.go index 3e4fe2abb..4833f8ad8 100644 --- a/backend/app/repo/runtime.go +++ b/backend/app/repo/runtime.go @@ -10,8 +10,9 @@ type RuntimeRepo struct { } type IRuntimeRepo interface { - WithNameOrImage(name string, image string) DBOption - WithOtherNameOrImage(name string, image string, id uint) DBOption + WithName(name string) DBOption + WithImage(image string) DBOption + WithNotId(id uint) DBOption Page(page, size int, opts ...DBOption) (int64, []model.Runtime, error) Create(ctx context.Context, runtime *model.Runtime) error Save(runtime *model.Runtime) error @@ -23,15 +24,21 @@ func NewIRunTimeRepo() IRuntimeRepo { return &RuntimeRepo{} } -func (r *RuntimeRepo) WithNameOrImage(name string, image string) DBOption { +func (r *RuntimeRepo) WithName(name string) DBOption { return func(g *gorm.DB) *gorm.DB { - return g.Where("name = ? or image = ?", name, image) + return g.Where("name = ?", name) } } -func (r *RuntimeRepo) WithOtherNameOrImage(name string, image string, id uint) DBOption { +func (r *RuntimeRepo) WithImage(image string) DBOption { return func(g *gorm.DB) *gorm.DB { - return g.Where("name = ? or image = ? and id != ?", name, image, id) + return g.Where("image = ?", image) + } +} + +func (r *RuntimeRepo) WithNotId(id uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("id != ?", id) } } diff --git a/backend/app/service/app.go b/backend/app/service/app.go index 014b0142d..bd3823e72 100644 --- a/backend/app/service/app.go +++ b/backend/app/service/app.go @@ -36,6 +36,7 @@ type IAppService interface { Install(ctx context.Context, req request.AppInstallCreate) (*model.AppInstall, error) SyncAppList() error GetAppUpdate() (*response.AppUpdateRes, error) + GetAppDetailByID(id uint) (*response.AppDetailDTO, error) } func NewIAppService() IAppService { @@ -206,6 +207,20 @@ func (a AppService) GetAppDetail(appId uint, version, appType string) (response. } return appDetailDTO, nil } +func (a AppService) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) { + res := &response.AppDetailDTO{} + appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return nil, err + } + res.AppDetail = appDetail + paramMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(appDetail.Params), ¶mMap); err != nil { + return nil, err + } + res.Params = paramMap + return res, nil +} func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) (*model.AppInstall, error) { if err := docker.CreateDefaultDockerNetwork(); err != nil { diff --git a/backend/app/service/app_utils.go b/backend/app/service/app_utils.go index 6c488207d..41cf51fce 100644 --- a/backend/app/service/app_utils.go +++ b/backend/app/service/app_utils.go @@ -7,6 +7,7 @@ import ( "github.com/subosito/gotenv" "math" "os" + "os/exec" "path" "reflect" "strconv" @@ -207,7 +208,21 @@ func updateInstall(installId uint, detailId uint) error { if err := NewIBackupService().AppBackup(dto.CommonBackup{Name: install.App.Key, DetailName: install.Name}); err != nil { return err } - if _, err = compose.Down(install.GetComposePath()); err != nil { + + detailDir := path.Join(constant.ResourceDir, "apps", install.App.Key, "versions", detail.Version) + cmd := exec.Command("/bin/bash", "-c", fmt.Sprintf("cp -rf %s/* %s", detailDir, install.GetPath())) + stdout, err := cmd.CombinedOutput() + if err != nil { + if stdout != nil { + return errors.New(string(stdout)) + } + return err + } + + if out, err := compose.Down(install.GetComposePath()); err != nil { + if out != "" { + return errors.New(out) + } return err } install.DockerCompose = detail.DockerCompose @@ -218,7 +233,10 @@ func updateInstall(installId uint, detailId uint) error { if err := fileOp.WriteFile(install.GetComposePath(), strings.NewReader(install.DockerCompose), 0775); err != nil { return err } - if _, err = compose.Up(install.GetComposePath()); err != nil { + if out, err := compose.Up(install.GetComposePath()); err != nil { + if out != "" { + return errors.New(out) + } return err } return appInstallRepo.Save(&install) diff --git a/backend/app/service/runtime.go b/backend/app/service/runtime.go index 2f51b5ccc..11cf2c4bf 100644 --- a/backend/app/service/runtime.go +++ b/backend/app/service/runtime.go @@ -35,19 +35,24 @@ func NewRuntimeService() IRuntimeService { } func (r *RuntimeService) Create(create request.RuntimeCreate) (err error) { - exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithNameOrImage(create.Name, create.Image)) + exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithName(create.Name)) if exist != nil { - return buserr.New(constant.ErrNameOrImageIsExist) + return buserr.New(constant.ErrNameIsExist) } if create.Resource == constant.ResourceLocal { runtime := &model.Runtime{ Name: create.Name, Resource: create.Resource, Type: create.Type, + Version: create.Version, Status: constant.RuntimeNormal, } return runtimeRepo.Create(context.Background(), runtime) } + exist, _ = runtimeRepo.GetFirst(runtimeRepo.WithImage(create.Image)) + if exist != nil { + return buserr.New(constant.ErrImageExist) + } appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(create.AppDetailID)) if err != nil { return err @@ -134,6 +139,7 @@ func (r *RuntimeService) Delete(id uint) error { return err } //TODO 校验网站关联 + //TODO 删除镜像 if runtime.Resource == constant.ResourceAppstore { runtimeDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name) if err := files.NewFileOp().DeleteDir(runtimeDir); err != nil { @@ -158,7 +164,6 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeRes, error) { return nil, err } res.AppID = appDetail.AppId - res.Version = appDetail.Version var ( appForm dto.AppForm appParams []response.AppParam @@ -207,10 +212,6 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeRes, error) { } func (r *RuntimeService) Update(req request.RuntimeUpdate) error { - exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithOtherNameOrImage(req.Name, req.Image, req.ID)) - if exist != nil { - return buserr.New(constant.ErrNameOrImageIsExist) - } runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID)) if err != nil { return err @@ -219,6 +220,10 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error { runtime.Version = req.Version return runtimeRepo.Save(runtime) } + exist, _ := runtimeRepo.GetFirst(runtimeRepo.WithImage(req.Name), runtimeRepo.WithNotId(req.ID)) + if exist != nil { + return buserr.New(constant.ErrImageExist) + } runtimeDir := path.Join(constant.RuntimeDir, runtime.Type, runtime.Name) composeContent, envContent, _, err := handleParams(req.Image, runtime.Type, runtimeDir, req.Params) if err != nil { diff --git a/backend/app/service/website.go b/backend/app/service/website.go index c6466c285..b2e2377f3 100644 --- a/backend/app/service/website.go +++ b/backend/app/service/website.go @@ -131,7 +131,10 @@ func (w WebsiteService) CreateWebsite(ctx context.Context, create request.Websit ErrorLog: true, } - var appInstall *model.AppInstall + var ( + appInstall *model.AppInstall + runtime *model.Runtime + ) switch create.Type { case constant.Deployment: if create.AppType == constant.NewApp { @@ -153,6 +156,30 @@ func (w WebsiteService) CreateWebsite(ctx context.Context, create request.Websit appInstall = &install website.AppInstallID = appInstall.ID } + case constant.Runtime: + var err error + runtime, err = runtimeRepo.GetFirst(commonRepo.WithByID(create.RuntimeID)) + if err != nil { + return err + } + if runtime.Resource == constant.ResourceAppstore { + var req request.AppInstallCreate + req.Name = create.PrimaryDomain + req.AppDetailId = create.AppInstall.AppDetailId + req.Params = create.AppInstall.Params + req.Params["IMAGE_NAME"] = runtime.Image + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + req.Params["PANEL_WEBSITE_DIR"] = path.Join(nginxInstall.GetPath(), "/www") + install, err := NewIAppService().Install(ctx, req) + if err != nil { + return err + } + website.AppInstallID = install.ID + appInstall = install + } } if err := websiteRepo.Create(ctx, website); err != nil { @@ -180,7 +207,7 @@ func (w WebsiteService) CreateWebsite(ctx context.Context, create request.Websit return err } } - return configDefaultNginx(website, domains, appInstall) + return configDefaultNginx(website, domains, appInstall, runtime) } func (w WebsiteService) OpWebsite(req request.WebsiteOp) error { diff --git a/backend/app/service/website_utils.go b/backend/app/service/website_utils.go index 08a8b3d27..ca44fa275 100644 --- a/backend/app/service/website_utils.go +++ b/backend/app/service/website_utils.go @@ -43,15 +43,28 @@ func getDomain(domainStr string, websiteID uint) (model.WebsiteDomain, error) { return model.WebsiteDomain{}, nil } -func createStaticHtml(website *model.Website) error { +func createIndexFile(website *model.Website, runtime *model.Runtime) error { nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) if err != nil { return err } indexFolder := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "www", "sites", website.Alias, "index") - indexPath := path.Join(indexFolder, "index.html") - indexContent := string(nginx_conf.Index) + indexPath := "" + indexContent := "" + switch website.Type { + case constant.Static: + indexPath = path.Join(indexFolder, "index.html") + indexContent = string(nginx_conf.Index) + case constant.Runtime: + if runtime.Type == constant.RuntimePHP { + indexPath = path.Join(indexFolder, "index.php") + indexContent = string(nginx_conf.IndexPHP) + } else { + return nil + } + } + fileOp := files.NewFileOp() if !fileOp.Stat(indexFolder) { if err := fileOp.CreateDir(indexFolder, 0755); err != nil { @@ -69,7 +82,7 @@ func createStaticHtml(website *model.Website) error { return nil } -func createWebsiteFolder(nginxInstall model.AppInstall, website *model.Website) error { +func createWebsiteFolder(nginxInstall model.AppInstall, website *model.Website, runtime *model.Runtime) error { nginxFolder := path.Join(constant.AppInstallDir, constant.AppOpenresty, nginxInstall.Name) siteFolder := path.Join(nginxFolder, "www", "sites", website.Alias) fileOp := files.NewFileOp() @@ -92,8 +105,8 @@ func createWebsiteFolder(nginxInstall model.AppInstall, website *model.Website) if err := fileOp.CreateDir(path.Join(siteFolder, "ssl"), 0755); err != nil { return err } - if website.Type == constant.Static { - if err := createStaticHtml(website); err != nil { + if website.Type == constant.Static || website.Type == constant.Runtime { + if err := createIndexFile(website, runtime); err != nil { return err } } @@ -101,12 +114,12 @@ func createWebsiteFolder(nginxInstall model.AppInstall, website *model.Website) return fileOp.CopyDir(path.Join(nginxFolder, "www", "common", "waf", "rules"), path.Join(siteFolder, "waf")) } -func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall) error { +func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime) error { nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) if err != nil { return err } - if err := createWebsiteFolder(nginxInstall, website); err != nil { + if err := createWebsiteFolder(nginxInstall, website, runtime); err != nil { return err } @@ -140,9 +153,21 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a server.UpdateRootProxy([]string{proxy}) case constant.Static: server.UpdateRoot(path.Join("/www/sites", website.Alias, "index")) - server.UpdateRootLocation() + //server.UpdateRootLocation() case constant.Proxy: server.UpdateRootProxy([]string{website.Proxy}) + case constant.Runtime: + if runtime.Resource == constant.ResourceLocal { + server.UpdateRoot(path.Join("/www/sites", website.Alias, "index")) + } + if runtime.Resource == constant.ResourceAppstore { + switch runtime.Type { + case constant.RuntimePHP: + server.UpdateRoot(path.Join("/www/sites", website.Alias, "index")) + proxy := fmt.Sprintf("127.0.0.1:%d", appInstall.HttpPort) + server.UpdatePHPProxy([]string{proxy}) + } + } } config.FilePath = configPath diff --git a/backend/constant/errs.go b/backend/constant/errs.go index 80b57b4c5..44700ec25 100644 --- a/backend/constant/errs.go +++ b/backend/constant/errs.go @@ -106,8 +106,8 @@ var ( // runtime var ( - ErrDirNotFound = "ErrDirNotFound" - ErrFileNotExist = "ErrFileNotExist" - ErrImageBuildErr = "ErrImageBuildErr" - ErrNameOrImageIsExist = "ErrNameOrImageIsExist" + ErrDirNotFound = "ErrDirNotFound" + ErrFileNotExist = "ErrFileNotExist" + ErrImageBuildErr = "ErrImageBuildErr" + ErrImageExist = "ErrImageExist" ) diff --git a/backend/constant/website.go b/backend/constant/website.go index 06fdba702..1ccd278f4 100644 --- a/backend/constant/website.go +++ b/backend/constant/website.go @@ -17,6 +17,7 @@ const ( Deployment = "deployment" Static = "static" Proxy = "proxy" + Runtime = "runtime" SSLExisted = "existed" SSLAuto = "auto" diff --git a/backend/i18n/lang/en.yaml b/backend/i18n/lang/en.yaml index 841706a2a..485499932 100644 --- a/backend/i18n/lang/en.yaml +++ b/backend/i18n/lang/en.yaml @@ -64,4 +64,4 @@ ErrObjectInUsed: "This object is in use and cannot be deleted" ErrDirNotFound: "The build folder does not exist! Please check file integrity!" ErrFileNotExist: "{{ .detail }} file does not exist! Please check source file integrity!" ErrImageBuildErr: "Image build failed" -ErrNameOrImageIsExist: "Duplicate name or image" \ No newline at end of file +ErrImageExist: "Image is already exist!" \ No newline at end of file diff --git a/backend/i18n/lang/zh.yaml b/backend/i18n/lang/zh.yaml index b002a7ecd..99e84072c 100644 --- a/backend/i18n/lang/zh.yaml +++ b/backend/i18n/lang/zh.yaml @@ -64,4 +64,4 @@ ErrObjectInUsed: "该对象正被使用,无法删除" ErrDirNotFound: "build 文件夹不存在!请检查文件完整性!" ErrFileNotExist: "{{ .detail }} 文件不存在!请检查源文件完整性!" ErrImageBuildErr: "镜像 build 失败" -ErrNameOrImageIsExist: "名称或者镜像重复" \ No newline at end of file +ErrImageExist: "镜像已存在!" \ No newline at end of file diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index 6c5919960..539605357 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -251,6 +251,6 @@ var AddDefaultGroup = &gormigrate.Migration{ var AddTableRuntime = &gormigrate.Migration{ ID: "20230330-add-table-runtime", Migrate: func(tx *gorm.DB) error { - return tx.AutoMigrate(&model.Runtime{}) + return tx.AutoMigrate(&model.Runtime{}, &model.Website{}) }, } diff --git a/backend/router/ro_app.go b/backend/router/ro_app.go index 9c253cbe4..7aeb00c92 100644 --- a/backend/router/ro_app.go +++ b/backend/router/ro_app.go @@ -20,6 +20,7 @@ func (a *AppRouter) InitAppRouter(Router *gin.RouterGroup) { appRouter.POST("/search", baseApi.SearchApp) appRouter.GET("/:key", baseApi.GetApp) appRouter.GET("/detail/:appId/:version/:type", baseApi.GetAppDetail) + appRouter.GET("/details/:id", baseApi.GetAppDetailByID) appRouter.POST("/install", baseApi.InstallApp) appRouter.GET("/tags", baseApi.GetAppTags) appRouter.GET("/installed/:appInstallId/versions", baseApi.GetUpdateVersions) diff --git a/backend/utils/files/file_op.go b/backend/utils/files/file_op.go index c6bb227c9..ef67bed5f 100644 --- a/backend/utils/files/file_op.go +++ b/backend/utils/files/file_op.go @@ -252,19 +252,15 @@ func (f FileOp) Copy(src, dst string) error { if src = path.Clean("/" + src); src == "" { return os.ErrNotExist } - if dst = path.Clean("/" + dst); dst == "" { return os.ErrNotExist } - if src == "/" || dst == "/" { return os.ErrInvalid } - if dst == src { return os.ErrInvalid } - info, err := f.Fs.Stat(src) if err != nil { return err @@ -272,7 +268,6 @@ func (f FileOp) Copy(src, dst string) error { if info.IsDir() { return f.CopyDir(src, dst) } - return f.CopyFile(src, dst) } diff --git a/backend/utils/nginx/components/server.go b/backend/utils/nginx/components/server.go index 961b71859..b5c12f4fa 100644 --- a/backend/utils/nginx/components/server.go +++ b/backend/utils/nginx/components/server.go @@ -237,6 +237,29 @@ func (s *Server) UpdateRootProxy(proxy []string) { s.UpdateDirectiveBySecondKey("location", "/", newDir) } +func (s *Server) UpdatePHPProxy(proxy []string) { + newDir := Directive{ + Name: "location", + Parameters: []string{"~ [^/]\\.php(/|$)"}, + Block: &Block{}, + } + block := &Block{} + block.Directives = append(block.Directives, &Directive{ + Name: "fastcgi_pass", + Parameters: proxy, + }) + block.Directives = append(block.Directives, &Directive{ + Name: "include", + Parameters: []string{"fastcgi-php.conf"}, + }) + block.Directives = append(block.Directives, &Directive{ + Name: "include", + Parameters: []string{"fastcgi_params"}, + }) + newDir.Block = block + s.UpdateDirectiveBySecondKey("location", "~ [^/]\\.php(/|$)", newDir) +} + func (s *Server) UpdateDirectiveBySecondKey(name string, key string, directive Directive) { directives := s.Directives index := -1 diff --git a/cmd/server/nginx_conf/index.php b/cmd/server/nginx_conf/index.php new file mode 100644 index 000000000..5cd4e8c46 --- /dev/null +++ b/cmd/server/nginx_conf/index.php @@ -0,0 +1,25 @@ +欢迎使用 PHP!'; +echo '