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:
Snrat
2026-07-23 13:55:42 +08:00
committed by GitHub
parent f6ed11ac55
commit 2a2e6607b8
27 changed files with 3471 additions and 275 deletions

View File

@@ -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"`
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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)
}
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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',

View File

@@ -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',

View File

@@ -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 پیش‌فرض',

View File

@@ -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 ハンドシェイクを拒否',

View File

@@ -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 핸드셰이크 거부',

View File

@@ -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 ເລີ່ມຕົ້ນ',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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-рукопожатие',

View File

@@ -3859,14 +3859,36 @@ const message = {
module: 'Modüller',
build: 'Oluştur',
buildWarn:
'OpenRestynin 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 OpenRestyyi 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ştura 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 OpenRestyyi 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',

View File

@@ -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 握手',

View File

@@ -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 握手',

View File

@@ -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;
}
};

View File

@@ -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,

View File

@@ -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;
}
};

View 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.

View 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 "$@"

View 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 "$@"