Files
1Panel/agent/app/service/nginx.go
CityFun 7c95becfeb feat: Optimize the OpenResty module compilation logic. (#13355)
* refactor: separate openresty module catalog and state

* feat: Optimize the OpenResty module compilation logic.
2026-07-23 16:53:56 +08:00

457 lines
14 KiB
Go

package service
import (
"fmt"
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
"io"
"net/http"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/subosito/gotenv"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/files"
)
type NginxService struct {
}
type INginxService interface {
GetNginxConfig() (*response.NginxFile, error)
GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error)
UpdateConfigByScope(req request.NginxConfigUpdate) error
GetStatus() (response.NginxStatus, error)
UpdateConfigFile(req request.NginxConfigFileUpdate) error
Build(req request.NginxBuildReq) error
GetModules() (*response.NginxBuildConfig, error)
UpdateModule(req request.NginxModuleUpdate) error
OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) error
GetDefaultHttpsStatus() (*response.NginxConfigRes, error)
}
func NewINginxService() INginxService {
return &NginxService{}
}
func (n NginxService) GetNginxConfig() (*response.NginxFile, error) {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return nil, err
}
configPath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, nginxModuleConfDir, "nginx.conf")
byteContent, err := files.NewFileOp().GetContent(configPath)
if err != nil {
return nil, err
}
return &response.NginxFile{Content: string(byteContent)}, nil
}
func (n NginxService) GetConfigByScope(req request.NginxScopeReq) ([]response.NginxParam, error) {
keys, ok := dto.ScopeKeyMap[req.Scope]
if !ok || len(keys) == 0 {
return nil, nil
}
return getNginxParamsByKeys(constant.NginxScopeHttp, keys, nil)
}
func (n NginxService) UpdateConfigByScope(req request.NginxConfigUpdate) error {
keys, ok := dto.ScopeKeyMap[req.Scope]
if !ok || len(keys) == 0 {
return nil
}
return updateNginxConfig(constant.NginxScopeHttp, getNginxParams(req.Params, keys), nil)
}
func (n NginxService) GetStatus() (response.NginxStatus, error) {
httpPort, _, err := getAppInstallPort(constant.AppOpenresty)
if err != nil {
return response.NginxStatus{}, err
}
url := "http://127.0.0.1/nginx_status"
if httpPort != 80 {
url = fmt.Sprintf("http://127.0.0.1:%v/nginx_status", httpPort)
}
res, err := http.Get(url)
if err != nil || res.StatusCode > 300 {
return response.NginxStatus{}, err
}
defer res.Body.Close()
content, err := io.ReadAll(res.Body)
if err != nil {
return response.NginxStatus{}, err
}
var status response.NginxStatus
resArray := strings.Split(string(content), " ")
active, err := strconv.Atoi(resArray[2])
if err == nil {
status.Active = active
}
accepts, err := strconv.Atoi(resArray[7])
if err == nil {
status.Accepts = accepts
}
handled, err := strconv.Atoi(resArray[8])
if err == nil {
status.Handled = handled
}
requests, err := strconv.Atoi(resArray[9])
if err == nil {
status.Requests = requests
}
reading, err := strconv.Atoi(resArray[11])
if err == nil {
status.Reading = reading
}
writing, err := strconv.Atoi(resArray[13])
if err == nil {
status.Writing = writing
}
waiting, err := strconv.Atoi(resArray[15])
if err == nil {
status.Waiting = waiting
}
return status, nil
}
func (n NginxService) UpdateConfigFile(req request.NginxConfigFileUpdate) error {
fileOp := files.NewFileOp()
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
filePath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, nginxModuleConfDir, "nginx.conf")
if req.Backup {
backupPath := path.Join(path.Dir(filePath), "bak")
if !fileOp.Stat(backupPath) {
if err := fileOp.CreateDir(backupPath, constant.DirPerm); err != nil {
return err
}
}
newFile := path.Join(backupPath, "nginx.bak"+"-"+time.Now().Format("2006-01-02-15-04-05"))
if err := fileOp.Copy(filePath, backupPath); err != nil {
return err
}
if err := fileOp.Rename(path.Join(backupPath, "nginx.conf"), newFile); err != nil {
return err
}
}
oldContent, err := os.ReadFile(filePath)
if err != nil {
return err
}
if err = fileOp.WriteFile(filePath, strings.NewReader(req.Content), constant.DirPerm); err != nil {
return err
}
if status, err := checkContainerStatus(nginxInstall.ContainerName); err == nil && status != "running" {
if out, err := compose.DownAndUp(nginxInstall.GetComposePath()); err != nil {
_ = fileOp.SaveFile(filePath, string(oldContent), constant.DirPerm)
return fmt.Errorf("nginx restart failed: %v", out)
} else {
return nginxCheckAndReload(string(oldContent), filePath, nginxInstall.ContainerName)
}
}
return nginxCheckAndReload(string(oldContent), filePath, nginxInstall.ContainerName)
}
func (n NginxService) Build(req request.NginxBuildReq) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
taskName := task.GetTaskName(nginxInstall.Name, task.TaskBuild, task.TaskScopeApp)
if err = task.CheckTaskIsExecuting(taskName); err != nil {
return err
}
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, nginxInstall.ID); err != nil {
return err
}
buildPath := path.Join(nginxInstall.GetPath(), nginxModuleBuildDir)
if !files.NewFileOp().Stat(buildPath) {
return buserr.New("ErrBuildDirNotFound")
}
buildTask, err := task.NewTaskWithOps(nginxInstall.Name, task.TaskBuild, task.TaskScopeApp, req.TaskID, nginxInstall.ID)
if err != nil {
return err
}
buildTask.AddSubTaskWithOps("", func(t *task.Task) error {
return executeNginxModuleBuild(nginxInstall, req.Modules, req.Force, req.Mirror, t, true)
}, nil, 0, 120*time.Minute)
go func() {
_ = buildTask.Execute()
}()
return nil
}
func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return nil, err
}
modules, err := loadNginxModules(nginxInstall)
if err != nil {
return nil, err
}
target, targetWarning, targetErr := resolveNginxModuleTarget(nginxInstall)
if targetWarning != "" {
global.LOG.Warn(targetWarning)
}
runtimeTarget := target
runtimeTarget.BuilderDigest = ""
setNginxModuleTargetKey(&runtimeTarget)
runtimeTargetErr := targetErr
if runtimeTarget.OpenRestyVersion != "" {
runtimeTargetErr = nil
}
var resList []response.NginxModule
for _, module := range modules {
buildStatus := nginxModuleStatusPending
loadStatus := nginxModuleLoadDisabled
var artifacts []dto.NginxModuleArtifact
if module.BuildMode == nginxModuleBuildStatic && runtimeTargetErr == nil {
if build := findCurrentNginxModuleBuild(module, runtimeTarget); build != nil && build.Status == nginxModuleStatusReady {
buildStatus = nginxModuleStatusReady
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
} else if findLatestNginxModuleBuild(module, runtimeTarget) != nil {
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
}
} else if targetErr == nil {
if build := findCurrentNginxModuleBuild(module, target); build != nil {
buildStatus = build.Status
artifacts = build.Artifacts
if build.Status == nginxModuleStatusReady {
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
}
} else if latestBuild := findLatestNginxModuleBuild(module, target); latestBuild != nil {
artifacts = latestBuild.Artifacts
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
}
}
if module.BuildMode != nginxModuleBuildStatic && module.LastError != "" {
buildStatus = nginxModuleStatusFailed
}
resList = append(resList, response.NginxModule{
Name: module.Name,
Custom: module.Custom,
Script: module.Script,
Packages: strings.Join(module.Packages, ","),
Params: module.Params,
Enable: module.Enable,
BuildMode: module.BuildMode,
Provider: module.Provider,
LoadOrder: module.LoadOrder,
BuildStatus: buildStatus,
LoadStatus: loadStatus,
Artifacts: artifacts,
LastError: module.LastError,
})
}
envs, err := gotenv.Read(nginxInstall.GetEnvPath())
if err != nil {
return nil, err
}
return &response.NginxBuildConfig{
Mirror: envs["CONTAINER_PACKAGE_URL"],
DynamicSupported: nginxModuleDynamicSupported(nginxInstall),
Modules: resList,
}, nil
}
func applyNginxModuleUpdate(modules []dto.NginxModule, req request.NginxModuleUpdate) ([]dto.NginxModule, *dto.NginxModule, error) {
switch req.Operate {
case nginxModuleOperateCreate:
if err := validateNginxModuleBuildMode(dto.NginxModule{Name: req.Name, BuildMode: req.BuildMode}); err != nil {
return nil, nil, err
}
for _, module := range modules {
if module.Name == req.Name {
return nil, nil, buserr.New("ErrNameIsExist")
}
}
modules = append(modules, dto.NginxModule{
Name: req.Name,
Custom: true,
Script: req.Script,
Packages: strings.Split(req.Packages, ","),
Params: req.Params,
Enable: req.Enable,
BuildMode: req.BuildMode,
Provider: req.Provider,
LoadOrder: req.LoadOrder,
})
case nginxModuleOperateUpdate:
for i := range modules {
if modules[i].Name != req.Name {
continue
}
if modules[i].Custom {
if err := validateNginxModuleBuildMode(dto.NginxModule{Name: req.Name, BuildMode: req.BuildMode}); err != nil {
return nil, nil, err
}
} else {
modules[i].Enable = req.Enable
return modules, nil, nil
}
modules[i].Enable = req.Enable
modules[i].Script = req.Script
modules[i].Packages = strings.Split(req.Packages, ",")
modules[i].Params = req.Params
modules[i].BuildMode = req.BuildMode
modules[i].Provider = req.Provider
modules[i].LoadOrder = req.LoadOrder
return modules, nil, nil
}
return nil, nil, fmt.Errorf("OpenResty module %s not found", req.Name)
case nginxModuleOperateDelete:
for i := range modules {
if modules[i].Name != req.Name {
continue
}
if !modules[i].Custom {
return nil, nil, fmt.Errorf("built-in OpenResty module %s cannot be deleted", req.Name)
}
deleted := modules[i]
modules = append(modules[:i], modules[i+1:]...)
return modules, &deleted, nil
}
return nil, nil, fmt.Errorf("OpenResty module %s not found", req.Name)
default:
return nil, nil, fmt.Errorf("unsupported OpenResty module operation %q", req.Operate)
}
return modules, nil, nil
}
func (n NginxService) UpdateModule(req request.NginxModuleUpdate) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, nginxInstall.ID); err != nil {
return err
}
modules, err := loadNginxModules(nginxInstall)
if err != nil {
return err
}
oldModules := cloneNginxModules(modules)
modules, deletedModule, err := applyNginxModuleUpdate(modules, req)
if err != nil {
return err
}
if err = saveNginxModules(nginxInstall, modules); err != nil {
return err
}
if err = reconcileDynamicNginxModuleConfig(nginxInstall, modules, true); err != nil {
_ = saveNginxModules(nginxInstall, oldModules)
_ = reconcileDynamicNginxModuleConfig(nginxInstall, oldModules, false)
return err
}
if deletedModule != nil {
_ = removeNginxModuleArtifacts(nginxInstall, *deletedModule)
}
return nil
}
func (n NginxService) OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) error {
appInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
websites, _ := websiteRepo.List()
hasDefaultWebsite := false
for _, website := range websites {
if website.DefaultServer {
hasDefaultWebsite = true
break
}
}
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
content, err := os.ReadFile(defaultConfigPath)
if err != nil {
return err
}
switch req.Operate {
case "enable":
if req.SSLRejectHandshake {
defaultWebsite, _ := websiteRepo.GetFirst(websiteRepo.WithDefaultServer())
if defaultWebsite.ID > 0 {
return buserr.New("ErrDefaultWebsite")
}
}
if err := handleSSLConfig(&appInstall, hasDefaultWebsite, req.SSLRejectHandshake); err != nil {
return err
}
case "disable":
defaultConfig, err := parser.NewStringParser(string(content)).Parse()
if err != nil {
return err
}
defaultConfig.FilePath = defaultConfigPath
defaultServer := defaultConfig.FindServers()[0]
defaultServer.RemoveListen(fmt.Sprintf("%d", appInstall.HttpsPort))
defaultServer.RemoveListen(fmt.Sprintf("[::]:%d", appInstall.HttpsPort))
defaultServer.RemoveDirective("include", []string{"/usr/local/openresty/nginx/conf/ssl/root_ssl.conf"})
defaultServer.RemoveDirective("http2", []string{"on"})
defaultServer.RemoveDirective("ssl_reject_handshake", []string{"on"})
if err = nginx.WriteConfig(defaultConfig, nginx.IndentedStyle); err != nil {
return err
}
}
return nginxCheckAndReload(string(content), defaultConfigPath, appInstall.ContainerName)
}
func (n NginxService) GetDefaultHttpsStatus() (*response.NginxConfigRes, error) {
appInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return nil, err
}
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
content, err := os.ReadFile(defaultConfigPath)
if err != nil {
return nil, err
}
defaultConfig, err := parser.NewStringParser(string(content)).Parse()
if err != nil {
return nil, err
}
defaultConfig.FilePath = defaultConfigPath
defaultServer := defaultConfig.FindServers()[0]
res := &response.NginxConfigRes{}
for _, directive := range defaultServer.GetDirectives() {
if directive.GetName() == "include" && directive.GetParameters()[0] == "/usr/local/openresty/nginx/conf/ssl/root_ssl.conf" {
res.Https = true
}
if directive.GetName() == "ssl_reject_handshake" && directive.GetParameters()[0] == "on" {
res.Https = true
res.SSLRejectHandshake = true
}
}
return res, nil
}