mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
feat: 重构OpenResty模块部分以支持动态编译 (#13291)
* feat: support dynamic module build for OpenResty * feat: add dynamic module build page for OpenResty * refactor: drop auto fallback, gate dynamic build by version support - remove auto-to-static fallback; dynamic build failure now reports the error and hints switching to static build manually - gate dynamic builds on module support files (Dockerfile.modules + module.catalog.json) instead of version numbers, expose dynamicSupported in the modules API - collect repeated path/status/operate strings into constants - move nginx module regex patterns into utils/re with semantic helpers - reorganize nginx_module.go around the main build flows and inline single-use thin helpers * feat: limit nginx module build mode options by version support - build mode radio offers only dynamic and static (auto maps to dynamic for legacy data) - disable the dynamic option with a hint when the installed OpenResty version lacks dynamic build support * feat: complete i18n for nginx module pages Fill in the new nginx module keys for all eleven language files (translations other than zh/en are draft machine translations). * feat: probe dynamic module support on load and drop the auto build mode - probe each non-static module's configure params when loading the module list and report dynamicSupport=supported/unsupported up front - normalize the legacy auto build mode to dynamic * feat: clarify module build modes in the UI - build drawer lists dynamic modules (tagged, hot-reload) and static modules (tagged, full rebuild + container restart) separately - disable the dynamic option per module when its params do not support dynamic build, distinct from the version gate hint - drop the auto build mode wording everywhere and sync all eleven language files * feat: clarify purpose of the nginx module build drawer - add a purpose hint explaining dynamic (hot reload) vs static (full rebuild + container restart) - drop the per-module mode tags now that section headers carry the semantics - allow submitting with zero dynamic modules selected when static modules are present, so static-only users can trigger a build * feat: pass apt mirror through to dynamic module builds The mirror selected in the build dialog (or CONTAINER_PACKAGE_URL in the app env as fallback) is now forwarded as a build arg so the module builder uses the same apt source as the static build path. test-builder gains a --mirror option. * feat: add Lao translations for nginx module pages
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
|
||||
)
|
||||
@@ -87,9 +89,41 @@ var LBAlgorithms = map[string]struct{}{"ip_hash": {}, "least_conn": {}}
|
||||
var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}}
|
||||
|
||||
type NginxModule struct {
|
||||
Name string `json:"name"`
|
||||
Script string `json:"script"`
|
||||
Packages []string `json:"packages"`
|
||||
Params string `json:"params"`
|
||||
Enable bool `json:"enable"`
|
||||
Name string `json:"name"`
|
||||
Script string `json:"script"`
|
||||
Packages []string `json:"packages"`
|
||||
Params string `json:"params"`
|
||||
Enable bool `json:"enable"`
|
||||
Deleted bool `json:"deleted,omitempty"`
|
||||
BuildMode string `json:"buildMode,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
DynamicSupport string `json:"dynamicSupport,omitempty"`
|
||||
LoadOrder int `json:"loadOrder,omitempty"`
|
||||
Builds []NginxModuleBuild `json:"builds,omitempty"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
}
|
||||
|
||||
type NginxModuleBuild struct {
|
||||
Provider string `json:"provider"`
|
||||
Status string `json:"status"`
|
||||
Hash string `json:"hash"`
|
||||
Target NginxModuleTarget `json:"target"`
|
||||
Artifacts []NginxModuleArtifact `json:"artifacts,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
BuiltAt time.Time `json:"builtAt,omitempty"`
|
||||
}
|
||||
|
||||
type NginxModuleTarget struct {
|
||||
Key string `json:"key"`
|
||||
OpenRestyVersion string `json:"openrestyVersion"`
|
||||
Architecture string `json:"architecture"`
|
||||
Image string `json:"image,omitempty"`
|
||||
ImageDigest string `json:"imageDigest,omitempty"`
|
||||
BuilderDigest string `json:"builderDigest,omitempty"`
|
||||
}
|
||||
|
||||
type NginxModuleArtifact struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
|
||||
@@ -114,17 +114,22 @@ type NginxRedirectUpdate struct {
|
||||
}
|
||||
|
||||
type NginxBuildReq struct {
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
Mirror string `json:"mirror" validate:"required"`
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
Mirror string `json:"mirror" validate:"required"`
|
||||
Modules []string `json:"modules"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
type NginxModuleUpdate struct {
|
||||
Operate string `json:"operate" validate:"required,oneof=create delete update"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Script string `json:"script"`
|
||||
Packages string `json:"packages"`
|
||||
Enable bool `json:"enable"`
|
||||
Params string `json:"params"`
|
||||
Operate string `json:"operate" validate:"required,oneof=create delete update"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Script string `json:"script"`
|
||||
Packages string `json:"packages"`
|
||||
Enable bool `json:"enable"`
|
||||
Params string `json:"params"`
|
||||
BuildMode string `json:"buildMode" validate:"omitempty,oneof=auto dynamic static"`
|
||||
Provider string `json:"provider" validate:"omitempty,oneof=local prebuilt"`
|
||||
LoadOrder int `json:"loadOrder" validate:"omitempty,min=0,max=9999"`
|
||||
}
|
||||
|
||||
type NginxOperateReq struct {
|
||||
|
||||
@@ -69,16 +69,26 @@ type NginxProxyCache struct {
|
||||
}
|
||||
|
||||
type NginxModule struct {
|
||||
Name string `json:"name"`
|
||||
Script string `json:"script"`
|
||||
Packages string `json:"packages"`
|
||||
Params string `json:"params"`
|
||||
Enable bool `json:"enable"`
|
||||
Name string `json:"name"`
|
||||
Script string `json:"script"`
|
||||
Packages string `json:"packages"`
|
||||
Params string `json:"params"`
|
||||
Enable bool `json:"enable"`
|
||||
BuildMode string `json:"buildMode"`
|
||||
Provider string `json:"provider"`
|
||||
DynamicSupport string `json:"dynamicSupport"`
|
||||
LoadOrder int `json:"loadOrder"`
|
||||
BuildStatus string `json:"buildStatus"`
|
||||
LoadStatus string `json:"loadStatus"`
|
||||
Compatibility string `json:"compatibility"`
|
||||
Artifacts []dto.NginxModuleArtifact `json:"artifacts"`
|
||||
LastError string `json:"lastError"`
|
||||
}
|
||||
|
||||
type NginxBuildConfig struct {
|
||||
Mirror string `json:"mirror"`
|
||||
Modules []NginxModule `json:"modules"`
|
||||
Mirror string `json:"mirror"`
|
||||
DynamicSupported bool `json:"dynamicSupported"`
|
||||
Modules []NginxModule `json:"modules"`
|
||||
}
|
||||
|
||||
type NginxConfigRes struct {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -668,11 +667,58 @@ func handleUpgradeCompose(install model.AppInstall, detail model.AppDetail) (map
|
||||
if oldServiceValue["restart"] != nil {
|
||||
serviceValue["restart"] = oldServiceValue["restart"]
|
||||
}
|
||||
if install.App.Key == constant.AppOpenresty {
|
||||
mergeOpenrestyModuleVolumes(serviceValue, oldServiceValue)
|
||||
}
|
||||
servicesMap[install.ServiceName] = serviceValue
|
||||
composeMap["services"] = servicesMap
|
||||
return composeMap, nil
|
||||
}
|
||||
|
||||
// mergeOpenrestyModuleVolumes carries the dynamic module mounts of the old
|
||||
// compose over to the upgraded one when it does not declare them, so built
|
||||
// module artifacts and their load configuration stay mounted across upgrades.
|
||||
func mergeOpenrestyModuleVolumes(serviceValue, oldServiceValue map[string]interface{}) {
|
||||
oldVolumes, ok := oldServiceValue["volumes"].([]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
newVolumes, _ := serviceValue["volumes"].([]interface{})
|
||||
existing := make(map[string]struct{}, len(newVolumes))
|
||||
for _, volume := range newVolumes {
|
||||
if containerPath, ok := composeVolumeContainerPath(volume); ok {
|
||||
existing[containerPath] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, volume := range oldVolumes {
|
||||
containerPath, ok := composeVolumeContainerPath(volume)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(containerPath, nginxModuleEnabledConfDir) && !strings.Contains(containerPath, "nginx/modules/1panel") {
|
||||
continue
|
||||
}
|
||||
if _, ok = existing[containerPath]; ok {
|
||||
continue
|
||||
}
|
||||
newVolumes = append(newVolumes, volume)
|
||||
existing[containerPath] = struct{}{}
|
||||
}
|
||||
serviceValue["volumes"] = newVolumes
|
||||
}
|
||||
|
||||
func composeVolumeContainerPath(volume interface{}) (string, bool) {
|
||||
volumeStr, ok := volume.(string)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
parts := strings.Split(volumeStr, ":")
|
||||
if len(parts) < 2 {
|
||||
return "", false
|
||||
}
|
||||
return parts[1], true
|
||||
}
|
||||
|
||||
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) {
|
||||
if detail.DockerCompose == "" {
|
||||
return "", nil
|
||||
@@ -706,74 +752,35 @@ func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string
|
||||
return string(composeByte), nil
|
||||
}
|
||||
|
||||
func buildNginx(parentTask *task.Task) error {
|
||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall) error {
|
||||
fileOp := files.NewFileOp()
|
||||
buildPath := path.Join(nginxInstall.GetPath(), "build")
|
||||
buildPath := path.Join(nginxInstall.GetPath(), nginxModuleBuildDir)
|
||||
if !fileOp.Stat(buildPath) {
|
||||
return buserr.New("ErrBuildDirNotFound")
|
||||
}
|
||||
moduleConfigPath := path.Join(buildPath, "module.json")
|
||||
moduleContent, err := fileOp.GetContent(moduleConfigPath)
|
||||
modules, err := loadNginxModules(nginxInstall)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
modules []dto.NginxModule
|
||||
addModuleParams []string
|
||||
addPackages []string
|
||||
)
|
||||
if len(moduleContent) > 0 {
|
||||
_ = json.Unmarshal(moduleContent, &modules)
|
||||
bashFile, err := os.OpenFile(path.Join(buildPath, "tmp", "pre.sh"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.DirPerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer bashFile.Close()
|
||||
bashFileWriter := bufio.NewWriter(bashFile)
|
||||
for _, module := range modules {
|
||||
if !module.Enable {
|
||||
continue
|
||||
}
|
||||
_, err = bashFileWriter.WriteString(module.Script + "\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addModuleParams = append(addModuleParams, module.Params)
|
||||
addPackages = append(addPackages, module.Packages...)
|
||||
}
|
||||
err = bashFileWriter.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
previousModules := cloneNginxModules(modules)
|
||||
staticBuild := hasEnabledStaticNginxModules(modules)
|
||||
if err = configureStaticNginxModules(nginxInstall, modules, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
envs, err := gotenv.Read(nginxInstall.GetEnvPath())
|
||||
if staticBuild {
|
||||
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
|
||||
parentTask.LogStart(logStr)
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(120*time.Minute))
|
||||
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil {
|
||||
return err
|
||||
}
|
||||
parentTask.LogSuccess(logStr)
|
||||
}
|
||||
modules, err = buildDynamicNginxModules(nginxInstall, modules, nil, false, "", parentTask)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
envs["RESTY_CONFIG_OPTIONS_MORE"] = ""
|
||||
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = ""
|
||||
if len(addModuleParams) > 0 {
|
||||
envs["RESTY_CONFIG_OPTIONS_MORE"] = strings.Join(addModuleParams, " ")
|
||||
}
|
||||
if len(addPackages) > 0 {
|
||||
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = strings.Join(addPackages, " ")
|
||||
}
|
||||
_ = gotenv.Write(envs, nginxInstall.GetEnvPath())
|
||||
if len(addModuleParams) == 0 && len(addPackages) == 0 {
|
||||
return nil
|
||||
}
|
||||
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
|
||||
parentTask.LogStart(logStr)
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(60*time.Minute))
|
||||
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil {
|
||||
return err
|
||||
}
|
||||
parentTask.LogSuccess(logStr)
|
||||
return nil
|
||||
return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false)
|
||||
}
|
||||
|
||||
func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
@@ -873,22 +880,32 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
}
|
||||
_ = copyAppDetailMissing(fileOp, detailDir, install.GetPath())
|
||||
if install.App.Key == constant.AppOpenresty {
|
||||
installBuildDir := path.Join(install.GetPath(), "build")
|
||||
detailBuildDir := path.Join(detailDir, "build")
|
||||
installBuildDir := path.Join(install.GetPath(), nginxModuleBuildDir)
|
||||
detailBuildDir := path.Join(detailDir, nginxModuleBuildDir)
|
||||
if !fileOp.Stat(installBuildDir) {
|
||||
if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := fileOp.DeleteDir(path.Join(installBuildDir, "tmp")); err != nil {
|
||||
if err := fileOp.DeleteDir(path.Join(installBuildDir, nginxModuleTmpDir)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.CopyDir(path.Join(detailBuildDir, "tmp"), installBuildDir); err != nil {
|
||||
if err := fileOp.CopyDir(path.Join(detailBuildDir, nginxModuleTmpDir), installBuildDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.CopyFile(path.Join(detailBuildDir, "Dockerfile"), installBuildDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if fileOp.Stat(path.Join(detailBuildDir, nginxModuleBuilderFile)) {
|
||||
if err := fileOp.CopyFile(path.Join(detailBuildDir, nginxModuleBuilderFile), installBuildDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if fileOp.Stat(path.Join(detailBuildDir, nginxModuleCatalogFile)) {
|
||||
if err := fileOp.CopyFile(path.Join(detailBuildDir, nginxModuleCatalogFile), installBuildDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.conf"), installBuildDir); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -966,6 +983,26 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
}
|
||||
}
|
||||
|
||||
if install.App.Key == constant.AppOpenresty {
|
||||
modules, moduleErr := loadNginxModules(install)
|
||||
if moduleErr != nil {
|
||||
return moduleErr
|
||||
}
|
||||
// Build dynamic modules for the target version before stopping the
|
||||
// current container. Static modules retain the full rebuild path.
|
||||
if !hasEnabledStaticNginxModules(modules) {
|
||||
previousModules := cloneNginxModules(modules)
|
||||
modules, moduleErr = buildDynamicNginxModules(install, modules, nil, false, "", t)
|
||||
if moduleErr != nil {
|
||||
return moduleErr
|
||||
}
|
||||
if moduleErr = saveNginxModules(install, modules); moduleErr != nil {
|
||||
removeNginxModuleOutputsNotReferenced(install, modules, previousModules)
|
||||
return moduleErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if out, err := compose.Down(install.GetComposePath()); err != nil {
|
||||
if out != "" {
|
||||
upErr = errors.New(out)
|
||||
@@ -1000,7 +1037,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
}
|
||||
|
||||
if install.App.Key == constant.AppOpenresty {
|
||||
if err = buildNginx(t); err != nil {
|
||||
if err = buildNginx(t, install); err != nil {
|
||||
t.Log(err.Error())
|
||||
return err
|
||||
}
|
||||
@@ -2300,7 +2337,7 @@ func handleOpenrestyFile(appInstall *model.AppInstall) error {
|
||||
|
||||
func handleDefaultServer(appInstall *model.AppInstall) error {
|
||||
installDir := appInstall.GetPath()
|
||||
defaultConfigPath := path.Join(installDir, "conf", "default", "00.default.conf")
|
||||
defaultConfigPath := path.Join(installDir, nginxModuleConfDir, "default", "00.default.conf")
|
||||
fileOp := files.NewFileOp()
|
||||
content, err := fileOp.GetContent(defaultConfigPath)
|
||||
if err != nil {
|
||||
@@ -2314,7 +2351,7 @@ func handleDefaultServer(appInstall *model.AppInstall) error {
|
||||
}
|
||||
|
||||
func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRejectHandshake bool) error {
|
||||
sslDir := path.Join(appInstall.GetPath(), "conf", "ssl")
|
||||
sslDir := path.Join(appInstall.GetPath(), nginxModuleConfDir, "ssl")
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(sslDir) {
|
||||
return errors.New("ssl dir not found")
|
||||
@@ -2344,7 +2381,7 @@ func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRe
|
||||
_ = NewIWebsiteSSLService().Delete([]uint{websiteSSL.ID})
|
||||
}()
|
||||
}
|
||||
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf")
|
||||
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
|
||||
content, err := os.ReadFile(defaultConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
|
||||
@@ -17,7 +15,6 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/subosito/gotenv"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
@@ -57,7 +54,7 @@ func (n NginxService) GetNginxConfig() (*response.NginxFile, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configPath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "nginx.conf")
|
||||
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
|
||||
@@ -138,7 +135,7 @@ func (n NginxService) UpdateConfigFile(req request.NginxConfigFileUpdate) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filePath := path.Join(global.Dir.AppInstallDir, constant.AppOpenresty, nginxInstall.Name, "conf", "nginx.conf")
|
||||
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) {
|
||||
@@ -181,71 +178,20 @@ func (n NginxService) Build(req request.NginxBuildReq) error {
|
||||
if err = task.CheckTaskIsExecuting(taskName); err != nil {
|
||||
return err
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
buildPath := path.Join(nginxInstall.GetPath(), "build")
|
||||
if !fileOp.Stat(buildPath) {
|
||||
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")
|
||||
}
|
||||
moduleConfigPath := path.Join(buildPath, "module.json")
|
||||
moduleContent, err := fileOp.GetContent(moduleConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
modules []dto.NginxModule
|
||||
addModuleParams []string
|
||||
addPackages []string
|
||||
)
|
||||
if len(moduleContent) > 0 {
|
||||
_ = json.Unmarshal(moduleContent, &modules)
|
||||
bashFile, err := os.OpenFile(path.Join(buildPath, "tmp", "pre.sh"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.DirPerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer bashFile.Close()
|
||||
bashFileWriter := bufio.NewWriter(bashFile)
|
||||
for _, module := range modules {
|
||||
if !module.Enable {
|
||||
continue
|
||||
}
|
||||
_, err = bashFileWriter.WriteString(module.Script + "\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addModuleParams = append(addModuleParams, module.Params)
|
||||
addPackages = append(addPackages, module.Packages...)
|
||||
}
|
||||
err = bashFileWriter.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
envs, err := gotenv.Read(nginxInstall.GetEnvPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
envs["CONTAINER_PACKAGE_URL"] = req.Mirror
|
||||
envs["RESTY_CONFIG_OPTIONS_MORE"] = ""
|
||||
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = ""
|
||||
if len(addModuleParams) > 0 {
|
||||
envs["RESTY_CONFIG_OPTIONS_MORE"] = strings.Join(addModuleParams, " ")
|
||||
}
|
||||
if len(addPackages) > 0 {
|
||||
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = strings.Join(addPackages, " ")
|
||||
}
|
||||
_ = gotenv.Write(envs, nginxInstall.GetEnvPath())
|
||||
|
||||
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 {
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*buildTask), cmd.WithTimeout(120*time.Minute))
|
||||
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = compose.DownAndUp(nginxInstall.GetComposePath())
|
||||
return err
|
||||
return executeNginxModuleBuild(nginxInstall, req.Modules, req.Force, req.Mirror, t, true)
|
||||
}, nil, 0, 120*time.Minute)
|
||||
|
||||
go func() {
|
||||
@@ -259,27 +205,65 @@ func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
var modules []dto.NginxModule
|
||||
moduleConfigPath := path.Join(nginxInstall.GetPath(), "build", "module.json")
|
||||
if !fileOp.Stat(moduleConfigPath) {
|
||||
return nil, nil
|
||||
}
|
||||
moduleContent, err := fileOp.GetContent(moduleConfigPath)
|
||||
modules, err := loadNginxModules(nginxInstall)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(moduleContent) > 0 {
|
||||
_ = json.Unmarshal(moduleContent, &modules)
|
||||
target, targetWarning, targetErr := resolveNginxModuleTarget(nginxInstall)
|
||||
if targetWarning != "" {
|
||||
global.LOG.Warn(targetWarning)
|
||||
}
|
||||
var resList []response.NginxModule
|
||||
for _, module := range modules {
|
||||
if module.Deleted {
|
||||
continue
|
||||
}
|
||||
buildStatus := nginxModuleStatusPending
|
||||
loadStatus := nginxModuleLoadDisabled
|
||||
compatibility := "unknown"
|
||||
var artifacts []dto.NginxModuleArtifact
|
||||
if module.BuildMode == nginxModuleBuildStatic {
|
||||
buildStatus = nginxModuleStatusReady
|
||||
compatibility = "static"
|
||||
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 {
|
||||
compatibility = "compatible"
|
||||
if module.Enable {
|
||||
loadStatus = nginxModuleLoadEnabled
|
||||
}
|
||||
}
|
||||
} else if latestBuild := findLatestNginxModuleBuild(module, target); latestBuild != nil {
|
||||
compatibility = "stale"
|
||||
artifacts = latestBuild.Artifacts
|
||||
if module.Enable {
|
||||
loadStatus = nginxModuleLoadEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
if module.BuildMode != nginxModuleBuildStatic && module.LastError != "" {
|
||||
buildStatus = nginxModuleStatusFailed
|
||||
}
|
||||
resList = append(resList, response.NginxModule{
|
||||
Name: module.Name,
|
||||
Script: module.Script,
|
||||
Packages: strings.Join(module.Packages, ","),
|
||||
Params: module.Params,
|
||||
Enable: module.Enable,
|
||||
Name: module.Name,
|
||||
Script: module.Script,
|
||||
Packages: strings.Join(module.Packages, ","),
|
||||
Params: module.Params,
|
||||
Enable: module.Enable,
|
||||
BuildMode: module.BuildMode,
|
||||
Provider: module.Provider,
|
||||
DynamicSupport: module.DynamicSupport,
|
||||
LoadOrder: module.LoadOrder,
|
||||
BuildStatus: buildStatus,
|
||||
LoadStatus: loadStatus,
|
||||
Compatibility: compatibility,
|
||||
Artifacts: artifacts,
|
||||
LastError: module.LastError,
|
||||
})
|
||||
}
|
||||
envs, err := gotenv.Read(nginxInstall.GetEnvPath())
|
||||
@@ -288,8 +272,9 @@ func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
|
||||
}
|
||||
|
||||
return &response.NginxBuildConfig{
|
||||
Mirror: envs["CONTAINER_PACKAGE_URL"],
|
||||
Modules: resList,
|
||||
Mirror: envs["CONTAINER_PACKAGE_URL"],
|
||||
DynamicSupported: nginxModuleDynamicSupported(nginxInstall),
|
||||
Modules: resList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -298,59 +283,91 @@ func (n NginxService) UpdateModule(req request.NginxModuleUpdate) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
var (
|
||||
modules []dto.NginxModule
|
||||
)
|
||||
moduleConfigPath := path.Join(nginxInstall.GetPath(), "build", "module.json")
|
||||
if !fileOp.Stat(moduleConfigPath) {
|
||||
_ = fileOp.CreateFile(moduleConfigPath)
|
||||
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, nginxInstall.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
moduleContent, err := fileOp.GetContent(moduleConfigPath)
|
||||
modules, err := loadNginxModules(nginxInstall)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(moduleContent) > 0 {
|
||||
_ = json.Unmarshal(moduleContent, &modules)
|
||||
}
|
||||
oldModules := cloneNginxModules(modules)
|
||||
var deletedModule *dto.NginxModule
|
||||
|
||||
switch req.Operate {
|
||||
case "create":
|
||||
for _, module := range modules {
|
||||
case nginxModuleOperateCreate:
|
||||
recreated := false
|
||||
for i, module := range modules {
|
||||
if module.Name == req.Name {
|
||||
if module.Deleted {
|
||||
modules[i] = dto.NginxModule{
|
||||
Name: req.Name, Script: req.Script, Packages: strings.Split(req.Packages, ","),
|
||||
Params: req.Params,
|
||||
Enable: req.Enable, BuildMode: req.BuildMode, Provider: req.Provider, LoadOrder: req.LoadOrder,
|
||||
}
|
||||
recreated = true
|
||||
break
|
||||
}
|
||||
return buserr.New("ErrNameIsExist")
|
||||
}
|
||||
}
|
||||
modules = append(modules, dto.NginxModule{
|
||||
Name: req.Name,
|
||||
Script: req.Script,
|
||||
Packages: strings.Split(req.Packages, ","),
|
||||
Params: req.Params,
|
||||
Enable: true,
|
||||
})
|
||||
case "update":
|
||||
if !recreated {
|
||||
modules = append(modules, dto.NginxModule{
|
||||
Name: req.Name,
|
||||
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:
|
||||
found := false
|
||||
for i, module := range modules {
|
||||
if module.Name == req.Name {
|
||||
found = true
|
||||
modules[i].Script = req.Script
|
||||
modules[i].Packages = strings.Split(req.Packages, ",")
|
||||
modules[i].Params = req.Params
|
||||
modules[i].Enable = req.Enable
|
||||
modules[i].BuildMode = req.BuildMode
|
||||
modules[i].Provider = req.Provider
|
||||
modules[i].LoadOrder = req.LoadOrder
|
||||
break
|
||||
}
|
||||
}
|
||||
case "delete":
|
||||
if !found {
|
||||
return fmt.Errorf("OpenResty module %s not found", req.Name)
|
||||
}
|
||||
case nginxModuleOperateDelete:
|
||||
found := false
|
||||
for i, module := range modules {
|
||||
if module.Name == req.Name {
|
||||
modules = append(modules[:i], modules[i+1:]...)
|
||||
found = true
|
||||
moduleCopy := module
|
||||
deletedModule = &moduleCopy
|
||||
modules[i].Deleted = true
|
||||
modules[i].Enable = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("OpenResty module %s not found", req.Name)
|
||||
}
|
||||
}
|
||||
moduleByte, err := json.Marshal(modules)
|
||||
if err != nil {
|
||||
if err = saveNginxModules(nginxInstall, modules); err != nil {
|
||||
return err
|
||||
}
|
||||
return fileOp.SaveFileWithByte(moduleConfigPath, moduleByte, constant.DirPerm)
|
||||
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 {
|
||||
@@ -366,7 +383,7 @@ func (n NginxService) OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) e
|
||||
break
|
||||
}
|
||||
}
|
||||
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf")
|
||||
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
|
||||
content, err := os.ReadFile(defaultConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -406,7 +423,7 @@ func (n NginxService) GetDefaultHttpsStatus() (*response.NginxConfigRes, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf")
|
||||
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf")
|
||||
content, err := os.ReadFile(defaultConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
1114
agent/app/service/nginx_module.go
Normal file
1114
agent/app/service/nginx_module.go
Normal file
File diff suppressed because it is too large
Load Diff
347
agent/app/service/nginx_module_test.go
Normal file
347
agent/app/service/nginx_module_test.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
)
|
||||
|
||||
func TestNormalizeNginxModulePreservesLegacyStaticMode(t *testing.T) {
|
||||
module := dto.NginxModule{
|
||||
Name: "legacy",
|
||||
Packages: []string{"git", "", "git", " curl "},
|
||||
}
|
||||
|
||||
normalizeNginxModule(&module)
|
||||
|
||||
if module.BuildMode != nginxModuleBuildStatic {
|
||||
t.Fatalf("expected legacy module to remain static, got %s", module.BuildMode)
|
||||
}
|
||||
if module.Provider != nginxModuleProviderLocal {
|
||||
t.Fatalf("expected local provider, got %s", module.Provider)
|
||||
}
|
||||
if len(module.Packages) != 2 || module.Packages[0] != "git" || module.Packages[1] != "curl" {
|
||||
t.Fatalf("unexpected normalized packages: %#v", module.Packages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeDynamicModuleParams(t *testing.T) {
|
||||
params, err := normalizeDynamicModuleParams("--with-http_dav_module --add-module=/tmp/nginx-dav-ext-module")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := "--with-http_dav_module --add-dynamic-module=/tmp/nginx-dav-ext-module"
|
||||
if params != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, params)
|
||||
}
|
||||
|
||||
if _, err = normalizeDynamicModuleParams("--add-module=/tmp/module;touch /tmp/unsafe"); err == nil {
|
||||
t.Fatal("expected shell metacharacters to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindCurrentAndLatestNginxModuleBuild(t *testing.T) {
|
||||
target := dto.NginxModuleTarget{Key: "target"}
|
||||
module := dto.NginxModule{
|
||||
Name: "example",
|
||||
Params: "--add-module=/tmp/example",
|
||||
BuildMode: nginxModuleBuildDynamic,
|
||||
Provider: nginxModuleProviderLocal,
|
||||
}
|
||||
params, err := normalizeDynamicModuleParams(module.Params)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
currentHash, err := nginxModuleBuildHash(module, target, params)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
oldTime := time.Now().Add(-time.Hour)
|
||||
newTime := time.Now()
|
||||
module.Builds = []dto.NginxModuleBuild{
|
||||
{Hash: "old", Status: nginxModuleStatusReady, Target: target, BuiltAt: oldTime},
|
||||
{Hash: currentHash, Status: nginxModuleStatusReady, Target: target, BuiltAt: newTime},
|
||||
}
|
||||
|
||||
if build := findCurrentNginxModuleBuild(module, target); build == nil || build.Hash != currentHash {
|
||||
t.Fatalf("current build was not selected: %#v", build)
|
||||
}
|
||||
if build := findLatestNginxModuleBuild(module, target); build == nil || build.Hash != currentHash {
|
||||
t.Fatalf("latest build was not selected: %#v", build)
|
||||
}
|
||||
|
||||
module.Params = "--add-module=/tmp/example-v2"
|
||||
if build := findCurrentNginxModuleBuild(module, target); build != nil {
|
||||
t.Fatalf("changed module input should be stale, got %#v", build)
|
||||
}
|
||||
if build := findLatestNginxModuleBuild(module, target); build == nil || build.Hash != currentHash {
|
||||
t.Fatal("the previous ready build should remain available until replacement")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxModulePathNameAvoidsSanitizedNameCollisions(t *testing.T) {
|
||||
first := nginxModulePathName("example/module")
|
||||
second := nginxModulePathName("example-module")
|
||||
if first == second {
|
||||
t.Fatalf("module path names collided: %s", first)
|
||||
}
|
||||
if len(nginxModulePathName(string(make([]byte, 256)))) > 57 {
|
||||
t.Fatal("module path name should remain safe for Docker resource names")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordNginxModuleBuildFailureKeepsPreviousReadyBuild(t *testing.T) {
|
||||
target := dto.NginxModuleTarget{Key: "target"}
|
||||
ready := dto.NginxModuleBuild{
|
||||
Hash: "ready", Status: nginxModuleStatusReady, Target: target, BuiltAt: time.Now().Add(-time.Hour),
|
||||
}
|
||||
original := []dto.NginxModule{{
|
||||
Name: "example", BuildMode: nginxModuleBuildDynamic, DynamicSupport: nginxModuleSupportUnknown,
|
||||
Builds: []dto.NginxModuleBuild{ready},
|
||||
}}
|
||||
failed := dto.NginxModuleBuild{
|
||||
Hash: "candidate", Status: nginxModuleStatusFailed, Target: target, Error: "load failed", BuiltAt: time.Now(),
|
||||
}
|
||||
|
||||
result := recordNginxModuleBuildFailure(original, "example", failed, &ready, true)
|
||||
|
||||
if len(result[0].Builds) != 1 || result[0].Builds[0].Hash != "ready" {
|
||||
t.Fatalf("previous ready build was replaced: %#v", result[0].Builds)
|
||||
}
|
||||
if result[0].LastError != failed.Error || result[0].DynamicSupport != nginxModuleSupportSupported {
|
||||
t.Fatalf("failure metadata was not retained: %#v", result[0])
|
||||
}
|
||||
result[0].Builds[0].Hash = "mutated"
|
||||
if original[0].Builds[0].Hash != "ready" {
|
||||
t.Fatal("module clone shares build state with the original")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasDynamicNginxModuleBuildTask(t *testing.T) {
|
||||
dynamicEnabled := dto.NginxModule{Name: "brotli", Enable: true, BuildMode: nginxModuleBuildDynamic}
|
||||
staticEnabled := dto.NginxModule{Name: "pagespeed", Enable: true, BuildMode: nginxModuleBuildStatic}
|
||||
deletedDynamic := dto.NginxModule{Name: "geoip", Enable: true, BuildMode: nginxModuleBuildDynamic, Deleted: true}
|
||||
disabledDynamic := dto.NginxModule{Name: "waf", Enable: false, BuildMode: nginxModuleBuildDynamic}
|
||||
|
||||
if hasDynamicNginxModuleBuildTask(nil, nil) {
|
||||
t.Fatal("empty module list should not require a dynamic build")
|
||||
}
|
||||
if hasDynamicNginxModuleBuildTask([]dto.NginxModule{staticEnabled}, nil) {
|
||||
t.Fatal("static-only modules should not require a dynamic build")
|
||||
}
|
||||
if hasDynamicNginxModuleBuildTask([]dto.NginxModule{deletedDynamic}, nil) {
|
||||
t.Fatal("deleted modules should not require a dynamic build")
|
||||
}
|
||||
if hasDynamicNginxModuleBuildTask([]dto.NginxModule{dynamicEnabled}, []string{"other"}) {
|
||||
t.Fatal("enabled module outside the selection should not require a dynamic build")
|
||||
}
|
||||
if !hasDynamicNginxModuleBuildTask([]dto.NginxModule{disabledDynamic}, []string{"waf"}) {
|
||||
t.Fatal("selected module should require a dynamic build even when disabled")
|
||||
}
|
||||
if !hasDynamicNginxModuleBuildTask([]dto.NginxModule{dynamicEnabled, staticEnabled}, nil) {
|
||||
t.Fatal("enabled dynamic module should require a dynamic build")
|
||||
}
|
||||
|
||||
legacy := dto.NginxModule{Name: "legacy", Enable: true}
|
||||
if hasDynamicNginxModuleBuildTask([]dto.NginxModule{legacy}, nil) {
|
||||
t.Fatal("legacy module without a build mode stays static and should not require a dynamic build")
|
||||
}
|
||||
if legacy.BuildMode != "" {
|
||||
t.Fatalf("prescan must normalize a copy, got mutated BuildMode %q", legacy.BuildMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveNginxModuleTargetWithoutBuilder(t *testing.T) {
|
||||
oldDir := global.Dir.AppInstallDir
|
||||
global.Dir.AppInstallDir = t.TempDir()
|
||||
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
|
||||
install := model.AppInstall{Name: "openresty", Version: "1.27.1.2"}
|
||||
install.App.Key = constant.AppOpenresty
|
||||
|
||||
_, _, err := resolveNginxModuleTarget(install)
|
||||
if !errors.Is(err, errNginxModuleBuilderMissing) {
|
||||
t.Fatalf("expected builder-missing sentinel, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "dynamic module builder not found") {
|
||||
t.Fatalf("unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeOpenrestyModuleVolumes(t *testing.T) {
|
||||
newService := map[string]interface{}{}
|
||||
oldService := map[string]interface{}{
|
||||
"volumes": []interface{}{
|
||||
"./conf:/etc/nginx/conf.d:ro",
|
||||
"./modules:/usr/local/openresty/nginx/modules/1panel:ro",
|
||||
"./conf/modules-enabled:/usr/local/openresty/nginx/conf/modules-enabled:ro",
|
||||
12345,
|
||||
},
|
||||
}
|
||||
|
||||
mergeOpenrestyModuleVolumes(newService, oldService)
|
||||
|
||||
merged, ok := newService["volumes"].([]interface{})
|
||||
if !ok || len(merged) != 2 {
|
||||
t.Fatalf("expected two module mounts to be merged, got %#v", newService["volumes"])
|
||||
}
|
||||
for _, volume := range merged {
|
||||
if volume == "./conf:/etc/nginx/conf.d:ro" {
|
||||
t.Fatal("unrelated mount should not be merged")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeOpenrestyModuleVolumesKeepsExisting(t *testing.T) {
|
||||
existing := "./modules:/usr/local/openresty/nginx/modules/1panel:ro"
|
||||
newService := map[string]interface{}{
|
||||
"volumes": []interface{}{existing, map[string]interface{}{"type": "bind"}},
|
||||
}
|
||||
oldService := map[string]interface{}{
|
||||
"volumes": []interface{}{
|
||||
"./modules:/usr/local/openresty/nginx/modules/1panel:ro",
|
||||
"./conf/modules-enabled:/usr/local/openresty/nginx/conf/modules-enabled:ro",
|
||||
},
|
||||
}
|
||||
|
||||
mergeOpenrestyModuleVolumes(newService, oldService)
|
||||
|
||||
merged, ok := newService["volumes"].([]interface{})
|
||||
if !ok || len(merged) != 3 {
|
||||
t.Fatalf("expected only the missing mount to be appended, got %#v", newService["volumes"])
|
||||
}
|
||||
if merged[0] != existing {
|
||||
t.Fatalf("existing mounts must keep their order, got %#v", merged)
|
||||
}
|
||||
if merged[2] != "./conf/modules-enabled:/usr/local/openresty/nginx/conf/modules-enabled:ro" {
|
||||
t.Fatalf("missing module mount was not appended, got %#v", merged)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeNginxModuleFoldsAutoIntoDynamic(t *testing.T) {
|
||||
module := dto.NginxModule{Name: "legacy-auto", BuildMode: nginxModuleBuildAuto}
|
||||
|
||||
normalizeNginxModule(&module)
|
||||
|
||||
if module.BuildMode != nginxModuleBuildDynamic {
|
||||
t.Fatalf("auto should normalize to dynamic, got %s", module.BuildMode)
|
||||
}
|
||||
}
|
||||
|
||||
func writeNginxModuleFixture(t *testing.T, install model.AppInstall, withBuilder bool, modules []dto.NginxModule) {
|
||||
t.Helper()
|
||||
buildDir := path.Join(install.GetPath(), nginxModuleBuildDir)
|
||||
if err := os.MkdirAll(buildDir, constant.DirPerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if withBuilder {
|
||||
if err := os.WriteFile(path.Join(buildDir, nginxModuleBuilderFile), []byte("FROM scratch\n"), constant.FilePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path.Join(buildDir, nginxModuleCatalogFile), []byte("[]"), constant.FilePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
content, err := json.Marshal(modules)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = os.WriteFile(path.Join(buildDir, nginxModuleStoreFile), content, constant.FilePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNginxModulesProbesDynamicSupport(t *testing.T) {
|
||||
oldDir := global.Dir.AppInstallDir
|
||||
global.Dir.AppInstallDir = t.TempDir()
|
||||
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
|
||||
install := model.AppInstall{Name: "openresty", Version: "1.27.1.2"}
|
||||
install.App.Key = constant.AppOpenresty
|
||||
writeNginxModuleFixture(t, install, true, []dto.NginxModule{
|
||||
{Name: "good", Enable: true, BuildMode: nginxModuleBuildDynamic, Params: "--add-module=/tmp/good"},
|
||||
{Name: "bad", Enable: true, BuildMode: nginxModuleBuildDynamic, Params: "--with-nothing"},
|
||||
{Name: "meta", Enable: true, BuildMode: nginxModuleBuildDynamic, Params: "--add-module=/tmp/x;touch /tmp/y"},
|
||||
{Name: "static-mod", Enable: true, BuildMode: nginxModuleBuildStatic},
|
||||
{Name: "deleted", Deleted: true, BuildMode: nginxModuleBuildDynamic, Params: "--with-nothing"},
|
||||
})
|
||||
|
||||
loaded, err := loadNginxModules(install)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
support := make(map[string]string, len(loaded))
|
||||
for _, module := range loaded {
|
||||
support[module.Name] = module.DynamicSupport
|
||||
}
|
||||
if support["good"] != nginxModuleSupportSupported {
|
||||
t.Fatalf("valid dynamic params should probe supported, got %q", support["good"])
|
||||
}
|
||||
if support["bad"] != nginxModuleSupportUnsupported {
|
||||
t.Fatalf("params without a dynamic option should probe unsupported, got %q", support["bad"])
|
||||
}
|
||||
if support["meta"] != nginxModuleSupportUnsupported {
|
||||
t.Fatalf("params with shell metacharacters should probe unsupported, got %q", support["meta"])
|
||||
}
|
||||
if support["static-mod"] != nginxModuleSupportUnknown {
|
||||
t.Fatalf("static module must not be probed, got %q", support["static-mod"])
|
||||
}
|
||||
if support["deleted"] != nginxModuleSupportUnknown {
|
||||
t.Fatalf("deleted module must not be probed, got %q", support["deleted"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNginxModulesWithoutBuilderKeepsUnknownSupport(t *testing.T) {
|
||||
oldDir := global.Dir.AppInstallDir
|
||||
global.Dir.AppInstallDir = t.TempDir()
|
||||
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
|
||||
install := model.AppInstall{Name: "openresty", Version: "1.27.1.2"}
|
||||
install.App.Key = constant.AppOpenresty
|
||||
writeNginxModuleFixture(t, install, false, []dto.NginxModule{
|
||||
{Name: "good", Enable: true, BuildMode: nginxModuleBuildDynamic, Params: "--add-module=/tmp/good"},
|
||||
})
|
||||
|
||||
loaded, err := loadNginxModules(install)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(loaded) != 1 || loaded[0].DynamicSupport != nginxModuleSupportUnknown {
|
||||
t.Fatalf("without the builder the support marker must stay unknown, got %#v", loaded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveNginxModuleBuildMirror(t *testing.T) {
|
||||
oldDir := global.Dir.AppInstallDir
|
||||
global.Dir.AppInstallDir = t.TempDir()
|
||||
t.Cleanup(func() { global.Dir.AppInstallDir = oldDir })
|
||||
install := model.AppInstall{Name: "openresty", Version: "1.27.1.2"}
|
||||
install.App.Key = constant.AppOpenresty
|
||||
if err := os.MkdirAll(install.GetPath(), constant.DirPerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got := resolveNginxModuleBuildMirror(install, "https://mirror.example.com"); got != "https://mirror.example.com" {
|
||||
t.Fatalf("request mirror should win, got %q", got)
|
||||
}
|
||||
if got := resolveNginxModuleBuildMirror(install, ""); got != "" {
|
||||
t.Fatalf("missing env file should yield an empty mirror, got %q", got)
|
||||
}
|
||||
|
||||
envContent := "CONTAINER_PACKAGE_URL=https://apt.example.com\nOTHER=value\n"
|
||||
if err := os.WriteFile(install.GetEnvPath(), []byte(envContent), constant.FilePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := resolveNginxModuleBuildMirror(install, ""); got != "https://apt.example.com" {
|
||||
t.Fatalf("env CONTAINER_PACKAGE_URL should be the fallback, got %q", got)
|
||||
}
|
||||
if got := resolveNginxModuleBuildMirror(install, "https://mirror.example.com"); got != "https://mirror.example.com" {
|
||||
t.Fatalf("request mirror should still win over the env value, got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,9 @@ const (
|
||||
SyslogRFC3164Pattern = `^([A-Z][a-z]{2}\s{1,2}\d{1,2}\s\d{2}:\d{2}:\d{2})\s+(.*)$`
|
||||
SyslogRFC3339Pattern = `^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\s+(.*)$`
|
||||
SyslogServicePattern = `^(?:\S+\s+)?([[:alnum:]_.@/-]+)(?:\[\d+\])?:\s*(.*)$`
|
||||
NginxModulePackagePattern = `^[a-zA-Z0-9][a-zA-Z0-9+.-]*$`
|
||||
NginxModuleArtifactPattern = `^[a-zA-Z0-9_./+-]+\.so$`
|
||||
NginxModuleChecksumPattern = `^[a-fA-F0-9]{64}$`
|
||||
)
|
||||
|
||||
var regexMap = make(map[string]*regexp.Regexp)
|
||||
@@ -96,6 +99,9 @@ func Init() {
|
||||
SyslogRFC3164Pattern,
|
||||
SyslogRFC3339Pattern,
|
||||
SyslogServicePattern,
|
||||
NginxModulePackagePattern,
|
||||
NginxModuleArtifactPattern,
|
||||
NginxModuleChecksumPattern,
|
||||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
@@ -118,3 +124,15 @@ func RegisterRegex(pattern string) {
|
||||
func StripAnsiControlSeq(value string) string {
|
||||
return GetRegex(AnsiControlSeqPattern).ReplaceAllString(value, "")
|
||||
}
|
||||
|
||||
func IsValidNginxModulePackage(value string) bool {
|
||||
return GetRegex(NginxModulePackagePattern).MatchString(value)
|
||||
}
|
||||
|
||||
func IsValidNginxModuleArtifact(value string) bool {
|
||||
return GetRegex(NginxModuleArtifactPattern).MatchString(value)
|
||||
}
|
||||
|
||||
func IsValidNginxModuleChecksum(value string) bool {
|
||||
return GetRegex(NginxModuleChecksumPattern).MatchString(value)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,14 @@ export namespace Nginx {
|
||||
export interface NginxBuildReq {
|
||||
taskID: string;
|
||||
mirror: string;
|
||||
modules?: string[];
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export interface NginxModuleArtifact {
|
||||
name: string;
|
||||
path: string;
|
||||
checksum: string;
|
||||
}
|
||||
|
||||
export interface NginxModule {
|
||||
@@ -40,15 +48,33 @@ export namespace Nginx {
|
||||
packages?: string;
|
||||
enable: boolean;
|
||||
params: string;
|
||||
buildMode: 'auto' | 'dynamic' | 'static';
|
||||
provider: 'local' | 'prebuilt';
|
||||
dynamicSupport: 'unknown' | 'supported' | 'unsupported';
|
||||
loadOrder: number;
|
||||
buildStatus: 'pending' | 'ready' | 'failed';
|
||||
loadStatus: 'enabled' | 'disabled';
|
||||
compatibility: 'unknown' | 'compatible' | 'stale' | 'static';
|
||||
artifacts?: NginxModuleArtifact[];
|
||||
lastError?: string;
|
||||
}
|
||||
|
||||
export interface NginxBuildConfig {
|
||||
mirror: string;
|
||||
modules: NginxModule[];
|
||||
dynamicSupported: boolean;
|
||||
}
|
||||
|
||||
export interface NginxModuleUpdate extends NginxModule {
|
||||
export interface NginxModuleUpdate {
|
||||
operate: string;
|
||||
name: string;
|
||||
script?: string;
|
||||
packages?: string;
|
||||
enable?: boolean;
|
||||
params?: string;
|
||||
buildMode?: 'auto' | 'dynamic' | 'static';
|
||||
provider?: 'local' | 'prebuilt';
|
||||
loadOrder?: number;
|
||||
}
|
||||
|
||||
export interface NginxHttpsStatus {
|
||||
|
||||
@@ -3799,14 +3799,36 @@ const message = {
|
||||
module: 'Modules',
|
||||
build: 'Build',
|
||||
buildWarn:
|
||||
'Building OpenResty requires reserving a certain amount of CPU and memory, which may take a long time, please be patient',
|
||||
'Local module builds use CPU and memory. Static modules also rebuild and restart OpenResty. Continue?',
|
||||
buildPurposeHint:
|
||||
'Dynamic modules take effect via hot reload after building (no container restart); including static modules triggers a full image rebuild and container recreation',
|
||||
buildMode: 'Build mode',
|
||||
buildModeDynamic: 'Dynamic',
|
||||
buildModeStatic: 'Static',
|
||||
buildStatus: 'Build status',
|
||||
compatibility: 'Compatibility',
|
||||
loadOrder: 'Load order',
|
||||
modulesToBuild: 'Dynamic modules (hot-reload after build, no container restart)',
|
||||
staticModules: 'Static modules',
|
||||
staticModulesHelper: 'Building will rebuild all modules and restart OpenResty',
|
||||
forceBuild: 'Rebuild without cache',
|
||||
buildFailed: 'Last build failed',
|
||||
pending: 'Pending',
|
||||
ready: 'Ready',
|
||||
failed: 'Failed',
|
||||
unknown: 'Unknown',
|
||||
compatible: 'Compatible',
|
||||
stale: 'Rebuild required',
|
||||
static: 'Static build',
|
||||
dynamicUnsupported: 'Dynamic build is not supported on the current OpenResty version',
|
||||
moduleDynamicUnsupported: 'The parameters of this module do not support dynamic build',
|
||||
mirrorUrl: 'Software Source',
|
||||
paramsHelper: 'For example: --add-module=/tmp/ngx_brotli',
|
||||
paramsHelper: 'For example: --add-module=/tmp/ngx_brotli; dynamic mode converts it automatically',
|
||||
packagesHelper: 'For example: git, curl (separated by commas)',
|
||||
scriptHelper:
|
||||
'Scripts to execute before compilation, usually for downloading module source code, installing dependencies, etc.',
|
||||
buildHelper:
|
||||
'Click build after adding/modifying a module. OpenResty will automatically restart upon successful build.',
|
||||
'Dynamic modules are loaded from configuration after building; static modules still rebuild OpenResty.',
|
||||
defaultHttps: 'HTTPS Anti-tampering',
|
||||
defaultHttpsHelper1: 'Enabling this can resolve HTTPS tampering issues.',
|
||||
sslRejectHandshake: 'Reject default SSL handshake',
|
||||
|
||||
@@ -3842,14 +3842,36 @@ const message = {
|
||||
script: 'Scripts',
|
||||
module: 'Módulos',
|
||||
build: 'Compilar',
|
||||
buildWarn: 'Compilar OpenResty requiere reservar CPU y memoria, puede tomar tiempo, ten paciencia',
|
||||
buildWarn: 'La compilación local de módulos consume CPU y memoria. Los módulos estáticos también reconstruyen y reinician OpenResty. ¿Continuar?',
|
||||
buildPurposeHint:
|
||||
'Los módulos dinámicos se aplican mediante carga en caliente tras la compilación (sin reiniciar el contenedor); si incluye módulos estáticos, se reconstruirá la imagen completa y se recreará el contenedor',
|
||||
buildMode: 'Modo de compilación',
|
||||
buildModeDynamic: 'Dinámico',
|
||||
buildModeStatic: 'Estático',
|
||||
buildStatus: 'Estado de compilación',
|
||||
compatibility: 'Compatibilidad',
|
||||
loadOrder: 'Orden de carga',
|
||||
modulesToBuild: 'Módulos dinámicos (carga en caliente tras la compilación, sin reiniciar el contenedor)',
|
||||
staticModules: 'Módulos estáticos',
|
||||
staticModulesHelper: 'La compilación reconstruirá todos los módulos y reiniciará OpenResty',
|
||||
forceBuild: 'Recompilar sin caché',
|
||||
buildFailed: 'La última compilación falló',
|
||||
pending: 'Pendiente',
|
||||
ready: 'Listo',
|
||||
failed: 'Fallido',
|
||||
unknown: 'Desconocido',
|
||||
compatible: 'Compatible',
|
||||
stale: 'Requiere recompilación',
|
||||
static: 'Compilación estática',
|
||||
dynamicUnsupported: 'La compilación dinámica no es compatible con la versión actual de OpenResty',
|
||||
moduleDynamicUnsupported: 'Los parámetros de este módulo no admiten la compilación dinámica',
|
||||
mirrorUrl: 'Fuente de software',
|
||||
paramsHelper: 'Por ejemplo: --add-module=/tmp/ngx_brotli',
|
||||
paramsHelper: 'Por ejemplo: --add-module=/tmp/ngx_brotli; el modo dinámico lo convierte automáticamente',
|
||||
packagesHelper: 'Por ejemplo: git, curl (separados por coma)',
|
||||
scriptHelper:
|
||||
'Scripts a ejecutar antes de compilar, usualmente para descargar código fuente de módulos, instalar dependencias, etc.',
|
||||
buildHelper:
|
||||
'Haz clic en compilar después de agregar/modificar un módulo. OpenResty se reiniciará automáticamente tras una compilación exitosa.',
|
||||
'Los módulos dinámicos se cargan desde la configuración tras compilarse; los módulos estáticos siguen reconstruyendo OpenResty.',
|
||||
defaultHttps: 'HTTPS Anti-manipulación',
|
||||
defaultHttpsHelper1: 'Habilitar esto puede resolver problemas de manipulación de HTTPS.',
|
||||
sslRejectHandshake: 'Rechazar handshake SSL predeterminado',
|
||||
|
||||
@@ -3768,14 +3768,36 @@ const message = {
|
||||
module: 'ماژولها',
|
||||
build: 'ساخت',
|
||||
buildWarn:
|
||||
'ساخت OpenResty نیاز به رزرو مقدار مشخصی از CPU و حافظه دارد که ممکن است زمانبر باشد، لطفاً صبور باشید',
|
||||
'ساخت محلی ماژولها به CPU و حافظه نیاز دارد. ماژولهای ایستا OpenResty را دوباره میسازند و راهاندازی مجدد میکنند. ادامه میدهید؟',
|
||||
buildPurposeHint:
|
||||
'ماژولهای پویا پس از ساخت با بارگذاری گرم اعمال میشوند (بدون راهاندازی مجدد کانتینر)؛ در صورت وجود ماژول ایستا، ایمیج بهطور کامل بازسازی و کانتینر دوباره ساخته میشود',
|
||||
buildMode: 'حالت ساخت',
|
||||
buildModeDynamic: 'پویا',
|
||||
buildModeStatic: 'ایستا',
|
||||
buildStatus: 'وضعیت ساخت',
|
||||
compatibility: 'سازگاری',
|
||||
loadOrder: 'ترتیب بارگذاری',
|
||||
modulesToBuild: 'ماژولهای پویا (بارگذاری گرم پس از ساخت، بدون راهاندازی مجدد کانتینر)',
|
||||
staticModules: 'ماژولهای ایستا',
|
||||
staticModulesHelper: 'هنگام ساخت، همه ماژولها بازسازی شده و OpenResty راهاندازی مجدد میشود',
|
||||
forceBuild: 'ساخت مجدد بدون کش',
|
||||
buildFailed: 'آخرین ساخت ناموفق بود',
|
||||
pending: 'در انتظار',
|
||||
ready: 'آماده',
|
||||
failed: 'ناموفق',
|
||||
unknown: 'نامشخص',
|
||||
compatible: 'سازگار',
|
||||
stale: 'نیاز به ساخت مجدد',
|
||||
static: 'ساخت ایستا',
|
||||
dynamicUnsupported: 'ساخت پویا در نسخه فعلی OpenResty پشتیبانی نمیشود',
|
||||
moduleDynamicUnsupported: 'پارامترهای این ماژول از ساخت پویا پشتیبانی نمیکنند',
|
||||
mirrorUrl: 'منبع نرمافزار',
|
||||
paramsHelper: 'مثال: --add-module=/tmp/ngx_brotli',
|
||||
paramsHelper: 'مثال: --add-module=/tmp/ngx_brotli؛ حالت پویا بهطور خودکار تبدیل میکند',
|
||||
packagesHelper: 'مثال: git, curl (با کاما جدا کنید)',
|
||||
scriptHelper:
|
||||
'اسکریپتهایی که قبل از کامپایل اجرا میشوند، معمولاً برای دانلود کد منبع ماژول، نصب وابستگیها و غیره.',
|
||||
buildHelper:
|
||||
'پس از افزودن/تغییر ماژول، روی ساخت کلیک کنید. OpenResty در صورت موفقیت آمیز بودن ساخت به طور خودکار راهاندازی مجدد میشود.',
|
||||
'ماژولهای پویا پس از ساخت از طریق پیکربندی بارگذاری میشوند؛ ماژولهای ایستا همچنان OpenResty را دوباره میسازند.',
|
||||
defaultHttps: 'ضد دستکاری HTTPS',
|
||||
defaultHttpsHelper1: 'فعالسازی این گزینه میتواند مشکلات دستکاری HTTPS را حل کند.',
|
||||
sslRejectHandshake: 'رد دست دادن SSL پیشفرض',
|
||||
|
||||
@@ -3817,14 +3817,36 @@ const message = {
|
||||
module: 'モジュール',
|
||||
build: 'ビルド',
|
||||
buildWarn:
|
||||
'OpenRestyのビルドには一定量のCPUとメモリを確保する必要があり、時間がかかる場合がありますので、お待ちください。',
|
||||
'ローカルでのモジュールビルドは CPU とメモリを使用します。静的モジュールは OpenResty の再ビルドと再起動も行います。続行しますか?',
|
||||
buildPurposeHint:
|
||||
'動的モジュールはビルド後にホットリロードで有効になります(コンテナは再起動しません)。静的モジュールが含まれる場合は、イメージが全量再ビルドされコンテナが再作成されます',
|
||||
buildMode: 'ビルド方式',
|
||||
buildModeDynamic: '動的',
|
||||
buildModeStatic: '静的',
|
||||
buildStatus: 'ビルドステータス',
|
||||
compatibility: '互換性',
|
||||
loadOrder: '読み込み順',
|
||||
modulesToBuild: '動的モジュール(ビルド後ホットリロード、コンテナ再起動なし)',
|
||||
staticModules: '静的モジュール',
|
||||
staticModulesHelper: 'ビルド時にすべてのモジュールが再ビルドされ、OpenResty が再起動します',
|
||||
forceBuild: 'キャッシュを使わず再ビルド',
|
||||
buildFailed: '前回のビルドに失敗しました',
|
||||
pending: 'ビルド待ち',
|
||||
ready: 'ビルド済み',
|
||||
failed: 'ビルド失敗',
|
||||
unknown: '不明',
|
||||
compatible: '互換',
|
||||
stale: '再ビルドが必要',
|
||||
static: '静的ビルド',
|
||||
dynamicUnsupported: '現在の OpenResty バージョンでは動的ビルドはサポートされていません',
|
||||
moduleDynamicUnsupported: 'このモジュールのパラメータは動的ビルドに対応していません',
|
||||
mirrorUrl: 'ソフトウェアソース',
|
||||
paramsHelper: '例:--add-module=/tmp/ngx_brotli',
|
||||
paramsHelper: '例:--add-module=/tmp/ngx_brotli(動的モードでは自動的に変換されます)',
|
||||
packagesHelper: '例:git,curl カンマ区切り',
|
||||
scriptHelper:
|
||||
'コンパイル前に実行するスクリプト、通常はモジュールソースコードのダウンロード、依存関係のインストールなど',
|
||||
buildHelper:
|
||||
'モジュールの追加/変更後にビルドをクリックします。ビルドが成功すると、OpenRestyは自動的に再起動します。',
|
||||
'動的モジュールはビルド後に設定から読み込まれます。静的モジュールは引き続き OpenResty を再ビルドします。',
|
||||
defaultHttps: 'HTTPS 改ざん防止',
|
||||
defaultHttpsHelper1: 'これを有効にすると、HTTPS 改ざん問題を解決できます。',
|
||||
sslRejectHandshake: 'デフォルト SSL ハンドシェイクを拒否',
|
||||
|
||||
@@ -3736,12 +3736,34 @@ const message = {
|
||||
script: '스크립트',
|
||||
module: '모듈',
|
||||
build: '빌드',
|
||||
buildWarn: 'OpenResty 빌드는 CPU와 메모리의 일정량을 예약해야 하며, 시간이 오래 걸릴 수 있으니 기다려 주세요.',
|
||||
buildWarn: '로컬 모듈 빌드는 CPU와 메모리를 사용합니다. 정적 모듈은 OpenResty를 다시 빌드하고 재시작합니다. 계속하시겠습니까?',
|
||||
buildPurposeHint:
|
||||
'동적 모듈은 빌드 후 핫 리로드로 적용됩니다(컨테이너 재시작 없음). 정적 모듈이 포함되면 이미지 전체가 재빌드되고 컨테이너가 재생성됩니다',
|
||||
buildMode: '빌드 방식',
|
||||
buildModeDynamic: '동적',
|
||||
buildModeStatic: '정적',
|
||||
buildStatus: '빌드 상태',
|
||||
compatibility: '호환성',
|
||||
loadOrder: '로드 순서',
|
||||
modulesToBuild: '동적 모듈 (빌드 후 핫 리로드, 컨테이너 재시작 없음)',
|
||||
staticModules: '정적 모듈',
|
||||
staticModulesHelper: '빌드 시 모든 모듈이 재빌드되고 OpenResty가 재시작됩니다',
|
||||
forceBuild: '캐시 없이 다시 빌드',
|
||||
buildFailed: '마지막 빌드 실패',
|
||||
pending: '대기 중',
|
||||
ready: '준비됨',
|
||||
failed: '실패',
|
||||
unknown: '알 수 없음',
|
||||
compatible: '호환됨',
|
||||
stale: '다시 빌드 필요',
|
||||
static: '정적 빌드',
|
||||
dynamicUnsupported: '현재 OpenResty 버전에서는 동적 빌드를 지원하지 않습니다',
|
||||
moduleDynamicUnsupported: '이 모듈의 파라미터는 동적 빌드를 지원하지 않습니다',
|
||||
mirrorUrl: '소프트웨어 소스',
|
||||
paramsHelper: '예: --add-module=/tmp/ngx_brotli',
|
||||
paramsHelper: '예: --add-module=/tmp/ngx_brotli; 동적 모드에서는 자동으로 변환됩니다',
|
||||
packagesHelper: '예: git,curl 쉼표로 구분',
|
||||
scriptHelper: '컴파일 전에 실행할 스크립트, 일반적으로 모듈 소스 코드 다운로드, 종속성 설치 등',
|
||||
buildHelper: '모듈 추가/수정 후 빌드를 클릭하세요. 빌드가 성공하면 OpenResty가 자동으로 재시작됩니다.',
|
||||
buildHelper: '동적 모듈은 빌드 후 설정을 통해 로드되며, 정적 모듈은 여전히 OpenResty를 다시 빌드합니다.',
|
||||
defaultHttps: 'HTTPS 변조 방지',
|
||||
defaultHttpsHelper1: '이를 활성화하면 HTTPS 변조 문제를 해결할 수 있습니다.',
|
||||
sslRejectHandshake: '기본 SSL 핸드셰이크 거부',
|
||||
|
||||
@@ -3708,12 +3708,33 @@ const message = {
|
||||
script: 'ສະຄຣິບ',
|
||||
module: 'ໂມດູນ',
|
||||
build: 'ບິວ (Build)',
|
||||
buildWarn: 'ການບິວ OpenResty ຕ້ອງໃຊ້ CPU ແລະ ໜ່ວຍຄວາມຈຳໃນລະດັບໜຶ່ງ ແລະ ອາດໃຊ້ເວລາດົນ, ກະລຸນາລໍຖ້າ',
|
||||
buildWarn: 'ການບິວໂມດູນໃນເຄື່ອງຕ້ອງໃຊ້ CPU ແລະໜ່ວຍຄວາມຈຳ, ໂມດູນສະແຕຕິກຍັງຈະຣີບິວແລະຣີສະຕາດ OpenResty, ສືບຕໍ່ບໍ?',
|
||||
buildPurposeHint: 'ໂມດູນໄດນາມິກມີຜົນໂຫຼດຮ້ອນຫຼັງຈາກບິວ (ຄອນເທນເນີບໍ່ຣີສະຕາດ); ຖ້າມີໂມດູນສະແຕຕິກ ຈະຣີບິວຮູບພາບແລະສ້າງຄອນເທນເນີໃໝ່',
|
||||
buildMode: 'ຮູບແບບການບິວ',
|
||||
buildModeDynamic: 'ໄດນາມິກ',
|
||||
buildModeStatic: 'ສະແຕຕິກ',
|
||||
buildStatus: 'ສະຖານະການບິວ',
|
||||
compatibility: 'ຄວາມເຂົ້າກັນ',
|
||||
loadOrder: 'ລຳດັບການໂຫຼດ',
|
||||
modulesToBuild: 'ໂມດູນໄດນາມິກ (ຫຼັງຈາກບິວໂຫຼດຮ້ອນ, ຄອນເທນເນີບໍ່ຣີສະຕາດ)',
|
||||
staticModules: 'ໂມດູນສະແຕຕິກ',
|
||||
staticModulesHelper: 'ໃນເວລາບິວ ຈະຣີບິວຮູບພາບແລະຣີສະຕາດ OpenResty',
|
||||
forceBuild: 'ບິວໃໝ່ໂດຍບໍ່ໃຊ້ແຄຊ',
|
||||
buildFailed: 'ການບິວຫຼ້າສຸດລົ້ມເຫຼວ',
|
||||
pending: 'ລໍຖ້າ',
|
||||
ready: 'ພ້ອມແລ້ວ',
|
||||
failed: 'ລົ້ມເຫຼວ',
|
||||
unknown: 'ບໍ່ຮູ້',
|
||||
compatible: 'ເຂົ້າກັນໄດ້',
|
||||
stale: 'ລ້າສະໄໝ',
|
||||
static: 'ສະແຕຕິກ',
|
||||
dynamicUnsupported: 'ເວີຊັນ OpenResty ປັດຈຸບັນບໍ່ຮອງຮັບການບິວແບບໄດນາມິກ',
|
||||
moduleDynamicUnsupported: 'ພາລາມິເຕີຂອງໂມດູນນີ້ບໍ່ຮອງຮັບການບິວແບບໄດນາມິກ',
|
||||
mirrorUrl: 'ແຫຼ່ງຊອບແວ',
|
||||
paramsHelper: 'ຕົວຢ່າງ: --add-module=/tmp/ngx_brotli',
|
||||
paramsHelper: 'ຕົວຢ່າງ: --add-module=/tmp/ngx_brotli; ໂໝດໄດນາມິກຈະແປງໃຫ້ໂດຍອັດຕະໂນມັດ',
|
||||
packagesHelper: 'ຕົວຢ່າງ: git, curl (ແຍກດ້ວຍເຄື່ອງໝາຍຈຸດ)',
|
||||
scriptHelper: 'ສະຄຣິບທີ່ຈະເຮັດວຽກກ່ອນການຄອມໄພລ໌, ມັກໃຊ້ເພື່ອດາວໂຫຼດຊອດໂຄ້ດໂມດູນ ຫຼື ຕິດຕັ້ງ dependency',
|
||||
buildHelper: 'ຄລິກບິວຫຼັງຈາກເພີ່ມ/ແກ້ໄຂໂມດູນ. OpenResty ຈະເລີ່ມໃໝ່ໂດຍອັດຕະໂນມັດເມື່ອບິວສຳເລັດ.',
|
||||
buildHelper: 'ໂມດູນໄດນາມິກໂຫຼດຈາກການຕັ້ງຄ່າຫຼັງຈາກບິວ; ໂມດູນສະແຕຕິກຍັງຄົງຣີບິວ OpenResty.',
|
||||
defaultHttps: 'ການປ້ອງກັນການປອມແປງ HTTPS',
|
||||
defaultHttpsHelper1: 'ການເປີດໃຊ້ສິ່ງນີ້ສາມາດແກ້ໄຂບັນຫາການປອມແປງ HTTPS ໄດ້.',
|
||||
sslRejectHandshake: 'ປະຕິເສດການ handshake SSL ເລີ່ມຕົ້ນ',
|
||||
|
||||
@@ -3872,14 +3872,36 @@ const message = {
|
||||
module: 'Modul',
|
||||
build: 'Bina',
|
||||
buildWarn:
|
||||
'Membina OpenResty memerlukan menyediakan sejumlah CPU dan memori, dan prosesnya mengambil masa yang lama, sila bersabar.',
|
||||
'Binaan modul setempat menggunakan CPU dan memori. Modul statik turut membina semula dan memulakan semula OpenResty. Teruskan?',
|
||||
buildPurposeHint:
|
||||
'Modul dinamik berkuat kuasa melalui muatan semula panas selepas dibina (kontena tidak dimulakan semula); jika modul statik disertakan, imej akan dibina semula sepenuhnya dan kontena dicipta semula',
|
||||
buildMode: 'Mod binaan',
|
||||
buildModeDynamic: 'Dinamik',
|
||||
buildModeStatic: 'Statik',
|
||||
buildStatus: 'Status binaan',
|
||||
compatibility: 'Keserasian',
|
||||
loadOrder: 'Turutan muatan',
|
||||
modulesToBuild: 'Modul dinamik (muatan semula panas selepas binaan, kontena tidak dimulakan semula)',
|
||||
staticModules: 'Modul statik',
|
||||
staticModulesHelper: 'Semasa membina, semua modul akan dibina semula dan OpenResty akan dimulakan semula',
|
||||
forceBuild: 'Bina semula tanpa cache',
|
||||
buildFailed: 'Binaan terakhir gagal',
|
||||
pending: 'Menunggu',
|
||||
ready: 'Sedia',
|
||||
failed: 'Gagal',
|
||||
unknown: 'Tidak diketahui',
|
||||
compatible: 'Serasi',
|
||||
stale: 'Perlu dibina semula',
|
||||
static: 'Binaan statik',
|
||||
dynamicUnsupported: 'Binaan dinamik tidak disokong pada versi OpenResty semasa',
|
||||
moduleDynamicUnsupported: 'Parameter modul ini tidak menyokong binaan dinamik',
|
||||
mirrorUrl: 'Sumber Perisian',
|
||||
paramsHelper: 'Contoh: --add-module=/tmp/ngx_brotli',
|
||||
paramsHelper: 'Contoh: --add-module=/tmp/ngx_brotli; mod dinamik menukarkannya secara automatik',
|
||||
packagesHelper: 'Contoh: git,curl dipisahkan oleh koma',
|
||||
scriptHelper:
|
||||
'Skrip yang dilaksanakan sebelum penyusunan, biasanya untuk memuat turun sumber kod modul, memasang kebergantungan, dll.',
|
||||
buildHelper:
|
||||
'Klik Bina selepas menambah/mengubah suai modul. Pembinaan yang berjaya akan memulakan semula OpenResty secara automatik.',
|
||||
'Modul dinamik dimuatkan melalui konfigurasi selepas dibina; modul statik tetap membina semula OpenResty.',
|
||||
defaultHttps: 'HTTPS Anti-tampering',
|
||||
defaultHttpsHelper1: 'Mengaktifkan ini dapat menyelesaikan masalah tampering HTTPS.',
|
||||
sslRejectHandshake: 'Tolak jabat tangan SSL lalai',
|
||||
|
||||
@@ -4008,14 +4008,36 @@ const message = {
|
||||
module: 'Módulo',
|
||||
build: 'Construir',
|
||||
buildWarn:
|
||||
'Construir OpenResty requer a reserva de certa quantidade de CPU e memória, e o processo pode ser demorado, por favor, seja paciente.',
|
||||
'A compilação local de módulos consome CPU e memória. Módulos estáticos também recompilam e reiniciam o OpenResty. Continuar?',
|
||||
buildPurposeHint:
|
||||
'Os módulos dinâmicos entram em vigor via hot reload após a compilação (sem reiniciar o contêiner); se houver módulos estáticos, a imagem será totalmente reconstruída e o contêiner recriado',
|
||||
buildMode: 'Modo de compilação',
|
||||
buildModeDynamic: 'Dinâmico',
|
||||
buildModeStatic: 'Estático',
|
||||
buildStatus: 'Status da compilação',
|
||||
compatibility: 'Compatibilidade',
|
||||
loadOrder: 'Ordem de carregamento',
|
||||
modulesToBuild: 'Módulos dinâmicos (hot reload após a compilação, sem reiniciar o contêiner)',
|
||||
staticModules: 'Módulos estáticos',
|
||||
staticModulesHelper: 'A compilação reconstruirá todos os módulos e reiniciará o OpenResty',
|
||||
forceBuild: 'Recompilar sem cache',
|
||||
buildFailed: 'A última compilação falhou',
|
||||
pending: 'Pendente',
|
||||
ready: 'Pronto',
|
||||
failed: 'Falhou',
|
||||
unknown: 'Desconhecido',
|
||||
compatible: 'Compatível',
|
||||
stale: 'Requer recompilação',
|
||||
static: 'Compilação estática',
|
||||
dynamicUnsupported: 'A compilação dinâmica não é suportada na versão atual do OpenResty',
|
||||
moduleDynamicUnsupported: 'Os parâmetros deste módulo não suportam compilação dinâmica',
|
||||
mirrorUrl: 'Fonte de Software',
|
||||
paramsHelper: 'Por exemplo: --add-module=/tmp/ngx_brotli',
|
||||
paramsHelper: 'Por exemplo: --add-module=/tmp/ngx_brotli; o modo dinâmico converte automaticamente',
|
||||
packagesHelper: 'Por exemplo: git,curl separados por vírgulas',
|
||||
scriptHelper:
|
||||
'Script a ser executado antes da compilação, geralmente para baixar o código-fonte do módulo, instalar dependências, etc.',
|
||||
buildHelper:
|
||||
'Clique em Construir após adicionar/modificar um módulo. Construção bem-sucedida reiniciará automaticamente o OpenResty.',
|
||||
'Módulos dinâmicos são carregados pela configuração após a compilação; módulos estáticos ainda recompilam o OpenResty.',
|
||||
defaultHttps: 'HTTPS Anti-tampering',
|
||||
defaultHttpsHelper1: 'A ativação desta opção pode resolver problemas de adulteração HTTPS.',
|
||||
sslRejectHandshake: 'Rejeitar handshake SSL padrão',
|
||||
|
||||
@@ -3860,14 +3860,36 @@ const message = {
|
||||
module: 'Модуль',
|
||||
build: 'Сборка',
|
||||
buildWarn:
|
||||
'Сборка OpenResty требует резервирования определенного количества CPU и памяти, процесс может занять много времени, пожалуйста, подождите.',
|
||||
'Локальная сборка модулей использует CPU и память. Статические модули также пересобирают и перезапускают OpenResty. Продолжить?',
|
||||
buildPurposeHint:
|
||||
'Динамические модули применяются горячей перезагрузкой после сборки (без перезапуска контейнера); при наличии статических модулей образ будет полностью пересобран, а контейнер пересоздан',
|
||||
buildMode: 'Режим сборки',
|
||||
buildModeDynamic: 'Динамическая',
|
||||
buildModeStatic: 'Статическая',
|
||||
buildStatus: 'Статус сборки',
|
||||
compatibility: 'Совместимость',
|
||||
loadOrder: 'Порядок загрузки',
|
||||
modulesToBuild: 'Динамические модули (горячая перезагрузка после сборки, без перезапуска контейнера)',
|
||||
staticModules: 'Статические модули',
|
||||
staticModulesHelper: 'При сборке все модули будут пересобраны, а OpenResty перезапущен',
|
||||
forceBuild: 'Пересобрать без кэша',
|
||||
buildFailed: 'Последняя сборка не удалась',
|
||||
pending: 'Ожидание',
|
||||
ready: 'Готово',
|
||||
failed: 'Ошибка',
|
||||
unknown: 'Неизвестно',
|
||||
compatible: 'Совместим',
|
||||
stale: 'Требуется пересборка',
|
||||
static: 'Статическая сборка',
|
||||
dynamicUnsupported: 'Динамическая сборка не поддерживается текущей версией OpenResty',
|
||||
moduleDynamicUnsupported: 'Параметры этого модуля не поддерживают динамическую сборку',
|
||||
mirrorUrl: 'Источник программного обеспечения',
|
||||
paramsHelper: 'Например: --add-module=/tmp/ngx_brotli',
|
||||
paramsHelper: 'Например: --add-module=/tmp/ngx_brotli; динамический режим преобразует автоматически',
|
||||
packagesHelper: 'Например: git,curl разделенные запятыми',
|
||||
scriptHelper:
|
||||
'Скрипт, выполняемый перед компиляцией, обычно для загрузки исходного кода модуля, установки зависимостей и т.д.',
|
||||
buildHelper:
|
||||
'Нажмите Сборка после добавления/изменения модуля. Успешная сборка автоматически перезапустит OpenResty.',
|
||||
'Динамические модули загружаются из конфигурации после сборки; статические модули по-прежнему пересобирают OpenResty.',
|
||||
defaultHttps: 'HTTPS Анти-вмешательство',
|
||||
defaultHttpsHelper1: 'Включение этого параметра может решить проблему вмешательства в HTTPS.',
|
||||
sslRejectHandshake: 'Отклонить стандартное SSL-рукопожатие',
|
||||
|
||||
@@ -3859,14 +3859,36 @@ const message = {
|
||||
module: 'Modüller',
|
||||
build: 'Oluştur',
|
||||
buildWarn:
|
||||
'OpenResty’nin oluşturulması belirli miktarda CPU ve bellek ayırmayı gerektirir, bu uzun sürebilir, lütfen sabırlı olun',
|
||||
'Yerel modül derlemeleri CPU ve bellek kullanır. Statik modüller OpenResty’yi yeniden derleyip yeniden başlatır. Devam edilsin mi?',
|
||||
buildPurposeHint:
|
||||
'Dinamik modüller derleme sonrası sıcak yeniden yükleme ile etkinleşir (konteyner yeniden başlatılmaz); statik modül varsa imaj tamamen yeniden derlenir ve konteyner yeniden oluşturulur',
|
||||
buildMode: 'Derleme modu',
|
||||
buildModeDynamic: 'Dinamik',
|
||||
buildModeStatic: 'Statik',
|
||||
buildStatus: 'Derleme durumu',
|
||||
compatibility: 'Uyumluluk',
|
||||
loadOrder: 'Yükleme sırası',
|
||||
modulesToBuild: 'Dinamik modüller (derleme sonrası sıcak yeniden yükleme, konteyner yeniden başlatılmaz)',
|
||||
staticModules: 'Statik modüller',
|
||||
staticModulesHelper: 'Derleme sırasında tüm modüller yeniden derlenir ve OpenResty yeniden başlatılır',
|
||||
forceBuild: 'Önbelleksiz yeniden derle',
|
||||
buildFailed: 'Son derleme başarısız',
|
||||
pending: 'Beklemede',
|
||||
ready: 'Hazır',
|
||||
failed: 'Başarısız',
|
||||
unknown: 'Bilinmiyor',
|
||||
compatible: 'Uyumlu',
|
||||
stale: 'Yeniden derleme gerekli',
|
||||
static: 'Statik derleme',
|
||||
dynamicUnsupported: 'Mevcut OpenResty sürümünde dinamik derleme desteklenmiyor',
|
||||
moduleDynamicUnsupported: 'Bu modülün parametreleri dinamik derlemeyi desteklemiyor',
|
||||
mirrorUrl: 'Yazılım Kaynağı',
|
||||
paramsHelper: 'Örnek: --add-module=/tmp/ngx_brotli',
|
||||
paramsHelper: 'Örnek: --add-module=/tmp/ngx_brotli; dinamik mod otomatik olarak dönüştürür',
|
||||
packagesHelper: 'Örnek: git, curl (virgülle ayrılmış)',
|
||||
scriptHelper:
|
||||
'Derlemeden önce çalıştırılacak betikler, genellikle modül kaynak kodunu indirmek, bağımlılıkları kurmak vb. için',
|
||||
buildHelper:
|
||||
'Modül ekledikten/düzenledikten sonra oluştur’a tıklayın. OpenResty, başarılı oluşturma üzerine otomatik olarak yeniden başlatılacaktır.',
|
||||
'Dinamik modüller derlemeden sonra yapılandırmadan yüklenir; statik modüller yine OpenResty’yi yeniden derler.',
|
||||
defaultHttps: 'HTTPS Anti-sızdırma',
|
||||
defaultHttpsHelper1: 'Bu özelliği etkinleştirerek HTTPS sızdırma sorunlarını çözebilirsiniz.',
|
||||
sslRejectHandshake: 'Varsayılan SSL el sıkışmasını reddet',
|
||||
|
||||
@@ -3526,12 +3526,33 @@ const message = {
|
||||
script: '腳本',
|
||||
module: '模組',
|
||||
build: '建構',
|
||||
buildWarn: '建構 OpenResty 需要預留一定的 CPU 和記憶體,時間較長,請耐心等待',
|
||||
buildWarn: '本地建構模組需要佔用一定的 CPU 和記憶體,靜態模組還會重建並重啟 OpenResty,是否繼續?',
|
||||
buildPurposeHint: '動態模組構建後熱載入生效(容器不重新啟動);包含靜態模組時將全量重建映像檔並重建容器',
|
||||
buildMode: '構建方式',
|
||||
buildModeDynamic: '動態模組',
|
||||
buildModeStatic: '靜態模組',
|
||||
buildStatus: '構建狀態',
|
||||
compatibility: '相容性',
|
||||
loadOrder: '載入順序',
|
||||
modulesToBuild: '動態模組(構建後熱載入,容器不重新啟動)',
|
||||
staticModules: '靜態模組',
|
||||
staticModulesHelper: '構建時將全量重建並重新啟動 OpenResty',
|
||||
forceBuild: '忽略快取重新構建',
|
||||
buildFailed: '上次構建失敗',
|
||||
pending: '待構建',
|
||||
ready: '已構建',
|
||||
failed: '構建失敗',
|
||||
unknown: '未知',
|
||||
compatible: '相容',
|
||||
stale: '需要重新構建',
|
||||
static: '靜態編譯',
|
||||
dynamicUnsupported: '目前 OpenResty 版本不支援動態構建',
|
||||
moduleDynamicUnsupported: '此模組參數不支援動態構建',
|
||||
mirrorUrl: '軟體源',
|
||||
paramsHelper: '例:--add-module=/tmp/ngx_brotli',
|
||||
paramsHelper: '例:--add-module=/tmp/ngx_brotli,動態模式會自動轉換參數',
|
||||
packagesHelper: '例:git,curl 以逗號分割',
|
||||
scriptHelper: '編譯之前執行的腳本,通常用於下載模組原始碼,安裝依賴等',
|
||||
buildHelper: '新增/修改模組後點選構建,構建成功後會自動重新啟動 OpenResty',
|
||||
buildHelper: '動態模組建構後透過設定載入,靜態模組仍會重建 OpenResty。',
|
||||
defaultHttps: 'HTTPS 防竄站',
|
||||
defaultHttpsHelper1: '開啟後可以解決 HTTPS 竄站問題',
|
||||
sslRejectHandshake: '拒絕預設 SSL 握手',
|
||||
|
||||
@@ -3525,12 +3525,33 @@ const message = {
|
||||
script: '脚本',
|
||||
module: '模块',
|
||||
build: '构建',
|
||||
buildWarn: '构建 OpenResty 需要预留一定的 CPU 和内存,时间较长,请耐心等待',
|
||||
buildWarn: '本地构建模块需要占用一定的 CPU 和内存,静态模块还会重建并重启 OpenResty,是否继续?',
|
||||
buildPurposeHint: '动态模块构建后热加载生效(容器不重启);包含静态模块时将全量重建镜像并重建容器',
|
||||
buildMode: '构建方式',
|
||||
buildModeDynamic: '动态模块',
|
||||
buildModeStatic: '静态模块',
|
||||
buildStatus: '构建状态',
|
||||
compatibility: '兼容性',
|
||||
loadOrder: '加载顺序',
|
||||
modulesToBuild: '动态模块(构建后热加载,容器不重启)',
|
||||
staticModules: '静态模块',
|
||||
staticModulesHelper: '构建时将全量重建并重启 OpenResty',
|
||||
forceBuild: '忽略缓存重新构建',
|
||||
buildFailed: '上次构建失败',
|
||||
pending: '待构建',
|
||||
ready: '已构建',
|
||||
failed: '构建失败',
|
||||
unknown: '未知',
|
||||
compatible: '兼容',
|
||||
stale: '需要重新构建',
|
||||
static: '静态编译',
|
||||
dynamicUnsupported: '当前 OpenResty 版本不支持动态构建',
|
||||
moduleDynamicUnsupported: '该模块参数不支持动态构建',
|
||||
mirrorUrl: '软件源',
|
||||
paramsHelper: '例如:--add-module=/tmp/ngx_brotli',
|
||||
paramsHelper: '例如:--add-module=/tmp/ngx_brotli,动态模式会自动转换参数',
|
||||
packagesHelper: '例如:git,curl 按,分割',
|
||||
scriptHelper: '编译之前执行的脚本,一般为下载模块源码,安装依赖等',
|
||||
buildHelper: '添加/修改模块之后点击构建,构建成功后会自动重启 OpenResty',
|
||||
buildHelper: '动态模块构建后通过配置加载;静态模块仍需重新构建 OpenResty',
|
||||
defaultHttps: 'HTTPS 防窜站',
|
||||
defaultHttpsHelper1: '开启后可以解决 HTTPS 窜站问题',
|
||||
sslRejectHandshake: '拒绝默认 SSL 握手',
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<DrawerPro v-model="open" :header="$t('nginx.build')" size="normal" @close="handleClose">
|
||||
<el-form ref="buildForm" label-position="top" :model="build" :rules="rules">
|
||||
<el-form-item>
|
||||
<el-text type="info">{{ $t('nginx.buildPurposeHint') }}</el-text>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('nginx.mirrorUrl')" prop="mirror">
|
||||
<el-select v-model="build.mirror">
|
||||
<el-option
|
||||
@@ -21,6 +24,27 @@
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="moduleOptions.length > 0" :label="$t('nginx.modulesToBuild')" prop="modules">
|
||||
<el-checkbox-group v-model="build.modules">
|
||||
<el-checkbox v-for="item in moduleOptions" :key="item.name" :value="item.name">
|
||||
{{ item.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!dynamicSupported">
|
||||
<el-text type="warning">{{ $t('nginx.dynamicUnsupported') }}</el-text>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="staticModules.length > 0" :label="$t('nginx.staticModules')">
|
||||
<div>
|
||||
<div v-for="item in staticModules" :key="item.name" class="!mb-1">
|
||||
<el-text>{{ item.name }}</el-text>
|
||||
</div>
|
||||
<el-text type="info" size="small">{{ $t('nginx.staticModulesHelper') }}</el-text>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('nginx.forceBuild')">
|
||||
<el-switch v-model="build.force" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
|
||||
@@ -32,23 +56,30 @@
|
||||
<TaskLog ref="taskLogRef" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { computed, ref } from 'vue';
|
||||
import { ElMessageBox, FormInstance } from 'element-plus';
|
||||
import { getNginxModules, buildNginx } from '@/api/modules/nginx';
|
||||
import i18n from '@/lang';
|
||||
import { newUUID } from '@/utils/id';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { Nginx } from '@/api/interface/nginx';
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const buildForm = ref<FormInstance>();
|
||||
const build = ref({
|
||||
mirror: 'http://archive.ubuntu.com/ubuntu',
|
||||
modules: [] as string[],
|
||||
force: false,
|
||||
});
|
||||
const rules = {
|
||||
const moduleOptions = ref<Nginx.NginxModule[]>([]);
|
||||
const staticModules = ref<Nginx.NginxModule[]>([]);
|
||||
const dynamicSupported = ref(true);
|
||||
const rules = computed(() => ({
|
||||
mirror: [Rules.requiredSelect],
|
||||
};
|
||||
modules: staticModules.value.length === 0 ? [Rules.requiredSelect] : [],
|
||||
}));
|
||||
const taskLogRef = ref();
|
||||
|
||||
const acceptParams = async () => {
|
||||
@@ -60,26 +91,34 @@ const getModules = async () => {
|
||||
try {
|
||||
const res = await getNginxModules();
|
||||
build.value.mirror = res.data.mirror;
|
||||
dynamicSupported.value = res.data.dynamicSupported;
|
||||
moduleOptions.value = res.data.dynamicSupported
|
||||
? res.data.modules.filter((item) => item.enable && item.buildMode === 'dynamic')
|
||||
: [];
|
||||
staticModules.value = res.data.modules.filter((item) => item.enable && item.buildMode === 'static');
|
||||
build.value.modules = moduleOptions.value.map((item) => item.name);
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const submit = async (form: FormInstance) => {
|
||||
await form.validate();
|
||||
if (form.validate()) {
|
||||
ElMessageBox.confirm(i18n.global.t('nginx.buildWarn'), i18n.global.t('nginx.build'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
}).then(async () => {
|
||||
const taskID = newUUID();
|
||||
try {
|
||||
await buildNginx({
|
||||
taskID: taskID,
|
||||
mirror: build.value.mirror,
|
||||
});
|
||||
handleClose();
|
||||
openTaskLog(taskID);
|
||||
} catch (error) {}
|
||||
await ElMessageBox.confirm(i18n.global.t('nginx.buildWarn'), i18n.global.t('nginx.build'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
});
|
||||
const taskID = newUUID();
|
||||
loading.value = true;
|
||||
try {
|
||||
await buildNginx({
|
||||
taskID: taskID,
|
||||
mirror: build.value.mirror,
|
||||
modules: build.value.modules,
|
||||
force: build.value.force,
|
||||
});
|
||||
handleClose();
|
||||
openTaskLog(taskID);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -11,10 +11,31 @@
|
||||
<el-text type="warning" class="!ml-2">{{ $t('nginx.buildHelper') }}</el-text>
|
||||
</template>
|
||||
<el-table-column prop="name" :label="$t('commons.table.name')" />
|
||||
<el-table-column prop="params" :label="$t('nginx.params')" />
|
||||
<el-table-column :label="$t('nginx.buildMode')" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag effect="plain" :type="row.buildMode === 'static' ? 'warning' : 'primary'">
|
||||
{{ $t('nginx.buildMode' + capitalize(displayBuildMode(row.buildMode))) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('nginx.buildStatus')" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.lastError" :content="row.lastError" placement="top">
|
||||
<el-tag :type="statusType(row.buildStatus)">{{ $t('nginx.' + row.buildStatus) }}</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag v-else :type="statusType(row.buildStatus)">{{ $t('nginx.' + row.buildStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('nginx.compatibility')" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag effect="plain" :type="compatibilityType(row.compatibility)">
|
||||
{{ $t('nginx.' + row.compatibility) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.status')" fix>
|
||||
<template #default="{ row }">
|
||||
<el-switch v-permission v-model="row.enable" @click="updateModule(row)" />
|
||||
<el-switch v-permission v-model="row.enable" @change="updateModule(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<fu-table-operations
|
||||
@@ -38,8 +59,9 @@ import { Nginx } from '@/api/interface/nginx';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import Operate from './operate/index.vue';
|
||||
import Build from './build/index.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const data = ref([]);
|
||||
const data = ref<Nginx.NginxModule[]>([]);
|
||||
const loading = ref(false);
|
||||
const buttons = [
|
||||
{
|
||||
@@ -60,6 +82,7 @@ const buttons = [
|
||||
const operateRef = ref();
|
||||
const deleteRef = ref();
|
||||
const buildRef = ref();
|
||||
const dynamicSupported = ref(true);
|
||||
|
||||
const buildNginx = async () => {
|
||||
buildRef.value.acceptParams();
|
||||
@@ -70,6 +93,7 @@ const search = () => {
|
||||
getNginxModules()
|
||||
.then((res) => {
|
||||
data.value = res.data.modules;
|
||||
dynamicSupported.value = res.data.dynamicSupported;
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
@@ -77,11 +101,11 @@ const search = () => {
|
||||
};
|
||||
|
||||
const openOperate = () => {
|
||||
operateRef.value.acceptParams('create');
|
||||
operateRef.value.acceptParams('create', undefined, dynamicSupported.value);
|
||||
};
|
||||
|
||||
const openEdit = (row: Nginx.NginxModule) => {
|
||||
operateRef.value.acceptParams('update', row);
|
||||
operateRef.value.acceptParams('update', row, dynamicSupported.value);
|
||||
};
|
||||
|
||||
const updateModule = (row: Nginx.NginxModule) => {
|
||||
@@ -94,11 +118,32 @@ const updateModule = (row: Nginx.NginxModule) => {
|
||||
.then(() => {
|
||||
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
row.enable = !row.enable;
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
search();
|
||||
});
|
||||
};
|
||||
|
||||
const capitalize = (value: string) => value.charAt(0).toUpperCase() + value.slice(1);
|
||||
|
||||
const displayBuildMode = (mode: string) => (mode === 'auto' ? 'dynamic' : mode);
|
||||
|
||||
const statusType = (status: string) => {
|
||||
if (status === 'ready') return 'success';
|
||||
if (status === 'failed') return 'danger';
|
||||
return 'info';
|
||||
};
|
||||
|
||||
const compatibilityType = (status: string) => {
|
||||
if (status === 'compatible') return 'success';
|
||||
if (status === 'stale') return 'warning';
|
||||
if (status === 'static') return 'info';
|
||||
return 'info';
|
||||
};
|
||||
|
||||
const deleteModule = async (row: Nginx.NginxModule) => {
|
||||
const data = {
|
||||
name: row.name,
|
||||
|
||||
@@ -10,6 +10,23 @@
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
<el-input v-model.trim="module.name" :disabled="mode === 'update'"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('nginx.buildMode')" prop="buildMode">
|
||||
<el-radio-group v-model="module.buildMode">
|
||||
<el-radio-button
|
||||
value="dynamic"
|
||||
:disabled="!dynamicSupported || module.dynamicSupport === 'unsupported'"
|
||||
>
|
||||
{{ $t('nginx.buildModeDynamic') }}
|
||||
</el-radio-button>
|
||||
<el-radio-button value="static">{{ $t('nginx.buildModeStatic') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-text v-if="!dynamicSupported" type="warning" class="!ml-2">
|
||||
{{ $t('nginx.dynamicUnsupported') }}
|
||||
</el-text>
|
||||
<el-text v-else-if="module.dynamicSupport === 'unsupported'" type="warning" class="!ml-2">
|
||||
{{ $t('nginx.moduleDynamicUnsupported') }}
|
||||
</el-text>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('nginx.params')" prop="params">
|
||||
<el-input v-model.trim="module.params" :placeholder="$t('nginx.paramsHelper')"></el-input>
|
||||
</el-form-item>
|
||||
@@ -24,6 +41,18 @@
|
||||
:placeholder="$t('nginx.scriptHelper')"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="module.buildMode !== 'static'" :label="$t('nginx.loadOrder')" prop="loadOrder">
|
||||
<el-input-number v-model="module.loadOrder" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
v-if="module.lastError"
|
||||
class="!mb-4"
|
||||
type="error"
|
||||
:title="$t('nginx.buildFailed')"
|
||||
:description="module.lastError"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
|
||||
@@ -41,23 +70,45 @@ import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const moduleForm = ref<FormInstance>();
|
||||
const open = ref(false);
|
||||
const em = defineEmits(['close']);
|
||||
const mode = ref('create');
|
||||
const loading = ref(false);
|
||||
const module = ref({
|
||||
const dynamicSupported = ref(true);
|
||||
type ModuleForm = {
|
||||
name: string;
|
||||
operate: string;
|
||||
script: string;
|
||||
enable: boolean;
|
||||
params: string;
|
||||
packages: string;
|
||||
buildMode: Nginx.NginxModule['buildMode'];
|
||||
provider: Nginx.NginxModule['provider'];
|
||||
dynamicSupport: Nginx.NginxModule['dynamicSupport'];
|
||||
loadOrder: number;
|
||||
lastError: string;
|
||||
};
|
||||
const defaultModule = (): ModuleForm => ({
|
||||
name: '',
|
||||
operate: 'create',
|
||||
script: '',
|
||||
enable: true,
|
||||
params: '',
|
||||
packages: '',
|
||||
buildMode: 'dynamic',
|
||||
provider: 'local',
|
||||
dynamicSupport: 'unknown',
|
||||
loadOrder: 50,
|
||||
lastError: '',
|
||||
});
|
||||
const module = ref(defaultModule());
|
||||
const rules = ref({
|
||||
name: [Rules.requiredInput, Rules.simpleName],
|
||||
params: [Rules.requiredInput],
|
||||
buildMode: [Rules.requiredSelect],
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -65,15 +116,22 @@ const handleClose = () => {
|
||||
em('close', false);
|
||||
};
|
||||
|
||||
const acceptParams = async (operate: string, editModule: Nginx.NginxModule) => {
|
||||
const acceptParams = async (operate: string, editModule?: Nginx.NginxModule, supported?: boolean) => {
|
||||
mode.value = operate;
|
||||
if (operate === 'update') {
|
||||
dynamicSupported.value = supported ?? true;
|
||||
module.value = defaultModule();
|
||||
if (operate === 'update' && editModule) {
|
||||
module.value = {
|
||||
name: editModule.name,
|
||||
script: editModule.script,
|
||||
script: editModule.script || '',
|
||||
enable: editModule.enable,
|
||||
params: editModule.params,
|
||||
packages: editModule.packages,
|
||||
packages: editModule.packages || '',
|
||||
buildMode: editModule.buildMode === 'auto' ? 'dynamic' : editModule.buildMode,
|
||||
provider: editModule.provider,
|
||||
dynamicSupport: editModule.dynamicSupport,
|
||||
loadOrder: editModule.loadOrder,
|
||||
lastError: editModule.lastError || '',
|
||||
operate: 'update',
|
||||
};
|
||||
}
|
||||
@@ -82,24 +140,21 @@ const acceptParams = async (operate: string, editModule: Nginx.NginxModule) => {
|
||||
|
||||
const submit = async (form: FormInstance) => {
|
||||
await form.validate();
|
||||
if (form.validate()) {
|
||||
loading.value = true;
|
||||
const data = {
|
||||
...module.value,
|
||||
operate: mode.value,
|
||||
};
|
||||
updateNginxModule(data)
|
||||
.then(() => {
|
||||
if (mode.value === 'update') {
|
||||
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
|
||||
} else if (mode.value === 'create') {
|
||||
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
|
||||
}
|
||||
handleClose();
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
loading.value = true;
|
||||
const data = {
|
||||
...module.value,
|
||||
operate: mode.value,
|
||||
};
|
||||
try {
|
||||
await updateNginxModule(data);
|
||||
if (mode.value === 'update') {
|
||||
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
|
||||
} else if (mode.value === 'create') {
|
||||
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
|
||||
}
|
||||
handleClose();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
171
scripts/openresty-modules/README.md
Normal file
171
scripts/openresty-modules/README.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# OpenResty Dynamic Module Linux Tests
|
||||
|
||||
These scripts test the local dynamic-module build path and collect diagnostics
|
||||
from an installed 1Panel OpenResty instance. Run them on a disposable Linux
|
||||
host with Docker access before testing on a production installation.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Bash 4.3 or newer
|
||||
- Docker Engine with the Compose v2 plugin
|
||||
- `jq`, `python3`, `file`, `binutils`, `tar`, and GNU coreutils
|
||||
- Internet access for runtime images and Ubuntu build packages
|
||||
- Go, only when `--source-checks` is used
|
||||
|
||||
On Debian or Ubuntu:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq python3 file binutils tar
|
||||
```
|
||||
|
||||
Make the scripts executable:
|
||||
|
||||
```bash
|
||||
chmod +x scripts/openresty-modules/*.sh
|
||||
```
|
||||
|
||||
## Builder Test
|
||||
|
||||
Start with one version and one small module:
|
||||
|
||||
```bash
|
||||
./scripts/openresty-modules/test-builder.sh \
|
||||
--appstore ../appstore \
|
||||
--versions 1.31.1.1-0-noble \
|
||||
--modules ngx_brotli \
|
||||
--source-checks
|
||||
```
|
||||
|
||||
Test every catalog module against all refactored OpenResty versions:
|
||||
|
||||
```bash
|
||||
./scripts/openresty-modules/test-builder.sh --appstore ../appstore
|
||||
```
|
||||
|
||||
Bypass Docker's module build cache when reproducing a compiler problem:
|
||||
|
||||
```bash
|
||||
./scripts/openresty-modules/test-builder.sh \
|
||||
--appstore ../appstore \
|
||||
--versions 1.31.1.1-0-noble \
|
||||
--modules geoip2 \
|
||||
--no-cache \
|
||||
--keep-context \
|
||||
--keep-docker
|
||||
```
|
||||
|
||||
The builder test performs these phases for every selected version:
|
||||
|
||||
1. Validate appstore JSON, shell scripts, Compose mounts, and Nginx include.
|
||||
2. Pull and identify the exact target runtime image.
|
||||
3. Convert catalog options to dynamic configure options.
|
||||
4. Build every module with `Dockerfile.modules` and copy `/out` locally.
|
||||
5. Record SHA-256, ELF metadata, compiler output, and runtime dependencies.
|
||||
6. Validate individual modules for debugging. Individual failures are warnings
|
||||
by default because modules may depend on an earlier module.
|
||||
7. Validate all modules together in catalog `loadOrder`.
|
||||
8. Start an isolated OpenResty master, add module configs, and hot reload.
|
||||
9. Inject a missing module, prove `nginx -t` rejects it, restore the config,
|
||||
and prove the running process remains healthy.
|
||||
|
||||
Use `--strict-individual` when every selected module is expected to load alone.
|
||||
|
||||
Use `--mirror URL` (environment variable `MIRROR`) to pass an apt mirror as
|
||||
`CONTAINER_PACKAGE_URL` to module builds, matching the 1Panel module build.
|
||||
|
||||
Results are written to:
|
||||
|
||||
```text
|
||||
openresty-module-test-results/<run-id>/
|
||||
```
|
||||
|
||||
Important files:
|
||||
|
||||
- `summary.tsv`: result per OpenResty version
|
||||
- `work/<version>/logs/build-*.log`: complete BuildKit output
|
||||
- `work/<version>/logs/load-combined.log`: authoritative ABI/load-order test
|
||||
- `work/<version>/artifacts.tsv`: module paths, checksums, and sizes
|
||||
- `work/<version>/image-inspect.json`: exact target image identity
|
||||
- `work/<version>/runtime/`: reload and rollback test logs
|
||||
- `<result-dir>.tar.gz`: automatically created when an unexpected failure occurs
|
||||
|
||||
## Installed Instance Diagnostics
|
||||
|
||||
Find the OpenResty installation directory first. A common path is similar to:
|
||||
|
||||
```text
|
||||
/opt/1panel/apps/openresty/openresty
|
||||
```
|
||||
|
||||
Run the diagnostic collector:
|
||||
|
||||
```bash
|
||||
./scripts/openresty-modules/diagnose-install.sh \
|
||||
/opt/1panel/apps/openresty/openresty
|
||||
```
|
||||
|
||||
Override container discovery when needed:
|
||||
|
||||
```bash
|
||||
./scripts/openresty-modules/diagnose-install.sh \
|
||||
/opt/1panel/apps/openresty/openresty \
|
||||
--container 1Panel-openresty
|
||||
```
|
||||
|
||||
The collector checks:
|
||||
|
||||
- `module.json` artifact paths and SHA-256 checksums
|
||||
- managed `load_module` files and host/container path mapping
|
||||
- read-only Compose mounts
|
||||
- current container image ID versus enabled module target image IDs
|
||||
- container state, Nginx build options, `nginx -t`, loaded module directives,
|
||||
module checksums inside the container, `ldd`, and recent logs
|
||||
|
||||
The default report does not retain full `nginx -T` output. Use
|
||||
`--full-config` only on a test host because the resulting archive may contain
|
||||
credentials or private site configuration.
|
||||
|
||||
Module scripts are redacted from the copied state files by default. Container
|
||||
logs and error strings can still contain site names, URLs, or command output;
|
||||
review an archive before sharing it outside your team.
|
||||
|
||||
## Final Manual Matrix
|
||||
|
||||
Run this matrix through the 1Panel UI on a disposable installation. Collect a
|
||||
diagnostic archive after each important transition.
|
||||
|
||||
1. Install the oldest selected OpenResty version with every module disabled.
|
||||
2. Switch one module to `auto`, enable it, and build it locally.
|
||||
3. Confirm `buildStatus=ready`, `compatibility=compatible`, and `nginx -t`.
|
||||
4. Force rebuild it. Confirm the artifact path changes and the old config is
|
||||
replaced only after the new artifact passes validation.
|
||||
5. Enable all catalog modules and verify catalog load order with the builder
|
||||
test and the installed-instance collector.
|
||||
6. On the test host, make one module script return a failure. Confirm the old
|
||||
managed config and old ready artifact remain active.
|
||||
7. Restore the module definition and rebuild successfully.
|
||||
8. Upgrade OpenResty. Confirm every enabled dynamic module has a ready build
|
||||
whose target image ID matches the new running container.
|
||||
9. Restart the container and host. Run the diagnostic collector again to prove
|
||||
the persisted mounts and configs remain valid.
|
||||
10. Switch a module to `static`, rebuild, then switch it back to `dynamic` and
|
||||
verify that all enabled dynamic modules are regenerated for the new image.
|
||||
|
||||
## Failure Triage
|
||||
|
||||
| Symptom | First evidence to inspect |
|
||||
| --- | --- |
|
||||
| Docker build fails | `logs/build-<module>.log`, `inputs/<module>/` |
|
||||
| `.so` missing | build log and the `module-output` stage `/out` checks |
|
||||
| Individual load fails, combined passes | module dependency and `loadOrder` |
|
||||
| Combined load fails | ABI mismatch, duplicate module, missing shared library |
|
||||
| `ldd` shows `not found` | bundled `lib/`, RPATH, or future runtime packages |
|
||||
| Checksum mismatch | interrupted copy, manual modification, stale state file |
|
||||
| Target image mismatch | module was not rebuilt after image upgrade/rebuild |
|
||||
| Builder passes, installed `nginx -t` fails | Compose mounts or managed config |
|
||||
| Reload fails but old process runs | inspect rollback logs and old config snapshot |
|
||||
|
||||
Do not edit generated module state or managed config files while a 1Panel app
|
||||
task is running. Preserve the result directory and archive before retrying a
|
||||
failed build.
|
||||
475
scripts/openresty-modules/diagnose-install.sh
Normal file
475
scripts/openresty-modules/diagnose-install.sh
Normal file
@@ -0,0 +1,475 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-$$"
|
||||
INSTALL_DIR=""
|
||||
OUTPUT_DIR="${OUTPUT_DIR:-${PWD}/openresty-module-diagnostics/${RUN_ID}}"
|
||||
CONTAINER=""
|
||||
FULL_CONFIG=0
|
||||
CREATE_ARCHIVE=1
|
||||
NGINX_TEST=1
|
||||
UNEXPECTED_FAILURE=""
|
||||
declare -a FAILED_CHECKS=()
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: diagnose-install.sh INSTALL_DIR [options]
|
||||
|
||||
Collect a mostly read-only diagnostic report for an installed 1Panel OpenResty.
|
||||
The only container command with behavior is `nginx -t`; no reload is performed.
|
||||
|
||||
Options:
|
||||
--output PATH Result directory
|
||||
--container NAME Override the container discovered from Docker Compose
|
||||
--full-config Retain full `nginx -T` output (may contain sensitive data)
|
||||
--no-nginx-test Do not execute nginx -t/-T in the running container
|
||||
--no-archive Do not create a .tar.gz report
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*" | tee -a "${OUTPUT_DIR}/run.log"
|
||||
}
|
||||
|
||||
mark_failed() {
|
||||
FAILED_CHECKS+=("$1")
|
||||
log "CHECK FAILED: $1"
|
||||
}
|
||||
|
||||
mark_passed() {
|
||||
log "CHECK PASSED: $1"
|
||||
}
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
printf 'Required command not found: %s\n' "$1" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
on_error() {
|
||||
local status="$1" line="$2" command="$3"
|
||||
UNEXPECTED_FAILURE="line ${line}: ${command} (exit ${status})"
|
||||
return "${status}"
|
||||
}
|
||||
|
||||
finalize() {
|
||||
local status=$?
|
||||
set +e
|
||||
if [[ -n "${UNEXPECTED_FAILURE}" ]]; then
|
||||
printf '%s\n' "${UNEXPECTED_FAILURE}" >"${OUTPUT_DIR}/unexpected-failure.txt"
|
||||
fi
|
||||
{
|
||||
printf 'install_dir=%s\n' "${INSTALL_DIR}"
|
||||
printf 'container=%s\n' "${CONTAINER}"
|
||||
printf 'failed_checks=%s\n' "${#FAILED_CHECKS[@]}"
|
||||
local check
|
||||
for check in "${FAILED_CHECKS[@]:-}"; do
|
||||
[[ -n "${check}" ]] && printf 'failure=%s\n' "${check}"
|
||||
done
|
||||
} >"${OUTPUT_DIR}/summary.txt"
|
||||
|
||||
if [[ "${CREATE_ARCHIVE}" -eq 1 ]]; then
|
||||
local archive="${OUTPUT_DIR%/}.tar.gz"
|
||||
tar -czf "${archive}" -C "$(dirname -- "${OUTPUT_DIR}")" "$(basename -- "${OUTPUT_DIR}")" 2>/dev/null || true
|
||||
printf 'Diagnostic archive: %s\n' "${archive}"
|
||||
fi
|
||||
printf 'Diagnostic directory: %s\n' "${OUTPUT_DIR}"
|
||||
|
||||
if [[ "${status}" -eq 0 && "${#FAILED_CHECKS[@]}" -gt 0 ]]; then
|
||||
status=1
|
||||
fi
|
||||
exit "${status}"
|
||||
}
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
INSTALL_DIR="$1"
|
||||
shift
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--container)
|
||||
CONTAINER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--full-config)
|
||||
FULL_CONFIG=1
|
||||
shift
|
||||
;;
|
||||
--no-nginx-test)
|
||||
NGINX_TEST=0
|
||||
shift
|
||||
;;
|
||||
--no-archive)
|
||||
CREATE_ARCHIVE=0
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf 'Unknown option: %s\n' "$1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -d "${INSTALL_DIR}" ]] || {
|
||||
printf 'Install directory not found: %s\n' "${INSTALL_DIR}" >&2
|
||||
exit 2
|
||||
}
|
||||
INSTALL_DIR="$(cd -- "${INSTALL_DIR}" && pwd -P)"
|
||||
if [[ -d "${OUTPUT_DIR}" ]] && find "${OUTPUT_DIR}" -mindepth 1 -print -quit | grep -q .; then
|
||||
printf 'Output directory must be empty: %s\n' "${OUTPUT_DIR}" >&2
|
||||
exit 2
|
||||
fi
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
OUTPUT_DIR="$(cd -- "${OUTPUT_DIR}" && pwd -P)"
|
||||
|
||||
trap 'on_error "$?" "$LINENO" "$BASH_COMMAND"' ERR
|
||||
trap finalize EXIT
|
||||
|
||||
preflight() {
|
||||
[[ "$(uname -s)" == "Linux" ]] || {
|
||||
printf 'This diagnostic script must run on Linux.\n' >&2
|
||||
exit 2
|
||||
}
|
||||
require_command docker
|
||||
require_command jq
|
||||
require_command python3
|
||||
require_command sha256sum
|
||||
require_command tar
|
||||
|
||||
docker version >"${OUTPUT_DIR}/docker-version.txt" 2>&1
|
||||
docker info >"${OUTPUT_DIR}/docker-info.txt" 2>&1
|
||||
uname -a >"${OUTPUT_DIR}/uname.txt"
|
||||
cp /etc/os-release "${OUTPUT_DIR}/os-release.txt" 2>/dev/null || true
|
||||
df -h >"${OUTPUT_DIR}/disk-free.txt"
|
||||
free -h >"${OUTPUT_DIR}/memory.txt" 2>&1 || true
|
||||
log "Inspecting ${INSTALL_DIR}"
|
||||
}
|
||||
|
||||
collect_filesystem_state() {
|
||||
[[ -d "${INSTALL_DIR}/modules" ]] || mark_failed "module artifact directory is missing"
|
||||
[[ -d "${INSTALL_DIR}/conf/modules-enabled" ]] || mark_failed "managed module config directory is missing"
|
||||
find "${INSTALL_DIR}/modules" -maxdepth 5 -printf '%M\t%u:%g\t%s\t%TY-%Tm-%TdT%TH:%TM:%TS\t%p\n' \
|
||||
>"${OUTPUT_DIR}/module-files.txt" 2>&1 || true
|
||||
find "${INSTALL_DIR}/conf/modules-enabled" -maxdepth 1 -type f -printf '%f\n' \
|
||||
>"${OUTPUT_DIR}/managed-config-files.txt" 2>&1 || true
|
||||
grep -RnsE '^[[:space:]]*load_module[[:space:]]+' "${INSTALL_DIR}/conf/modules-enabled" \
|
||||
>"${OUTPUT_DIR}/load-module-directives.txt" 2>&1 || true
|
||||
grep -E '^(RESTY_|CONTAINER_NAME=|PANEL_APP_PORT_HTTP=)' "${INSTALL_DIR}/.env" \
|
||||
>"${OUTPUT_DIR}/relevant-env.txt" 2>/dev/null || true
|
||||
|
||||
if [[ -f "${INSTALL_DIR}/build/module.json" ]]; then
|
||||
jq 'map(if has("script") then .script = "<redacted>" else . end)' \
|
||||
"${INSTALL_DIR}/build/module.json" >"${OUTPUT_DIR}/module-state.json"
|
||||
else
|
||||
mark_failed "module state file is missing"
|
||||
fi
|
||||
if [[ -f "${INSTALL_DIR}/build/module.catalog.json" ]]; then
|
||||
jq 'map(if has("script") then .script = "<redacted>" else . end)' \
|
||||
"${INSTALL_DIR}/build/module.catalog.json" >"${OUTPUT_DIR}/module-catalog.json"
|
||||
fi
|
||||
}
|
||||
|
||||
validate_artifacts() {
|
||||
local state="${INSTALL_DIR}/build/module.json"
|
||||
[[ -f "${state}" ]] || return 0
|
||||
if python3 - "${state}" "${INSTALL_DIR}/modules" >"${OUTPUT_DIR}/artifact-validation.tsv" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
state_path = pathlib.Path(sys.argv[1])
|
||||
modules_root = pathlib.Path(sys.argv[2]).resolve()
|
||||
modules = json.loads(state_path.read_text(encoding="utf-8"))
|
||||
failed = False
|
||||
print("module\tbuild_status\ttarget_key\tartifact\texpected\tactual\tresult")
|
||||
for module in modules:
|
||||
for build in module.get("builds") or []:
|
||||
target_key = (build.get("target") or {}).get("key", "")
|
||||
for artifact in build.get("artifacts") or []:
|
||||
relative = artifact.get("path", "")
|
||||
expected = artifact.get("checksum", "")
|
||||
result = "OK"
|
||||
actual = ""
|
||||
try:
|
||||
pure = pathlib.PurePosixPath(relative)
|
||||
if not relative or pure.is_absolute() or ".." in pure.parts or "\\" in relative:
|
||||
raise ValueError("unsafe-path")
|
||||
candidate = modules_root / pathlib.Path(*pure.parts)
|
||||
if candidate.is_symlink():
|
||||
raise ValueError("symlink-not-allowed")
|
||||
full_path = candidate.resolve(strict=True)
|
||||
if modules_root not in full_path.parents:
|
||||
raise ValueError("outside-module-root")
|
||||
if not full_path.is_file():
|
||||
raise ValueError("not-regular-file")
|
||||
digest = hashlib.sha256()
|
||||
with full_path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
actual = digest.hexdigest()
|
||||
if actual.lower() != expected.lower():
|
||||
raise ValueError("checksum-mismatch")
|
||||
except Exception as error:
|
||||
result = str(error)
|
||||
failed = True
|
||||
print("\t".join([
|
||||
module.get("name", ""), build.get("status", ""), target_key,
|
||||
relative, expected, actual, result,
|
||||
]))
|
||||
sys.exit(1 if failed else 0)
|
||||
PY
|
||||
then
|
||||
mark_passed "artifact paths and checksums"
|
||||
else
|
||||
mark_failed "artifact paths or checksums"
|
||||
fi
|
||||
}
|
||||
|
||||
validate_managed_configs() {
|
||||
if python3 - "${INSTALL_DIR}/conf/modules-enabled" "${INSTALL_DIR}/modules" \
|
||||
>"${OUTPUT_DIR}/managed-config-validation.tsv" <<'PY'
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
config_root = pathlib.Path(sys.argv[1])
|
||||
modules_root = pathlib.Path(sys.argv[2]).resolve()
|
||||
container_prefix = "/usr/local/openresty/nginx/modules/1panel/"
|
||||
pattern = re.compile(r"^\s*load_module\s+([^;]+);", re.MULTILINE)
|
||||
failed = False
|
||||
print("config\tcontainer_path\thost_path\tresult")
|
||||
if config_root.exists():
|
||||
for config in sorted(config_root.glob("1panel-module-*.conf")):
|
||||
content = config.read_text(encoding="utf-8")
|
||||
for value in pattern.findall(content):
|
||||
container_path = value.strip().strip('"\'')
|
||||
result = "OK"
|
||||
host_path = ""
|
||||
try:
|
||||
if not container_path.startswith(container_prefix):
|
||||
raise ValueError("unexpected-container-path")
|
||||
relative = pathlib.PurePosixPath(container_path[len(container_prefix):])
|
||||
if ".." in relative.parts:
|
||||
raise ValueError("unsafe-path")
|
||||
resolved = (modules_root / pathlib.Path(*relative.parts)).resolve(strict=True)
|
||||
if modules_root not in resolved.parents or not resolved.is_file():
|
||||
raise ValueError("missing-artifact")
|
||||
host_path = str(resolved)
|
||||
except Exception as error:
|
||||
result = str(error)
|
||||
failed = True
|
||||
print("\t".join([config.name, container_path, host_path, result]))
|
||||
sys.exit(1 if failed else 0)
|
||||
PY
|
||||
then
|
||||
mark_passed "managed load_module configs"
|
||||
else
|
||||
mark_failed "managed load_module configs"
|
||||
fi
|
||||
}
|
||||
|
||||
collect_compose_state() {
|
||||
local compose_file="${INSTALL_DIR}/docker-compose.yml"
|
||||
if [[ ! -f "${compose_file}" ]]; then
|
||||
mark_failed "docker-compose.yml is missing"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if (cd "${INSTALL_DIR}" && docker compose config --format json) >"${OUTPUT_DIR}/compose.json" 2>"${OUTPUT_DIR}/compose-config.log"; then
|
||||
mark_passed "docker compose config"
|
||||
else
|
||||
mark_failed "docker compose config"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if jq -e '
|
||||
[.services[].volumes[]?]
|
||||
| (any(.[]; (.target | rtrimstr("/")) == "/usr/local/openresty/nginx/modules/1panel" and .read_only == true))
|
||||
and (any(.[]; (.target | rtrimstr("/")) == "/usr/local/openresty/nginx/conf/modules-enabled" and .read_only == true))
|
||||
' "${OUTPUT_DIR}/compose.json" >/dev/null; then
|
||||
mark_passed "read-only module mounts"
|
||||
else
|
||||
mark_failed "read-only module mounts"
|
||||
fi
|
||||
|
||||
(cd "${INSTALL_DIR}" && docker compose ps -a --format json) >"${OUTPUT_DIR}/compose-ps.json" 2>&1 || true
|
||||
if [[ -z "${CONTAINER}" ]]; then
|
||||
local cid
|
||||
cid="$(cd "${INSTALL_DIR}" && docker compose ps -q 2>/dev/null | head -n 1)"
|
||||
if [[ -n "${cid}" ]]; then
|
||||
CONTAINER="$(docker inspect --format '{{.Name}}' "${cid}" | sed 's#^/##')"
|
||||
else
|
||||
CONTAINER="$(jq -r '[.services[] | select((.image // "") | test("openresty"; "i")) | .container_name][0] // empty' \
|
||||
"${OUTPUT_DIR}/compose.json")"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
compare_target_identity() {
|
||||
local state="${INSTALL_DIR}/build/module.json"
|
||||
local current_image_id="$1"
|
||||
[[ -f "${state}" ]] || return 0
|
||||
if python3 - "${state}" "${current_image_id}" "${INSTALL_DIR}/conf/modules-enabled" \
|
||||
>"${OUTPUT_DIR}/target-identity.tsv" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
modules = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
current = sys.argv[2]
|
||||
config_root = pathlib.Path(sys.argv[3])
|
||||
pattern = re.compile(r"^\s*load_module\s+([^;]+);", re.MULTILINE)
|
||||
prefix = "/usr/local/openresty/nginx/modules/1panel/"
|
||||
loaded = set()
|
||||
if config_root.exists():
|
||||
for config in config_root.glob("1panel-module-*.conf"):
|
||||
for value in pattern.findall(config.read_text(encoding="utf-8")):
|
||||
container_path = value.strip().strip('"\'')
|
||||
if container_path.startswith(prefix):
|
||||
loaded.add(container_path[len(prefix):])
|
||||
failed = False
|
||||
print("module\tenabled\tmode\tready_image_ids\tmanaged_artifacts\tresult")
|
||||
for module in modules:
|
||||
mode = module.get("buildMode") or "static"
|
||||
if module.get("deleted") or not module.get("enable") or mode == "static":
|
||||
continue
|
||||
ready = [build for build in module.get("builds") or [] if build.get("status") == "ready"]
|
||||
digests = sorted({(build.get("target") or {}).get("imageDigest", "") for build in ready})
|
||||
candidates = []
|
||||
if not ready:
|
||||
result = "NO-READY-BUILD"
|
||||
failed = True
|
||||
elif current in digests:
|
||||
result = "MATCH"
|
||||
candidates = [build for build in ready if (build.get("target") or {}).get("imageDigest") == current]
|
||||
elif not any(digests):
|
||||
result = "UNKNOWN-NO-IMAGE-DIGEST"
|
||||
candidates = ready
|
||||
else:
|
||||
result = "MISMATCH"
|
||||
failed = True
|
||||
|
||||
managed = []
|
||||
if candidates:
|
||||
for build in candidates:
|
||||
paths = [artifact.get("path", "") for artifact in build.get("artifacts") or []]
|
||||
if paths and all(path in loaded for path in paths):
|
||||
managed = paths
|
||||
break
|
||||
if not managed:
|
||||
result += "+NOT-IN-MANAGED-CONFIG"
|
||||
failed = True
|
||||
print("\t".join([
|
||||
module.get("name", ""), str(module.get("enable", False)),
|
||||
mode, ",".join(digests), ",".join(managed), result,
|
||||
]))
|
||||
sys.exit(1 if failed else 0)
|
||||
PY
|
||||
then
|
||||
mark_passed "enabled module target image identity"
|
||||
else
|
||||
mark_failed "enabled module target image identity"
|
||||
fi
|
||||
}
|
||||
|
||||
collect_container_state() {
|
||||
if [[ -z "${CONTAINER}" ]]; then
|
||||
mark_failed "OpenResty container could not be discovered"
|
||||
return 0
|
||||
fi
|
||||
if ! docker inspect "${CONTAINER}" >/dev/null 2>&1; then
|
||||
mark_failed "container ${CONTAINER} does not exist"
|
||||
return 0
|
||||
fi
|
||||
|
||||
docker inspect "${CONTAINER}" | jq '.[0] | {
|
||||
Id, Name, Image, State,
|
||||
Config: {Image: .Config.Image},
|
||||
Mounts: [.Mounts[] | {Type, Source, Destination, RW}]
|
||||
}' >"${OUTPUT_DIR}/container.json"
|
||||
docker logs --tail 1000 --timestamps "${CONTAINER}" >"${OUTPUT_DIR}/container.log" 2>&1 || true
|
||||
|
||||
local running image_id image_name
|
||||
running="$(docker inspect --format '{{.State.Running}}' "${CONTAINER}")"
|
||||
image_id="$(docker inspect --format '{{.Image}}' "${CONTAINER}")"
|
||||
image_name="$(docker inspect --format '{{.Config.Image}}' "${CONTAINER}")"
|
||||
docker image inspect "${image_id}" | jq '.[0] | {Id, RepoTags, RepoDigests, Architecture, Os, Created}' \
|
||||
>"${OUTPUT_DIR}/runtime-image.json" 2>&1 || true
|
||||
printf 'container=%s\nrunning=%s\nimage_name=%s\nimage_id=%s\n' \
|
||||
"${CONTAINER}" "${running}" "${image_name}" "${image_id}" >"${OUTPUT_DIR}/runtime.txt"
|
||||
compare_target_identity "${image_id}"
|
||||
|
||||
if [[ "${running}" != "true" ]]; then
|
||||
mark_failed "container ${CONTAINER} is not running"
|
||||
return 0
|
||||
fi
|
||||
mark_passed "container is running"
|
||||
|
||||
docker exec "${CONTAINER}" /usr/local/openresty/nginx/sbin/nginx -V \
|
||||
>"${OUTPUT_DIR}/nginx-version.txt" 2>&1 || mark_failed "nginx -V"
|
||||
docker exec "${CONTAINER}" /bin/sh -c \
|
||||
'find /usr/local/openresty/nginx/modules/1panel -type f -name "*.so" -exec sha256sum {} \; | sort' \
|
||||
>"${OUTPUT_DIR}/container-artifact-checksums.txt" 2>&1 || true
|
||||
docker exec "${CONTAINER}" /bin/sh -c \
|
||||
'for f in $(find /usr/local/openresty/nginx/modules/1panel -type f -name "*.so" | sort); do echo "### $f"; ldd "$f" || true; done' \
|
||||
>"${OUTPUT_DIR}/container-artifact-ldd.txt" 2>&1 || true
|
||||
|
||||
if [[ "${NGINX_TEST}" -eq 1 ]]; then
|
||||
if docker exec "${CONTAINER}" /usr/local/openresty/nginx/sbin/nginx -t \
|
||||
>"${OUTPUT_DIR}/nginx-test.txt" 2>&1; then
|
||||
mark_passed "running container nginx -t"
|
||||
else
|
||||
mark_failed "running container nginx -t"
|
||||
fi
|
||||
|
||||
local full_output="${OUTPUT_DIR}/nginx-T.full.tmp"
|
||||
docker exec "${CONTAINER}" /usr/local/openresty/nginx/sbin/nginx -T >"${full_output}" 2>&1 || true
|
||||
if [[ "${FULL_CONFIG}" -eq 1 ]]; then
|
||||
mv "${full_output}" "${OUTPUT_DIR}/nginx-T.full.txt"
|
||||
log "WARNING: nginx-T.full.txt may contain credentials or private configuration"
|
||||
else
|
||||
grep -nE 'load_module|modules-enabled|nginx version:|configure arguments:' "${full_output}" \
|
||||
>"${OUTPUT_DIR}/nginx-T-modules.txt" 2>/dev/null || true
|
||||
rm -f "${full_output}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
preflight
|
||||
collect_filesystem_state
|
||||
validate_artifacts
|
||||
validate_managed_configs
|
||||
collect_compose_state
|
||||
collect_container_state
|
||||
|
||||
if [[ "${#FAILED_CHECKS[@]}" -gt 0 ]]; then
|
||||
log "Diagnostics completed with ${#FAILED_CHECKS[@]} failed checks"
|
||||
return 0
|
||||
fi
|
||||
log "Diagnostics completed without failed checks"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
542
scripts/openresty-modules/test-builder.sh
Normal file
542
scripts/openresty-modules/test-builder.sh
Normal file
@@ -0,0 +1,542 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
export DOCKER_BUILDKIT="${DOCKER_BUILDKIT:-1}"
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd -P)"
|
||||
DEFAULT_APPSTORE_ROOT="$(cd -- "${REPO_ROOT}/.." && pwd -P)/appstore"
|
||||
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-$$"
|
||||
|
||||
APPSTORE_ROOT="${APPSTORE_ROOT:-${DEFAULT_APPSTORE_ROOT}}"
|
||||
VERSIONS_CSV="1.27.1.2-5-1-focal,1.29.2.5-0-noble,1.31.1.1-0-noble"
|
||||
MODULES_CSV=""
|
||||
OUTPUT_DIR="${OUTPUT_DIR:-${PWD}/openresty-module-test-results/${RUN_ID}}"
|
||||
MIRROR="${MIRROR:-}"
|
||||
SKIP_PULL=0
|
||||
NO_CACHE=0
|
||||
KEEP_DOCKER=0
|
||||
KEEP_CONTEXT=0
|
||||
RUN_SOURCE_CHECKS=0
|
||||
STRICT_INDIVIDUAL=0
|
||||
CLEANUP_READY=0
|
||||
|
||||
declare -a CREATED_CONTAINERS=()
|
||||
declare -a CREATED_IMAGES=()
|
||||
declare -a VERSIONS=()
|
||||
declare -a REQUESTED_MODULES=()
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: test-builder.sh [options]
|
||||
|
||||
Build and load-test OpenResty dynamic modules directly from the appstore tree.
|
||||
|
||||
Options:
|
||||
--appstore PATH Appstore repository root (default: sibling appstore repo)
|
||||
--versions CSV App versions to test
|
||||
--modules CSV Module names to test (default: every catalog module)
|
||||
--output PATH Persistent result directory
|
||||
--mirror URL apt mirror for module build packages (CONTAINER_PACKAGE_URL)
|
||||
--skip-pull Use local runtime images without pulling
|
||||
--no-cache Pass --no-cache to every module Docker build
|
||||
--strict-individual Fail when a module cannot load by itself
|
||||
--source-checks Run Go module tests and go vet before Docker tests
|
||||
--keep-docker Keep temporary images and containers
|
||||
--keep-context Keep copied Docker build contexts
|
||||
-h, --help Show this help
|
||||
|
||||
Environment equivalents: APPSTORE_ROOT, OUTPUT_DIR, MIRROR, DOCKER_BUILDKIT.
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date -u +%H:%M:%S)" "$*" | tee -a "${OUTPUT_DIR}/run.log"
|
||||
}
|
||||
|
||||
die() {
|
||||
log "ERROR: $*"
|
||||
return 1
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
|
||||
}
|
||||
|
||||
split_csv() {
|
||||
local value="$1"
|
||||
local -n destination="$2"
|
||||
IFS=',' read -r -a destination <<<"${value}"
|
||||
}
|
||||
|
||||
safe_name() {
|
||||
local value="$1"
|
||||
local base digest
|
||||
base="$(printf '%s' "${value}" | sed -E 's/[^a-zA-Z0-9._-]+/-/g; s/^-+//; s/-+$//' | cut -c1-48)"
|
||||
[[ -n "${base}" ]] || base="module"
|
||||
digest="$(printf '%s' "${value}" | sha256sum | awk '{print substr($1,1,8)}')"
|
||||
printf '%s-%s' "${base}" "${digest}"
|
||||
}
|
||||
|
||||
run_logged() {
|
||||
local log_file="$1"
|
||||
shift
|
||||
mkdir -p "$(dirname -- "${log_file}")"
|
||||
set +e
|
||||
"$@" > >(tee "${log_file}") 2>&1
|
||||
local status=$?
|
||||
set -e
|
||||
return "${status}"
|
||||
}
|
||||
|
||||
docker_rm_container() {
|
||||
local name="$1"
|
||||
docker rm -f "${name}" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local status=$?
|
||||
if [[ "${CLEANUP_READY}" -eq 0 ]]; then
|
||||
exit "${status}"
|
||||
fi
|
||||
if [[ "${KEEP_DOCKER}" -eq 0 ]]; then
|
||||
local item
|
||||
for item in "${CREATED_CONTAINERS[@]:-}"; do
|
||||
[[ -n "${item}" ]] && docker_rm_container "${item}"
|
||||
done
|
||||
for item in "${CREATED_IMAGES[@]:-}"; do
|
||||
[[ -n "${item}" ]] && docker image rm -f "${item}" >/dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
if [[ "${KEEP_CONTEXT}" -eq 0 && -d "${OUTPUT_DIR}/work" ]]; then
|
||||
find "${OUTPUT_DIR}/work" -mindepth 2 -maxdepth 2 -type d -name context -prune -exec rm -rf -- {} + 2>/dev/null || true
|
||||
fi
|
||||
exit "${status}"
|
||||
}
|
||||
|
||||
write_debug_bundle() {
|
||||
local status="$1" line="$2" command="$3"
|
||||
{
|
||||
printf 'exit_status=%s\n' "${status}"
|
||||
printf 'line=%s\n' "${line}"
|
||||
printf 'command=%s\n' "${command}"
|
||||
printf 'run_id=%s\n' "${RUN_ID}"
|
||||
} >"${OUTPUT_DIR}/failure.txt"
|
||||
|
||||
docker ps -a --no-trunc >"${OUTPUT_DIR}/docker-ps.txt" 2>&1 || true
|
||||
docker image ls --digests --no-trunc >"${OUTPUT_DIR}/docker-images.txt" 2>&1 || true
|
||||
docker system df >"${OUTPUT_DIR}/docker-system-df.txt" 2>&1 || true
|
||||
|
||||
local item
|
||||
for item in "${CREATED_CONTAINERS[@]:-}"; do
|
||||
[[ -n "${item}" ]] || continue
|
||||
docker inspect "${item}" >"${OUTPUT_DIR}/container-${item}.json" 2>&1 || true
|
||||
docker logs "${item}" >"${OUTPUT_DIR}/container-${item}.log" 2>&1 || true
|
||||
done
|
||||
|
||||
local archive="${OUTPUT_DIR%/}.tar.gz"
|
||||
tar --exclude='*/context' -czf "${archive}" -C "$(dirname -- "${OUTPUT_DIR}")" "$(basename -- "${OUTPUT_DIR}")" 2>/dev/null || true
|
||||
printf 'Debug bundle: %s\n' "${archive}" >&2
|
||||
}
|
||||
|
||||
on_error() {
|
||||
local status="$1" line="$2" command="$3"
|
||||
set +e
|
||||
log "FAILED at line ${line}: ${command} (exit ${status})"
|
||||
write_debug_bundle "${status}" "${line}" "${command}"
|
||||
return "${status}"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--appstore)
|
||||
APPSTORE_ROOT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--versions)
|
||||
VERSIONS_CSV="$2"
|
||||
shift 2
|
||||
;;
|
||||
--modules)
|
||||
MODULES_CSV="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--mirror)
|
||||
MIRROR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-pull)
|
||||
SKIP_PULL=1
|
||||
shift
|
||||
;;
|
||||
--no-cache)
|
||||
NO_CACHE=1
|
||||
shift
|
||||
;;
|
||||
--strict-individual)
|
||||
STRICT_INDIVIDUAL=1
|
||||
shift
|
||||
;;
|
||||
--source-checks)
|
||||
RUN_SOURCE_CHECKS=1
|
||||
shift
|
||||
;;
|
||||
--keep-docker)
|
||||
KEEP_DOCKER=1
|
||||
shift
|
||||
;;
|
||||
--keep-context)
|
||||
KEEP_CONTEXT=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf 'Unknown option: %s\n' "$1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -d "${OUTPUT_DIR}" ]] && find "${OUTPUT_DIR}" -mindepth 1 -print -quit | grep -q .; then
|
||||
printf 'Output directory must be empty: %s\n' "${OUTPUT_DIR}" >&2
|
||||
exit 2
|
||||
fi
|
||||
mkdir -p "${OUTPUT_DIR}/work"
|
||||
OUTPUT_DIR="$(cd -- "${OUTPUT_DIR}" && pwd -P)"
|
||||
CLEANUP_READY=1
|
||||
trap 'on_error "$?" "$LINENO" "$BASH_COMMAND"' ERR
|
||||
APPSTORE_ROOT="$(cd -- "${APPSTORE_ROOT}" && pwd -P)"
|
||||
split_csv "${VERSIONS_CSV}" VERSIONS
|
||||
if [[ "${#VERSIONS[@]}" -eq 0 || -z "${VERSIONS[0]}" ]]; then
|
||||
die "at least one OpenResty version is required"
|
||||
fi
|
||||
if [[ -n "${MODULES_CSV}" ]]; then
|
||||
split_csv "${MODULES_CSV}" REQUESTED_MODULES
|
||||
fi
|
||||
|
||||
preflight() {
|
||||
[[ "$(uname -s)" == "Linux" ]] || die "this integration test must run on Linux"
|
||||
require_command docker
|
||||
require_command jq
|
||||
require_command python3
|
||||
require_command sha256sum
|
||||
require_command sed
|
||||
require_command awk
|
||||
require_command tar
|
||||
require_command file
|
||||
require_command readelf
|
||||
|
||||
docker version >"${OUTPUT_DIR}/docker-version.txt" 2>&1
|
||||
docker info >"${OUTPUT_DIR}/docker-info.txt" 2>&1
|
||||
docker compose version >"${OUTPUT_DIR}/docker-compose-version.txt" 2>&1
|
||||
uname -a >"${OUTPUT_DIR}/uname.txt"
|
||||
cp /etc/os-release "${OUTPUT_DIR}/os-release.txt" 2>/dev/null || true
|
||||
df -h >"${OUTPUT_DIR}/disk-free.txt"
|
||||
free -h >"${OUTPUT_DIR}/memory.txt" 2>&1 || true
|
||||
|
||||
[[ -d "${APPSTORE_ROOT}/apps/openresty" ]] || die "invalid appstore root: ${APPSTORE_ROOT}"
|
||||
log "Results: ${OUTPUT_DIR}"
|
||||
log "Appstore: ${APPSTORE_ROOT}"
|
||||
log "Docker architecture: $(docker info --format '{{.Architecture}}')"
|
||||
}
|
||||
|
||||
run_source_checks() {
|
||||
[[ "${RUN_SOURCE_CHECKS}" -eq 1 ]] || return 0
|
||||
require_command go
|
||||
log "Running Go dynamic-module tests"
|
||||
run_logged "${OUTPUT_DIR}/go-test.log" bash -c \
|
||||
"cd '${REPO_ROOT}/agent' && go test ./app/service -run 'NginxModule|DynamicModule' -count=1 -v"
|
||||
log "Running go vet"
|
||||
run_logged "${OUTPUT_DIR}/go-vet.log" bash -c "cd '${REPO_ROOT}/agent' && go vet ./..."
|
||||
}
|
||||
|
||||
validate_template() {
|
||||
local version="$1"
|
||||
local app_dir="${APPSTORE_ROOT}/apps/openresty/${version}"
|
||||
local catalog="${app_dir}/build/module.catalog.json"
|
||||
|
||||
[[ "${version}" =~ ^[a-zA-Z0-9._-]+$ ]] || die "unsafe version value: ${version}"
|
||||
[[ -f "${app_dir}/build/Dockerfile.modules" ]] || die "missing Dockerfile.modules for ${version}"
|
||||
[[ -f "${catalog}" ]] || die "missing module catalog for ${version}"
|
||||
jq -e 'type == "array" and length > 0 and all(.[]; .name and .params and .provider == "local")' \
|
||||
"${catalog}" >/dev/null
|
||||
bash -n "${app_dir}/scripts/init.sh"
|
||||
bash -n "${app_dir}/scripts/upgrade.sh"
|
||||
grep -Fq 'conf/modules-enabled:/usr/local/openresty/nginx/conf/modules-enabled/:ro' "${app_dir}/docker-compose.yml"
|
||||
grep -Fq './modules:/usr/local/openresty/nginx/modules/1panel/:ro' "${app_dir}/docker-compose.yml"
|
||||
grep -Fq 'include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;' "${app_dir}/conf/nginx.conf"
|
||||
|
||||
mkdir -p "${OUTPUT_DIR}/work/${version}/website/conf.d" "${OUTPUT_DIR}/work/${version}/website/stream.d"
|
||||
(
|
||||
cd "${app_dir}"
|
||||
CONTAINER_NAME="openresty-template-check" \
|
||||
WEBSITE_DIR="${OUTPUT_DIR}/work/${version}/website" \
|
||||
PANEL_APP_PORT_HTTP=18080 \
|
||||
docker compose config -q
|
||||
) >"${OUTPUT_DIR}/work/${version}/compose-config.log" 2>&1
|
||||
}
|
||||
|
||||
write_module_inputs() {
|
||||
local catalog="$1" module="$2" context="$3" input_dir="$4"
|
||||
local module_script params dynamic_params packages
|
||||
|
||||
module_script="$(jq -er --arg name "${module}" '.[] | select(.name == $name) | .script' "${catalog}")"
|
||||
params="$(jq -er --arg name "${module}" '.[] | select(.name == $name) | .params' "${catalog}")"
|
||||
packages="$(jq -er --arg name "${module}" '.[] | select(.name == $name) | (.packages // []) | join(" ")' "${catalog}")"
|
||||
dynamic_params="${params//--add-module=/--add-dynamic-module=}"
|
||||
|
||||
mkdir -p "${input_dir}"
|
||||
printf '%s\n' "${module_script}" >"${input_dir}/script.txt"
|
||||
printf '%s\n' "${params}" >"${input_dir}/params.original.txt"
|
||||
printf '%s\n' "${dynamic_params}" >"${input_dir}/params.dynamic.txt"
|
||||
printf '%s\n' "${packages}" >"${input_dir}/packages.txt"
|
||||
printf '#!/bin/bash\nset -e\n%s\n' "${module_script}" >"${context}/tmp/module-pre.sh"
|
||||
|
||||
python3 - "${dynamic_params}" >"${context}/tmp/module-config.args" <<'PY'
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
params = sys.argv[1]
|
||||
args = shlex.split(params, posix=True)
|
||||
if not args:
|
||||
raise SystemExit("dynamic module parameters are empty")
|
||||
if not any(arg.startswith("--add-dynamic-module=") or "=dynamic" in arg for arg in args):
|
||||
raise SystemExit("module does not declare a dynamic configure option")
|
||||
for arg in args:
|
||||
if not arg.startswith("--"):
|
||||
raise SystemExit(f"unsupported configure argument: {arg!r}")
|
||||
if any(char in arg for char in "\x00\r\n;&|<>"):
|
||||
raise SystemExit(f"unsafe configure argument: {arg!r}")
|
||||
print(arg)
|
||||
PY
|
||||
}
|
||||
|
||||
validate_load_directives() {
|
||||
local image="$1" modules_root="$2" directives_file="$3" config_path="$4" log_file="$5"
|
||||
{
|
||||
cat "${directives_file}"
|
||||
printf 'error_log stderr notice;\npid /tmp/nginx.pid;\nevents {}\nhttp {}\n'
|
||||
} >"${config_path}"
|
||||
|
||||
run_logged "${log_file}" docker run --rm --network none \
|
||||
-v "${modules_root}:/usr/local/openresty/nginx/modules/1panel:ro" \
|
||||
-v "${config_path}:/tmp/1panel-module-test.conf:ro" \
|
||||
--entrypoint /usr/local/openresty/nginx/sbin/nginx \
|
||||
"${image}" -t -c /tmp/1panel-module-test.conf
|
||||
}
|
||||
|
||||
build_module() {
|
||||
local version="$1" module="$2" image="$3" app_dir="$4" version_dir="$5" context="$6" sequence="$7"
|
||||
local catalog="${app_dir}/build/module.catalog.json"
|
||||
local module_key module_dir input_dir tag build_log cid packages artifact relative checksum
|
||||
local -a artifacts=()
|
||||
module_key="$(safe_name "${module}")"
|
||||
module_dir="${version_dir}/modules/${module_key}/${RUN_ID}"
|
||||
input_dir="${version_dir}/inputs/${module_key}"
|
||||
tag="1panel/openresty-module-test:${module_key}-$(safe_name "${version}")-${RUN_ID}"
|
||||
tag="${tag:0:127}"
|
||||
build_log="${version_dir}/logs/build-${module_key}.log"
|
||||
|
||||
log "[${version}] building ${module}"
|
||||
write_module_inputs "${catalog}" "${module}" "${context}" "${input_dir}"
|
||||
packages="$(cat "${input_dir}/packages.txt")"
|
||||
|
||||
local -a build_args=(
|
||||
build --progress=plain --target module-output
|
||||
-f "${context}/Dockerfile.modules"
|
||||
-t "${tag}"
|
||||
--build-arg "PANEL_OPENRESTY_VERSION=${version}"
|
||||
--build-arg "RESTY_ADD_PACKAGE_BUILDDEPS=${packages}"
|
||||
)
|
||||
[[ "${NO_CACHE}" -eq 0 ]] || build_args+=(--no-cache)
|
||||
[[ -z "${MIRROR}" ]] || build_args+=(--build-arg "CONTAINER_PACKAGE_URL=${MIRROR}")
|
||||
build_args+=("${context}")
|
||||
|
||||
run_logged "${build_log}" docker "${build_args[@]}"
|
||||
CREATED_IMAGES+=("${tag}")
|
||||
|
||||
cid="1panel-module-copy-${module_key}-${RUN_ID}"
|
||||
cid="${cid:0:63}"
|
||||
docker create --name "${cid}" "${tag}" /bin/true >"${input_dir}/container-id.txt"
|
||||
CREATED_CONTAINERS+=("${cid}")
|
||||
mkdir -p "${module_dir}"
|
||||
docker cp "${cid}:/out/." "${module_dir}"
|
||||
docker_rm_container "${cid}"
|
||||
|
||||
mapfile -t artifacts < <(find "${module_dir}" -maxdepth 1 -type f -name '*.so' -print | sort)
|
||||
[[ "${#artifacts[@]}" -gt 0 ]] || die "${module} produced no top-level .so files"
|
||||
|
||||
: >"${input_dir}/load-directives.conf"
|
||||
for artifact in "${artifacts[@]}"; do
|
||||
relative="${artifact#${version_dir}/modules/}"
|
||||
[[ "${relative}" =~ ^[a-zA-Z0-9_./+-]+\.so$ ]] || \
|
||||
die "unsafe module artifact path: ${relative}"
|
||||
checksum="$(sha256sum "${artifact}" | awk '{print $1}')"
|
||||
printf '%s\t%s\t%s\t%s\n' "${module}" "${relative}" "${checksum}" "$(stat -c '%s' "${artifact}")" \
|
||||
>>"${version_dir}/artifacts.tsv"
|
||||
printf 'load_module /usr/local/openresty/nginx/modules/1panel/%s;\n' "${relative}" \
|
||||
>>"${input_dir}/load-directives.conf"
|
||||
file "${artifact}" >>"${input_dir}/file.txt"
|
||||
readelf -d "${artifact}" >>"${input_dir}/readelf-dynamic.txt" 2>&1 || true
|
||||
done
|
||||
|
||||
if ! validate_load_directives "${image}" "${version_dir}/modules" \
|
||||
"${input_dir}/load-directives.conf" "${input_dir}/individual-nginx.conf" \
|
||||
"${version_dir}/logs/load-${module_key}.log"; then
|
||||
printf '%s\tindividual-load-failed\n' "${module}" >>"${version_dir}/status.tsv"
|
||||
if [[ "${STRICT_INDIVIDUAL}" -eq 1 ]]; then
|
||||
die "${module} failed individual load validation"
|
||||
fi
|
||||
log "[${version}] ${module} cannot load alone; combined validation will decide"
|
||||
else
|
||||
printf '%s\tindividual-load-ok\n' "${module}" >>"${version_dir}/status.tsv"
|
||||
fi
|
||||
|
||||
cat "${input_dir}/load-directives.conf" >>"${version_dir}/combined-load-directives.conf"
|
||||
cp "${input_dir}/load-directives.conf" \
|
||||
"${version_dir}/ordered-configs/$(printf '%04d' "${sequence}")-${module_key}.conf"
|
||||
}
|
||||
|
||||
runtime_reload_test() {
|
||||
local version="$1" image="$2" version_dir="$3"
|
||||
local runtime_dir="${version_dir}/runtime" container="1panel-module-runtime-$(safe_name "${version}")-${RUN_ID}"
|
||||
local -a module_configs=()
|
||||
container="${container:0:63}"
|
||||
mkdir -p "${runtime_dir}/modules-enabled"
|
||||
|
||||
cat >"${runtime_dir}/nginx.conf" <<'EOF'
|
||||
error_log stderr notice;
|
||||
pid /tmp/nginx.pid;
|
||||
include /tmp/modules-enabled/*.conf;
|
||||
events {}
|
||||
http {}
|
||||
EOF
|
||||
printf '# empty initial module set\n' >"${runtime_dir}/modules-enabled/0000-empty.conf"
|
||||
|
||||
mapfile -t module_configs < <(find "${version_dir}/ordered-configs" -type f -name '*.conf' -print | sort)
|
||||
[[ "${#module_configs[@]}" -gt 0 ]] || die "no module configs available for runtime test"
|
||||
|
||||
log "[${version}] starting runtime reload test"
|
||||
docker run -d --name "${container}" --network none \
|
||||
-v "${version_dir}/modules:/usr/local/openresty/nginx/modules/1panel:ro" \
|
||||
-v "${runtime_dir}/modules-enabled:/tmp/modules-enabled:ro" \
|
||||
-v "${runtime_dir}/nginx.conf:/tmp/1panel-runtime-nginx.conf:ro" \
|
||||
--entrypoint /usr/local/openresty/nginx/sbin/nginx \
|
||||
"${image}" -c /tmp/1panel-runtime-nginx.conf -g 'daemon off;' \
|
||||
>"${runtime_dir}/container-id.txt"
|
||||
CREATED_CONTAINERS+=("${container}")
|
||||
|
||||
local attempt
|
||||
for ((attempt = 1; attempt <= 20; attempt++)); do
|
||||
if [[ "$(docker inspect --format '{{.State.Running}}' "${container}" 2>/dev/null || true)" == "true" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [[ "$(docker inspect --format '{{.State.Running}}' "${container}" 2>/dev/null || true)" != "true" ]]; then
|
||||
docker logs "${container}" >"${runtime_dir}/startup-failure.log" 2>&1 || true
|
||||
die "runtime container failed to start"
|
||||
fi
|
||||
|
||||
local config
|
||||
for config in "${module_configs[@]}"; do
|
||||
cp "${config}" "${runtime_dir}/modules-enabled/$(basename -- "${config}")"
|
||||
done
|
||||
run_logged "${runtime_dir}/nginx-test.log" docker exec "${container}" \
|
||||
/usr/local/openresty/nginx/sbin/nginx -t -c /tmp/1panel-runtime-nginx.conf
|
||||
run_logged "${runtime_dir}/nginx-reload.log" docker exec "${container}" \
|
||||
/usr/local/openresty/nginx/sbin/nginx -s reload -c /tmp/1panel-runtime-nginx.conf
|
||||
|
||||
printf 'load_module /usr/local/openresty/nginx/modules/1panel/not-found.so;\n' \
|
||||
>"${runtime_dir}/modules-enabled/9999-invalid.conf"
|
||||
if docker exec "${container}" /usr/local/openresty/nginx/sbin/nginx \
|
||||
-t -c /tmp/1panel-runtime-nginx.conf >"${runtime_dir}/expected-invalid.log" 2>&1; then
|
||||
die "nginx -t unexpectedly accepted a missing module"
|
||||
fi
|
||||
rm -f "${runtime_dir}/modules-enabled/9999-invalid.conf"
|
||||
run_logged "${runtime_dir}/rollback-nginx-test.log" docker exec "${container}" \
|
||||
/usr/local/openresty/nginx/sbin/nginx -t -c /tmp/1panel-runtime-nginx.conf
|
||||
[[ "$(docker inspect --format '{{.State.Running}}' "${container}")" == "true" ]] || \
|
||||
die "runtime container stopped during rollback test"
|
||||
|
||||
docker logs "${container}" >"${runtime_dir}/container.log" 2>&1 || true
|
||||
docker exec "${container}" /bin/sh -c \
|
||||
'for f in /usr/local/openresty/nginx/modules/1panel/*/*/*.so; do echo "### $f"; ldd "$f" || true; done' \
|
||||
>"${runtime_dir}/ldd.txt" 2>&1 || true
|
||||
docker_rm_container "${container}"
|
||||
}
|
||||
|
||||
test_version() {
|
||||
local version="$1"
|
||||
local app_dir="${APPSTORE_ROOT}/apps/openresty/${version}"
|
||||
local version_dir="${OUTPUT_DIR}/work/${version}"
|
||||
local context="${version_dir}/context"
|
||||
local catalog="${app_dir}/build/module.catalog.json"
|
||||
local image="1panel/openresty:${version}"
|
||||
local -a modules=()
|
||||
|
||||
validate_template "${version}"
|
||||
mkdir -p "${version_dir}/logs" "${version_dir}/inputs" "${version_dir}/modules" \
|
||||
"${version_dir}/ordered-configs"
|
||||
cp -a "${app_dir}/build" "${context}"
|
||||
: >"${version_dir}/artifacts.tsv"
|
||||
: >"${version_dir}/status.tsv"
|
||||
: >"${version_dir}/combined-load-directives.conf"
|
||||
|
||||
if [[ "${#REQUESTED_MODULES[@]}" -gt 0 ]]; then
|
||||
modules=("${REQUESTED_MODULES[@]}")
|
||||
else
|
||||
mapfile -t modules < <(jq -r 'sort_by([.loadOrder // 50, .name])[] | .name' "${catalog}")
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_PULL}" -eq 0 ]]; then
|
||||
log "[${version}] pulling ${image}"
|
||||
run_logged "${version_dir}/logs/image-pull.log" docker pull "${image}"
|
||||
fi
|
||||
docker image inspect "${image}" >"${version_dir}/image-inspect.json"
|
||||
run_logged "${version_dir}/logs/nginx-version.log" docker run --rm \
|
||||
--entrypoint /usr/local/openresty/nginx/sbin/nginx "${image}" -V
|
||||
sha256sum "${app_dir}/build/Dockerfile.modules" >"${version_dir}/builder.sha256"
|
||||
find "${app_dir}/build/tmp" -maxdepth 1 -type f -print0 | sort -z | xargs -0 sha256sum \
|
||||
>"${version_dir}/build-inputs.sha256"
|
||||
|
||||
local module sequence=0
|
||||
for module in "${modules[@]}"; do
|
||||
sequence=$((sequence + 1))
|
||||
jq -e --arg name "${module}" 'any(.[]; .name == $name)' "${catalog}" >/dev/null || \
|
||||
die "module ${module} is not present in ${version} catalog"
|
||||
build_module "${version}" "${module}" "${image}" "${app_dir}" "${version_dir}" "${context}" "${sequence}"
|
||||
done
|
||||
|
||||
log "[${version}] validating the combined load order"
|
||||
validate_load_directives "${image}" "${version_dir}/modules" \
|
||||
"${version_dir}/combined-load-directives.conf" "${version_dir}/combined-nginx.conf" \
|
||||
"${version_dir}/logs/load-combined.log"
|
||||
runtime_reload_test "${version}" "${image}" "${version_dir}"
|
||||
printf '%s\tPASS\n' "${version}" >>"${OUTPUT_DIR}/summary.tsv"
|
||||
log "[${version}] PASS"
|
||||
}
|
||||
|
||||
main() {
|
||||
preflight
|
||||
run_source_checks
|
||||
printf 'version\tresult\n' >"${OUTPUT_DIR}/summary.tsv"
|
||||
local version
|
||||
for version in "${VERSIONS[@]}"; do
|
||||
test_version "${version}"
|
||||
done
|
||||
log "All requested OpenResty module tests passed"
|
||||
log "Summary: ${OUTPUT_DIR}/summary.tsv"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user