Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.22
github.com/robfig/cron/v3 v3.0.1
gopkg.in/yaml.v3 v3.0.1
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZ
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
Expand Down
256 changes: 256 additions & 0 deletions internal/api/backup_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package api

import (
"net/http"
"strconv"

"github.com/flatrun/agent/internal/backup"
"github.com/flatrun/agent/pkg/models"
"github.com/gin-gonic/gin"
)

func (s *Server) listBackups(c *gin.Context) {
if s.backupManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
return
}
Comment on lines +13 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check for s.backupManager == nil is repeated at the beginning of almost every handler in this file. It would be more idiomatic and cleaner to extract this into a Gin middleware function. This reduces boilerplate and centralizes the logic.

Suggested change
if s.backupManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
return
}
func requireBackupManager(s *Server) gin.HandlerFunc {return func(c *gin.Context) {if s.backupManager == nil {c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})c.Abort()return}c.Next()}}


filter := &backup.BackupListFilter{
DeploymentName: c.Query("deployment"),
}

if limit := c.Query("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil {
filter.Limit = l
}
}

backups, err := s.backupManager.ListBackups(filter)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"backups": backups})
}

func (s *Server) getBackup(c *gin.Context) {
if s.backupManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
return
}

backupID := c.Param("id")
b, err := s.backupManager.GetBackup(backupID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"backup": b})
}

func (s *Server) createBackup(c *gin.Context) {
if s.backupManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
return
}

var req backup.CreateBackupRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

deployment, err := s.manager.GetDeployment(req.DeploymentName)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Deployment not found: " + req.DeploymentName})
return
Comment on lines +67 to +68
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning req.DeploymentName directly in the error message for a Deployment not found can be an information disclosure risk, especially if deployment names are sensitive. It's better to keep error messages generic for StatusNotFound to avoid leaking internal system details.

Suggested change
c.JSON(http.StatusBadRequest, gin.H{"error": "Deployment not found: " + req.DeploymentName})
return
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})

}

var spec *backup.BackupSpec
if deployment.Metadata != nil && deployment.Metadata.Backup != nil {
spec = deployment.Metadata.Backup
}

jobID := s.backupManager.StartBackupJob(req.DeploymentName, spec)
c.JSON(http.StatusAccepted, gin.H{"job_id": jobID, "message": "Backup job started"})
}

func (s *Server) createDeploymentBackup(c *gin.Context) {
if s.backupManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
return
}

deploymentName := c.Param("name")
deployment, err := s.manager.GetDeployment(deploymentName)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
return
}

var spec *backup.BackupSpec
if deployment.Metadata != nil && deployment.Metadata.Backup != nil {
spec = deployment.Metadata.Backup
}

jobID := s.backupManager.StartBackupJob(deploymentName, spec)
c.JSON(http.StatusAccepted, gin.H{"job_id": jobID, "message": "Backup job started"})
}

func (s *Server) listDeploymentBackups(c *gin.Context) {
if s.backupManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
return
}

deploymentName := c.Param("name")

filter := &backup.BackupListFilter{
DeploymentName: deploymentName,
}

if limit := c.Query("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil {
filter.Limit = l
}
}

backups, err := s.backupManager.ListBackups(filter)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"backups": backups})
}

func (s *Server) deleteBackup(c *gin.Context) {
if s.backupManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
return
}

backupID := c.Param("id")
if err := s.backupManager.DeleteBackup(backupID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"message": "Backup deleted successfully"})
}

func (s *Server) downloadBackup(c *gin.Context) {
if s.backupManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
return
}

backupID := c.Param("id")
backupPath, err := s.backupManager.GetBackupPath(backupID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}

c.Header("Content-Description", "File Transfer")
c.Header("Content-Disposition", "attachment; filename="+backupID+".tar.gz")
c.Header("Content-Type", "application/gzip")
c.File(backupPath)
}

func (s *Server) getDeploymentBackupConfig(c *gin.Context) {
deploymentName := c.Param("name")
deployment, err := s.manager.GetDeployment(deploymentName)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
return
}

var spec *backup.BackupSpec
if deployment.Metadata != nil && deployment.Metadata.Backup != nil {
spec = deployment.Metadata.Backup
}

c.JSON(http.StatusOK, gin.H{"backup_config": spec})
}

func (s *Server) updateDeploymentBackupConfig(c *gin.Context) {
deploymentName := c.Param("name")
deployment, err := s.manager.GetDeployment(deploymentName)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
return
}

var spec backup.BackupSpec
if err := c.ShouldBindJSON(&spec); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

if deployment.Metadata == nil {
deployment.Metadata = &models.ServiceMetadata{}
}
deployment.Metadata.Backup = &spec

if err := s.manager.SaveMetadata(deploymentName, deployment.Metadata); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"backup_config": spec})
}

func (s *Server) restoreBackup(c *gin.Context) {
if s.backupManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
return
}

backupID := c.Param("id")

var req backup.RestoreBackupRequest
if err := c.ShouldBindJSON(&req); err != nil {
req = backup.RestoreBackupRequest{}
}
Comment on lines +216 to +217
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If c.ShouldBindJSON(&req) fails, the req struct is completely reinitialized. This means if any JSON fields were successfully bound before the error (e.g., RestoreData), they would be reset to their zero values. It's generally better to handle binding errors more granularly or check for Content-Length before attempting JSON binding if the body is optional.

Suggested change
req = backup.RestoreBackupRequest{}
}
var req backup.RestoreBackupRequest
if c.Request.ContentLength > 0 {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
}
req.BackupID = backupID

req.BackupID = backupID

jobID := s.backupManager.StartRestoreJob(&req)
c.JSON(http.StatusAccepted, gin.H{"job_id": jobID, "message": "Restore job started"})
}

func (s *Server) getBackupJob(c *gin.Context) {
if s.backupManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
return
}

jobID := c.Param("id")
job := s.backupManager.GetJob(jobID)
if job == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"})
return
}

c.JSON(http.StatusOK, gin.H{"job": job})
}

func (s *Server) listBackupJobs(c *gin.Context) {
if s.backupManager == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
return
}

deploymentName := c.Query("deployment")
limit := 50
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil {
limit = parsed
}
}

jobs := s.backupManager.ListJobs(deploymentName, limit)
c.JSON(http.StatusOK, gin.H{"jobs": jobs})
}
Loading
Loading