mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
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:
@@ -89,22 +89,22 @@ var LBAlgorithms = map[string]struct{}{"ip_hash": {}, "least_conn": {}}
|
||||
var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}}
|
||||
|
||||
type NginxModule struct {
|
||||
Name string `json:"name"`
|
||||
Script string `json:"script"`
|
||||
Packages []string `json:"packages"`
|
||||
Params string `json:"params"`
|
||||
Enable bool `json:"enable"`
|
||||
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"`
|
||||
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"`
|
||||
BuildMode string `json:"buildMode,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
LoadOrder int `json:"loadOrder,omitempty"`
|
||||
Builds []NginxModuleBuild `json:"builds,omitempty"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
}
|
||||
|
||||
type NginxModuleBuild struct {
|
||||
Provider string `json:"provider"`
|
||||
BuildMode string `json:"buildMode"`
|
||||
Status string `json:"status"`
|
||||
Hash string `json:"hash"`
|
||||
Target NginxModuleTarget `json:"target"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -69,20 +69,19 @@ type NginxProxyCache struct {
|
||||
}
|
||||
|
||||
type NginxModule struct {
|
||||
Name string `json:"name"`
|
||||
Script string `json:"script"`
|
||||
Packages string `json:"packages"`
|
||||
Params string `json:"params"`
|
||||
Enable bool `json:"enable"`
|
||||
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"`
|
||||
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"`
|
||||
LoadOrder int `json:"loadOrder"`
|
||||
BuildStatus string `json:"buildStatus"`
|
||||
LoadStatus string `json:"loadStatus"`
|
||||
Artifacts []dto.NginxModuleArtifact `json:"artifacts"`
|
||||
LastError string `json:"lastError"`
|
||||
}
|
||||
|
||||
type NginxBuildConfig struct {
|
||||
|
||||
@@ -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 {
|
||||
@@ -807,8 +808,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
install.Status = constant.StatusUpgrading
|
||||
|
||||
var (
|
||||
upErr error
|
||||
backupFile string
|
||||
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,15 +905,16 @@ 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 {
|
||||
return err
|
||||
}
|
||||
if err := syncNginxModuleBuilder(detailBuildDir, installBuildDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if fileOp.Stat(path.Join(detailBuildDir, nginxModuleCatalogFile)) {
|
||||
if err := fileOp.CopyFile(path.Join(detailBuildDir, nginxModuleCatalogFile), installBuildDir); err != nil {
|
||||
return err
|
||||
}
|
||||
targetCatalogSource := path.Join(detailBuildDir, nginxModuleCatalogFile)
|
||||
if !fileOp.Stat(targetCatalogSource) {
|
||||
return fmt.Errorf("target OpenResty module catalog not found: %s", targetCatalogSource)
|
||||
}
|
||||
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,8 +1063,24 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||
}
|
||||
t.LogSuccess(logStr)
|
||||
install.Status = constant.StatusRunning
|
||||
if err = appInstallRepo.Save(context.Background(), &install); err != nil {
|
||||
return err
|
||||
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())
|
||||
@@ -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)
|
||||
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 {
|
||||
|
||||
@@ -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 {
|
||||
buildStatus = nginxModuleStatusReady
|
||||
compatibility = "static"
|
||||
if module.Enable {
|
||||
loadStatus = nginxModuleLoadEnabled
|
||||
if module.BuildMode == nginxModuleBuildStatic && runtimeTargetErr == nil {
|
||||
if build := findCurrentNginxModuleBuild(module, runtimeTarget); build != nil && build.Status == nginxModuleStatusReady {
|
||||
buildStatus = nginxModuleStatusReady
|
||||
if module.Enable {
|
||||
loadStatus = nginxModuleLoadEnabled
|
||||
}
|
||||
} else if findLatestNginxModuleBuild(module, runtimeTarget) != nil {
|
||||
if module.Enable {
|
||||
loadStatus = nginxModuleLoadEnabled
|
||||
}
|
||||
}
|
||||
} else if targetErr == nil {
|
||||
if build := findCurrentNginxModuleBuild(module, target); build != nil {
|
||||
buildStatus = build.Status
|
||||
artifacts = build.Artifacts
|
||||
if build.Status == nginxModuleStatusReady {
|
||||
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
|
||||
@@ -250,20 +256,19 @@ func (n NginxService) GetModules() (*response.NginxBuildConfig, error) {
|
||||
buildStatus = nginxModuleStatusFailed
|
||||
}
|
||||
resList = append(resList, response.NginxModule{
|
||||
Name: module.Name,
|
||||
Script: module.Script,
|
||||
Packages: strings.Join(module.Packages, ","),
|
||||
Params: module.Params,
|
||||
Enable: module.Enable,
|
||||
BuildMode: module.BuildMode,
|
||||
Provider: module.Provider,
|
||||
DynamicSupport: module.DynamicSupport,
|
||||
LoadOrder: module.LoadOrder,
|
||||
BuildStatus: buildStatus,
|
||||
LoadStatus: loadStatus,
|
||||
Compatibility: compatibility,
|
||||
Artifacts: artifacts,
|
||||
LastError: module.LastError,
|
||||
Name: module.Name,
|
||||
Custom: module.Custom,
|
||||
Script: module.Script,
|
||||
Packages: strings.Join(module.Packages, ","),
|
||||
Params: module.Params,
|
||||
Enable: module.Enable,
|
||||
BuildMode: module.BuildMode,
|
||||
Provider: module.Provider,
|
||||
LoadOrder: module.LoadOrder,
|
||||
BuildStatus: buildStatus,
|
||||
LoadStatus: loadStatus,
|
||||
Artifacts: artifacts,
|
||||
LastError: module.LastError,
|
||||
})
|
||||
}
|
||||
envs, err := gotenv.Read(nginxInstall.GetEnvPath())
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -51,20 +46,21 @@ const (
|
||||
nginxModuleOperateUpdate = "update"
|
||||
nginxModuleOperateDelete = "delete"
|
||||
|
||||
nginxModuleBuildDir = "build"
|
||||
nginxModuleModulesDir = "modules"
|
||||
nginxModuleConfDir = "conf"
|
||||
nginxModuleTmpDir = "tmp"
|
||||
nginxModuleEnabledConfDir = "modules-enabled"
|
||||
nginxModuleStagingDir = ".staging"
|
||||
nginxModuleLibDir = "lib"
|
||||
nginxModuleBuilderFile = "Dockerfile.modules"
|
||||
nginxModuleStoreFile = "module.json"
|
||||
nginxModuleCatalogFile = "module.catalog.json"
|
||||
nginxModulePreScriptFile = "module-pre.sh"
|
||||
nginxModuleConfigArgsFile = "module-config.args"
|
||||
nginxModuleStaticPreScript = "pre.sh"
|
||||
nginxModuleManifestFile = "manifest.json"
|
||||
nginxModuleBuildDir = "build"
|
||||
nginxModuleModulesDir = "modules"
|
||||
nginxModuleConfDir = "conf"
|
||||
nginxModuleTmpDir = "tmp"
|
||||
nginxModuleEnabledConfDir = "modules-enabled"
|
||||
nginxModuleStagingDir = ".staging"
|
||||
nginxModuleLibDir = "lib"
|
||||
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"
|
||||
nginxModuleManifestFile = "manifest.json"
|
||||
|
||||
nginxModuleContainerRoot = "/usr/local/openresty/nginx/modules/1panel"
|
||||
nginxModuleConfigPrefix = "1panel-module-"
|
||||
@@ -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,26 +201,7 @@ 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
|
||||
return modules, err
|
||||
}
|
||||
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)
|
||||
@@ -257,10 +275,11 @@ func (localNginxModuleProvider) Resolve(spec nginxModuleBuildSpec) (dto.NginxMod
|
||||
return dto.NginxModuleBuild{}, err
|
||||
}
|
||||
result := dto.NginxModuleBuild{
|
||||
Provider: nginxModuleProviderLocal,
|
||||
Status: nginxModuleStatusPending,
|
||||
Hash: buildHash,
|
||||
Target: spec.Target,
|
||||
Provider: nginxModuleProviderLocal,
|
||||
BuildMode: nginxModuleBuildDynamic,
|
||||
Status: nginxModuleStatusPending,
|
||||
Hash: buildHash,
|
||||
Target: spec.Target,
|
||||
}
|
||||
modulesRoot := path.Join(spec.Install.GetPath(), nginxModuleModulesDir)
|
||||
if !spec.Force {
|
||||
@@ -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
|
||||
}
|
||||
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
|
||||
}
|
||||
// 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 != "" {
|
||||
global.LOG.Warn(targetWarning)
|
||||
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())
|
||||
return err
|
||||
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
|
||||
}
|
||||
if modules[index].DynamicSupport == "" {
|
||||
modules[index].DynamicSupport = catalogModule.DynamicSupport
|
||||
}
|
||||
if modules[index].LoadOrder == 0 {
|
||||
modules[index].LoadOrder = catalogModule.LoadOrder
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
for i := range modules {
|
||||
if modules[i].Deleted || modules[i].BuildMode == nginxModuleBuildStatic {
|
||||
continue
|
||||
}
|
||||
if _, err := normalizeDynamicModuleParams(modules[i].Params); err != nil {
|
||||
modules[i].DynamicSupport = nginxModuleSupportUnsupported
|
||||
} else {
|
||||
modules[i].DynamicSupport = nginxModuleSupportSupported
|
||||
func readNginxModuleStateFile(filePath string) ([]nginxModuleState, error) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []nginxModuleState{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'منبع نرمافزار',
|
||||
|
||||
@@ -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: 'ソフトウェアソース',
|
||||
|
||||
@@ -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: '소프트웨어 소스',
|
||||
|
||||
@@ -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: 'ແຫຼ່ງຊອບແວ',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'Источник программного обеспечения',
|
||||
|
||||
@@ -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ğı',
|
||||
|
||||
@@ -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: '軟體源',
|
||||
|
||||
@@ -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: '软件源',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 "$@"
|
||||
Reference in New Issue
Block a user