Skip to content

Commit 4eb70fe

Browse files
committed
feat(backup): Add backup and scheduler system
Implement complete backup and scheduled task functionality: - Add scheduler package with cron-based task execution - Add backup package for deployment backup/restore - Add API endpoints for managing backups and scheduled tasks - Extend ServiceMetadata model with backup specifications - Update WordPress and Laravel templates with backup definitions Backup features: - Backup compose files, env files, and metadata - Export container data using docker cp - Dump MySQL/PostgreSQL databases - Pre/post backup hooks for maintenance mode - Retention-based cleanup Scheduler features: - Cron expression support for task scheduling - Backup and command task types - Task execution history tracking - On-demand task execution Signed-off-by: nfebe <fenn25.fn@gmail.com>
1 parent eecaa0b commit 4eb70fe

File tree

14 files changed

+2104
-0
lines changed

14 files changed

+2104
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ require (
3333
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3434
github.com/modern-go/reflect2 v1.0.2 // indirect
3535
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
36+
github.com/robfig/cron/v3 v3.0.1 // indirect
3637
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
3738
github.com/ugorji/go/codec v1.2.11 // indirect
3839
golang.org/x/arch v0.3.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZ
6161
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
6262
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6363
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
64+
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
65+
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
6466
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
6567
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
6668
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

internal/api/backup_handlers.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
"strconv"
6+
7+
"github.com/flatrun/agent/internal/backup"
8+
"github.com/flatrun/agent/pkg/models"
9+
"github.com/gin-gonic/gin"
10+
)
11+
12+
func (s *Server) listBackups(c *gin.Context) {
13+
if s.backupManager == nil {
14+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
15+
return
16+
}
17+
18+
filter := &backup.BackupListFilter{
19+
DeploymentName: c.Query("deployment"),
20+
}
21+
22+
if limit := c.Query("limit"); limit != "" {
23+
if l, err := strconv.Atoi(limit); err == nil {
24+
filter.Limit = l
25+
}
26+
}
27+
28+
backups, err := s.backupManager.ListBackups(filter)
29+
if err != nil {
30+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
31+
return
32+
}
33+
34+
c.JSON(http.StatusOK, gin.H{"backups": backups})
35+
}
36+
37+
func (s *Server) getBackup(c *gin.Context) {
38+
if s.backupManager == nil {
39+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
40+
return
41+
}
42+
43+
backupID := c.Param("id")
44+
b, err := s.backupManager.GetBackup(backupID)
45+
if err != nil {
46+
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
47+
return
48+
}
49+
50+
c.JSON(http.StatusOK, gin.H{"backup": b})
51+
}
52+
53+
func (s *Server) createBackup(c *gin.Context) {
54+
if s.backupManager == nil {
55+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
56+
return
57+
}
58+
59+
var req backup.CreateBackupRequest
60+
if err := c.ShouldBindJSON(&req); err != nil {
61+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
62+
return
63+
}
64+
65+
deployment, err := s.manager.GetDeployment(req.DeploymentName)
66+
if err != nil {
67+
c.JSON(http.StatusBadRequest, gin.H{"error": "Deployment not found: " + req.DeploymentName})
68+
return
69+
}
70+
71+
var spec *backup.BackupSpec
72+
if deployment.Metadata != nil && deployment.Metadata.Backup != nil {
73+
spec = deployment.Metadata.Backup
74+
}
75+
76+
b, err := s.backupManager.CreateBackup(c.Request.Context(), req.DeploymentName, spec)
77+
if err != nil {
78+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
79+
return
80+
}
81+
82+
c.JSON(http.StatusCreated, gin.H{"backup": b})
83+
}
84+
85+
func (s *Server) createDeploymentBackup(c *gin.Context) {
86+
if s.backupManager == nil {
87+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
88+
return
89+
}
90+
91+
deploymentName := c.Param("name")
92+
deployment, err := s.manager.GetDeployment(deploymentName)
93+
if err != nil {
94+
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
95+
return
96+
}
97+
98+
var spec *backup.BackupSpec
99+
if deployment.Metadata != nil && deployment.Metadata.Backup != nil {
100+
spec = deployment.Metadata.Backup
101+
}
102+
103+
b, err := s.backupManager.CreateBackup(c.Request.Context(), deploymentName, spec)
104+
if err != nil {
105+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
106+
return
107+
}
108+
109+
c.JSON(http.StatusCreated, gin.H{"backup": b})
110+
}
111+
112+
func (s *Server) listDeploymentBackups(c *gin.Context) {
113+
if s.backupManager == nil {
114+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
115+
return
116+
}
117+
118+
deploymentName := c.Param("name")
119+
120+
filter := &backup.BackupListFilter{
121+
DeploymentName: deploymentName,
122+
}
123+
124+
if limit := c.Query("limit"); limit != "" {
125+
if l, err := strconv.Atoi(limit); err == nil {
126+
filter.Limit = l
127+
}
128+
}
129+
130+
backups, err := s.backupManager.ListBackups(filter)
131+
if err != nil {
132+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
133+
return
134+
}
135+
136+
c.JSON(http.StatusOK, gin.H{"backups": backups})
137+
}
138+
139+
func (s *Server) deleteBackup(c *gin.Context) {
140+
if s.backupManager == nil {
141+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
142+
return
143+
}
144+
145+
backupID := c.Param("id")
146+
if err := s.backupManager.DeleteBackup(backupID); err != nil {
147+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
148+
return
149+
}
150+
151+
c.JSON(http.StatusOK, gin.H{"message": "Backup deleted successfully"})
152+
}
153+
154+
func (s *Server) downloadBackup(c *gin.Context) {
155+
if s.backupManager == nil {
156+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Backup manager not enabled"})
157+
return
158+
}
159+
160+
backupID := c.Param("id")
161+
backupPath, err := s.backupManager.GetBackupPath(backupID)
162+
if err != nil {
163+
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
164+
return
165+
}
166+
167+
c.Header("Content-Description", "File Transfer")
168+
c.Header("Content-Disposition", "attachment; filename="+backupID+".tar.gz")
169+
c.Header("Content-Type", "application/gzip")
170+
c.File(backupPath)
171+
}
172+
173+
func (s *Server) getDeploymentBackupConfig(c *gin.Context) {
174+
deploymentName := c.Param("name")
175+
deployment, err := s.manager.GetDeployment(deploymentName)
176+
if err != nil {
177+
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
178+
return
179+
}
180+
181+
var spec *backup.BackupSpec
182+
if deployment.Metadata != nil && deployment.Metadata.Backup != nil {
183+
spec = deployment.Metadata.Backup
184+
}
185+
186+
c.JSON(http.StatusOK, gin.H{"backup_config": spec})
187+
}
188+
189+
func (s *Server) updateDeploymentBackupConfig(c *gin.Context) {
190+
deploymentName := c.Param("name")
191+
deployment, err := s.manager.GetDeployment(deploymentName)
192+
if err != nil {
193+
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
194+
return
195+
}
196+
197+
var spec backup.BackupSpec
198+
if err := c.ShouldBindJSON(&spec); err != nil {
199+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
200+
return
201+
}
202+
203+
if deployment.Metadata == nil {
204+
deployment.Metadata = &models.ServiceMetadata{}
205+
}
206+
deployment.Metadata.Backup = &spec
207+
208+
if err := s.manager.SaveMetadata(deploymentName, deployment.Metadata); err != nil {
209+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
210+
return
211+
}
212+
213+
c.JSON(http.StatusOK, gin.H{"backup_config": spec})
214+
}

0 commit comments

Comments
 (0)