feat: comprehensive debug logging across all controller modules
Add detailed [DEBUG] logging to every controller module when logging.level is set to "debug". Each module with stateful debug uses SetDebug(bool) wired from main.go. Covers stacks, backup, cloudflare, integrations, system, monitor, settings, scheduler, web handlers, storage, metrics, API, selfupdate, and assets. Also includes the app export/import (.fab bundles) feature from v0.32.0 and its debug page integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,10 @@ func ProtectedHDDPaths(hddPath string) map[string]bool {
|
||||
// DeleteStack removes an orphaned stack: stops containers, removes volumes,
|
||||
// optionally removes HDD data, and deletes the stack directory.
|
||||
func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse, error) {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] DeleteStack called: name=%q, removeHDDData=%v", name, removeHDDData)
|
||||
}
|
||||
|
||||
// Safety: never delete protected stacks
|
||||
if m.cfg.IsProtectedStack(name) {
|
||||
return nil, fmt.Errorf("stack %q is protected and cannot be deleted", name)
|
||||
@@ -81,6 +85,11 @@ func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse,
|
||||
return nil, fmt.Errorf("stack %q not found", name)
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: state=%s, deployed=%v, orphaned=%v, deploying=%v",
|
||||
name, stack.State, stack.Deployed, stack.Orphaned, stack.Deploying)
|
||||
}
|
||||
|
||||
// Must be orphaned
|
||||
if !stack.Orphaned {
|
||||
return nil, fmt.Errorf("stack %q is not orphaned — only orphaned stacks can be deleted", name)
|
||||
@@ -108,11 +117,20 @@ func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse,
|
||||
|
||||
// Step 1: Parse compose file for HDD bind mounts
|
||||
hddMounts := ParseComposeHDDMounts(stack.ComposePath, hddPath)
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: found %d HDD mounts from compose file", name, len(hddMounts))
|
||||
for i, mount := range hddMounts {
|
||||
m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: HDD mount[%d]=%s", name, i, mount)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Run docker compose down --rmi local --volumes
|
||||
// H14: Return error if docker compose down fails — continuing would leave orphaned containers.
|
||||
env := m.stackEnv(stackDir)
|
||||
output, err := m.composeExecCustomEnv(stackDir, env, "down", "--rmi", "local", "--volumes")
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: compose down output: %s", name, truncateStr(output, 500))
|
||||
}
|
||||
if err != nil {
|
||||
m.logger.Printf("[ERROR] docker compose down for %s failed: %v (output: %s)", name, err, truncateStr(output, 200))
|
||||
return resp, fmt.Errorf("docker compose down failed for %s: %w", name, err)
|
||||
@@ -137,12 +155,18 @@ func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse,
|
||||
}
|
||||
|
||||
if _, err := os.Stat(cleanPath); os.IsNotExist(err) {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: HDD path does not exist, skipping: %s", name, cleanPath)
|
||||
}
|
||||
continue // path doesn't exist, nothing to do
|
||||
}
|
||||
|
||||
if removeHDDData {
|
||||
// Get size before removal
|
||||
sizeHuman := getDirSizeHuman(cleanPath)
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: removing HDD path %s (%s)", name, cleanPath, sizeHuman)
|
||||
}
|
||||
if err := os.RemoveAll(cleanPath); err != nil {
|
||||
m.logger.Printf("[ERROR] Failed to remove HDD data %s: %v", cleanPath, err)
|
||||
} else {
|
||||
@@ -151,11 +175,17 @@ func (m *Manager) DeleteStack(name string, removeHDDData bool) (*DeleteResponse,
|
||||
}
|
||||
} else {
|
||||
sizeHuman := getDirSizeHuman(cleanPath)
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: preserving HDD path %s (%s)", name, cleanPath, sizeHuman)
|
||||
}
|
||||
resp.HDDPathsPreserved = append(resp.HDDPathsPreserved, fmt.Sprintf("%s (%s)", cleanPath, sizeHuman))
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Remove stack directory
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] DeleteStack %s: removing stack directory %s", name, stackDir)
|
||||
}
|
||||
if err := os.RemoveAll(stackDir); err != nil {
|
||||
m.logger.Printf("[ERROR] Failed to remove stack directory %s: %v", stackDir, err)
|
||||
return resp, fmt.Errorf("failed to remove stack directory: %w", err)
|
||||
@@ -188,12 +218,19 @@ func (m *Manager) GetStackHDDData(name string) (*HDDDataResponse, error) {
|
||||
}
|
||||
|
||||
if hddPath == "" {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] GetStackHDDData %s: no HDD path configured, returning empty", name)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
mounts := ParseComposeHDDMounts(stack.ComposePath, hddPath)
|
||||
protected := ProtectedHDDPaths(hddPath)
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] GetStackHDDData %s: found %d raw HDD mounts from compose", name, len(mounts))
|
||||
}
|
||||
|
||||
for _, mount := range mounts {
|
||||
cleanPath := filepath.Clean(mount)
|
||||
|
||||
@@ -221,6 +258,14 @@ func (m *Manager) GetStackHDDData(name string) (*HDDDataResponse, error) {
|
||||
}
|
||||
|
||||
resp.HasHDDData = len(resp.HDDPaths) > 0
|
||||
|
||||
if m.isDebug() {
|
||||
for _, p := range resp.HDDPaths {
|
||||
m.logger.Printf("[DEBUG] [stacks] GetStackHDDData %s: path=%s exists=%v size=%s", name, p.Path, p.Exists, p.SizeHuman)
|
||||
}
|
||||
m.logger.Printf("[DEBUG] [stacks] GetStackHDDData %s: hasHDDData=%v, %d paths returned", name, resp.HasHDDData, len(resp.HDDPaths))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -229,6 +274,10 @@ func (m *Manager) GetStackHDDData(name string) (*HDDDataResponse, error) {
|
||||
// so the stack reverts to "not deployed" state. The template files (docker-compose.yml,
|
||||
// .felhom.yml) are preserved so the user can redeploy.
|
||||
func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemove []string) (*RemoveResponse, error) {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RemoveStack called: name=%q, removeHDDData=%v, backupPathsToRemove=%d", name, removeHDDData, len(backupPathsToRemove))
|
||||
}
|
||||
|
||||
// Safety: never remove protected stacks
|
||||
if m.cfg.IsProtectedStack(name) {
|
||||
return nil, fmt.Errorf("stack %q is protected and cannot be removed", name)
|
||||
@@ -239,6 +288,11 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
|
||||
return nil, fmt.Errorf("stack %q not found", name)
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: state=%s, deployed=%v, orphaned=%v, deploying=%v",
|
||||
name, stack.State, stack.Deployed, stack.Orphaned, stack.Deploying)
|
||||
}
|
||||
|
||||
// Must be deployed
|
||||
if !stack.Deployed {
|
||||
return nil, fmt.Errorf("stack %q is not deployed", name)
|
||||
@@ -266,10 +320,19 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
|
||||
|
||||
// Step 1: Parse compose file for HDD bind mounts
|
||||
hddMounts := ParseComposeHDDMounts(stack.ComposePath, hddPath)
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: found %d HDD mounts from compose file", name, len(hddMounts))
|
||||
for i, mount := range hddMounts {
|
||||
m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: HDD mount[%d]=%s", name, i, mount)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Run docker compose down --volumes (keep images for potential redeploy)
|
||||
env := m.stackEnv(stackDir)
|
||||
output, err := m.composeExecCustomEnv(stackDir, env, "down", "--volumes")
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: compose down output: %s", name, truncateStr(output, 500))
|
||||
}
|
||||
if err != nil {
|
||||
m.logger.Printf("[ERROR] docker compose down for %s failed: %v (output: %s)", name, err, truncateStr(output, 200))
|
||||
return resp, fmt.Errorf("docker compose down failed for %s: %w", name, err)
|
||||
@@ -293,11 +356,17 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
|
||||
}
|
||||
|
||||
if _, err := os.Stat(cleanPath); os.IsNotExist(err) {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: HDD path does not exist, skipping: %s", name, cleanPath)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if removeHDDData {
|
||||
sizeHuman := getDirSizeHuman(cleanPath)
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: removing HDD path %s (%s)", name, cleanPath, sizeHuman)
|
||||
}
|
||||
if err := os.RemoveAll(cleanPath); err != nil {
|
||||
m.logger.Printf("[ERROR] Failed to remove HDD data %s: %v", cleanPath, err)
|
||||
} else {
|
||||
@@ -306,12 +375,18 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
|
||||
}
|
||||
} else {
|
||||
sizeHuman := getDirSizeHuman(cleanPath)
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: preserving HDD path %s (%s)", name, cleanPath, sizeHuman)
|
||||
}
|
||||
resp.HDDPathsPreserved = append(resp.HDDPathsPreserved, fmt.Sprintf("%s (%s)", cleanPath, sizeHuman))
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Handle backup data cleanup
|
||||
backupsBase := filepath.Join(hddPath, felhomDataDir, "backups")
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: processing %d backup paths for removal (base=%s)", name, len(backupPathsToRemove), backupsBase)
|
||||
}
|
||||
for _, bkPath := range backupPathsToRemove {
|
||||
cleanPath := filepath.Clean(bkPath)
|
||||
// Validate path is under the expected backups directory
|
||||
@@ -333,6 +408,9 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
|
||||
|
||||
// Step 6: Remove app.yaml only (keep template files for redeploy)
|
||||
appYAMLPath := filepath.Join(stackDir, "app.yaml")
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: removing app.yaml at %s", name, appYAMLPath)
|
||||
}
|
||||
if err := os.Remove(appYAMLPath); err != nil && !os.IsNotExist(err) {
|
||||
m.logger.Printf("[ERROR] Failed to remove %s: %v", appYAMLPath, err)
|
||||
return resp, fmt.Errorf("failed to remove app.yaml: %w", err)
|
||||
@@ -368,6 +446,9 @@ func (m *Manager) GetStackBackupData(name string, drivePath string) (*BackupData
|
||||
}
|
||||
|
||||
if drivePath == "" {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] GetStackBackupData %s: no drive path provided, returning empty", name)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -379,6 +460,12 @@ func (m *Manager) GetStackBackupData(name string, drivePath string) (*BackupData
|
||||
rsyncPath := filepath.Join(drivePath, felhomDataDir, "backups", "secondary", name, "rsync")
|
||||
resp.BackupPaths = append(resp.BackupPaths, buildPathInfo(rsyncPath))
|
||||
|
||||
if m.isDebug() {
|
||||
for _, p := range resp.BackupPaths {
|
||||
m.logger.Printf("[DEBUG] [stacks] GetStackBackupData %s: checked path=%s exists=%v size=%s", name, p.Path, p.Exists, p.SizeHuman)
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range resp.BackupPaths {
|
||||
if p.Exists {
|
||||
resp.HasBackups = true
|
||||
@@ -386,6 +473,10 @@ func (m *Manager) GetStackBackupData(name string, drivePath string) (*BackupData
|
||||
}
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] GetStackBackupData %s: hasBackups=%v", name, resp.HasBackups)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -375,6 +375,10 @@ func (m *Manager) runComposeDeploy(name, stackDir string, env map[string]string,
|
||||
|
||||
// UpdateStackConfig updates non-locked fields for a deployed stack.
|
||||
func (m *Manager) UpdateStackConfig(name string, values map[string]string) error {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] UpdateStackConfig called: name=%q, %d values to update", name, len(values))
|
||||
}
|
||||
|
||||
stack, ok := m.GetStack(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("stack %q not found", name)
|
||||
@@ -396,13 +400,21 @@ func (m *Manager) UpdateStackConfig(name string, values map[string]string) error
|
||||
}
|
||||
|
||||
meta := LoadMetadata(stackDir)
|
||||
var changedKeys []string
|
||||
for key, val := range values {
|
||||
if lockedSet[key] {
|
||||
return fmt.Errorf("field %q is locked and cannot be changed after deployment", key)
|
||||
}
|
||||
if appCfg.Env[key] != val {
|
||||
changedKeys = append(changedKeys, key)
|
||||
}
|
||||
appCfg.Env[key] = val
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] UpdateStackConfig %s: changed keys: [%s], locked keys: %d", name, strings.Join(changedKeys, ", "), len(lockedSet))
|
||||
}
|
||||
|
||||
if err := SaveAppConfig(stackDir, appCfg, m.encKey, SensitiveEnvVars(&meta)); err != nil {
|
||||
return fmt.Errorf("saving updated config: %w", err)
|
||||
}
|
||||
@@ -445,6 +457,10 @@ func (m *Manager) GetDeployFields(name string) (*Metadata, *AppConfig, error) {
|
||||
// UpdateOptionalConfig updates optional env vars in app.yaml and restarts the stack if deployed.
|
||||
// Only updates env vars that are listed in the metadata's optional_config sections.
|
||||
func (m *Manager) UpdateOptionalConfig(stackName string, values map[string]string) error {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] UpdateOptionalConfig called: stack=%q, %d values provided", stackName, len(values))
|
||||
}
|
||||
|
||||
stack, ok := m.GetStack(stackName)
|
||||
if !ok {
|
||||
return fmt.Errorf("stack %q not found", stackName)
|
||||
@@ -461,6 +477,14 @@ func (m *Manager) UpdateOptionalConfig(stackName string, values map[string]strin
|
||||
return fmt.Errorf("no optional config fields defined for %s", stackName)
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
allowedKeys := make([]string, 0, len(allowed))
|
||||
for k := range allowed {
|
||||
allowedKeys = append(allowedKeys, k)
|
||||
}
|
||||
m.logger.Printf("[DEBUG] [stacks] UpdateOptionalConfig %s: allowed fields: [%s]", stackName, strings.Join(allowedKeys, ", "))
|
||||
}
|
||||
|
||||
// Load existing app.yaml (or create empty one)
|
||||
stackDir := filepath.Dir(stack.ComposePath)
|
||||
appCfg := LoadAppConfig(stackDir)
|
||||
@@ -564,12 +588,14 @@ func LoadAppConfig(stackDir string) *AppConfig {
|
||||
}
|
||||
cfg := &AppConfig{}
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
log.Printf("[DEBUG] [stacks] LoadAppConfig: failed to parse %s: %v", path, err)
|
||||
return nil
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func SaveAppConfig(stackDir string, cfg *AppConfig, encKey []byte, sensitiveVars []string) error {
|
||||
encryptedCount := 0
|
||||
// Clone env and encrypt sensitive values
|
||||
saveCfg := &AppConfig{
|
||||
Deployed: cfg.Deployed,
|
||||
@@ -585,6 +611,7 @@ func SaveAppConfig(stackDir string, cfg *AppConfig, encKey []byte, sensitiveVars
|
||||
if encKey != nil && sensitiveSet[k] && !crypto.IsEncrypted(v) && v != "" {
|
||||
if enc, err := crypto.Encrypt(encKey, v); err == nil {
|
||||
saveCfg.Env[k] = enc
|
||||
encryptedCount++
|
||||
continue
|
||||
} else {
|
||||
// H10 fix: log encryption failure — value will be saved in plaintext.
|
||||
@@ -594,6 +621,9 @@ func SaveAppConfig(stackDir string, cfg *AppConfig, encKey []byte, sensitiveVars
|
||||
saveCfg.Env[k] = v
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] [stacks] SaveAppConfig: saving %s — %d env vars, %d encrypted, %d sensitive fields",
|
||||
stackDir, len(saveCfg.Env), encryptedCount, len(sensitiveVars))
|
||||
|
||||
data, err := yaml.Marshal(saveCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling app config: %w", err)
|
||||
@@ -617,7 +647,11 @@ func SaveAppConfig(stackDir string, cfg *AppConfig, encKey []byte, sensitiveVars
|
||||
// LoadAppConfigDecrypted loads app.yaml and decrypts any encrypted values.
|
||||
func LoadAppConfigDecrypted(stackDir string, encKey []byte) *AppConfig {
|
||||
cfg := LoadAppConfig(stackDir)
|
||||
if cfg == nil || encKey == nil {
|
||||
if cfg == nil {
|
||||
return cfg
|
||||
}
|
||||
if encKey == nil {
|
||||
log.Printf("[DEBUG] [stacks] LoadAppConfigDecrypted: no encryption key, returning raw config for %s", stackDir)
|
||||
return cfg
|
||||
}
|
||||
cfg.Env = crypto.DecryptMap(encKey, cfg.Env)
|
||||
@@ -686,6 +720,10 @@ func generateValue(spec string) (string, error) {
|
||||
// yet in app.yaml and auto-generates values for secret/domain fields.
|
||||
// Called after sync (for updated stacks) and on startup (for all deployed stacks).
|
||||
func (m *Manager) InjectMissingFields(stackNames []string) {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] InjectMissingFields: checking %d stacks", len(stackNames))
|
||||
}
|
||||
|
||||
for _, name := range stackNames {
|
||||
stack, ok := m.GetStack(name)
|
||||
if !ok {
|
||||
@@ -696,9 +734,17 @@ func (m *Manager) InjectMissingFields(stackNames []string) {
|
||||
meta := LoadMetadata(stackDir)
|
||||
appCfg := LoadAppConfig(stackDir)
|
||||
if appCfg == nil || !appCfg.Deployed {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] InjectMissingFields: skipping %s (not deployed or no app config)", name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] InjectMissingFields: checking stack %s — %d deploy fields, %d existing env vars",
|
||||
name, len(meta.DeployFields), len(appCfg.Env))
|
||||
}
|
||||
|
||||
var injected []string
|
||||
for _, field := range meta.DeployFields {
|
||||
if _, exists := appCfg.Env[field.EnvVar]; exists {
|
||||
|
||||
@@ -24,6 +24,8 @@ func (m *Manager) RunHealthProbes() error {
|
||||
// Phase 1: collect targets (under lock)
|
||||
m.mu.RLock()
|
||||
var targets []probeTarget
|
||||
skippedNotDue := 0
|
||||
skippedNoContainer := 0
|
||||
for name, stack := range m.stacks {
|
||||
if stack.State != StateRunning && stack.State != StateUnhealthy {
|
||||
continue
|
||||
@@ -43,6 +45,12 @@ func (m *Manager) RunHealthProbes() error {
|
||||
effectiveInterval = 10 * time.Second
|
||||
}
|
||||
if time.Since(stack.HealthProbe.LastCheck) < effectiveInterval {
|
||||
skippedNotDue++
|
||||
if m.isDebug() {
|
||||
sinceLastCheck := time.Since(stack.HealthProbe.LastCheck).Round(time.Second)
|
||||
m.logger.Printf("[DEBUG] [stacks] RunHealthProbes: skipping %s — last check %s ago, effective interval %s, healthy=%v",
|
||||
name, sinceLastCheck, effectiveInterval, stack.HealthProbe.Healthy)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -50,6 +58,7 @@ func (m *Manager) RunHealthProbes() error {
|
||||
// Find the main container to probe (matching stack name)
|
||||
containerName := findProbeContainer(name, stack.Containers)
|
||||
if containerName == "" {
|
||||
skippedNoContainer++
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -61,6 +70,11 @@ func (m *Manager) RunHealthProbes() error {
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RunHealthProbes: collected %d targets (%d skipped not due, %d skipped no container)",
|
||||
len(targets), skippedNotDue, skippedNoContainer)
|
||||
}
|
||||
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -117,15 +117,33 @@ func (m *Manager) SetEncryptionKey(key []byte) {
|
||||
m.encKey = key
|
||||
}
|
||||
|
||||
// GetStacksBaseDir returns the base directory where stacks live.
|
||||
func (m *Manager) GetStacksBaseDir() string {
|
||||
return m.cfg.Paths.StacksDir
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] MigrateEncryption: no encryption key set, skipping")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
deployedCount := 0
|
||||
for _, s := range m.stacks {
|
||||
if s.Deployed {
|
||||
deployedCount++
|
||||
}
|
||||
}
|
||||
m.logger.Printf("[DEBUG] [stacks] MigrateEncryption: checking %d deployed stacks for plaintext sensitive values", deployedCount)
|
||||
}
|
||||
|
||||
migrated := 0
|
||||
for _, s := range m.stacks {
|
||||
if !s.Deployed {
|
||||
@@ -141,6 +159,11 @@ func (m *Manager) MigrateEncryption() {
|
||||
if len(sensitive) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] MigrateEncryption: checking stack %q (%d sensitive fields)", s.Name, len(sensitive))
|
||||
}
|
||||
|
||||
needsMigration := false
|
||||
for _, envVar := range sensitive {
|
||||
if v, ok := appCfg.Env[envVar]; ok && v != "" && !crypto.IsEncrypted(v) {
|
||||
@@ -149,6 +172,9 @@ func (m *Manager) MigrateEncryption() {
|
||||
}
|
||||
}
|
||||
if needsMigration {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] MigrateEncryption: stack %q needs migration — re-saving with encryption", s.Name)
|
||||
}
|
||||
if err := SaveAppConfig(stackDir, appCfg, m.encKey, sensitive); err != nil {
|
||||
m.logger.Printf("[WARN] Encryption migration failed for %s: %v", s.Name, err)
|
||||
} else {
|
||||
@@ -229,6 +255,10 @@ func (m *Manager) ScanStacks() error {
|
||||
appCfg := LoadAppConfig(stackDir)
|
||||
deployed := appCfg != nil && appCfg.Deployed
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] ScanStacks: found stack %q deployed=%v composePath=%s", name, deployed, composePath)
|
||||
}
|
||||
|
||||
if existing, ok := m.stacks[name]; ok {
|
||||
existing.ComposePath = composePath
|
||||
existing.Meta = meta
|
||||
@@ -261,6 +291,13 @@ func (m *Manager) ScanStacks() error {
|
||||
|
||||
// Detect orphaned stacks (deployed but no longer in catalog)
|
||||
catalogTemplates := m.getCatalogTemplateSlugs()
|
||||
if m.isDebug() {
|
||||
if catalogTemplates != nil {
|
||||
m.logger.Printf("[DEBUG] [stacks] ScanStacks: catalog has %d template slugs for orphan detection", len(catalogTemplates))
|
||||
} else {
|
||||
m.logger.Printf("[DEBUG] [stacks] ScanStacks: catalog templates unavailable, skipping orphan detection")
|
||||
}
|
||||
}
|
||||
if catalogTemplates != nil {
|
||||
orphanCount := 0
|
||||
for _, stack := range m.stacks {
|
||||
@@ -271,6 +308,9 @@ func (m *Manager) ScanStacks() error {
|
||||
stack.Orphaned = !catalogTemplates[stack.Name]
|
||||
if stack.Orphaned {
|
||||
orphanCount++
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] ScanStacks: stack %q is orphaned (deployed but not in catalog)", stack.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if orphanCount > 0 {
|
||||
@@ -306,6 +346,7 @@ func (m *Manager) refreshStatusLocked() error {
|
||||
|
||||
projectContainers := make(map[string][]ContainerInfo)
|
||||
|
||||
totalContainers := 0
|
||||
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
@@ -322,6 +363,11 @@ func (m *Manager) refreshStatusLocked() error {
|
||||
Status: parts[3],
|
||||
}
|
||||
projectContainers[parts[4]] = append(projectContainers[parts[4]], ci)
|
||||
totalContainers++
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] refreshStatusLocked: docker ps returned %d containers across %d projects", totalContainers, len(projectContainers))
|
||||
}
|
||||
|
||||
for name, stack := range m.stacks {
|
||||
@@ -346,6 +392,10 @@ func (m *Manager) refreshStatusLocked() error {
|
||||
stack.State = StateUnhealthy
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] refreshStatusLocked: stack %q → state=%s containers=%d", name, stack.State, len(stack.Containers))
|
||||
}
|
||||
|
||||
stack.LastUpdated = time.Now()
|
||||
}
|
||||
|
||||
@@ -569,12 +619,20 @@ func (m *Manager) StartStack(name string) error {
|
||||
return fmt.Errorf("stack %q not found", name)
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] StartStack %s: current state=%s deployed=%v", name, stack.State, stack.Deployed)
|
||||
}
|
||||
|
||||
m.logger.Printf("[INFO] Starting stack: %s", name)
|
||||
start := time.Now()
|
||||
|
||||
dir := filepath.Dir(stack.ComposePath)
|
||||
env := m.stackEnv(dir)
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] StartStack %s: prepared %d env vars for compose", name, len(env))
|
||||
}
|
||||
|
||||
if _, err := m.composeExecCustomEnv(dir, env, "up", "-d"); err != nil {
|
||||
m.logger.Printf("[ERROR] Stack %s start failed after %.1fs: %v", name, time.Since(start).Seconds(), err)
|
||||
return fmt.Errorf("starting stack %s: %w", name, err)
|
||||
@@ -604,6 +662,10 @@ func (m *Manager) StopStack(name string) error {
|
||||
return fmt.Errorf("stack %q not found", name)
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] StopStack %s: current state=%s deployed=%v containers=%d", name, stack.State, stack.Deployed, len(stack.Containers))
|
||||
}
|
||||
|
||||
m.logger.Printf("[INFO] Stopping stack: %s", name)
|
||||
start := time.Now()
|
||||
dir := filepath.Dir(stack.ComposePath)
|
||||
@@ -623,6 +685,10 @@ func (m *Manager) RestartStack(name string) error {
|
||||
return fmt.Errorf("stack %q not found", name)
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RestartStack %s: current state=%s deployed=%v containers=%d", name, stack.State, stack.Deployed, len(stack.Containers))
|
||||
}
|
||||
|
||||
m.logger.Printf("[INFO] Restarting stack: %s", name)
|
||||
start := time.Now()
|
||||
dir := filepath.Dir(stack.ComposePath)
|
||||
@@ -997,5 +1063,8 @@ func (m *Manager) getCatalogTemplateSlugs() map[string]bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] getCatalogTemplateSlugs: found %d template slugs in %s", len(slugs), cacheDir)
|
||||
}
|
||||
return slugs
|
||||
}
|
||||
Reference in New Issue
Block a user