package report import ( "encoding/base64" "fmt" "log" "os" "path/filepath" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/backup" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) // InfraBackup is the payload pushed to the Hub for disaster recovery. type InfraBackup struct { CustomerID string `json:"customer_id"` Domain string `json:"domain"` ControllerVersion string `json:"controller_version"` Timestamp string `json:"timestamp"` ControllerConfigB64 string `json:"controller_config_b64"` SettingsJSONB64 string `json:"settings_json_b64,omitempty"` DiskLayout backup.DiskLayout `json:"disk_layout"` DeployedStacks []InfraStack `json:"deployed_stacks"` ResticPassword string `json:"restic_password,omitempty"` CrossDrivePassword string `json:"cross_drive_password,omitempty"` EncryptionKeyB64 string `json:"encryption_key_b64,omitempty"` } // InfraStack identifies a deployed app for disaster recovery. // Note: AppYamlB64 contains encrypted secrets (ENC:... values). // The encryption key is also in this backup (EncryptionKeyB64). // This is intentional — the infra backup must be self-contained for DR. // Physical security of the backup media protects both. type InfraStack struct { Name string `json:"name"` DisplayName string `json:"display_name"` HDDPath string `json:"hdd_path,omitempty"` NeedsHDD bool `json:"needs_hdd"` DockerComposeB64 string `json:"docker_compose_b64,omitempty"` AppYamlB64 string `json:"app_yaml_b64,omitempty"` FelhomYamlB64 string `json:"felhom_yaml_b64,omitempty"` } // BuildInfraBackup collects all infrastructure state for Hub backup. func BuildInfraBackup( customerID, domain, version string, controllerYAMLPath string, settingsPath string, resticPasswordFile string, encryptionKeyFile string, systemDataPath string, sett *settings.Settings, stackProvider backup.StackDataProvider, logger *log.Logger, ) (*InfraBackup, error) { ib := &InfraBackup{ CustomerID: customerID, Domain: domain, ControllerVersion: version, Timestamp: time.Now().UTC().Format(time.RFC3339), } // Read and encode controller.yaml (critical — fail if unreadable) data, err := os.ReadFile(controllerYAMLPath) if err != nil { return nil, fmt.Errorf("reading controller config %s: %w", controllerYAMLPath, err) } ib.ControllerConfigB64 = base64.StdEncoding.EncodeToString(data) // Read and encode settings.json (important but non-fatal) if data, err := os.ReadFile(settingsPath); err == nil { ib.SettingsJSONB64 = base64.StdEncoding.EncodeToString(data) } else if !os.IsNotExist(err) { logger.Printf("[WARN] [report] Infra backup: could not read settings.json: %v", err) } // Read primary restic password (important but non-fatal) if data, err := os.ReadFile(resticPasswordFile); err == nil { ib.ResticPassword = base64.StdEncoding.EncodeToString(data) } else if !os.IsNotExist(err) { logger.Printf("[WARN] [report] Infra backup: could not read restic password file: %v", err) } // Read encryption key for app.yaml secrets (important but non-fatal) if encryptionKeyFile != "" { if data, err := os.ReadFile(encryptionKeyFile); err == nil { ib.EncryptionKeyB64 = base64.StdEncoding.EncodeToString(data) } else if !os.IsNotExist(err) { logger.Printf("[WARN] [report] Infra backup: could not read encryption key file: %v", err) } } // Collect disk layout from fstab + blkid ib.DiskLayout = collectDiskLayout(systemDataPath) // Collect deployed stacks (including actual config files for DR) deployed := stackProvider.ListDeployedStacks() for _, s := range deployed { is := InfraStack{ Name: s.Name, DisplayName: s.DisplayName, HDDPath: stackProvider.GetStackHDDPath(s.Name), NeedsHDD: s.NeedsHDD, } if composePath, ok := stackProvider.GetStackComposePath(s.Name); ok { stackDir := filepath.Dir(composePath) if data, err := os.ReadFile(filepath.Join(stackDir, "docker-compose.yml")); err == nil { is.DockerComposeB64 = base64.StdEncoding.EncodeToString(data) } if data, err := os.ReadFile(filepath.Join(stackDir, "app.yaml")); err == nil { is.AppYamlB64 = base64.StdEncoding.EncodeToString(data) } if data, err := os.ReadFile(filepath.Join(stackDir, ".felhom.yml")); err == nil { is.FelhomYamlB64 = base64.StdEncoding.EncodeToString(data) } } ib.DeployedStacks = append(ib.DeployedStacks, is) } if ib.DeployedStacks == nil { ib.DeployedStacks = []InfraStack{} } logger.Printf("[INFO] [report] InfraBackup built successfully (stacks=%d)", len(ib.DeployedStacks)) return ib, nil }