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:
@@ -120,11 +120,11 @@ func (m *Manager) SetEncryptionKey(key []byte) {
|
||||
// MigrateEncryption re-saves app.yaml for deployed stacks that still have
|
||||
// plaintext values in sensitive fields. Called once on startup.
|
||||
func (m *Manager) MigrateEncryption() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.encKey == nil {
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
migrated := 0
|
||||
for _, s := range m.stacks {
|
||||
@@ -233,8 +233,12 @@ func (m *Manager) ScanStacks() error {
|
||||
existing.ComposePath = composePath
|
||||
existing.Meta = meta
|
||||
existing.Protected = m.cfg.IsProtectedStack(name)
|
||||
existing.Deployed = deployed
|
||||
existing.AppConfig = appCfg
|
||||
// Don't overwrite Deployed/AppConfig while an async deploy is in
|
||||
// progress — the goroutine manages these fields (H3 fix).
|
||||
if !existing.Deploying {
|
||||
existing.Deployed = deployed
|
||||
existing.AppConfig = appCfg
|
||||
}
|
||||
} else {
|
||||
m.stacks[name] = &Stack{
|
||||
Name: name,
|
||||
@@ -507,10 +511,44 @@ func deepCopyStack(s *Stack) Stack {
|
||||
cp.HealthProbe = &hpCopy
|
||||
}
|
||||
|
||||
// Deep-copy Meta.DeployFields slice
|
||||
// Deep-copy Meta.DeployFields slice (including nested Options)
|
||||
if s.Meta.DeployFields != nil {
|
||||
cp.Meta.DeployFields = make([]DeployField, len(s.Meta.DeployFields))
|
||||
copy(cp.Meta.DeployFields, s.Meta.DeployFields)
|
||||
for i, f := range s.Meta.DeployFields {
|
||||
if f.Options != nil {
|
||||
cp.Meta.DeployFields[i].Options = make([]SelectOption, len(f.Options))
|
||||
copy(cp.Meta.DeployFields[i].Options, f.Options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deep-copy Meta.OptionalConfig (slice of groups with nested Fields slices)
|
||||
if s.Meta.OptionalConfig != nil {
|
||||
cp.Meta.OptionalConfig = make([]OptionalConfigGroup, len(s.Meta.OptionalConfig))
|
||||
copy(cp.Meta.OptionalConfig, s.Meta.OptionalConfig)
|
||||
for i, g := range s.Meta.OptionalConfig {
|
||||
if g.Fields != nil {
|
||||
cp.Meta.OptionalConfig[i].Fields = make([]OptionalConfigField, len(g.Fields))
|
||||
copy(cp.Meta.OptionalConfig[i].Fields, g.Fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deep-copy Meta.HealthCheck pointer
|
||||
if s.Meta.HealthCheck != nil {
|
||||
hcCopy := *s.Meta.HealthCheck
|
||||
if s.Meta.HealthCheck.Checks != nil {
|
||||
hcCopy.Checks = make([]HealthCheckItem, len(s.Meta.HealthCheck.Checks))
|
||||
copy(hcCopy.Checks, s.Meta.HealthCheck.Checks)
|
||||
for i, c := range s.Meta.HealthCheck.Checks {
|
||||
if c.Expect != nil {
|
||||
eCopy := *c.Expect
|
||||
hcCopy.Checks[i].Expect = &eCopy
|
||||
}
|
||||
}
|
||||
}
|
||||
cp.Meta.HealthCheck = &hcCopy
|
||||
}
|
||||
|
||||
return cp
|
||||
|
||||
Reference in New Issue
Block a user