feat(migrations): Add managed migration operations - #230
Conversation
Hostname additions and updates now validate conflicting entries and save reliably through the API, including installations without a proxy orchestrator.
Certificate issuance and renewal can read the shared challenge webroot after an HTTPS redirect without application-specific mounts.
Each deployment can publish backups only to its selected stores. Existing policies continue publishing to every enabled destination until a selection is saved.
Recovery sets now carry an archive checksum and remote copies are read back before a backup is reported as protected.
Recovery copies can now start under a new deployment name with published ports and external network access disabled. Restore options sent through the API are now honored.
Operators can manage retention, exclusions, storage thresholds, schedules, failures, and cleanup previews from one deployment policy.
Migration plans now persist source inventory, transfer and synchronization progress, DNS propagation, cutover timing, and retirement blockers.
Existing deployments can now receive managed, shared, existing, or external database connections without manual server edits.
| protected.POST("/deployments/:name/backup-cleanup", s.authMiddleware.RequirePermission(auth.PermBackupsDelete), s.authMiddleware.RequireDeploymentAccess(auth.AccessLevelAdmin), s.cleanupDeploymentBackups) | ||
| protected.GET("/deployments/:name/migration", s.authMiddleware.RequirePermission(auth.PermDeploymentsRead), s.authMiddleware.RequireDeploymentAccess(auth.AccessLevelRead), s.getDeploymentMigration) | ||
| protected.PUT("/deployments/:name/migration", s.authMiddleware.RequirePermission(auth.PermDeploymentsWrite), s.authMiddleware.RequireDeploymentAccess(auth.AccessLevelWrite), s.updateDeploymentMigration) | ||
| protected.POST("/deployments/:name/migration/check-dns", s.authMiddleware.RequirePermission(auth.PermDeploymentsRead), s.authMiddleware.RequireDeploymentAccess(auth.AccessLevelRead), s.checkDeploymentMigrationDNS) |
There was a problem hiding this comment.
checkDeploymentMigrationDNS persists the refreshed plan through SaveMetadata, yet this route only requires deployments:read, so a read-only principal mutates stored deployment metadata and the retirement gate. Require PermDeploymentsWrite/AccessLevelWrite here, or make the handler stop persisting.
| protected.POST("/deployments/:name/backup-cleanup", s.authMiddleware.RequirePermission(auth.PermBackupsDelete), s.authMiddleware.RequireDeploymentAccess(auth.AccessLevelAdmin), s.cleanupDeploymentBackups) | ||
| protected.GET("/deployments/:name/migration", s.authMiddleware.RequirePermission(auth.PermDeploymentsRead), s.authMiddleware.RequireDeploymentAccess(auth.AccessLevelRead), s.getDeploymentMigration) | ||
| protected.PUT("/deployments/:name/migration", s.authMiddleware.RequirePermission(auth.PermDeploymentsWrite), s.authMiddleware.RequireDeploymentAccess(auth.AccessLevelWrite), s.updateDeploymentMigration) | ||
| protected.POST("/deployments/:name/migration/check-dns", s.authMiddleware.RequirePermission(auth.PermDeploymentsRead), s.authMiddleware.RequireDeploymentAccess(auth.AccessLevelRead), s.checkDeploymentMigrationDNS) |
There was a problem hiding this comment.
This route only requires deployments:read, but checkDeploymentMigrationDNS writes DNS results into the plan and persists it with SaveMetadata. A read-scoped token therefore mutates stored migration state, contradicting the read permission also declared in openapi.json. Require deployments:write or stop persisting.
| c.JSON(http.StatusForbidden, gin.H{"error": "Deployment write permission required"}) | ||
| return | ||
| } | ||
| if s.authManager != nil && actor != nil && actor.User != nil && actor.Role != auth.RoleAdmin { |
There was a problem hiding this comment.
The grant runs before RestoreBackup, so a caller holding backups:write can grant themselves admin on any existing deployment. The restore then fails with "deployment already exists", but the ACL grant persists. Reject an existing target here, or move the grant after a successful restore.
| if s.authManager != nil && actor != nil && actor.User != nil && actor.Role != auth.RoleAdmin { | |
| if _, lookupErr := s.manager.GetDeployment(targetDeployment); lookupErr == nil { | |
| c.JSON(http.StatusConflict, gin.H{"error": "Deployment already exists"}) | |
| return | |
| } | |
| if s.authManager != nil && actor != nil && actor.User != nil && actor.Role != auth.RoleAdmin { | |
| if err := s.authManager.AssignDeployment(actor.User.ID, targetDeployment, auth.AccessLevelAdmin, actor.User.ID); err != nil { | |
| c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to grant access to restored deployment"}) | |
| return | |
| } | |
| } |
Code Review SummaryAdds managed migration, backup policy, database attach, and cleanup preview APIs so deployments can be operated without editing server files or using SSH. Migration retirement stays blocked until inventory, transfers, DNS propagation, a recent sync, and cutover are recorded. Backups record SHA-256 checksums and count remote copies as protected only when checksums match, while isolated restores require a new deployment name. 🚀 Key Improvements
|
Recovery access is granted only after a successful isolated restore. Restored services no longer reuse production storage or project identity. Database attachment preserves imported environment values. Backup cleanup follows deployment policy, and DNS checks require write access.
4ec62b1 to
abc51d6
Compare
Make migrations operable without editing server files or using SSH. Backup copies must verify successfully before they count as protected, and retirement stays blocked until cutover checks pass.