Files
deploy-felhom-compose/controller/internal/report/infra_backup.go
T

130 lines
4.6 KiB
Go

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
}