mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
RunMigrationIfNeeded IS called from pkg/server/server.go, so removed the misleading comment about it being dormant.
194 lines
5.7 KiB
Go
194 lines
5.7 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// MigrationStatus tracks the status of a data migration.
|
|
type MigrationStatus struct {
|
|
Version string `json:"version"`
|
|
MigratedAt time.Time `json:"migratedAt"`
|
|
SourceFiles []string `json:"sourceFiles"`
|
|
TargetDir string `json:"targetDir"`
|
|
}
|
|
|
|
// filesToMigrate lists files that should be moved to the default org directory.
|
|
// NOTE: If adding new files to migrate, .encryption.key should be handled first.
|
|
var filesToMigrate = []string{
|
|
"nodes.enc",
|
|
"system.json",
|
|
"alerts.json",
|
|
"notifications.json",
|
|
"audit.db",
|
|
}
|
|
|
|
// MigrateToMultiTenant migrates existing data to the multi-tenant directory structure.
|
|
// It moves files from the root data directory to /orgs/default/ and creates symlinks
|
|
// for backward compatibility.
|
|
//
|
|
// Migration structure:
|
|
//
|
|
// /data/
|
|
// orgs/
|
|
// default/
|
|
// nodes.enc <- moved from /data/nodes.enc
|
|
// system.json <- moved from /data/system.json
|
|
// alerts.json <- moved from /data/alerts.json
|
|
// nodes.enc <- symlink to orgs/default/nodes.enc
|
|
// system.json <- symlink to orgs/default/system.json
|
|
// alerts.json <- symlink to orgs/default/alerts.json
|
|
func MigrateToMultiTenant(dataDir string) error {
|
|
if dataDir == "" {
|
|
return fmt.Errorf("data directory cannot be empty")
|
|
}
|
|
|
|
// Check if migration is needed
|
|
defaultOrgDir := filepath.Join(dataDir, "orgs", "default")
|
|
migrationMarker := filepath.Join(defaultOrgDir, ".migrated")
|
|
|
|
// If migration marker exists, migration has already been completed
|
|
if _, err := os.Stat(migrationMarker); err == nil {
|
|
log.Debug().Str("data_dir", dataDir).Msg("Multi-tenant migration already completed")
|
|
return nil
|
|
}
|
|
|
|
log.Info().Str("data_dir", dataDir).Msg("Starting multi-tenant data migration")
|
|
|
|
// Create the default org directory
|
|
if err := os.MkdirAll(defaultOrgDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create default org directory: %w", err)
|
|
}
|
|
|
|
migratedFiles := []string{}
|
|
skippedFiles := []string{}
|
|
|
|
// Migrate each file
|
|
for _, filename := range filesToMigrate {
|
|
srcPath := filepath.Join(dataDir, filename)
|
|
dstPath := filepath.Join(defaultOrgDir, filename)
|
|
|
|
// Check if source file exists
|
|
srcInfo, err := os.Lstat(srcPath)
|
|
if os.IsNotExist(err) {
|
|
log.Debug().Str("file", filename).Msg("File does not exist, skipping")
|
|
skippedFiles = append(skippedFiles, filename)
|
|
continue
|
|
}
|
|
if err != nil {
|
|
log.Warn().Err(err).Str("file", filename).Msg("Error checking source file, skipping")
|
|
skippedFiles = append(skippedFiles, filename)
|
|
continue
|
|
}
|
|
|
|
// Skip if source is already a symlink (already migrated)
|
|
if srcInfo.Mode()&os.ModeSymlink != 0 {
|
|
log.Debug().Str("file", filename).Msg("File is already a symlink, skipping")
|
|
skippedFiles = append(skippedFiles, filename)
|
|
continue
|
|
}
|
|
|
|
// Check if destination already exists
|
|
if _, err := os.Stat(dstPath); err == nil {
|
|
log.Warn().Str("file", filename).Msg("Destination already exists, skipping")
|
|
skippedFiles = append(skippedFiles, filename)
|
|
continue
|
|
}
|
|
|
|
// Move the file
|
|
if err := os.Rename(srcPath, dstPath); err != nil {
|
|
// If rename fails (cross-device), try copy + delete
|
|
if err := copyFile(srcPath, dstPath); err != nil {
|
|
log.Error().Err(err).Str("file", filename).Msg("Failed to migrate file")
|
|
continue
|
|
}
|
|
if err := os.Remove(srcPath); err != nil {
|
|
log.Warn().Err(err).Str("file", filename).Msg("Failed to remove original file after copy")
|
|
}
|
|
}
|
|
|
|
log.Info().Str("file", filename).Msg("Migrated file to default org directory")
|
|
migratedFiles = append(migratedFiles, filename)
|
|
|
|
// Create symlink for backward compatibility
|
|
relPath, _ := filepath.Rel(dataDir, dstPath)
|
|
if err := os.Symlink(relPath, srcPath); err != nil {
|
|
log.Warn().Err(err).Str("file", filename).Msg("Failed to create symlink for backward compatibility")
|
|
} else {
|
|
log.Debug().Str("file", filename).Str("target", relPath).Msg("Created symlink for backward compatibility")
|
|
}
|
|
}
|
|
|
|
// Create migration marker
|
|
markerContent := fmt.Sprintf("migrated_at=%s\nversion=1.0\nmigrated_files=%d\nskipped_files=%d\n",
|
|
time.Now().Format(time.RFC3339), len(migratedFiles), len(skippedFiles))
|
|
if err := os.WriteFile(migrationMarker, []byte(markerContent), 0644); err != nil {
|
|
log.Warn().Err(err).Msg("Failed to write migration marker")
|
|
}
|
|
|
|
log.Info().
|
|
Int("migrated", len(migratedFiles)).
|
|
Int("skipped", len(skippedFiles)).
|
|
Str("data_dir", dataDir).
|
|
Msg("Multi-tenant data migration completed")
|
|
|
|
return nil
|
|
}
|
|
|
|
// copyFile copies a file from src to dst.
|
|
func copyFile(src, dst string) error {
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get original file permissions
|
|
info, err := os.Stat(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(dst, data, info.Mode())
|
|
}
|
|
|
|
// IsMigrationNeeded checks if multi-tenant migration is needed.
|
|
func IsMigrationNeeded(dataDir string) bool {
|
|
if dataDir == "" {
|
|
return false
|
|
}
|
|
|
|
defaultOrgDir := filepath.Join(dataDir, "orgs", "default")
|
|
migrationMarker := filepath.Join(defaultOrgDir, ".migrated")
|
|
|
|
// If marker exists, migration is not needed
|
|
if _, err := os.Stat(migrationMarker); err == nil {
|
|
return false
|
|
}
|
|
|
|
// Check if any files to migrate exist
|
|
for _, filename := range filesToMigrate {
|
|
srcPath := filepath.Join(dataDir, filename)
|
|
info, err := os.Lstat(srcPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
// File exists and is not a symlink - migration needed
|
|
if info.Mode()&os.ModeSymlink == 0 {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// RunMigrationIfNeeded checks if migration is needed and runs it.
|
|
func RunMigrationIfNeeded(dataDir string) error {
|
|
if !IsMigrationNeeded(dataDir) {
|
|
return nil
|
|
}
|
|
return MigrateToMultiTenant(dataDir)
|
|
}
|