fix: deep bug hunt II — concurrency, security & optimization (25 files)
Critical: watchdog mutex panic safety, SetGeoAppOverride nil guard, SSD-only app DB restore fallback. High: double deploy race (atomic Deploying flag), delete/remove during deploy guard, ScanStacks overwrite protection, FileBrowser mount mutex, PushEvent history, PushOnce error handling, DB dump sync+close before rename, restic retry fresh context, encrypt failure logging, cross-backup path traversal validation, deepCopyStack completeness. Security: constant-time API key comparison, login rate limiting (5/min), git credential masking in logs, storage path prefix traversal fix. Concurrency: MigrateEncryption lock ordering, SubdomainInUse I/O outside lock, scheduler late-registered jobs, SQLite WAL verification, metrics shutdown context, telemetry scan error logging, asset sync lock scope. Optimization: streaming file copy for DB dumps, restic stats dedup, atomic infra config copy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -56,24 +57,35 @@ func validateSubdomain(s string) error {
|
||||
// SubdomainInUse checks if a subdomain is already used by any deployed stack
|
||||
// other than excludeStack.
|
||||
func (m *Manager) SubdomainInUse(subdomain, excludeStack string) bool {
|
||||
// Collect stack dirs and metadata under lock, then do I/O outside the lock.
|
||||
type candidate struct {
|
||||
dir string
|
||||
metaSubdomain string
|
||||
}
|
||||
var candidates []candidate
|
||||
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for name, stack := range m.stacks {
|
||||
if name == excludeStack || !stack.Deployed {
|
||||
continue
|
||||
}
|
||||
stackDir := filepath.Dir(stack.ComposePath)
|
||||
appCfg := LoadAppConfig(stackDir)
|
||||
candidates = append(candidates, candidate{
|
||||
dir: filepath.Dir(stack.ComposePath),
|
||||
metaSubdomain: stack.Meta.Subdomain,
|
||||
})
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
for _, c := range candidates {
|
||||
appCfg := LoadAppConfig(c.dir)
|
||||
if appCfg == nil {
|
||||
continue
|
||||
}
|
||||
// Check stored SUBDOMAIN first
|
||||
if sd, ok := appCfg.Env["SUBDOMAIN"]; ok && sd == subdomain {
|
||||
return true
|
||||
}
|
||||
// Backward compat: check metadata subdomain for apps without SUBDOMAIN in env
|
||||
if _, hasSub := appCfg.Env["SUBDOMAIN"]; !hasSub {
|
||||
if stack.Meta.Subdomain == subdomain {
|
||||
if c.metaSubdomain == subdomain {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -107,20 +119,43 @@ type DeployRequest struct {
|
||||
// 7. Run docker compose up -d with env vars
|
||||
// 8. Update in-memory stack state
|
||||
func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||
// Atomically check and set the Deploying flag to prevent concurrent deploys (H1 fix).
|
||||
m.mu.Lock()
|
||||
sPtr, sOk := m.stacks[req.StackName]
|
||||
if !sOk {
|
||||
m.mu.Unlock()
|
||||
return "", fmt.Errorf("stack %q not found", req.StackName)
|
||||
}
|
||||
if sPtr.Deploying {
|
||||
m.mu.Unlock()
|
||||
return "", fmt.Errorf("stack %q is already being deployed — please wait", req.StackName)
|
||||
}
|
||||
if sPtr.Deployed {
|
||||
m.mu.Unlock()
|
||||
return "", fmt.Errorf("stack %q is already deployed; use update instead", req.StackName)
|
||||
}
|
||||
sPtr.Deploying = true
|
||||
sPtr.DeployError = ""
|
||||
m.mu.Unlock()
|
||||
|
||||
// If any validation below fails, clear the Deploying flag.
|
||||
clearDeploying := func() {
|
||||
m.mu.Lock()
|
||||
if s, ok := m.stacks[req.StackName]; ok {
|
||||
s.Deploying = false
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
stack, ok := m.GetStack(req.StackName)
|
||||
if !ok {
|
||||
clearDeploying()
|
||||
return "", fmt.Errorf("stack %q not found", req.StackName)
|
||||
}
|
||||
|
||||
stackDir := filepath.Dir(stack.ComposePath)
|
||||
meta := LoadMetadata(stackDir)
|
||||
|
||||
// Check if already deployed
|
||||
existing := LoadAppConfig(stackDir)
|
||||
if existing != nil && existing.Deployed {
|
||||
return "", fmt.Errorf("stack %q is already deployed; use update instead", req.StackName)
|
||||
}
|
||||
|
||||
// --- Memory validation ---
|
||||
var deployWarning string
|
||||
reservedMB := m.cfg.System.ReservedMemoryMB
|
||||
@@ -136,6 +171,7 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||
|
||||
// Hard block: real used + new request exceeds usable memory
|
||||
if newReqMB > 0 && usedMB+newReqMB > usableMB {
|
||||
clearDeploying()
|
||||
return "", fmt.Errorf(
|
||||
"Nincs elég memória az alkalmazás telepítéséhez. "+
|
||||
"Szükséges: %d MB, Elérhető: %d MB "+
|
||||
@@ -187,12 +223,15 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||
value = field.Default
|
||||
}
|
||||
if err := validateSubdomain(value); err != nil {
|
||||
clearDeploying()
|
||||
return "", err
|
||||
}
|
||||
if reservedSubdomains[value] {
|
||||
clearDeploying()
|
||||
return "", fmt.Errorf("a(z) %q aldomain foglalt rendszer számára", value)
|
||||
}
|
||||
if m.SubdomainInUse(value, req.StackName) {
|
||||
clearDeploying()
|
||||
return "", fmt.Errorf("a(z) %q aldomain már használatban van egy másik alkalmazásban", value)
|
||||
}
|
||||
|
||||
@@ -204,6 +243,7 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||
} else {
|
||||
generated, err := generateValue(field.Generate)
|
||||
if err != nil {
|
||||
clearDeploying()
|
||||
return "", fmt.Errorf("generating %s: %w", field.EnvVar, err)
|
||||
}
|
||||
value = generated
|
||||
@@ -215,6 +255,7 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||
if userVal, ok := req.Values[field.EnvVar]; ok && userVal != "" {
|
||||
value = userVal
|
||||
} else {
|
||||
clearDeploying()
|
||||
return "", fmt.Errorf("a(z) %q mező kitöltése kötelező — használja a Generálás gombot vagy írjon be egy jelszót", field.Label)
|
||||
}
|
||||
|
||||
@@ -229,12 +270,14 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||
|
||||
// Validate required fields
|
||||
if field.Required && value == "" {
|
||||
clearDeploying()
|
||||
return "", fmt.Errorf("a(z) %q (%s) mező kitöltése kötelező", field.Label, field.EnvVar)
|
||||
}
|
||||
|
||||
// Validate path fields exist on disk (inside the container's filesystem)
|
||||
// Validate path fields exist on the host filesystem
|
||||
if field.Type == "path" && value != "" {
|
||||
if _, err := os.Stat(value); os.IsNotExist(err) {
|
||||
clearDeploying()
|
||||
return "", fmt.Errorf("path %q does not exist for field %q", value, field.Label)
|
||||
}
|
||||
}
|
||||
@@ -257,6 +300,7 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||
}
|
||||
|
||||
if err := SaveAppConfig(stackDir, appCfg, m.encKey, SensitiveEnvVars(&meta)); err != nil {
|
||||
clearDeploying()
|
||||
return "", fmt.Errorf("saving app config: %w", err)
|
||||
}
|
||||
|
||||
@@ -272,14 +316,12 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||
m.checkLocalImages(req.StackName, stackDir)
|
||||
}
|
||||
|
||||
// Update in-memory stack state and mark as deploying. The compose-up
|
||||
// runs in a goroutine so the API can return immediately and the UI
|
||||
// shows progress via polling (image pull can take 30-60s).
|
||||
// Update in-memory stack state. Deploying was already set at the top (H1 fix).
|
||||
// The compose-up runs in a goroutine so the API can return immediately
|
||||
// and the UI shows progress via polling (image pull can take 30-60s).
|
||||
m.mu.Lock()
|
||||
if s, ok := m.stacks[req.StackName]; ok {
|
||||
s.Deployed = true
|
||||
s.Deploying = true
|
||||
s.DeployError = ""
|
||||
s.AppConfig = appCfg
|
||||
}
|
||||
m.mu.Unlock()
|
||||
@@ -544,6 +586,9 @@ func SaveAppConfig(stackDir string, cfg *AppConfig, encKey []byte, sensitiveVars
|
||||
if enc, err := crypto.Encrypt(encKey, v); err == nil {
|
||||
saveCfg.Env[k] = enc
|
||||
continue
|
||||
} else {
|
||||
// H10 fix: log encryption failure — value will be saved in plaintext.
|
||||
log.Printf("[WARN] Failed to encrypt env var %q: %v — saving as plaintext", k, err)
|
||||
}
|
||||
}
|
||||
saveCfg.Env[k] = v
|
||||
|
||||
Reference in New Issue
Block a user