mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 16:13:59 +08:00
1389 lines
47 KiB
Go
1389 lines
47 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
|
dockerUtils "github.com/1Panel-dev/1Panel/agent/utils/docker"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
|
"github.com/mattn/go-shellwords"
|
|
"github.com/subosito/gotenv"
|
|
)
|
|
|
|
const (
|
|
nginxModuleBuildDynamic = "dynamic"
|
|
nginxModuleBuildStatic = "static"
|
|
|
|
nginxModuleProviderLocal = "local"
|
|
|
|
nginxModuleStatusPending = "pending"
|
|
nginxModuleStatusReady = "ready"
|
|
nginxModuleStatusFailed = "failed"
|
|
|
|
nginxModuleLoadEnabled = "enabled"
|
|
nginxModuleLoadDisabled = "disabled"
|
|
|
|
nginxModuleOperateCreate = "create"
|
|
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"
|
|
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-"
|
|
|
|
nginxModuleStaticBuildHint = "; switch the module to static build mode to compile it with the full image"
|
|
)
|
|
|
|
var errNginxModuleBuilderMissing = errors.New("dynamic module builder not found")
|
|
|
|
type nginxModuleBuildSpec struct {
|
|
Install model.AppInstall
|
|
Module dto.NginxModule
|
|
Target dto.NginxModuleTarget
|
|
BuildPath string
|
|
Force bool
|
|
Mirror string
|
|
Task *task.Task
|
|
}
|
|
|
|
// nginxModuleArtifactProvider keeps local builds replaceable by a future
|
|
// precompiled-artifact resolver without changing module state or API contracts.
|
|
type nginxModuleArtifactProvider interface {
|
|
Name() string
|
|
Resolve(spec nginxModuleBuildSpec) (dto.NginxModuleBuild, error)
|
|
}
|
|
|
|
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 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,
|
|
}
|
|
envContent, _ := os.ReadFile(install.GetEnvPath())
|
|
images, imageErr := dockerUtils.GetImagesFromDockerCompose(envContent, []byte(install.DockerCompose))
|
|
if imageErr != nil || len(images) == 0 {
|
|
images, imageErr = compose.GetComposeImages(install.GetComposePath())
|
|
}
|
|
var warning string
|
|
if imageErr == nil && len(images) > 0 {
|
|
target.Image = images[0]
|
|
inspectMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute))
|
|
inspectOut, inspectErr := inspectMgr.RunWithStdout("docker", "image", "inspect", "--format={{.Id}}\t{{.Architecture}}", target.Image)
|
|
if inspectErr != nil {
|
|
warning = fmt.Sprintf("inspect OpenResty image %s failed, module rebuilds will fall back to host architecture %s: %v", target.Image, target.Architecture, inspectErr)
|
|
} else if fields := strings.Fields(inspectOut); len(fields) >= 2 {
|
|
target.ImageDigest = fields[0]
|
|
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]))
|
|
}
|
|
|
|
// nginxModuleDynamicSupported reports whether the installed version ships both
|
|
// the dynamic module builder and the module catalog.
|
|
func nginxModuleDynamicSupported(install model.AppInstall) bool {
|
|
fileOp := files.NewFileOp()
|
|
buildPath := path.Join(install.GetPath(), nginxModuleBuildDir)
|
|
return fileOp.Stat(path.Join(buildPath, nginxModuleBuilderFile)) &&
|
|
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.
|
|
func resolveNginxModuleBuildMirror(install model.AppInstall, mirror string) string {
|
|
if mirror != "" {
|
|
return mirror
|
|
}
|
|
envs, err := gotenv.Read(install.GetEnvPath())
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(envs["CONTAINER_PACKAGE_URL"])
|
|
}
|
|
|
|
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) {
|
|
return modules, nil
|
|
}
|
|
target, targetWarning, err := resolveNginxModuleTarget(install)
|
|
if err != nil {
|
|
return modules, err
|
|
}
|
|
if targetWarning != "" {
|
|
parentTask.Logf("WARNING: %s", targetWarning)
|
|
}
|
|
originalModules := cloneNginxModules(modules)
|
|
selectedNames := make(map[string]struct{}, len(selected))
|
|
for _, name := range selected {
|
|
selectedNames[name] = struct{}{}
|
|
}
|
|
buildPath := path.Join(install.GetPath(), nginxModuleBuildDir)
|
|
buildMirror := resolveNginxModuleBuildMirror(install, mirror)
|
|
for i := range modules {
|
|
module := &modules[i]
|
|
normalizeNginxModule(module)
|
|
if !nginxModuleNeedsDynamicBuild(*module, selectedNames) {
|
|
continue
|
|
}
|
|
provider, providerErr := getNginxModuleProvider(module.Provider)
|
|
if providerErr != nil {
|
|
return modules, providerErr
|
|
}
|
|
previousBuild := findCurrentNginxModuleBuild(*module, target)
|
|
build, buildErr := provider.Resolve(nginxModuleBuildSpec{
|
|
Install: install, Module: *module, Target: target, BuildPath: buildPath, Force: force, Mirror: buildMirror, Task: parentTask,
|
|
})
|
|
if buildErr != nil {
|
|
build.Provider = provider.Name()
|
|
build.Status = nginxModuleStatusFailed
|
|
build.Target = target
|
|
build.Error = buildErr.Error()
|
|
build.BuiltAt = time.Now()
|
|
failedModules := recordNginxModuleBuildFailure(originalModules, module.Name, build, previousBuild)
|
|
removeNginxModuleOutputsNotReferenced(install, modules, failedModules)
|
|
_ = saveNginxModulesWithCatalog(install, failedModules, catalogPath)
|
|
return failedModules, fmt.Errorf("build dynamic module %s: %w%s", module.Name, buildErr, nginxModuleStaticBuildErrorHint(*module))
|
|
}
|
|
err = validateNginxModuleArtifacts(install, build.Artifacts)
|
|
if err != nil {
|
|
for outputDir := range nginxModuleOutputDirectories(install, []dto.NginxModule{{Builds: []dto.NginxModuleBuild{build}}}) {
|
|
_ = os.RemoveAll(outputDir)
|
|
}
|
|
build.Status = nginxModuleStatusFailed
|
|
build.Error = err.Error()
|
|
build.Artifacts = nil
|
|
build.BuiltAt = time.Now()
|
|
failedModules := recordNginxModuleBuildFailure(originalModules, module.Name, build, previousBuild)
|
|
removeNginxModuleOutputsNotReferenced(install, modules, failedModules)
|
|
_ = saveNginxModulesWithCatalog(install, failedModules, catalogPath)
|
|
return failedModules, fmt.Errorf("validate dynamic module %s: %w%s", module.Name, err, nginxModuleStaticBuildErrorHint(*module))
|
|
}
|
|
module.LastError = ""
|
|
upsertNginxModuleBuild(module, build)
|
|
}
|
|
return modules, nil
|
|
}
|
|
|
|
func (localNginxModuleProvider) Resolve(spec nginxModuleBuildSpec) (dto.NginxModuleBuild, error) {
|
|
params, err := normalizeDynamicModuleParams(spec.Module.Params)
|
|
if err != nil {
|
|
return dto.NginxModuleBuild{}, err
|
|
}
|
|
configureArgs, err := parseDynamicModuleParams(params)
|
|
if err != nil {
|
|
return dto.NginxModuleBuild{}, err
|
|
}
|
|
if err = validateNginxModulePackages(spec.Module.Packages); err != nil {
|
|
return dto.NginxModuleBuild{}, err
|
|
}
|
|
buildHash, err := nginxModuleBuildHash(spec.Module, spec.Target, params)
|
|
if err != nil {
|
|
return dto.NginxModuleBuild{}, err
|
|
}
|
|
result := dto.NginxModuleBuild{
|
|
Provider: nginxModuleProviderLocal,
|
|
BuildMode: nginxModuleBuildDynamic,
|
|
Status: nginxModuleStatusPending,
|
|
Hash: buildHash,
|
|
Target: spec.Target,
|
|
}
|
|
modulesRoot := path.Join(spec.Install.GetPath(), nginxModuleModulesDir)
|
|
if !spec.Force {
|
|
if current := findCurrentNginxModuleBuild(spec.Module, spec.Target); current != nil && current.Status == nginxModuleStatusReady {
|
|
if validateNginxModuleArtifacts(spec.Install, current.Artifacts) == nil {
|
|
return *current, nil
|
|
}
|
|
}
|
|
}
|
|
outputRevision := buildHash
|
|
if spec.Force {
|
|
outputRevision = fmt.Sprintf("%s-r%d", buildHash, time.Now().UnixNano())
|
|
}
|
|
modulePathName := nginxModulePathName(spec.Module.Name)
|
|
finalPath := path.Join(modulesRoot, spec.Target.Key, modulePathName, outputRevision)
|
|
buildComplete := false
|
|
defer func() {
|
|
if !buildComplete {
|
|
_ = os.RemoveAll(finalPath)
|
|
}
|
|
}()
|
|
|
|
preScriptPath := path.Join(spec.BuildPath, nginxModuleTmpDir, nginxModulePreScriptFile)
|
|
if err = os.WriteFile(preScriptPath, []byte("#!/bin/bash\nset -e\n"+spec.Module.Script+"\n"), constant.FilePerm); err != nil {
|
|
return result, err
|
|
}
|
|
defer os.Remove(preScriptPath)
|
|
configureArgsPath := path.Join(spec.BuildPath, nginxModuleTmpDir, nginxModuleConfigArgsFile)
|
|
if err = os.WriteFile(configureArgsPath, []byte(strings.Join(configureArgs, "\n")+"\n"), constant.FilePerm); err != nil {
|
|
return result, err
|
|
}
|
|
defer os.Remove(configureArgsPath)
|
|
|
|
shortHash := buildHash
|
|
if len(shortHash) > 12 {
|
|
shortHash = shortHash[:12]
|
|
}
|
|
taskSuffix := sanitizeModulePathPart(spec.Task.TaskID)
|
|
if len(taskSuffix) > 8 {
|
|
taskSuffix = taskSuffix[:8]
|
|
}
|
|
tempImage := fmt.Sprintf("1panel/openresty-module-builder:%s-%s-%s", modulePathName, shortHash, taskSuffix)
|
|
tempContainer := fmt.Sprintf("1panel-module-%s-%s-%s", modulePathName, shortHash, taskSuffix)
|
|
commandMgr := cmd.NewCommandMgr(cmd.WithTask(*spec.Task), cmd.WithTimeout(120*time.Minute))
|
|
buildArgs := []string{
|
|
"build", "--target", "module-output",
|
|
"-f", path.Join(spec.BuildPath, nginxModuleBuilderFile),
|
|
"-t", tempImage,
|
|
"--build-arg", "PANEL_OPENRESTY_VERSION=" + spec.Install.Version,
|
|
"--build-arg", "RESTY_ADD_PACKAGE_BUILDDEPS=" + strings.Join(spec.Module.Packages, " "),
|
|
}
|
|
if spec.Mirror != "" {
|
|
buildArgs = append(buildArgs, "--build-arg", "CONTAINER_PACKAGE_URL="+spec.Mirror)
|
|
}
|
|
buildArgs = append(buildArgs, spec.BuildPath)
|
|
if err = commandMgr.Run("docker", buildArgs...); err != nil {
|
|
return result, err
|
|
}
|
|
cleanupMgr := cmd.NewCommandMgr(cmd.WithTimeout(5 * time.Minute))
|
|
defer func() {
|
|
_ = cleanupMgr.Run("docker", "rm", "-f", tempContainer)
|
|
_ = cleanupMgr.Run("docker", "image", "rm", "-f", tempImage)
|
|
}()
|
|
if err = commandMgr.Run("docker", "create", "--name", tempContainer, tempImage, "/bin/true"); err != nil {
|
|
return result, err
|
|
}
|
|
|
|
stagingPath := path.Join(spec.Install.GetPath(), nginxModuleModulesDir, nginxModuleStagingDir, modulePathName+"-"+shortHash)
|
|
_ = os.RemoveAll(stagingPath)
|
|
if err = os.MkdirAll(stagingPath, constant.DirPerm); err != nil {
|
|
return result, err
|
|
}
|
|
defer os.RemoveAll(stagingPath)
|
|
if err = commandMgr.Run("docker", "cp", tempContainer+":/out/.", stagingPath); err != nil {
|
|
return result, err
|
|
}
|
|
if artifacts, artifactErr := collectNginxModuleArtifacts(stagingPath, stagingPath); artifactErr != nil || len(artifacts) == 0 {
|
|
if artifactErr != nil {
|
|
return result, artifactErr
|
|
}
|
|
return result, errors.New("dynamic module build produced no loadable .so files")
|
|
}
|
|
if err = os.MkdirAll(path.Dir(finalPath), constant.DirPerm); err != nil {
|
|
return result, err
|
|
}
|
|
_ = os.RemoveAll(finalPath)
|
|
if err = os.Rename(stagingPath, finalPath); err != nil {
|
|
return result, err
|
|
}
|
|
artifacts, err := collectNginxModuleArtifacts(finalPath, modulesRoot)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Status = nginxModuleStatusReady
|
|
result.Artifacts = artifacts
|
|
result.BuiltAt = time.Now()
|
|
manifest, _ := json.MarshalIndent(result, "", " ")
|
|
_ = os.WriteFile(path.Join(finalPath, nginxModuleManifestFile), manifest, constant.FilePerm)
|
|
buildComplete = true
|
|
return result, 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)
|
|
_ = saveNginxModulesWithCatalog(install, rollbackModules, catalogPath)
|
|
removeNginxModuleOutputsNotReferenced(install, modules, previousModules)
|
|
return err
|
|
}
|
|
removeNginxModuleOutputsNotReferenced(install, previousModules, modules)
|
|
return nil
|
|
}
|
|
|
|
func validateNginxModuleArtifacts(install model.AppInstall, artifacts []dto.NginxModuleArtifact) error {
|
|
if len(artifacts) == 0 {
|
|
return errors.New("module build produced no loadable artifacts")
|
|
}
|
|
modulesRoot := filepath.Clean(path.Join(install.GetPath(), nginxModuleModulesDir))
|
|
seen := make(map[string]struct{}, len(artifacts))
|
|
for _, artifact := range artifacts {
|
|
if _, ok := seen[artifact.Path]; ok {
|
|
return fmt.Errorf("duplicate module artifact path %q", artifact.Path)
|
|
}
|
|
seen[artifact.Path] = struct{}{}
|
|
if artifact.Name != path.Base(artifact.Path) {
|
|
return fmt.Errorf("module artifact name %q does not match path %q", artifact.Name, artifact.Path)
|
|
}
|
|
if !re.IsValidNginxModuleChecksum(artifact.Checksum) {
|
|
return fmt.Errorf("invalid checksum for module artifact %q", artifact.Path)
|
|
}
|
|
if !re.IsValidNginxModuleArtifact(artifact.Path) {
|
|
return fmt.Errorf("invalid module artifact path %q", artifact.Path)
|
|
}
|
|
artifactFullPath := filepath.Clean(filepath.Join(modulesRoot, filepath.FromSlash(artifact.Path)))
|
|
if !strings.HasPrefix(artifactFullPath, modulesRoot+string(os.PathSeparator)) {
|
|
return fmt.Errorf("module artifact path %q escapes the module directory", artifact.Path)
|
|
}
|
|
info, err := os.Lstat(artifactFullPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return fmt.Errorf("module artifact %q is not a regular file", artifact.Path)
|
|
}
|
|
file, err := os.Open(artifactFullPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hash := sha256.New()
|
|
_, copyErr := io.Copy(hash, file)
|
|
closeErr := file.Close()
|
|
if copyErr != nil {
|
|
return copyErr
|
|
}
|
|
if closeErr != nil {
|
|
return closeErr
|
|
}
|
|
actualChecksum := hex.EncodeToString(hash.Sum(nil))
|
|
if !strings.EqualFold(actualChecksum, artifact.Checksum) {
|
|
return fmt.Errorf("checksum mismatch for module artifact %q", artifact.Path)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func collectNginxModuleArtifacts(root, relativeRoot string) ([]dto.NginxModuleArtifact, error) {
|
|
var artifacts []dto.NginxModuleArtifact
|
|
err := filepath.Walk(root, func(filePath string, info os.FileInfo, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if info.IsDir() || filepath.Ext(info.Name()) != ".so" || filepath.Base(filepath.Dir(filePath)) == nginxModuleLibDir {
|
|
return nil
|
|
}
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hash := sha256.New()
|
|
_, copyErr := io.Copy(hash, file)
|
|
_ = file.Close()
|
|
if copyErr != nil {
|
|
return copyErr
|
|
}
|
|
relativePath, err := filepath.Rel(relativeRoot, filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
artifacts = append(artifacts, dto.NginxModuleArtifact{
|
|
Name: info.Name(), Path: filepath.ToSlash(relativePath), Checksum: hex.EncodeToString(hash.Sum(nil)),
|
|
})
|
|
return nil
|
|
})
|
|
sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].Name < artifacts[j].Name })
|
|
return artifacts, err
|
|
}
|
|
|
|
func validateNginxModuleLoadConfig(install model.AppInstall, target dto.NginxModuleTarget, validationName, loadDirectives string) error {
|
|
if target.Image == "" {
|
|
return errors.New("target OpenResty image is not resolved")
|
|
}
|
|
var config strings.Builder
|
|
config.WriteString(loadDirectives)
|
|
config.WriteString("events {}\nhttp {}\n")
|
|
configSum := sha256.Sum256([]byte(config.String()))
|
|
testConfig := path.Join(install.GetPath(), nginxModuleModulesDir, ".test-"+nginxModulePathName(validationName)+"-"+hex.EncodeToString(configSum[:6])+".conf")
|
|
if err := os.WriteFile(testConfig, []byte(config.String()), constant.FilePerm); err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(testConfig)
|
|
modulesRoot := path.Join(install.GetPath(), nginxModuleModulesDir)
|
|
args := []string{
|
|
"run", "--rm", "--network", "none",
|
|
"-v", modulesRoot + ":" + nginxModuleContainerRoot + ":ro",
|
|
"-v", testConfig + ":/tmp/1panel-module-test.conf:ro",
|
|
"--entrypoint", "/usr/local/openresty/nginx/sbin/nginx",
|
|
target.Image, "-t", "-c", "/tmp/1panel-module-test.conf",
|
|
}
|
|
return cmd.NewCommandMgr(cmd.WithTimeout(5*time.Minute)).Run("docker", args...)
|
|
}
|
|
|
|
type nginxModuleConfigSnapshot map[string][]byte
|
|
|
|
type openrestyUpgradeSnapshot struct {
|
|
installPath string
|
|
backupPath string
|
|
existing map[string]bool
|
|
}
|
|
|
|
var openrestyUpgradeSnapshotPaths = []string{
|
|
nginxModuleBuildDir,
|
|
nginxModuleModulesDir,
|
|
"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 {
|
|
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
|
|
}
|
|
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 {
|
|
return err
|
|
}
|
|
snapshot, err := snapshotManagedNginxModuleConfigs(configDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
desired := make(map[string][]byte)
|
|
var combinedLoadDirectives strings.Builder
|
|
sortedModules := append([]dto.NginxModule(nil), modules...)
|
|
sort.SliceStable(sortedModules, func(i, j int) bool {
|
|
if sortedModules[i].LoadOrder == sortedModules[j].LoadOrder {
|
|
return sortedModules[i].Name < sortedModules[j].Name
|
|
}
|
|
return sortedModules[i].LoadOrder < sortedModules[j].LoadOrder
|
|
})
|
|
for _, module := range sortedModules {
|
|
normalizeNginxModule(&module)
|
|
if !module.Enable || module.BuildMode == nginxModuleBuildStatic {
|
|
continue
|
|
}
|
|
build := findCurrentNginxModuleBuild(module, target)
|
|
if build == nil || build.Status != nginxModuleStatusReady {
|
|
build = findLatestNginxModuleBuild(module, target)
|
|
}
|
|
if build == nil || build.Status != nginxModuleStatusReady {
|
|
continue
|
|
}
|
|
// Artifacts were already validated after the build; re-validating here
|
|
// is a deliberate guard against on-disk tampering before writing
|
|
// load_module directives.
|
|
if err = validateNginxModuleArtifacts(install, build.Artifacts); err != nil {
|
|
return fmt.Errorf("validate artifacts for dynamic module %s: %w", module.Name, err)
|
|
}
|
|
var content strings.Builder
|
|
content.WriteString("# Managed by 1Panel. Manual changes will be overwritten.\n")
|
|
for _, artifact := range build.Artifacts {
|
|
content.WriteString("load_module ")
|
|
content.WriteString(path.Join(nginxModuleContainerRoot, artifact.Path))
|
|
content.WriteString(";\n")
|
|
combinedLoadDirectives.WriteString("load_module ")
|
|
combinedLoadDirectives.WriteString(path.Join(nginxModuleContainerRoot, artifact.Path))
|
|
combinedLoadDirectives.WriteString(";\n")
|
|
}
|
|
fileName := fmt.Sprintf("%s%04d-%s.conf", nginxModuleConfigPrefix, module.LoadOrder, nginxModulePathName(module.Name))
|
|
desired[fileName] = []byte(content.String())
|
|
}
|
|
if combinedLoadDirectives.Len() > 0 {
|
|
if err = validateNginxModuleLoadConfig(install, target, "combined", combinedLoadDirectives.String()); err != nil {
|
|
return fmt.Errorf("validate combined dynamic module configuration: %w", err)
|
|
}
|
|
}
|
|
if err = applyManagedNginxModuleConfigs(configDir, desired); err != nil {
|
|
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
|
|
return err
|
|
}
|
|
if !reload {
|
|
return nil
|
|
}
|
|
status, statusErr := checkContainerStatus(install.ContainerName)
|
|
if statusErr != nil || status != "running" {
|
|
return nil
|
|
}
|
|
if err = opNginx(install.ContainerName, constant.NginxCheck); err != nil {
|
|
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
|
|
return err
|
|
}
|
|
if err = opNginx(install.ContainerName, constant.NginxReload); err != nil {
|
|
_ = applyManagedNginxModuleConfigs(configDir, snapshot)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func snapshotManagedNginxModuleConfigs(configDir string) (nginxModuleConfigSnapshot, error) {
|
|
snapshot := make(nginxModuleConfigSnapshot)
|
|
entries, err := os.ReadDir(configDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxModuleConfigPrefix) {
|
|
continue
|
|
}
|
|
content, readErr := os.ReadFile(path.Join(configDir, entry.Name()))
|
|
if readErr != nil {
|
|
return nil, readErr
|
|
}
|
|
snapshot[entry.Name()] = content
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
func applyManagedNginxModuleConfigs(configDir string, desired map[string][]byte) error {
|
|
entries, err := os.ReadDir(configDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for fileName, content := range desired {
|
|
tmpPath := path.Join(configDir, "."+fileName+".tmp")
|
|
if err = os.WriteFile(tmpPath, content, constant.FilePerm); err != nil {
|
|
return err
|
|
}
|
|
if err = os.Rename(tmpPath, path.Join(configDir, fileName)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasPrefix(entry.Name(), nginxModuleConfigPrefix) {
|
|
continue
|
|
}
|
|
if _, ok := desired[entry.Name()]; !ok {
|
|
if err = os.Remove(path.Join(configDir, entry.Name())); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasEnabledStaticNginxModules(modules []dto.NginxModule) bool {
|
|
for _, module := range modules {
|
|
normalizeNginxModule(&module)
|
|
if module.Enable && module.BuildMode == nginxModuleBuildStatic {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func staticNginxBuildRequired(install model.AppInstall, modules []dto.NginxModule) bool {
|
|
if hasEnabledStaticNginxModules(modules) {
|
|
return true
|
|
}
|
|
envs, err := gotenv.Read(install.GetEnvPath())
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return strings.TrimSpace(envs["RESTY_CONFIG_OPTIONS_MORE"]) != ""
|
|
}
|
|
|
|
func configureStaticNginxModules(install model.AppInstall, modules []dto.NginxModule, mirror string) error {
|
|
buildPath := path.Join(install.GetPath(), nginxModuleBuildDir)
|
|
var params, packages []string
|
|
preScriptPath := path.Join(buildPath, nginxModuleTmpDir, nginxModuleStaticPreScript)
|
|
preScript, err := os.OpenFile(preScriptPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, module := range modules {
|
|
normalizeNginxModule(&module)
|
|
if !module.Enable || module.BuildMode != nginxModuleBuildStatic {
|
|
continue
|
|
}
|
|
if _, err = preScript.WriteString(module.Script + "\n"); err != nil {
|
|
_ = preScript.Close()
|
|
return err
|
|
}
|
|
params = append(params, module.Params)
|
|
packages = append(packages, module.Packages...)
|
|
}
|
|
if err = preScript.Close(); err != nil {
|
|
return err
|
|
}
|
|
envs, err := gotenv.Read(install.GetEnvPath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if mirror != "" {
|
|
envs["CONTAINER_PACKAGE_URL"] = mirror
|
|
}
|
|
envs["RESTY_CONFIG_OPTIONS_MORE"] = strings.Join(compactStrings(params), " ")
|
|
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = strings.Join(compactStrings(packages), " ")
|
|
return gotenv.Write(envs, install.GetEnvPath())
|
|
}
|
|
|
|
func executeStaticNginxModuleBuild(install model.AppInstall, modules []dto.NginxModule, mirror string, force bool, parentTask *task.Task) error {
|
|
if err := configureStaticNginxModules(install, modules, mirror); err != nil {
|
|
return err
|
|
}
|
|
commandMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(120*time.Minute))
|
|
if err := commandMgr.Run("docker", "compose", "-f", install.GetComposePath(), "build"); err != nil {
|
|
return err
|
|
}
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = commitNginxModuleBuilds(install, previousModules, modules, false, ""); err != nil {
|
|
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 {
|
|
modules, err := loadNginxModules(install)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
staticBuild := staticNginxBuildRequired(install, modules)
|
|
if !staticBuild && hasDynamicNginxModuleBuildTask(modules, reqModules) {
|
|
if !nginxModuleDynamicSupported(install) {
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return commitNginxModuleBuilds(install, previousModules, modules, reload, "")
|
|
}
|
|
|
|
func removeNginxModuleArtifacts(install model.AppInstall, module dto.NginxModule) error {
|
|
for _, build := range module.Builds {
|
|
moduleDir := path.Join(install.GetPath(), nginxModuleModulesDir, build.Target.Key, nginxModulePathName(module.Name))
|
|
if err := files.NewFileOp().DeleteDir(moduleDir); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
states, err := readNginxModuleStateFile(modulePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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
|
|
}
|
|
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
|
|
}
|
|
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])
|
|
}
|
|
return modules, nil
|
|
}
|
|
|
|
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) {
|
|
content, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return []dto.NginxModule{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
if len(strings.TrimSpace(string(content))) == 0 {
|
|
return []dto.NginxModule{}, nil
|
|
}
|
|
var modules []dto.NginxModule
|
|
if err = json.Unmarshal(content, &modules); err != nil {
|
|
return nil, fmt.Errorf("parse OpenResty module configuration %s: %w", filePath, err)
|
|
}
|
|
return modules, nil
|
|
}
|
|
|
|
func saveNginxModules(install model.AppInstall, modules []dto.NginxModule) error {
|
|
return saveNginxModulesWithCatalog(install, modules, "")
|
|
}
|
|
|
|
func saveNginxModulesWithCatalog(install model.AppInstall, modules []dto.NginxModule, catalogPath string) error {
|
|
if catalogPath == "" {
|
|
catalogPath = path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
|
|
}
|
|
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
|
|
}
|
|
modulePath := path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleStoreFile)
|
|
if err = os.MkdirAll(path.Dir(modulePath), constant.DirPerm); err != nil {
|
|
return err
|
|
}
|
|
tmpPath := modulePath + ".tmp"
|
|
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.Provider == "" {
|
|
module.Provider = nginxModuleProviderLocal
|
|
}
|
|
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{})
|
|
for _, item := range items {
|
|
item = strings.TrimSpace(item)
|
|
if item == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[item]; ok {
|
|
continue
|
|
}
|
|
seen[item] = struct{}{}
|
|
result = append(result, item)
|
|
}
|
|
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.BuildMode != nginxModuleBuildDynamic {
|
|
return false
|
|
}
|
|
if len(selectedNames) > 0 {
|
|
_, ok := selectedNames[module.Name]
|
|
return ok
|
|
}
|
|
return module.Enable
|
|
}
|
|
|
|
func hasDynamicNginxModuleBuildTask(modules []dto.NginxModule, selected []string) bool {
|
|
selectedNames := make(map[string]struct{}, len(selected))
|
|
for _, name := range selected {
|
|
selectedNames[name] = struct{}{}
|
|
}
|
|
for _, module := range modules {
|
|
if nginxModuleNeedsDynamicBuild(module, selectedNames) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func findLatestNginxModuleBuild(module dto.NginxModule, target dto.NginxModuleTarget) *dto.NginxModuleBuild {
|
|
var latest *dto.NginxModuleBuild
|
|
for i := range module.Builds {
|
|
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) {
|
|
latest = &module.Builds[i]
|
|
}
|
|
}
|
|
return latest
|
|
}
|
|
|
|
func findCurrentNginxModuleBuild(module dto.NginxModule, target dto.NginxModuleTarget) *dto.NginxModuleBuild {
|
|
params, err := nginxModuleBuildParams(module)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
buildHash, err := nginxModuleBuildHash(module, target, params)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
for i := range module.Builds {
|
|
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 {
|
|
module.Builds[i] = build
|
|
return
|
|
}
|
|
}
|
|
module.Builds = append(module.Builds, build)
|
|
}
|
|
|
|
func cloneNginxModules(modules []dto.NginxModule) []dto.NginxModule {
|
|
cloned := make([]dto.NginxModule, len(modules))
|
|
copy(cloned, modules)
|
|
for i := range cloned {
|
|
cloned[i].Packages = append([]string(nil), modules[i].Packages...)
|
|
cloned[i].Builds = make([]dto.NginxModuleBuild, len(modules[i].Builds))
|
|
copy(cloned[i].Builds, modules[i].Builds)
|
|
for j := range cloned[i].Builds {
|
|
cloned[i].Builds[j].Artifacts = append([]dto.NginxModuleArtifact(nil), modules[i].Builds[j].Artifacts...)
|
|
}
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
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 previousBuild == nil || previousBuild.Status != nginxModuleStatusReady {
|
|
upsertNginxModuleBuild(&failedModules[i], build)
|
|
}
|
|
break
|
|
}
|
|
return failedModules
|
|
}
|
|
|
|
func recordNginxModuleActivationFailure(previousModules, candidateModules []dto.NginxModule, activationErr error) []dto.NginxModule {
|
|
rollbackModules := cloneNginxModules(previousModules)
|
|
candidates := make(map[string]dto.NginxModule, len(candidateModules))
|
|
for _, module := range candidateModules {
|
|
candidates[module.Name] = module
|
|
}
|
|
for i := range rollbackModules {
|
|
candidate, ok := candidates[rollbackModules[i].Name]
|
|
if !ok || !candidate.Enable || candidate.BuildMode == nginxModuleBuildStatic {
|
|
continue
|
|
}
|
|
rollbackModules[i].LastError = activationErr.Error()
|
|
}
|
|
return rollbackModules
|
|
}
|
|
|
|
func normalizeDynamicModuleParams(params string) (string, error) {
|
|
params = strings.TrimSpace(params)
|
|
params = strings.ReplaceAll(params, "--add-module=", "--add-dynamic-module=")
|
|
if !strings.Contains(params, "--add-dynamic-module=") && !strings.Contains(params, "=dynamic") {
|
|
return "", errors.New("module does not declare a dynamic configure option")
|
|
}
|
|
if _, err := parseDynamicModuleParams(params); err != nil {
|
|
return "", err
|
|
}
|
|
return params, nil
|
|
}
|
|
|
|
func parseDynamicModuleParams(params string) ([]string, error) {
|
|
// shellwords silently stops at unquoted shell metacharacters instead of
|
|
// reporting them, so reject them on the raw input before parsing.
|
|
if strings.ContainsAny(params, "\x00\r\n;&|<>") {
|
|
return nil, errors.New("dynamic module parameters contain unsupported characters")
|
|
}
|
|
parser := shellwords.NewParser()
|
|
parser.ParseBacktick = false
|
|
parser.ParseEnv = false
|
|
args, err := parser.Parse(params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse dynamic module parameters: %w", err)
|
|
}
|
|
if len(args) == 0 {
|
|
return nil, errors.New("dynamic module parameters are empty")
|
|
}
|
|
for _, arg := range args {
|
|
if !strings.HasPrefix(arg, "--") {
|
|
return nil, fmt.Errorf("unsupported configure argument %q", arg)
|
|
}
|
|
if strings.ContainsAny(arg, "\x00\r\n;&|<>") {
|
|
return nil, fmt.Errorf("configure argument %q contains unsupported characters", arg)
|
|
}
|
|
}
|
|
return args, nil
|
|
}
|
|
|
|
func validateNginxModulePackages(packages []string) error {
|
|
for _, item := range packages {
|
|
if !re.IsValidNginxModulePackage(item) {
|
|
return fmt.Errorf("invalid build package %q", item)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func nginxModuleBuildHash(module dto.NginxModule, target dto.NginxModuleTarget, params string) (string, error) {
|
|
payload := struct {
|
|
Name string
|
|
Script string
|
|
Packages []string
|
|
Params string
|
|
BuildMode string
|
|
TargetKey string
|
|
Provider string
|
|
}{module.Name, module.Script, module.Packages, params, module.BuildMode, target.Key, module.Provider}
|
|
content, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sum := sha256.Sum256(content)
|
|
return hex.EncodeToString(sum[:]), nil
|
|
}
|
|
|
|
func getNginxModuleProvider(name string) (nginxModuleArtifactProvider, error) {
|
|
switch name {
|
|
case "", nginxModuleProviderLocal:
|
|
return localNginxModuleProvider{}, nil
|
|
case "prebuilt":
|
|
return nil, errors.New("prebuilt module provider is not configured; use the local provider")
|
|
default:
|
|
return nil, fmt.Errorf("unknown module artifact provider %q", name)
|
|
}
|
|
}
|
|
|
|
func nginxModuleOutputDirectories(install model.AppInstall, modules []dto.NginxModule) map[string]struct{} {
|
|
result := make(map[string]struct{})
|
|
modulesRoot := filepath.Clean(path.Join(install.GetPath(), nginxModuleModulesDir))
|
|
for _, module := range modules {
|
|
for _, build := range module.Builds {
|
|
for _, artifact := range build.Artifacts {
|
|
if !re.IsValidNginxModuleArtifact(artifact.Path) {
|
|
continue
|
|
}
|
|
artifactFullPath := filepath.Clean(filepath.Join(modulesRoot, filepath.FromSlash(artifact.Path)))
|
|
if !strings.HasPrefix(artifactFullPath, modulesRoot+string(os.PathSeparator)) {
|
|
continue
|
|
}
|
|
outputDir := filepath.Dir(artifactFullPath)
|
|
if outputDir != modulesRoot {
|
|
result[outputDir] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func removeNginxModuleOutputsNotReferenced(install model.AppInstall, fromModules, referencedModules []dto.NginxModule) {
|
|
referenced := nginxModuleOutputDirectories(install, referencedModules)
|
|
for outputDir := range nginxModuleOutputDirectories(install, fromModules) {
|
|
if _, ok := referenced[outputDir]; !ok {
|
|
_ = os.RemoveAll(outputDir)
|
|
}
|
|
}
|
|
}
|
|
|
|
func sanitizeModulePathPart(value string) string {
|
|
var result strings.Builder
|
|
for _, char := range value {
|
|
if char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' || char == '.' || char == '-' || char == '_' {
|
|
result.WriteRune(char)
|
|
} else {
|
|
result.WriteByte('-')
|
|
}
|
|
}
|
|
return strings.Trim(result.String(), "-")
|
|
}
|
|
|
|
func nginxModulePathName(value string) string {
|
|
base := sanitizeModulePathPart(value)
|
|
if base == "" {
|
|
base = "module"
|
|
}
|
|
if len(base) > 48 {
|
|
base = base[:48]
|
|
}
|
|
sum := sha256.Sum256([]byte(value))
|
|
return fmt.Sprintf("%s-%s", base, hex.EncodeToString(sum[:4]))
|
|
}
|