feat: Optimize the OpenResty module compilation logic. (#13355)

* refactor: separate openresty module catalog and state

* feat: Optimize the OpenResty module compilation logic.
This commit is contained in:
CityFun
2026-07-23 16:53:56 +08:00
committed by GitHub
parent 6b1480c5fa
commit 7c95becfeb
25 changed files with 1134 additions and 1137 deletions

View File

@@ -90,14 +90,13 @@ var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF
type NginxModule struct {
Name string `json:"name"`
Custom bool `json:"custom,omitempty"`
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"`
@@ -105,6 +104,7 @@ type NginxModule struct {
type NginxModuleBuild struct {
Provider string `json:"provider"`
BuildMode string `json:"buildMode"`
Status string `json:"status"`
Hash string `json:"hash"`
Target NginxModuleTarget `json:"target"`

View File

@@ -127,7 +127,7 @@ type NginxModuleUpdate struct {
Packages string `json:"packages"`
Enable bool `json:"enable"`
Params string `json:"params"`
BuildMode string `json:"buildMode" validate:"omitempty,oneof=auto dynamic static"`
BuildMode string `json:"buildMode" validate:"omitempty,oneof=dynamic static"`
Provider string `json:"provider" validate:"omitempty,oneof=local prebuilt"`
LoadOrder int `json:"loadOrder" validate:"omitempty,min=0,max=9999"`
}

View File

@@ -70,17 +70,16 @@ type NginxProxyCache struct {
type NginxModule struct {
Name string `json:"name"`
Custom bool `json:"custom"`
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"`
}

View File

@@ -752,13 +752,13 @@ func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string
return string(composeByte), nil
}
func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall) error {
func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall, catalogPath string) error {
fileOp := files.NewFileOp()
buildPath := path.Join(nginxInstall.GetPath(), nginxModuleBuildDir)
if !fileOp.Stat(buildPath) {
return buserr.New("ErrBuildDirNotFound")
}
modules, err := loadNginxModules(nginxInstall)
modules, err := loadNginxModulesWithCatalog(nginxInstall, catalogPath)
if err != nil {
return err
}
@@ -776,11 +776,11 @@ func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall) error {
}
parentTask.LogSuccess(logStr)
}
modules, err = buildDynamicNginxModules(nginxInstall, modules, nil, false, "", parentTask)
modules, err = buildDynamicNginxModules(nginxInstall, modules, nil, false, "", catalogPath, parentTask)
if err != nil {
return err
}
return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false)
return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false, catalogPath)
}
func upgradeInstall(req request.AppInstallUpgrade) error {
@@ -788,6 +788,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil {
return err
}
originalInstall := install
oldVersion := install.Version
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
if err != nil {
@@ -809,6 +810,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
var (
upErr error
backupFile string
nginxUpgradeSnapshot *openrestyUpgradeSnapshot
)
backUpApp := func(t *task.Task) error {
backupService := NewIBackupService()
@@ -864,6 +866,13 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
}
oldEnvContent := append([]byte(nil), content...)
oldDockerCompose := install.DockerCompose
targetNginxCatalogPath := ""
if install.App.Key == constant.AppOpenresty {
nginxUpgradeSnapshot, err = createOpenrestyUpgradeSnapshot(install.GetPath())
if err != nil {
return err
}
}
if install.App.Key == vllmAppKeyForUpgrade {
envs := make(map[string]interface{})
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
@@ -896,16 +905,17 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
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 {
if err := syncNginxModuleBuilder(detailBuildDir, installBuildDir); err != nil {
return err
}
targetCatalogSource := path.Join(detailBuildDir, nginxModuleCatalogFile)
if !fileOp.Stat(targetCatalogSource) {
return fmt.Errorf("target OpenResty module catalog not found: %s", targetCatalogSource)
}
if fileOp.Stat(path.Join(detailBuildDir, nginxModuleCatalogFile)) {
if err := fileOp.CopyFile(path.Join(detailBuildDir, nginxModuleCatalogFile), installBuildDir); err != nil {
targetNginxCatalogPath = path.Join(installBuildDir, nginxModuleCatalogPendingFile)
if err := stageNginxModuleCatalog(targetCatalogSource, targetNginxCatalogPath); err != nil {
return err
}
}
if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.conf"), installBuildDir); err != nil {
return err
}
@@ -984,7 +994,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
}
if install.App.Key == constant.AppOpenresty {
modules, moduleErr := loadNginxModules(install)
modules, moduleErr := loadNginxModulesWithCatalog(install, targetNginxCatalogPath)
if moduleErr != nil {
return moduleErr
}
@@ -992,11 +1002,11 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
// current container. Static modules retain the full rebuild path.
if !hasEnabledStaticNginxModules(modules) {
previousModules := cloneNginxModules(modules)
modules, moduleErr = buildDynamicNginxModules(install, modules, nil, false, "", t)
modules, moduleErr = buildDynamicNginxModules(install, modules, nil, false, "", targetNginxCatalogPath, t)
if moduleErr != nil {
return moduleErr
}
if moduleErr = saveNginxModules(install, modules); moduleErr != nil {
if moduleErr = saveNginxModulesWithCatalog(install, modules, targetNginxCatalogPath); moduleErr != nil {
removeNginxModuleOutputsNotReferenced(install, modules, previousModules)
return moduleErr
}
@@ -1037,7 +1047,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
}
if install.App.Key == constant.AppOpenresty {
if err = buildNginx(t, install); err != nil {
if err = buildNginx(t, install, targetNginxCatalogPath); err != nil {
t.Log(err.Error())
return err
}
@@ -1053,9 +1063,25 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
}
t.LogSuccess(logStr)
install.Status = constant.StatusRunning
if install.App.Key == constant.AppOpenresty {
if err = commitStaticNginxModuleBuilds(install, targetNginxCatalogPath, t); err != nil {
return err
}
activeCatalogPath := path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
if err = activateNginxModuleCatalogAndCommit(targetNginxCatalogPath, activeCatalogPath, func() error {
return appInstallRepo.Save(context.Background(), &install)
}); err != nil {
return err
}
} else {
if err = appInstallRepo.Save(context.Background(), &install); err != nil {
return err
}
}
if nginxUpgradeSnapshot != nil {
nginxUpgradeSnapshot.Cleanup()
nginxUpgradeSnapshot = nil
}
if req.DeleteImage {
newEnvContent, err := fileOp.GetContent(install.GetEnvPath())
if err != nil {
@@ -1083,30 +1109,88 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
rollBackApp := func(t *task.Task) {
if req.Backup {
t.Log(i18n.GetWithName("AppRecover", install.Name))
if err := NewIBackupService().AppRecover(dto.CommonRecover{Name: install.App.Key, DetailName: install.Name, Type: "app", DownloadAccountID: 1, File: backupFile}); err != nil {
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), err)
return
recoverErr := NewIBackupService().AppRecover(dto.CommonRecover{
Name: install.App.Key, DetailName: install.Name, Type: "app", DownloadAccountID: 1, File: backupFile,
})
if recoverErr == nil {
if nginxUpgradeSnapshot != nil {
nginxUpgradeSnapshot.Cleanup()
nginxUpgradeSnapshot = nil
}
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
return
}
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), recoverErr)
if install.App.Key != constant.AppOpenresty {
return
}
}
if install.App.Key == constant.AppOpenresty && nginxUpgradeSnapshot != nil {
if out, rollbackErr := compose.Down(install.GetComposePath()); rollbackErr != nil {
if out != "" {
rollbackErr = fmt.Errorf("%s: %w", out, rollbackErr)
}
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
}
if rollbackErr := nginxUpgradeSnapshot.Restore(); rollbackErr != nil {
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
return
}
nginxUpgradeSnapshot.Cleanup()
nginxUpgradeSnapshot = nil
if out, rollbackErr := compose.Up(originalInstall.GetComposePath()); rollbackErr != nil {
if out != "" {
rollbackErr = fmt.Errorf("%s: %w", out, rollbackErr)
}
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
return
}
originalInstall.Status = constant.StatusRunning
originalInstall.Message = ""
if rollbackErr := appInstallRepo.Save(context.Background(), &originalInstall); rollbackErr != nil {
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
return
}
install = originalInstall
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
return
}
if install.App.Key == constant.AppOpenresty {
if rollbackErr := appInstallRepo.Save(context.Background(), &originalInstall); rollbackErr != nil {
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
return
}
install = originalInstall
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
}
}
upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, 1*time.Hour)
upgradeTimeout := 1 * time.Hour
if install.App.Key == constant.AppOpenresty {
// Dynamic modules are built serially and each Docker build has its own
// timeout. An outer deadline would start rollback while upgradeApp is
// still mutating the installation because SubTask does not stop its
// action goroutine on timeout.
upgradeTimeout = 0
}
upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, upgradeTimeout)
upgradingInstall := install
if err = appInstallRepo.Save(context.Background(), &upgradingInstall); err != nil {
return err
}
go func() {
err = upgradeTask.Execute()
if err != nil {
if taskErr := upgradeTask.Execute(); taskErr != nil {
existInstall, _ := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
if existInstall.ID > 0 && existInstall.Status != constant.StatusRunning {
existInstall.Status = constant.StatusUpgradeErr
existInstall.Message = err.Error()
existInstall.Message = taskErr.Error()
_ = appInstallRepo.Save(context.Background(), &existInstall)
}
}
}()
return appInstallRepo.Save(context.Background(), &install)
return nil
}
func skipCheckStatus(service types.ServiceConfig) bool {

View File

@@ -213,33 +213,39 @@ func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
if targetWarning != "" {
global.LOG.Warn(targetWarning)
}
runtimeTarget := target
runtimeTarget.BuilderDigest = ""
setNginxModuleTargetKey(&runtimeTarget)
runtimeTargetErr := targetErr
if runtimeTarget.OpenRestyVersion != "" {
runtimeTargetErr = nil
}
var resList []response.NginxModule
for _, module := range modules {
if module.Deleted {
continue
}
buildStatus := nginxModuleStatusPending
loadStatus := nginxModuleLoadDisabled
compatibility := "unknown"
var artifacts []dto.NginxModuleArtifact
if module.BuildMode == nginxModuleBuildStatic {
if module.BuildMode == nginxModuleBuildStatic && runtimeTargetErr == nil {
if build := findCurrentNginxModuleBuild(module, runtimeTarget); build != nil && build.Status == nginxModuleStatusReady {
buildStatus = nginxModuleStatusReady
compatibility = "static"
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
} else if findLatestNginxModuleBuild(module, runtimeTarget) != nil {
if module.Enable {
loadStatus = nginxModuleLoadEnabled
}
}
} else if targetErr == nil {
if build := findCurrentNginxModuleBuild(module, target); build != nil {
buildStatus = build.Status
artifacts = build.Artifacts
if build.Status == nginxModuleStatusReady {
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
@@ -251,17 +257,16 @@ func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
}
resList = append(resList, response.NginxModule{
Name: module.Name,
Custom: module.Custom,
Script: module.Script,
Packages: strings.Join(module.Packages, ","),
Params: module.Params,
Enable: module.Enable,
BuildMode: module.BuildMode,
Provider: module.Provider,
DynamicSupport: module.DynamicSupport,
LoadOrder: module.LoadOrder,
BuildStatus: buildStatus,
LoadStatus: loadStatus,
Compatibility: compatibility,
Artifacts: artifacts,
LastError: module.LastError,
})
@@ -278,6 +283,70 @@ func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
}, nil
}
func applyNginxModuleUpdate(modules []dto.NginxModule, req request.NginxModuleUpdate) ([]dto.NginxModule, *dto.NginxModule, error) {
switch req.Operate {
case nginxModuleOperateCreate:
if err := validateNginxModuleBuildMode(dto.NginxModule{Name: req.Name, BuildMode: req.BuildMode}); err != nil {
return nil, nil, err
}
for _, module := range modules {
if module.Name == req.Name {
return nil, nil, buserr.New("ErrNameIsExist")
}
}
modules = append(modules, dto.NginxModule{
Name: req.Name,
Custom: true,
Script: req.Script,
Packages: strings.Split(req.Packages, ","),
Params: req.Params,
Enable: req.Enable,
BuildMode: req.BuildMode,
Provider: req.Provider,
LoadOrder: req.LoadOrder,
})
case nginxModuleOperateUpdate:
for i := range modules {
if modules[i].Name != req.Name {
continue
}
if modules[i].Custom {
if err := validateNginxModuleBuildMode(dto.NginxModule{Name: req.Name, BuildMode: req.BuildMode}); err != nil {
return nil, nil, err
}
} else {
modules[i].Enable = req.Enable
return modules, nil, nil
}
modules[i].Enable = req.Enable
modules[i].Script = req.Script
modules[i].Packages = strings.Split(req.Packages, ",")
modules[i].Params = req.Params
modules[i].BuildMode = req.BuildMode
modules[i].Provider = req.Provider
modules[i].LoadOrder = req.LoadOrder
return modules, nil, nil
}
return nil, nil, fmt.Errorf("OpenResty module %s not found", req.Name)
case nginxModuleOperateDelete:
for i := range modules {
if modules[i].Name != req.Name {
continue
}
if !modules[i].Custom {
return nil, nil, fmt.Errorf("built-in OpenResty module %s cannot be deleted", req.Name)
}
deleted := modules[i]
modules = append(modules[:i], modules[i+1:]...)
return modules, &deleted, nil
}
return nil, nil, fmt.Errorf("OpenResty module %s not found", req.Name)
default:
return nil, nil, fmt.Errorf("unsupported OpenResty module operation %q", req.Operate)
}
return modules, nil, nil
}
func (n NginxService) UpdateModule(req request.NginxModuleUpdate) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
@@ -291,70 +360,9 @@ func (n NginxService) UpdateModule(req request.NginxModuleUpdate) error {
return err
}
oldModules := cloneNginxModules(modules)
var deletedModule *dto.NginxModule
switch req.Operate {
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")
}
}
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
}
}
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 {
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)
}
modules, deletedModule, err := applyNginxModuleUpdate(modules, req)
if err != nil {
return err
}
if err = saveNginxModules(nginxInstall, modules); err != nil {
return err

View File

@@ -30,16 +30,11 @@ import (
)
const (
nginxModuleBuildAuto = "auto"
nginxModuleBuildDynamic = "dynamic"
nginxModuleBuildStatic = "static"
nginxModuleProviderLocal = "local"
nginxModuleSupportUnknown = "unknown"
nginxModuleSupportSupported = "supported"
nginxModuleSupportUnsupported = "unsupported"
nginxModuleStatusPending = "pending"
nginxModuleStatusReady = "ready"
nginxModuleStatusFailed = "failed"
@@ -61,6 +56,7 @@ const (
nginxModuleBuilderFile = "Dockerfile.modules"
nginxModuleStoreFile = "module.json"
nginxModuleCatalogFile = "module.catalog.json"
nginxModuleCatalogPendingFile = "module.catalog.pending.json"
nginxModulePreScriptFile = "module-pre.sh"
nginxModuleConfigArgsFile = "module-config.args"
nginxModuleStaticPreScript = "pre.sh"
@@ -93,21 +89,44 @@ type nginxModuleArtifactProvider interface {
type localNginxModuleProvider struct{}
type nginxModuleState struct {
Name string `json:"name"`
Custom bool `json:"custom,omitempty"`
Script string `json:"script,omitempty"`
Packages []string `json:"packages,omitempty"`
Params string `json:"params,omitempty"`
Enable bool `json:"enable"`
BuildMode string `json:"buildMode,omitempty"`
Provider string `json:"provider,omitempty"`
LoadOrder int `json:"loadOrder,omitempty"`
Builds []dto.NginxModuleBuild `json:"builds,omitempty"`
LastError string `json:"lastError,omitempty"`
}
func (localNginxModuleProvider) Name() string {
return nginxModuleProviderLocal
}
func resolveNginxModuleTarget(install model.AppInstall) (dto.NginxModuleTarget, string, error) {
target, warning, err := resolveNginxRuntimeTarget(install)
if err != nil {
return target, warning, err
}
builderPath := path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleBuilderFile)
builderContent, err := os.ReadFile(builderPath)
if err != nil {
return dto.NginxModuleTarget{}, "", fmt.Errorf("%w: %v", errNginxModuleBuilderMissing, err)
return target, warning, fmt.Errorf("%w: %v", errNginxModuleBuilderMissing, err)
}
builderSum := sha256.Sum256(builderContent)
target.BuilderDigest = hex.EncodeToString(builderSum[:])
setNginxModuleTargetKey(&target)
return target, warning, nil
}
func resolveNginxRuntimeTarget(install model.AppInstall) (dto.NginxModuleTarget, string, error) {
target := dto.NginxModuleTarget{
OpenRestyVersion: install.Version,
Architecture: runtime.GOARCH,
BuilderDigest: hex.EncodeToString(builderSum[:]),
}
envContent, _ := os.ReadFile(install.GetEnvPath())
images, imageErr := dockerUtils.GetImagesFromDockerCompose(envContent, []byte(install.DockerCompose))
@@ -126,10 +145,14 @@ func resolveNginxModuleTarget(install model.AppInstall) (dto.NginxModuleTarget,
target.Architecture = fields[1]
}
}
setNginxModuleTargetKey(&target)
return target, warning, nil
}
func setNginxModuleTargetKey(target *dto.NginxModuleTarget) {
keyInput := strings.Join([]string{target.OpenRestyVersion, target.Architecture, target.ImageDigest, target.BuilderDigest}, "\x00")
keySum := sha256.Sum256([]byte(keyInput))
target.Key = fmt.Sprintf("%s-%s-%s", sanitizeModulePathPart(target.OpenRestyVersion), target.Architecture, hex.EncodeToString(keySum[:6]))
return target, warning, nil
}
// nginxModuleDynamicSupported reports whether the installed version ships both
@@ -141,6 +164,21 @@ func nginxModuleDynamicSupported(install model.AppInstall) bool {
fileOp.Stat(path.Join(buildPath, nginxModuleCatalogFile))
}
func syncNginxModuleBuilder(detailBuildDir, installBuildDir string) error {
sourcePath := path.Join(detailBuildDir, nginxModuleBuilderFile)
targetPath := path.Join(installBuildDir, nginxModuleBuilderFile)
if _, err := os.Stat(sourcePath); err != nil {
if os.IsNotExist(err) {
if removeErr := os.Remove(targetPath); removeErr != nil && !os.IsNotExist(removeErr) {
return removeErr
}
return nil
}
return err
}
return files.NewFileOp().CopyFile(sourcePath, installBuildDir)
}
// resolveNginxModuleBuildMirror picks the apt mirror for module builds: the
// explicit request value wins, otherwise the install's saved
// CONTAINER_PACKAGE_URL.
@@ -155,7 +193,7 @@ func resolveNginxModuleBuildMirror(install model.AppInstall, mirror string) stri
return strings.TrimSpace(envs["CONTAINER_PACKAGE_URL"])
}
func buildDynamicNginxModules(install model.AppInstall, modules []dto.NginxModule, selected []string, force bool, mirror string, parentTask *task.Task) ([]dto.NginxModule, error) {
func buildDynamicNginxModules(install model.AppInstall, modules []dto.NginxModule, selected []string, force bool, mirror string, catalogPath string, parentTask *task.Task) ([]dto.NginxModule, error) {
// Skip target resolution entirely when nothing needs a dynamic build, so
// installs without dynamic modules do not require Dockerfile.modules.
if !hasDynamicNginxModuleBuildTask(modules, selected) {
@@ -163,27 +201,8 @@ func buildDynamicNginxModules(install model.AppInstall, modules []dto.NginxModul
}
target, targetWarning, err := resolveNginxModuleTarget(install)
if err != nil {
if !errors.Is(err, errNginxModuleBuilderMissing) {
return modules, err
}
// The new app version ships no dynamic builder: degrade to inactive
// modules instead of failing the surrounding install or upgrade.
degraded := fmt.Sprintf("dynamic module builder missing, enabled dynamic modules stay inactive: %v", err)
if parentTask != nil {
parentTask.Logf("WARNING: %s", degraded)
} else {
global.LOG.Warn(degraded)
}
for i := range modules {
module := &modules[i]
normalizeNginxModule(module)
if module.Deleted || !module.Enable || module.BuildMode == nginxModuleBuildStatic {
continue
}
module.LastError = "dynamic module builder (Dockerfile.modules) not found for target version; module kept inactive"
}
return modules, nil
}
if targetWarning != "" {
parentTask.Logf("WARNING: %s", targetWarning)
}
@@ -214,12 +233,11 @@ func buildDynamicNginxModules(install model.AppInstall, modules []dto.NginxModul
build.Target = target
build.Error = buildErr.Error()
build.BuiltAt = time.Now()
failedModules := recordNginxModuleBuildFailure(originalModules, module.Name, build, previousBuild, false)
failedModules := recordNginxModuleBuildFailure(originalModules, module.Name, build, previousBuild)
removeNginxModuleOutputsNotReferenced(install, modules, failedModules)
_ = saveNginxModules(install, failedModules)
return failedModules, fmt.Errorf("build dynamic module %s: %w%s", module.Name, buildErr, nginxModuleStaticBuildHint)
_ = saveNginxModulesWithCatalog(install, failedModules, catalogPath)
return failedModules, fmt.Errorf("build dynamic module %s: %w%s", module.Name, buildErr, nginxModuleStaticBuildErrorHint(*module))
}
module.DynamicSupport = nginxModuleSupportSupported
err = validateNginxModuleArtifacts(install, build.Artifacts)
if err != nil {
for outputDir := range nginxModuleOutputDirectories(install, []dto.NginxModule{{Builds: []dto.NginxModuleBuild{build}}}) {
@@ -229,10 +247,10 @@ func buildDynamicNginxModules(install model.AppInstall, modules []dto.NginxModul
build.Error = err.Error()
build.Artifacts = nil
build.BuiltAt = time.Now()
failedModules := recordNginxModuleBuildFailure(originalModules, module.Name, build, previousBuild, true)
failedModules := recordNginxModuleBuildFailure(originalModules, module.Name, build, previousBuild)
removeNginxModuleOutputsNotReferenced(install, modules, failedModules)
_ = saveNginxModules(install, failedModules)
return failedModules, fmt.Errorf("validate dynamic module %s: %w%s", module.Name, err, nginxModuleStaticBuildHint)
_ = saveNginxModulesWithCatalog(install, failedModules, catalogPath)
return failedModules, fmt.Errorf("validate dynamic module %s: %w%s", module.Name, err, nginxModuleStaticBuildErrorHint(*module))
}
module.LastError = ""
upsertNginxModuleBuild(module, build)
@@ -258,6 +276,7 @@ func (localNginxModuleProvider) Resolve(spec nginxModuleBuildSpec) (dto.NginxMod
}
result := dto.NginxModuleBuild{
Provider: nginxModuleProviderLocal,
BuildMode: nginxModuleBuildDynamic,
Status: nginxModuleStatusPending,
Hash: buildHash,
Target: spec.Target,
@@ -363,14 +382,14 @@ func (localNginxModuleProvider) Resolve(spec nginxModuleBuildSpec) (dto.NginxMod
return result, nil
}
func commitNginxModuleBuilds(install model.AppInstall, previousModules, modules []dto.NginxModule, reload bool) error {
if err := saveNginxModules(install, modules); err != nil {
func commitNginxModuleBuilds(install model.AppInstall, previousModules, modules []dto.NginxModule, reload bool, catalogPath string) error {
if err := saveNginxModulesWithCatalog(install, modules, catalogPath); err != nil {
removeNginxModuleOutputsNotReferenced(install, modules, previousModules)
return err
}
if err := reconcileDynamicNginxModuleConfig(install, modules, reload); err != nil {
rollbackModules := recordNginxModuleActivationFailure(previousModules, modules, err)
_ = saveNginxModules(install, rollbackModules)
_ = saveNginxModulesWithCatalog(install, rollbackModules, catalogPath)
removeNginxModuleOutputsNotReferenced(install, modules, previousModules)
return err
}
@@ -488,20 +507,101 @@ func validateNginxModuleLoadConfig(install model.AppInstall, target dto.NginxMod
type nginxModuleConfigSnapshot map[string][]byte
func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.NginxModule, reload bool) error {
target, targetWarning, err := resolveNginxModuleTarget(install)
type openrestyUpgradeSnapshot struct {
installPath string
backupPath string
existing map[string]bool
}
var openrestyUpgradeSnapshotPaths = []string{
nginxModuleBuildDir,
"scripts",
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
path.Join(nginxModuleConfDir, "nginx.conf"),
"docker-compose.yml",
".env",
}
func createOpenrestyUpgradeSnapshot(installPath string) (*openrestyUpgradeSnapshot, error) {
backupPath, err := os.MkdirTemp("", "1panel-openresty-upgrade-*")
if err != nil {
if !errors.Is(err, errNginxModuleBuilderMissing) {
return nil, err
}
snapshot := &openrestyUpgradeSnapshot{
installPath: installPath,
backupPath: backupPath,
existing: make(map[string]bool, len(openrestyUpgradeSnapshotPaths)),
}
for _, relativePath := range openrestyUpgradeSnapshotPaths {
sourcePath := path.Join(installPath, relativePath)
if _, err = os.Stat(sourcePath); err != nil {
if os.IsNotExist(err) {
continue
}
snapshot.Cleanup()
return nil, err
}
snapshot.existing[relativePath] = true
if err = copyOpenrestyUpgradeSnapshotEntry(sourcePath, path.Join(backupPath, relativePath)); err != nil {
snapshot.Cleanup()
return nil, err
}
}
return snapshot, nil
}
func copyOpenrestyUpgradeSnapshotEntry(sourcePath, targetPath string) error {
if err := os.MkdirAll(path.Dir(targetPath), constant.DirPerm); err != nil {
return err
}
// Without the builder every dynamic module stays inactive: reconcile
// towards an empty desired state instead of failing the caller.
global.LOG.Warn(err.Error())
} else if targetWarning != "" {
info, err := os.Stat(sourcePath)
if err != nil {
return err
}
fileOp := files.NewFileOp()
if info.IsDir() {
return fileOp.CopyDir(sourcePath, path.Dir(targetPath))
}
return fileOp.CopyFile(sourcePath, path.Dir(targetPath))
}
func (s *openrestyUpgradeSnapshot) Restore() error {
for _, relativePath := range openrestyUpgradeSnapshotPaths {
targetPath := path.Join(s.installPath, relativePath)
if err := os.RemoveAll(targetPath); err != nil {
return err
}
if !s.existing[relativePath] {
continue
}
if err := copyOpenrestyUpgradeSnapshotEntry(path.Join(s.backupPath, relativePath), targetPath); err != nil {
return err
}
}
return nil
}
func (s *openrestyUpgradeSnapshot) Cleanup() {
if s != nil && s.backupPath != "" {
_ = os.RemoveAll(s.backupPath)
}
}
func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.NginxModule, reload bool) error {
var target dto.NginxModuleTarget
if hasDynamicNginxModuleBuildTask(modules, nil) {
var targetWarning string
var err error
target, targetWarning, err = resolveNginxModuleTarget(install)
if err != nil {
return err
}
if targetWarning != "" {
global.LOG.Warn(targetWarning)
}
}
configDir := path.Join(install.GetPath(), nginxModuleConfDir, nginxModuleEnabledConfDir)
if err = os.MkdirAll(configDir, constant.DirPerm); err != nil {
if err := os.MkdirAll(configDir, constant.DirPerm); err != nil {
return err
}
snapshot, err := snapshotManagedNginxModuleConfigs(configDir)
@@ -519,7 +619,7 @@ func reconcileDynamicNginxModuleConfig(install model.AppInstall, modules []dto.N
})
for _, module := range sortedModules {
normalizeNginxModule(&module)
if module.Deleted || !module.Enable || module.BuildMode == nginxModuleBuildStatic {
if !module.Enable || module.BuildMode == nginxModuleBuildStatic {
continue
}
build := findCurrentNginxModuleBuild(module, target)
@@ -624,7 +724,7 @@ func applyManagedNginxModuleConfigs(configDir string, desired map[string][]byte)
func hasEnabledStaticNginxModules(modules []dto.NginxModule) bool {
for _, module := range modules {
normalizeNginxModule(&module)
if !module.Deleted && module.Enable && module.BuildMode == nginxModuleBuildStatic {
if module.Enable && module.BuildMode == nginxModuleBuildStatic {
return true
}
}
@@ -652,7 +752,7 @@ func configureStaticNginxModules(install model.AppInstall, modules []dto.NginxMo
}
for _, module := range modules {
normalizeNginxModule(&module)
if module.Deleted || !module.Enable || module.BuildMode != nginxModuleBuildStatic {
if !module.Enable || module.BuildMode != nginxModuleBuildStatic {
continue
}
if _, err = preScript.WriteString(module.Script + "\n"); err != nil {
@@ -688,15 +788,17 @@ func executeStaticNginxModuleBuild(install model.AppInstall, modules []dto.Nginx
previousModules := cloneNginxModules(modules)
// A rebuilt runtime changes the target ABI, so every enabled dynamic module
// must be rebuilt even when the user selected only one module.
modules, err := buildDynamicNginxModules(install, modules, nil, force, mirror, parentTask)
modules, err := buildDynamicNginxModules(install, modules, nil, force, mirror, "", parentTask)
if err != nil {
return err
}
if err = commitNginxModuleBuilds(install, previousModules, modules, false); err != nil {
if err = commitNginxModuleBuilds(install, previousModules, modules, false, ""); err != nil {
return err
}
_, err = compose.DownAndUp(install.GetComposePath())
if _, err = compose.DownAndUp(install.GetComposePath()); err != nil {
return err
}
return commitStaticNginxModuleBuilds(install, "", parentTask)
}
func executeNginxModuleBuild(install model.AppInstall, reqModules []string, force bool, mirror string, parentTask *task.Task, reload bool) error {
@@ -706,21 +808,19 @@ func executeNginxModuleBuild(install model.AppInstall, reqModules []string, forc
}
staticBuild := staticNginxBuildRequired(install, modules)
if !staticBuild && hasDynamicNginxModuleBuildTask(modules, reqModules) {
// An explicit build request must fail loudly when the installed version
// does not support dynamic builds; the automatic flows degrade instead.
if !nginxModuleDynamicSupported(install) {
return errors.New("the installed OpenResty version does not support dynamic module builds; use static build mode instead")
return errors.New("the installed OpenResty version does not support dynamic module builds")
}
}
if staticBuild {
return executeStaticNginxModuleBuild(install, modules, mirror, force, parentTask)
}
previousModules := cloneNginxModules(modules)
modules, err = buildDynamicNginxModules(install, modules, reqModules, force, mirror, parentTask)
modules, err = buildDynamicNginxModules(install, modules, reqModules, force, mirror, "", parentTask)
if err != nil {
return err
}
return commitNginxModuleBuilds(install, previousModules, modules, reload)
return commitNginxModuleBuilds(install, previousModules, modules, reload, "")
}
func removeNginxModuleArtifacts(install model.AppInstall, module dto.NginxModule) error {
@@ -734,59 +834,82 @@ func removeNginxModuleArtifacts(install model.AppInstall, module dto.NginxModule
}
func loadNginxModules(install model.AppInstall) ([]dto.NginxModule, error) {
return loadNginxModulesWithCatalog(install, "")
}
func loadNginxModulesWithCatalog(install model.AppInstall, catalogPath string) ([]dto.NginxModule, error) {
modulePath := path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile)
modules, err := readNginxModuleFile(modulePath)
states, err := readNginxModuleStateFile(modulePath)
if err != nil {
return nil, err
}
catalogPath := path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
catalog, err := readNginxModuleFile(catalogPath)
if catalogPath == "" {
catalogPath = path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
}
modules, err := readNginxModuleFile(catalogPath)
if err != nil {
return nil, err
}
moduleIndexes := make(map[string]int, len(modules))
for i := range modules {
modules[i].Custom = false
modules[i].Enable = false
modules[i].Builds = nil
modules[i].LastError = ""
if _, exists := moduleIndexes[modules[i].Name]; exists {
return nil, fmt.Errorf("duplicate OpenResty module catalog name %q", modules[i].Name)
}
moduleIndexes[modules[i].Name] = i
}
for _, catalogModule := range catalog {
if index, ok := moduleIndexes[catalogModule.Name]; ok {
if modules[index].Provider == "" {
modules[index].Provider = catalogModule.Provider
stateNames := make(map[string]struct{}, len(states))
for _, state := range states {
if _, exists := stateNames[state.Name]; exists {
return nil, fmt.Errorf("duplicate OpenResty module state name %q", state.Name)
}
if modules[index].DynamicSupport == "" {
modules[index].DynamicSupport = catalogModule.DynamicSupport
}
if modules[index].LoadOrder == 0 {
modules[index].LoadOrder = catalogModule.LoadOrder
stateNames[state.Name] = struct{}{}
if index, ok := moduleIndexes[state.Name]; ok {
if state.Custom {
return nil, fmt.Errorf("custom OpenResty module %s conflicts with the module catalog", state.Name)
}
modules[index].Enable = state.Enable
modules[index].Builds = state.Builds
modules[index].LastError = state.LastError
continue
}
modules = append(modules, catalogModule)
if !state.Custom {
return nil, fmt.Errorf("OpenResty module state %s is missing from the module catalog", state.Name)
}
modules = append(modules, dto.NginxModule{
Name: state.Name, Custom: true, Script: state.Script, Packages: state.Packages, Params: state.Params,
Enable: state.Enable, BuildMode: state.BuildMode, Provider: state.Provider, LoadOrder: state.LoadOrder,
Builds: state.Builds, LastError: state.LastError,
})
}
for i := range modules {
if err = validateNginxModuleBuildMode(modules[i]); err != nil {
return nil, err
}
normalizeNginxModule(&modules[i])
}
probeNginxModuleDynamicSupport(install, modules)
return modules, nil
}
// probeNginxModuleDynamicSupport marks the dynamic build capability of each
// dynamic module from its configure params. It only runs when the installed
// version supports dynamic builds; otherwise the marker stays "unknown".
func probeNginxModuleDynamicSupport(install model.AppInstall, modules []dto.NginxModule) {
if !nginxModuleDynamicSupported(install) {
return
func readNginxModuleStateFile(filePath string) ([]nginxModuleState, error) {
content, err := os.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {
return []nginxModuleState{}, nil
}
for i := range modules {
if modules[i].Deleted || modules[i].BuildMode == nginxModuleBuildStatic {
continue
return nil, err
}
if _, err := normalizeDynamicModuleParams(modules[i].Params); err != nil {
modules[i].DynamicSupport = nginxModuleSupportUnsupported
} else {
modules[i].DynamicSupport = nginxModuleSupportSupported
if len(strings.TrimSpace(string(content))) == 0 {
return []nginxModuleState{}, nil
}
var states []nginxModuleState
if err = json.Unmarshal(content, &states); err != nil {
return nil, fmt.Errorf("parse OpenResty module state %s: %w", filePath, err)
}
return states, nil
}
func readNginxModuleFile(filePath string) ([]dto.NginxModule, error) {
@@ -808,10 +931,53 @@ func readNginxModuleFile(filePath string) ([]dto.NginxModule, error) {
}
func saveNginxModules(install model.AppInstall, modules []dto.NginxModule) error {
for i := range modules {
normalizeNginxModule(&modules[i])
return saveNginxModulesWithCatalog(install, modules, "")
}
func saveNginxModulesWithCatalog(install model.AppInstall, modules []dto.NginxModule, catalogPath string) error {
if catalogPath == "" {
catalogPath = path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
}
content, err := json.MarshalIndent(modules, "", " ")
catalog, err := readNginxModuleFile(catalogPath)
if err != nil {
return err
}
catalogNames := make(map[string]struct{}, len(catalog))
for _, module := range catalog {
if _, exists := catalogNames[module.Name]; exists {
return fmt.Errorf("duplicate OpenResty module catalog name %q", module.Name)
}
catalogNames[module.Name] = struct{}{}
}
states := make([]nginxModuleState, 0, len(modules))
for i := range modules {
module := &modules[i]
if err = validateNginxModuleBuildMode(*module); err != nil {
return err
}
normalizeNginxModule(module)
if _, builtin := catalogNames[module.Name]; builtin {
if module.Custom {
return fmt.Errorf("custom OpenResty module %s conflicts with the module catalog", module.Name)
}
if !module.Enable && len(module.Builds) == 0 && module.LastError == "" {
continue
}
states = append(states, nginxModuleState{
Name: module.Name, Enable: module.Enable, Builds: module.Builds, LastError: module.LastError,
})
continue
}
if !module.Custom {
return fmt.Errorf("OpenResty module %s is missing from the module catalog", module.Name)
}
states = append(states, nginxModuleState{
Name: module.Name, Custom: true, Script: module.Script, Packages: module.Packages, Params: module.Params,
Enable: module.Enable, BuildMode: module.BuildMode, Provider: module.Provider, LoadOrder: module.LoadOrder,
Builds: module.Builds, LastError: module.LastError,
})
}
content, err := json.MarshalIndent(states, "", " ")
if err != nil {
return err
}
@@ -820,33 +986,64 @@ func saveNginxModules(install model.AppInstall, modules []dto.NginxModule) error
return err
}
tmpPath := modulePath + ".tmp"
if err = os.WriteFile(tmpPath, content, constant.FilePerm); err != nil {
if err := os.WriteFile(tmpPath, content, constant.FilePerm); err != nil {
return err
}
return os.Rename(tmpPath, modulePath)
}
func activateNginxModuleCatalog(pendingPath, activePath string) error {
return os.Rename(pendingPath, activePath)
}
func activateNginxModuleCatalogAndCommit(pendingPath, activePath string, commit func() error) error {
previous, err := os.ReadFile(activePath)
if err != nil {
return err
}
if err = activateNginxModuleCatalog(pendingPath, activePath); err != nil {
return err
}
if err = commit(); err == nil {
return nil
}
if restoreErr := writeNginxModuleCatalog(activePath, previous); restoreErr != nil {
return fmt.Errorf("%w; restore previous OpenResty module catalog: %v", err, restoreErr)
}
return err
}
func stageNginxModuleCatalog(sourcePath, pendingPath string) error {
content, err := os.ReadFile(sourcePath)
if err != nil {
return err
}
return writeNginxModuleCatalog(pendingPath, content)
}
func writeNginxModuleCatalog(targetPath string, content []byte) error {
tmpPath := targetPath + ".tmp"
defer func() { _ = os.Remove(tmpPath) }()
if err := os.WriteFile(tmpPath, content, constant.FilePerm); err != nil {
return err
}
return os.Rename(tmpPath, targetPath)
}
func normalizeNginxModule(module *dto.NginxModule) {
if module.BuildMode == "" {
// Entries created before the dynamic-module schema must preserve their old behavior.
module.BuildMode = nginxModuleBuildStatic
}
if module.BuildMode == nginxModuleBuildAuto {
// auto is a legacy alias for the dynamic build mode.
module.BuildMode = nginxModuleBuildDynamic
}
if module.Provider == "" {
module.Provider = nginxModuleProviderLocal
}
if module.DynamicSupport == "" {
module.DynamicSupport = nginxModuleSupportUnknown
}
if module.LoadOrder == 0 {
module.LoadOrder = 50
}
module.Packages = compactStrings(module.Packages)
}
func validateNginxModuleBuildMode(module dto.NginxModule) error {
if module.BuildMode != nginxModuleBuildDynamic && module.BuildMode != nginxModuleBuildStatic {
return fmt.Errorf("OpenResty module %s has invalid build mode %q", module.Name, module.BuildMode)
}
return nil
}
func compactStrings(items []string) []string {
result := make([]string, 0, len(items))
seen := make(map[string]struct{})
@@ -864,12 +1061,19 @@ func compactStrings(items []string) []string {
return result
}
func nginxModuleStaticBuildErrorHint(module dto.NginxModule) string {
if module.Custom {
return nginxModuleStaticBuildHint
}
return ""
}
// nginxModuleNeedsDynamicBuild mirrors the dynamic-build filter of
// buildDynamicNginxModules. It normalizes a copy so prescan callers never
// mutate the stored entities.
func nginxModuleNeedsDynamicBuild(module dto.NginxModule, selectedNames map[string]struct{}) bool {
normalizeNginxModule(&module)
if module.Deleted || module.BuildMode == nginxModuleBuildStatic {
if module.BuildMode != nginxModuleBuildDynamic {
return false
}
if len(selectedNames) > 0 {
@@ -895,7 +1099,9 @@ func hasDynamicNginxModuleBuildTask(modules []dto.NginxModule, selected []string
func findLatestNginxModuleBuild(module dto.NginxModule, target dto.NginxModuleTarget) *dto.NginxModuleBuild {
var latest *dto.NginxModuleBuild
for i := range module.Builds {
if module.Builds[i].Target.Key != target.Key || module.Builds[i].Status != nginxModuleStatusReady {
if module.Builds[i].BuildMode != module.BuildMode ||
module.Builds[i].Target.Key != target.Key ||
module.Builds[i].Status != nginxModuleStatusReady {
continue
}
if latest == nil || module.Builds[i].BuiltAt.After(latest.BuiltAt) {
@@ -906,7 +1112,7 @@ func findLatestNginxModuleBuild(module dto.NginxModule, target dto.NginxModuleTa
}
func findCurrentNginxModuleBuild(module dto.NginxModule, target dto.NginxModuleTarget) *dto.NginxModuleBuild {
params, err := normalizeDynamicModuleParams(module.Params)
params, err := nginxModuleBuildParams(module)
if err != nil {
return nil
}
@@ -915,13 +1121,83 @@ func findCurrentNginxModuleBuild(module dto.NginxModule, target dto.NginxModuleT
return nil
}
for i := range module.Builds {
if module.Builds[i].Target.Key == target.Key && module.Builds[i].Hash == buildHash {
if module.Builds[i].BuildMode == module.BuildMode &&
module.Builds[i].Target.Key == target.Key &&
module.Builds[i].Hash == buildHash {
return &module.Builds[i]
}
}
return nil
}
func nginxModuleBuildParams(module dto.NginxModule) (string, error) {
if module.BuildMode == nginxModuleBuildStatic {
params := strings.TrimSpace(module.Params)
if params == "" {
return "", errors.New("static module parameters are empty")
}
return params, nil
}
return normalizeDynamicModuleParams(module.Params)
}
func recordStaticNginxModuleBuilds(modules []dto.NginxModule, target dto.NginxModuleTarget) ([]dto.NginxModule, error) {
for i := range modules {
module := &modules[i]
normalizeNginxModule(module)
if !module.Enable || module.BuildMode != nginxModuleBuildStatic {
continue
}
params, err := nginxModuleBuildParams(*module)
if err != nil {
return modules, err
}
buildHash, err := nginxModuleBuildHash(*module, target, params)
if err != nil {
return modules, err
}
upsertNginxModuleBuild(module, dto.NginxModuleBuild{
Provider: nginxModuleProviderLocal,
BuildMode: nginxModuleBuildStatic,
Status: nginxModuleStatusReady,
Hash: buildHash,
Target: target,
BuiltAt: time.Now(),
})
module.LastError = ""
}
return modules, nil
}
func commitStaticNginxModuleBuilds(install model.AppInstall, catalogPath string, parentTask *task.Task) error {
modules, err := loadNginxModulesWithCatalog(install, catalogPath)
if err != nil || !hasEnabledStaticNginxModules(modules) {
return err
}
status, err := checkContainerStatus(install.ContainerName)
if err != nil {
return err
}
if status != "running" {
return fmt.Errorf("OpenResty container %s is not running after static module build", install.ContainerName)
}
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
return err
}
target, targetWarning, err := resolveNginxRuntimeTarget(install)
if err != nil {
return err
}
if targetWarning != "" && parentTask != nil {
parentTask.Logf("WARNING: %s", targetWarning)
}
modules, err = recordStaticNginxModuleBuilds(modules, target)
if err != nil {
return err
}
return saveNginxModulesWithCatalog(install, modules, catalogPath)
}
func upsertNginxModuleBuild(module *dto.NginxModule, build dto.NginxModuleBuild) {
for i := range module.Builds {
if module.Builds[i].Target.Key == build.Target.Key && module.Builds[i].Hash == build.Hash {
@@ -946,16 +1222,13 @@ func cloneNginxModules(modules []dto.NginxModule) []dto.NginxModule {
return cloned
}
func recordNginxModuleBuildFailure(originalModules []dto.NginxModule, moduleName string, build dto.NginxModuleBuild, previousBuild *dto.NginxModuleBuild, dynamicCompileSucceeded bool) []dto.NginxModule {
func recordNginxModuleBuildFailure(originalModules []dto.NginxModule, moduleName string, build dto.NginxModuleBuild, previousBuild *dto.NginxModuleBuild) []dto.NginxModule {
failedModules := cloneNginxModules(originalModules)
for i := range failedModules {
if failedModules[i].Name != moduleName {
continue
}
failedModules[i].LastError = build.Error
if dynamicCompileSucceeded {
failedModules[i].DynamicSupport = nginxModuleSupportSupported
}
if previousBuild == nil || previousBuild.Status != nginxModuleStatusReady {
upsertNginxModuleBuild(&failedModules[i], build)
}
@@ -972,11 +1245,10 @@ func recordNginxModuleActivationFailure(previousModules, candidateModules []dto.
}
for i := range rollbackModules {
candidate, ok := candidates[rollbackModules[i].Name]
if !ok || candidate.Deleted || !candidate.Enable || candidate.BuildMode == nginxModuleBuildStatic {
if !ok || !candidate.Enable || candidate.BuildMode == nginxModuleBuildStatic {
continue
}
rollbackModules[i].LastError = activationErr.Error()
rollbackModules[i].DynamicSupport = candidate.DynamicSupport
}
return rollbackModules
}
@@ -1035,9 +1307,10 @@ func nginxModuleBuildHash(module dto.NginxModule, target dto.NginxModuleTarget,
Script string
Packages []string
Params string
BuildMode string
TargetKey string
Provider string
}{module.Name, module.Script, module.Packages, params, target.Key, module.Provider}
}{module.Name, module.Script, module.Packages, params, module.BuildMode, target.Key, module.Provider}
content, err := json.Marshal(payload)
if err != nil {
return "", err

View File

@@ -10,12 +10,13 @@ import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"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) {
func TestNormalizeNginxModuleDoesNotInferBuildMode(t *testing.T) {
module := dto.NginxModule{
Name: "legacy",
Packages: []string{"git", "", "git", " curl "},
@@ -23,8 +24,8 @@ func TestNormalizeNginxModulePreservesLegacyStaticMode(t *testing.T) {
normalizeNginxModule(&module)
if module.BuildMode != nginxModuleBuildStatic {
t.Fatalf("expected legacy module to remain static, got %s", module.BuildMode)
if module.BuildMode != "" {
t.Fatalf("build mode must be explicit, got %s", module.BuildMode)
}
if module.Provider != nginxModuleProviderLocal {
t.Fatalf("expected local provider, got %s", module.Provider)
@@ -105,19 +106,19 @@ func TestRecordNginxModuleBuildFailureKeepsPreviousReadyBuild(t *testing.T) {
Hash: "ready", Status: nginxModuleStatusReady, Target: target, BuiltAt: time.Now().Add(-time.Hour),
}
original := []dto.NginxModule{{
Name: "example", BuildMode: nginxModuleBuildDynamic, DynamicSupport: nginxModuleSupportUnknown,
Name: "example", BuildMode: nginxModuleBuildDynamic,
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)
result := recordNginxModuleBuildFailure(original, "example", failed, &ready)
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 {
if result[0].LastError != failed.Error {
t.Fatalf("failure metadata was not retained: %#v", result[0])
}
result[0].Builds[0].Hash = "mutated"
@@ -129,7 +130,6 @@ func TestRecordNginxModuleBuildFailureKeepsPreviousReadyBuild(t *testing.T) {
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) {
@@ -138,9 +138,6 @@ func TestHasDynamicNginxModuleBuildTask(t *testing.T) {
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")
}
@@ -160,6 +157,15 @@ func TestHasDynamicNginxModuleBuildTask(t *testing.T) {
}
}
func TestNginxModuleStaticBuildErrorHintOnlyForCustomModules(t *testing.T) {
if hint := nginxModuleStaticBuildErrorHint(dto.NginxModule{Name: "builtin"}); hint != "" {
t.Fatalf("built-in module cannot switch build mode, got hint %q", hint)
}
if hint := nginxModuleStaticBuildErrorHint(dto.NginxModule{Name: "custom", Custom: true}); hint == "" {
t.Fatal("custom module should receive the static-build alternative")
}
}
func TestResolveNginxModuleTargetWithoutBuilder(t *testing.T) {
oldDir := global.Dir.AppInstallDir
global.Dir.AppInstallDir = t.TempDir()
@@ -176,6 +182,22 @@ func TestResolveNginxModuleTargetWithoutBuilder(t *testing.T) {
}
}
func TestBuildDynamicNginxModulesFailsWithoutBuilder(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.31.1.1"}
install.App.Key = constant.AppOpenresty
modules := []dto.NginxModule{{
Name: "rtmp", Enable: true, BuildMode: nginxModuleBuildDynamic,
}}
_, err := buildDynamicNginxModules(install, modules, nil, false, "", "", nil)
if !errors.Is(err, errNginxModuleBuilderMissing) {
t.Fatalf("missing target builder must fail the dynamic build, got %v", err)
}
}
func TestMergeOpenrestyModuleVolumes(t *testing.T) {
newService := map[string]interface{}{}
oldService := map[string]interface{}{
@@ -226,97 +248,6 @@ func TestMergeOpenrestyModuleVolumesKeepsExisting(t *testing.T) {
}
}
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()
@@ -345,3 +276,401 @@ func TestResolveNginxModuleBuildMirror(t *testing.T) {
t.Fatalf("request mirror should still win over the env value, got %q", got)
}
}
func writeNginxModuleCatalogStateFixture(t *testing.T, install model.AppInstall, catalog []dto.NginxModule, state string) {
t.Helper()
buildDir := path.Join(install.GetPath(), nginxModuleBuildDir)
if err := os.MkdirAll(buildDir, constant.DirPerm); err != nil {
t.Fatal(err)
}
catalogContent, err := json.Marshal(catalog)
if err != nil {
t.Fatal(err)
}
if err = os.WriteFile(path.Join(buildDir, nginxModuleCatalogFile), catalogContent, constant.FilePerm); err != nil {
t.Fatal(err)
}
if state != "" {
if err = os.WriteFile(path.Join(buildDir, nginxModuleStoreFile), []byte(state), constant.FilePerm); err != nil {
t.Fatal(err)
}
}
}
func TestLoadNginxModulesBuiltinStateCannotOverrideCatalogDefinition(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.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{
Name: "rtmp", Script: "catalog-script", Packages: []string{"unzip"}, Params: "--add-module=/tmp/rtmp",
BuildMode: nginxModuleBuildDynamic, Provider: nginxModuleProviderLocal, LoadOrder: 20,
}}
state := `[{"name":"rtmp","enable":true,"script":"user-script","params":"--with-user","buildMode":"static","loadOrder":99,"lastError":"failed"}]`
writeNginxModuleCatalogStateFixture(t, install, catalog, state)
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
if len(modules) != 1 {
t.Fatalf("expected one merged module, got %#v", modules)
}
module := modules[0]
if !module.Enable || module.LastError != "failed" {
t.Fatalf("builtin state was not applied: %#v", module)
}
if module.Script != "catalog-script" || module.Params != "--add-module=/tmp/rtmp" ||
module.BuildMode != nginxModuleBuildDynamic || module.LoadOrder != 20 {
t.Fatalf("builtin definition must come from catalog: %#v", module)
}
}
func TestLoadNginxModulesWithoutStateShowsDisabledCatalogModules(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.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{
{Name: "rtmp", Enable: true, BuildMode: nginxModuleBuildDynamic},
{Name: "geoip2", BuildMode: nginxModuleBuildDynamic},
}
writeNginxModuleCatalogStateFixture(t, install, catalog, "")
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
if len(modules) != len(catalog) {
t.Fatalf("expected all catalog modules, got %#v", modules)
}
for _, module := range modules {
if module.Enable || module.Custom {
t.Fatalf("catalog modules must default to disabled built-ins: %#v", module)
}
}
}
func TestSaveNginxModulesPersistsOnlyBuiltinState(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.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{
Name: "rtmp", Script: "catalog-script", Params: "--add-module=/tmp/rtmp",
BuildMode: nginxModuleBuildDynamic, Provider: nginxModuleProviderLocal, LoadOrder: 20,
}}
writeNginxModuleCatalogStateFixture(t, install, catalog, "")
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
modules[0].Enable = true
modules[0].LastError = "failed"
if err = saveNginxModules(install, modules); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile))
if err != nil {
t.Fatal(err)
}
var states []map[string]any
if err = json.Unmarshal(content, &states); err != nil {
t.Fatal(err)
}
if len(states) != 1 || states[0]["name"] != "rtmp" || states[0]["enable"] != true || states[0]["lastError"] != "failed" {
t.Fatalf("unexpected builtin state: %s", content)
}
for _, key := range []string{"script", "packages", "params", "buildMode", "provider", "loadOrder"} {
if _, exists := states[0][key]; exists {
t.Fatalf("builtin definition field %q leaked into module state: %s", key, content)
}
}
}
func TestSaveNginxModulesOmitsPristineBuiltinState(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.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{Name: "rtmp", BuildMode: nginxModuleBuildDynamic}}
writeNginxModuleCatalogStateFixture(t, install, catalog, "")
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
if err = saveNginxModules(install, modules); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile))
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(content)) != "[]" {
t.Fatalf("pristine built-in state should not be persisted: %s", content)
}
}
func TestLoadAndSaveNginxCustomModule(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.31.1.1"}
install.App.Key = constant.AppOpenresty
state := `[{"name":"custom","custom":true,"script":"prepare","packages":["git"],"params":"--add-module=/tmp/custom","enable":true,"buildMode":"static","provider":"local","loadOrder":100}]`
writeNginxModuleCatalogStateFixture(t, install, nil, state)
modules, err := loadNginxModules(install)
if err != nil {
t.Fatal(err)
}
if len(modules) != 1 || !modules[0].Custom {
t.Fatalf("custom module source was not restored: %#v", modules)
}
if modules[0].Script != "prepare" || modules[0].BuildMode != nginxModuleBuildStatic || modules[0].LoadOrder != 100 {
t.Fatalf("custom module definition was not restored: %#v", modules[0])
}
if err = saveNginxModules(install, modules); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(content), `"custom": true`) || !strings.Contains(string(content), `"buildMode": "static"`) {
t.Fatalf("custom module definition was not persisted: %s", content)
}
}
func TestActivateNginxModuleCatalogReplacesCatalogAtomically(t *testing.T) {
buildDir := t.TempDir()
activePath := path.Join(buildDir, nginxModuleCatalogFile)
pendingPath := path.Join(buildDir, nginxModuleCatalogPendingFile)
sourcePath := path.Join(buildDir, "target.catalog.json")
if err := os.WriteFile(activePath, []byte(`[{"name":"old"}]`), constant.FilePerm); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(sourcePath, []byte(`[{"name":"new"}]`), constant.FilePerm); err != nil {
t.Fatal(err)
}
if err := stageNginxModuleCatalog(sourcePath, pendingPath); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(activePath)
if err != nil {
t.Fatal(err)
}
if string(content) != `[{"name":"old"}]` {
t.Fatalf("staging target catalog changed the active catalog: %s", content)
}
if err := activateNginxModuleCatalog(pendingPath, activePath); err != nil {
t.Fatal(err)
}
content, err = os.ReadFile(activePath)
if err != nil {
t.Fatal(err)
}
if string(content) != `[{"name":"new"}]` {
t.Fatalf("active catalog was not replaced: %s", content)
}
if _, err = os.Stat(pendingPath); !os.IsNotExist(err) {
t.Fatalf("pending catalog should be consumed, got %v", err)
}
}
func TestActivateNginxModuleCatalogRestoresCatalogWhenCommitFails(t *testing.T) {
buildDir := t.TempDir()
activePath := path.Join(buildDir, nginxModuleCatalogFile)
pendingPath := path.Join(buildDir, nginxModuleCatalogPendingFile)
if err := os.WriteFile(activePath, []byte(`[{"name":"old"}]`), constant.FilePerm); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(pendingPath, []byte(`[{"name":"new"}]`), constant.FilePerm); err != nil {
t.Fatal(err)
}
commitErr := errors.New("save install failed")
err := activateNginxModuleCatalogAndCommit(pendingPath, activePath, func() error {
return commitErr
})
if !errors.Is(err, commitErr) {
t.Fatalf("expected commit error, got %v", err)
}
content, readErr := os.ReadFile(activePath)
if readErr != nil {
t.Fatal(readErr)
}
if string(content) != `[{"name":"old"}]` {
t.Fatalf("failed upgrade must restore the old catalog: %s", content)
}
}
func TestLoadNginxModulesRejectsDuplicateCatalogNames(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.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{
{Name: "rtmp", Params: "--add-module=/tmp/one", BuildMode: nginxModuleBuildDynamic},
{Name: "rtmp", Params: "--add-module=/tmp/two", BuildMode: nginxModuleBuildDynamic},
}
writeNginxModuleCatalogStateFixture(t, install, catalog, "")
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "duplicate") {
t.Fatalf("expected duplicate catalog name error, got %v", err)
}
}
func TestLoadNginxModulesRejectsDuplicateStateNames(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.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{Name: "rtmp", Params: "--add-module=/tmp/rtmp", BuildMode: nginxModuleBuildDynamic}}
writeNginxModuleCatalogStateFixture(t, install, catalog, `[{"name":"rtmp","enable":true},{"name":"rtmp","enable":false}]`)
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "duplicate") {
t.Fatalf("expected duplicate state name error, got %v", err)
}
}
func TestLoadNginxModulesRejectsOrphanBuiltinState(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.31.1.1"}
install.App.Key = constant.AppOpenresty
writeNginxModuleCatalogStateFixture(t, install, nil, `[{"name":"removed","enable":true}]`)
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "missing from the module catalog") {
t.Fatalf("expected orphan builtin state error, got %v", err)
}
}
func TestLoadNginxModulesRejectsCustomCatalogNameConflict(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.31.1.1"}
install.App.Key = constant.AppOpenresty
catalog := []dto.NginxModule{{Name: "rtmp", BuildMode: nginxModuleBuildDynamic}}
writeNginxModuleCatalogStateFixture(t, install, catalog, `[{"name":"rtmp","custom":true,"enable":true}]`)
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "conflicts") {
t.Fatalf("expected custom/catalog conflict error, got %v", err)
}
}
func TestLoadNginxModulesRejectsInvalidBuildModes(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.31.1.1"}
install.App.Key = constant.AppOpenresty
writeNginxModuleCatalogStateFixture(t, install, []dto.NginxModule{{
Name: "rtmp", BuildMode: "auto",
}}, "")
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "invalid build mode") {
t.Fatalf("expected invalid catalog build mode error, got %v", err)
}
writeNginxModuleCatalogStateFixture(t, install, nil, `[{"name":"custom","custom":true,"buildMode":"auto"}]`)
if _, err := loadNginxModules(install); err == nil || !strings.Contains(err.Error(), "invalid build mode") {
t.Fatalf("expected invalid custom build mode error, got %v", err)
}
}
func TestApplyNginxModuleUpdateKeepsBuiltinDefinitionImmutable(t *testing.T) {
modules := []dto.NginxModule{{
Name: "rtmp", Script: "catalog-script", Params: "--add-module=/tmp/rtmp",
BuildMode: nginxModuleBuildDynamic, Provider: nginxModuleProviderLocal, LoadOrder: 20,
}}
req := request.NginxModuleUpdate{
Operate: nginxModuleOperateUpdate, Name: "rtmp", Enable: true, Script: "user-script",
Params: "--with-user", BuildMode: nginxModuleBuildStatic, Provider: "prebuilt", LoadOrder: 99,
}
updated, _, err := applyNginxModuleUpdate(modules, req)
if err != nil {
t.Fatal(err)
}
if !updated[0].Enable {
t.Fatal("builtin enable state was not updated")
}
if updated[0].Script != "catalog-script" || updated[0].Params != "--add-module=/tmp/rtmp" ||
updated[0].BuildMode != nginxModuleBuildDynamic || updated[0].Provider != nginxModuleProviderLocal ||
updated[0].LoadOrder != 20 {
t.Fatalf("builtin definition was modified: %#v", updated[0])
}
}
func TestApplyNginxModuleUpdateRejectsBuiltinDelete(t *testing.T) {
modules := []dto.NginxModule{{Name: "rtmp", BuildMode: nginxModuleBuildDynamic}}
_, _, err := applyNginxModuleUpdate(modules, request.NginxModuleUpdate{
Operate: nginxModuleOperateDelete, Name: "rtmp",
})
if err == nil || !strings.Contains(err.Error(), "cannot be deleted") {
t.Fatalf("expected builtin delete error, got %v", err)
}
}
func TestApplyNginxModuleUpdateCreatesAndDeletesCustomModule(t *testing.T) {
if _, _, err := applyNginxModuleUpdate(nil, request.NginxModuleUpdate{
Operate: nginxModuleOperateCreate, Name: "invalid", BuildMode: "auto",
}); err == nil || !strings.Contains(err.Error(), "invalid build mode") {
t.Fatalf("expected invalid custom build mode error, got %v", err)
}
created, _, err := applyNginxModuleUpdate(nil, request.NginxModuleUpdate{
Operate: nginxModuleOperateCreate, Name: "custom", Script: "prepare", Packages: "git,curl",
Params: "--add-module=/tmp/custom", Enable: true, BuildMode: nginxModuleBuildStatic,
Provider: nginxModuleProviderLocal, LoadOrder: 100,
})
if err != nil {
t.Fatal(err)
}
if len(created) != 1 || !created[0].Custom || created[0].BuildMode != nginxModuleBuildStatic {
t.Fatalf("custom module was not created correctly: %#v", created)
}
updated, _, err := applyNginxModuleUpdate(created, request.NginxModuleUpdate{
Operate: nginxModuleOperateUpdate, Name: "custom", Script: "updated",
Params: "--add-module=/tmp/custom-v2", Enable: false, BuildMode: nginxModuleBuildDynamic,
Provider: nginxModuleProviderLocal, LoadOrder: 75,
})
if err != nil {
t.Fatal(err)
}
if updated[0].Script != "updated" || updated[0].BuildMode != nginxModuleBuildDynamic || updated[0].LoadOrder != 75 {
t.Fatalf("custom module was not updated correctly: %#v", updated[0])
}
remaining, deleted, err := applyNginxModuleUpdate(updated, request.NginxModuleUpdate{
Operate: nginxModuleOperateDelete, Name: "custom",
})
if err != nil {
t.Fatal(err)
}
if len(remaining) != 0 || deleted == nil || deleted.Name != "custom" {
t.Fatalf("custom module was not removed: remaining=%#v deleted=%#v", remaining, deleted)
}
}

View File

@@ -44,17 +44,16 @@ export namespace Nginx {
export interface NginxModule {
name: string;
custom: boolean;
script?: string;
packages?: string;
enable: boolean;
params: string;
buildMode: 'auto' | 'dynamic' | 'static';
buildMode: '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;
}
@@ -72,7 +71,7 @@ export namespace Nginx {
packages?: string;
enable?: boolean;
params?: string;
buildMode?: 'auto' | 'dynamic' | 'static';
buildMode?: 'dynamic' | 'static';
provider?: 'local' | 'prebuilt';
loadOrder?: number;
}

View File

@@ -8,7 +8,15 @@
:trigger="trigger"
:dropdown-style="dropdownStyle"
/>
<el-table-column v-else v-bind="$attrs" :label="label" :width="resolvedWidth" :align="align" :fixed="resolvedFixed">
<el-table-column
v-else
v-bind="$attrs"
:label="label"
:width="resolvedWidth"
:min-width="resolvedMinWidth"
:align="align"
:fixed="resolvedFixed"
>
<template #default="{ row }">
<FuTableOperationActions
:buttons="buttons"
@@ -137,9 +145,16 @@ const estimatedWidth = computed(() => {
});
const resolvedWidth = computed(() => {
if (props.width === 'auto') {
return undefined;
}
return normalizeWidth(props.width) ?? estimatedWidth.value;
});
const resolvedMinWidth = computed(() => {
return props.width === 'auto' ? normalizeWidth(props.minWidth) : undefined;
});
const dropdownStyle = computed(() => {
if (props.maxHeight === undefined || props.maxHeight === null || props.maxHeight === '') {
return undefined;

View File

@@ -3806,7 +3806,6 @@ const message = {
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',
@@ -3816,10 +3815,6 @@ const message = {
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',

View File

@@ -3849,7 +3849,6 @@ const message = {
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',
@@ -3859,10 +3858,6 @@ const message = {
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',

View File

@@ -3775,7 +3775,6 @@ const message = {
buildModeDynamic: 'پویا',
buildModeStatic: 'ایستا',
buildStatus: 'وضعیت ساخت',
compatibility: 'سازگاری',
loadOrder: 'ترتیب بارگذاری',
modulesToBuild: 'ماژول‌های پویا (بارگذاری گرم پس از ساخت، بدون راه‌اندازی مجدد کانتینر)',
staticModules: 'ماژول‌های ایستا',
@@ -3785,10 +3784,6 @@ const message = {
pending: 'در انتظار',
ready: 'آماده',
failed: 'ناموفق',
unknown: 'نامشخص',
compatible: 'سازگار',
stale: 'نیاز به ساخت مجدد',
static: 'ساخت ایستا',
dynamicUnsupported: 'ساخت پویا در نسخه فعلی OpenResty پشتیبانی نمی‌شود',
moduleDynamicUnsupported: 'پارامترهای این ماژول از ساخت پویا پشتیبانی نمی‌کنند',
mirrorUrl: 'منبع نرم‌افزار',

View File

@@ -3824,7 +3824,6 @@ const message = {
buildModeDynamic: '動的',
buildModeStatic: '静的',
buildStatus: 'ビルドステータス',
compatibility: '互換性',
loadOrder: '読み込み順',
modulesToBuild: '動的モジュールビルド後ホットリロードコンテナ再起動なし',
staticModules: '静的モジュール',
@@ -3834,10 +3833,6 @@ const message = {
pending: 'ビルド待ち',
ready: 'ビルド済み',
failed: 'ビルド失敗',
unknown: '不明',
compatible: '互換',
stale: '再ビルドが必要',
static: '静的ビルド',
dynamicUnsupported: '現在の OpenResty バージョンでは動的ビルドはサポートされていません',
moduleDynamicUnsupported: 'このモジュールのパラメータは動的ビルドに対応していません',
mirrorUrl: 'ソフトウェアソース',

View File

@@ -3743,7 +3743,6 @@ const message = {
buildModeDynamic: '동적',
buildModeStatic: '정적',
buildStatus: '빌드 상태',
compatibility: '호환성',
loadOrder: '로드 순서',
modulesToBuild: '동적 모듈 (빌드 후 핫 리로드, 컨테이너 재시작 없음)',
staticModules: '정적 모듈',
@@ -3753,10 +3752,6 @@ const message = {
pending: '대기 중',
ready: '준비됨',
failed: '실패',
unknown: '알 수 없음',
compatible: '호환됨',
stale: '다시 빌드 필요',
static: '정적 빌드',
dynamicUnsupported: '현재 OpenResty 버전에서는 동적 빌드를 지원하지 않습니다',
moduleDynamicUnsupported: '이 모듈의 파라미터는 동적 빌드를 지원하지 않습니다',
mirrorUrl: '소프트웨어 소스',

View File

@@ -3714,7 +3714,6 @@ const message = {
buildModeDynamic: 'ໄດນາມິກ',
buildModeStatic: 'ສະແຕຕິກ',
buildStatus: 'ສະຖານະການບິວ',
compatibility: 'ຄວາມເຂົ້າກັນ',
loadOrder: 'ລຳດັບການໂຫຼດ',
modulesToBuild: 'ໂມດູນໄດນາມິກ (ຫຼັງຈາກບິວໂຫຼດຮ້ອນ, ຄອນເທນເນີບໍ່ຣີສະຕາດ)',
staticModules: 'ໂມດູນສະແຕຕິກ',
@@ -3724,10 +3723,6 @@ const message = {
pending: 'ລໍຖ້າ',
ready: 'ພ້ອມແລ້ວ',
failed: 'ລົ້ມເຫຼວ',
unknown: 'ບໍ່ຮູ້',
compatible: 'ເຂົ້າກັນໄດ້',
stale: 'ລ້າສະໄໝ',
static: 'ສະແຕຕິກ',
dynamicUnsupported: 'ເວີຊັນ OpenResty ປັດຈຸບັນບໍ່ຮອງຮັບການບິວແບບໄດນາມິກ',
moduleDynamicUnsupported: 'ພາລາມິເຕີຂອງໂມດູນນີ້ບໍ່ຮອງຮັບການບິວແບບໄດນາມິກ',
mirrorUrl: 'ແຫຼ່ງຊອບແວ',

View File

@@ -3879,7 +3879,6 @@ const message = {
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',
@@ -3889,10 +3888,6 @@ const message = {
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',

View File

@@ -4015,7 +4015,6 @@ const message = {
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',
@@ -4025,10 +4024,6 @@ const message = {
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',

View File

@@ -3867,7 +3867,6 @@ const message = {
buildModeDynamic: 'Динамическая',
buildModeStatic: 'Статическая',
buildStatus: 'Статус сборки',
compatibility: 'Совместимость',
loadOrder: 'Порядок загрузки',
modulesToBuild: 'Динамические модули (горячая перезагрузка после сборки, без перезапуска контейнера)',
staticModules: 'Статические модули',
@@ -3877,10 +3876,6 @@ const message = {
pending: 'Ожидание',
ready: 'Готово',
failed: 'Ошибка',
unknown: 'Неизвестно',
compatible: 'Совместим',
stale: 'Требуется пересборка',
static: 'Статическая сборка',
dynamicUnsupported: 'Динамическая сборка не поддерживается текущей версией OpenResty',
moduleDynamicUnsupported: 'Параметры этого модуля не поддерживают динамическую сборку',
mirrorUrl: 'Источник программного обеспечения',

View File

@@ -3866,7 +3866,6 @@ const message = {
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',
@@ -3876,10 +3875,6 @@ const message = {
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ğı',

View File

@@ -3532,7 +3532,6 @@ const message = {
buildModeDynamic: '動態模組',
buildModeStatic: '靜態模組',
buildStatus: '構建狀態',
compatibility: '相容性',
loadOrder: '載入順序',
modulesToBuild: '動態模組(構建後熱載入,容器不重新啟動)',
staticModules: '靜態模組',
@@ -3542,10 +3541,6 @@ const message = {
pending: '待構建',
ready: '已構建',
failed: '構建失敗',
unknown: '未知',
compatible: '相容',
stale: '需要重新構建',
static: '靜態編譯',
dynamicUnsupported: '目前 OpenResty 版本不支援動態構建',
moduleDynamicUnsupported: '此模組參數不支援動態構建',
mirrorUrl: '軟體源',

View File

@@ -3531,7 +3531,6 @@ const message = {
buildModeDynamic: '动态模块',
buildModeStatic: '静态模块',
buildStatus: '构建状态',
compatibility: '兼容性',
loadOrder: '加载顺序',
modulesToBuild: '动态模块(构建后热加载,容器不重启)',
staticModules: '静态模块',
@@ -3541,10 +3540,6 @@ const message = {
pending: '待构建',
ready: '已构建',
failed: '构建失败',
unknown: '未知',
compatible: '兼容',
stale: '需要重新构建',
static: '静态编译',
dynamicUnsupported: '当前 OpenResty 版本不支持动态构建',
moduleDynamicUnsupported: '该模块参数不支持动态构建',
mirrorUrl: '软件源',

View File

@@ -10,15 +10,15 @@
</el-button>
<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 :label="$t('nginx.buildMode')" width="150">
<el-table-column prop="name" :label="$t('commons.table.name')" min-width="320" />
<el-table-column :label="$t('nginx.buildMode')" min-width="160" align="center">
<template #default="{ row }">
<el-tag effect="plain" :type="row.buildMode === 'static' ? 'warning' : 'primary'">
{{ $t('nginx.buildMode' + capitalize(displayBuildMode(row.buildMode))) }}
{{ $t('nginx.buildMode' + capitalize(row.buildMode)) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('nginx.buildStatus')" width="120">
<el-table-column :label="$t('nginx.buildStatus')" min-width="160" align="center">
<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>
@@ -26,21 +26,15 @@
<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>
<el-table-column :label="$t('commons.table.status')" min-width="160" align="center" fix>
<template #default="{ row }">
<el-switch v-permission v-model="row.enable" @change="updateModule(row)" />
</template>
</el-table-column>
<fu-table-operations
:ellipsis="2"
width="200px"
width="auto"
min-width="160"
:buttons="buttons"
:label="$t('commons.table.operate')"
fixed="right"
@@ -64,9 +58,18 @@ import { onMounted, ref } from 'vue';
const data = ref<Nginx.NginxModule[]>([]);
const loading = ref(false);
const buttons = [
{
label: i18n.global.t('commons.button.view'),
permission: true,
show: (row: Nginx.NginxModule) => !row.custom,
click: function (row: Nginx.NginxModule) {
openView(row);
},
},
{
label: i18n.global.t('commons.button.edit'),
permission: true,
show: (row: Nginx.NginxModule) => row.custom,
click: function (row: Nginx.NginxModule) {
openEdit(row);
},
@@ -74,6 +77,7 @@ const buttons = [
{
label: i18n.global.t('commons.button.delete'),
permission: true,
show: (row: Nginx.NginxModule) => row.custom,
click: function (row: Nginx.NginxModule) {
deleteModule(row);
},
@@ -108,6 +112,10 @@ const openEdit = (row: Nginx.NginxModule) => {
operateRef.value.acceptParams('update', row, dynamicSupported.value);
};
const openView = (row: Nginx.NginxModule) => {
operateRef.value.acceptParams('view', row, dynamicSupported.value);
};
const updateModule = (row: Nginx.NginxModule) => {
loading.value = true;
const data = {
@@ -129,21 +137,12 @@ const updateModule = (row: Nginx.NginxModule) => {
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

@@ -1,21 +1,18 @@
<template>
<DrawerPro
v-model="open"
:header="$t('nginx.' + mode)"
:header="mode === 'view' ? $t('commons.button.view') : $t('nginx.' + mode)"
size="large"
:resource="mode === 'update' ? module.name : ''"
:resource="mode === 'create' ? '' : module.name"
@close="handleClose"
>
<el-form ref="moduleForm" label-position="top" :model="module" :rules="rules">
<el-form ref="moduleForm" label-position="top" :model="module" :rules="rules" :disabled="mode === 'view'">
<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'"
>
<el-radio-button value="dynamic" :disabled="!dynamicSupported">
{{ $t('nginx.buildModeDynamic') }}
</el-radio-button>
<el-radio-button value="static">{{ $t('nginx.buildModeStatic') }}</el-radio-button>
@@ -23,9 +20,6 @@
<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>
@@ -55,8 +49,16 @@
/>
</el-form>
<template #footer>
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button v-permission type="primary" @click="submit(moduleForm)" :disabled="loading">
<el-button @click="handleClose" :disabled="loading">
{{ $t(mode === 'view' ? 'commons.button.close' : 'commons.button.cancel') }}
</el-button>
<el-button
v-if="mode !== 'view'"
v-permission
type="primary"
@click="submit(moduleForm)"
:disabled="loading"
>
{{ $t('commons.button.confirm') }}
</el-button>
</template>
@@ -87,7 +89,6 @@ type ModuleForm = {
packages: string;
buildMode: Nginx.NginxModule['buildMode'];
provider: Nginx.NginxModule['provider'];
dynamicSupport: Nginx.NginxModule['dynamicSupport'];
loadOrder: number;
lastError: string;
};
@@ -100,7 +101,6 @@ const defaultModule = (): ModuleForm => ({
packages: '',
buildMode: 'dynamic',
provider: 'local',
dynamicSupport: 'unknown',
loadOrder: 50,
lastError: '',
});
@@ -120,19 +120,18 @@ const acceptParams = async (operate: string, editModule?: Nginx.NginxModule, sup
mode.value = operate;
dynamicSupported.value = supported ?? true;
module.value = defaultModule();
if (operate === 'update' && editModule) {
if ((operate === 'update' || operate === 'view') && editModule) {
module.value = {
name: editModule.name,
script: editModule.script || '',
enable: editModule.enable,
params: editModule.params,
packages: editModule.packages || '',
buildMode: editModule.buildMode === 'auto' ? 'dynamic' : editModule.buildMode,
buildMode: editModule.buildMode,
provider: editModule.provider,
dynamicSupport: editModule.dynamicSupport,
loadOrder: editModule.loadOrder,
lastError: editModule.lastError || '',
operate: 'update',
operate,
};
}
open.value = true;

View File

@@ -1,16 +1,12 @@
# OpenResty Dynamic Module Linux Tests
# OpenResty Dynamic Module Diagnostics
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.
This script collects diagnostics from an installed 1Panel OpenResty instance.
## 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:
@@ -25,71 +21,6 @@ Make the scripts executable:
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:
@@ -130,42 +61,5 @@ 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.
task is running. Review the diagnostic archive before sharing it.

View File

@@ -1,542 +0,0 @@
#!/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 "$@"